Skip to main content

cdk_sql_common/wallet/
mod.rs

1//! SQLite Wallet Database
2
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use bitcoin::bip32::DerivationPath;
10use cdk_common::database::{ConversionError, Error, WalletDatabase};
11use cdk_common::mint_url::MintUrl;
12use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
13use cdk_common::secret::Secret;
14use cdk_common::util::unix_time;
15use cdk_common::wallet::{
16    self, MintQuote, ProofInfo, Transaction, TransactionDirection, TransactionId, TransactionStatus,
17};
18use cdk_common::{
19    database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod, Proof,
20    ProofDleq, PublicKey, SecretKey, SpendingConditions, State,
21};
22use tracing::instrument;
23use uuid::Uuid;
24
25use crate::common::migrate;
26use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
27use crate::pool::{DatabasePool, Pool, PooledResource};
28use crate::stmt::{query, Column};
29use crate::{
30    column_as_binary, column_as_nullable_binary, column_as_nullable_number,
31    column_as_nullable_string, column_as_number, column_as_string, unpack_into,
32};
33
34#[rustfmt::skip]
35mod migrations {
36    include!(concat!(env!("OUT_DIR"), "/migrations_wallet.rs"));
37}
38
39/// Wallet SQLite Database
40#[derive(Debug, Clone)]
41pub struct SQLWalletDatabase<RM>
42where
43    RM: DatabasePool + 'static,
44{
45    pool: Arc<Pool<RM>>,
46}
47
48impl<RM> SQLWalletDatabase<RM>
49where
50    RM: DatabasePool + 'static,
51{
52    /// Creates a new instance
53    pub async fn new<X>(db: X) -> Result<Self, Error>
54    where
55        X: Into<RM::Config>,
56    {
57        let pool = Pool::new(db.into());
58        Self::migrate(pool.get().await.map_err(|e| Error::Database(Box::new(e)))?).await?;
59
60        Ok(Self { pool })
61    }
62
63    /// Migrate [`WalletSqliteDatabase`]
64    async fn migrate(conn: PooledResource<RM>) -> Result<(), Error> {
65        let tx = ConnectionWithTransaction::new(conn).await?;
66        migrate(&tx, RM::Connection::name(), migrations::MIGRATIONS).await?;
67        // Update any existing keys with missing keyset_u32 values
68        Self::add_keyset_u32(&tx).await?;
69        tx.commit().await?;
70
71        Ok(())
72    }
73
74    async fn add_keyset_u32<T>(conn: &T) -> Result<(), Error>
75    where
76        T: DatabaseExecutor,
77    {
78        // First get the keysets where keyset_u32 on key is null
79        let keys_without_u32: Vec<Vec<Column>> = query(
80            r#"
81            SELECT
82                id
83            FROM key
84            WHERE keyset_u32 IS NULL
85            "#,
86        )?
87        .fetch_all(conn)
88        .await?;
89
90        for row in keys_without_u32 {
91            unpack_into!(let (id) = row);
92            let id = column_as_string!(id);
93
94            if let Ok(id) = Id::from_str(&id) {
95                query(
96                    r#"
97            UPDATE
98                key
99            SET keyset_u32 = :u32_keyset
100            WHERE id = :keyset_id
101            "#,
102                )?
103                .bind("u32_keyset", u32::from(id))
104                .bind("keyset_id", id.to_string())
105                .execute(conn)
106                .await?;
107            }
108        }
109
110        // Also update keysets where keyset_u32 is null
111        let keysets_without_u32: Vec<Vec<Column>> = query(
112            r#"
113            SELECT
114                id
115            FROM keyset
116            WHERE keyset_u32 IS NULL
117            "#,
118        )?
119        .fetch_all(conn)
120        .await?;
121
122        for row in keysets_without_u32 {
123            unpack_into!(let (id) = row);
124            let id = column_as_string!(id);
125
126            if let Ok(id) = Id::from_str(&id) {
127                query(
128                    r#"
129            UPDATE
130                keyset
131            SET keyset_u32 = :u32_keyset
132            WHERE id = :keyset_id
133            "#,
134                )?
135                .bind("u32_keyset", u32::from(id))
136                .bind("keyset_id", id.to_string())
137                .execute(conn)
138                .await?;
139            }
140        }
141
142        Ok(())
143    }
144}
145
146#[async_trait]
147impl<RM> WalletDatabase<database::Error> for SQLWalletDatabase<RM>
148where
149    RM: DatabasePool + 'static,
150{
151    #[instrument(skip(self))]
152    async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, database::Error> {
153        let conn = self
154            .pool
155            .get()
156            .await
157            .map_err(|e| Error::Database(Box::new(e)))?;
158
159        Ok(query(
160            r#"
161              SELECT
162                  id,
163                  unit,
164                  amount,
165                  request,
166                  fee_reserve,
167                  state,
168                  expiry,
169                  payment_proof,
170                  payment_method,
171                  estimated_blocks,
172                  fee_index,
173                  used_by_operation,
174                  version,
175                  mint_url
176              FROM
177                  melt_quote
178              "#,
179        )?
180        .fetch_all(&*conn)
181        .await?
182        .into_iter()
183        .map(sql_row_to_melt_quote)
184        .collect::<Result<_, _>>()?)
185    }
186
187    #[instrument(skip(self))]
188    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, database::Error> {
189        let conn = self
190            .pool
191            .get()
192            .await
193            .map_err(|e| Error::Database(Box::new(e)))?;
194        Ok(query(
195            r#"
196            SELECT
197                name,
198                pubkey,
199                version,
200                description,
201                description_long,
202                contact,
203                nuts,
204                icon_url,
205                motd,
206                urls,
207                mint_time,
208                tos_url
209            FROM
210                mint
211            WHERE mint_url = :mint_url
212            "#,
213        )?
214        .bind("mint_url", mint_url.to_string())
215        .fetch_one(&*conn)
216        .await?
217        .map(sql_row_to_mint_info)
218        .transpose()?)
219    }
220
221    #[instrument(skip(self))]
222    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, database::Error> {
223        let conn = self
224            .pool
225            .get()
226            .await
227            .map_err(|e| Error::Database(Box::new(e)))?;
228        Ok(query(
229            r#"
230                SELECT
231                    name,
232                    pubkey,
233                    version,
234                    description,
235                    description_long,
236                    contact,
237                    nuts,
238                    icon_url,
239                    motd,
240                    urls,
241                    mint_time,
242                    tos_url,
243                    mint_url
244                FROM
245                    mint
246                "#,
247        )?
248        .fetch_all(&*conn)
249        .await?
250        .into_iter()
251        .map(|mut row| {
252            let url = column_as_string!(
253                row.pop().ok_or(ConversionError::MissingColumn(0, 1))?,
254                MintUrl::from_str
255            );
256
257            Ok((url, sql_row_to_mint_info(row).ok()))
258        })
259        .collect::<Result<HashMap<_, _>, Error>>()?)
260    }
261
262    #[instrument(skip(self))]
263    async fn get_mint_keysets(
264        &self,
265        mint_url: MintUrl,
266    ) -> Result<Option<Vec<KeySetInfo>>, database::Error> {
267        let conn = self
268            .pool
269            .get()
270            .await
271            .map_err(|e| Error::Database(Box::new(e)))?;
272
273        let keysets = query(
274            r#"
275            SELECT
276                id,
277                unit,
278                active,
279                input_fee_ppk,
280                final_expiry
281            FROM
282                keyset
283            WHERE mint_url = :mint_url
284            "#,
285        )?
286        .bind("mint_url", mint_url.to_string())
287        .fetch_all(&*conn)
288        .await?
289        .into_iter()
290        .map(sql_row_to_keyset)
291        .collect::<Result<Vec<_>, Error>>()?;
292
293        match keysets.is_empty() {
294            false => Ok(Some(keysets)),
295            true => Ok(None),
296        }
297    }
298
299    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
300    async fn get_keyset_by_id(
301        &self,
302        keyset_id: &Id,
303    ) -> Result<Option<KeySetInfo>, database::Error> {
304        let conn = self
305            .pool
306            .get()
307            .await
308            .map_err(|e| Error::Database(Box::new(e)))?;
309        query(
310            r#"
311            SELECT
312                id,
313                unit,
314                active,
315                input_fee_ppk,
316                final_expiry
317            FROM
318                keyset
319            WHERE id = :id
320            "#,
321        )?
322        .bind("id", keyset_id.to_string())
323        .fetch_one(&*conn)
324        .await?
325        .map(sql_row_to_keyset)
326        .transpose()
327    }
328
329    #[instrument(skip(self))]
330    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, database::Error> {
331        let conn = self
332            .pool
333            .get()
334            .await
335            .map_err(|e| Error::Database(Box::new(e)))?;
336        query(
337            r#"
338            SELECT
339                id,
340                mint_url,
341                amount,
342                unit,
343                request,
344                state,
345                expiry,
346                secret_key,
347                payment_method,
348                amount_issued,
349                amount_paid,
350                updated_at,
351                estimated_blocks,
352                used_by_operation,
353                version
354            FROM
355                mint_quote
356            WHERE
357                id = :id
358            "#,
359        )?
360        .bind("id", quote_id.to_string())
361        .fetch_one(&*conn)
362        .await?
363        .map(sql_row_to_mint_quote)
364        .transpose()
365    }
366
367    #[instrument(skip(self))]
368    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
369        let conn = self
370            .pool
371            .get()
372            .await
373            .map_err(|e| Error::Database(Box::new(e)))?;
374        Ok(query(
375            r#"
376            SELECT
377                id,
378                mint_url,
379                amount,
380                unit,
381                request,
382                state,
383                expiry,
384                secret_key,
385                payment_method,
386                amount_issued,
387                amount_paid,
388                updated_at,
389                estimated_blocks,
390                used_by_operation,
391                version
392            FROM
393                mint_quote
394            "#,
395        )?
396        .fetch_all(&*conn)
397        .await?
398        .into_iter()
399        .map(sql_row_to_mint_quote)
400        .collect::<Result<_, _>>()?)
401    }
402
403    #[instrument(skip(self))]
404    async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
405        let conn = self
406            .pool
407            .get()
408            .await
409            .map_err(|e| Error::Database(Box::new(e)))?;
410        Ok(query(
411            r#"
412            SELECT
413                id,
414                mint_url,
415                amount,
416                unit,
417                request,
418                state,
419                expiry,
420                secret_key,
421                payment_method,
422                amount_issued,
423                amount_paid,
424                updated_at,
425                estimated_blocks,
426                used_by_operation,
427                version
428            FROM
429                mint_quote
430            WHERE
431                amount_issued = 0
432                OR
433                payment_method = 'bolt12'
434            "#,
435        )?
436        .fetch_all(&*conn)
437        .await?
438        .into_iter()
439        .map(sql_row_to_mint_quote)
440        .collect::<Result<_, _>>()?)
441    }
442
443    #[instrument(skip(self))]
444    async fn get_melt_quote(
445        &self,
446        quote_id: &str,
447    ) -> Result<Option<wallet::MeltQuote>, database::Error> {
448        let conn = self
449            .pool
450            .get()
451            .await
452            .map_err(|e| Error::Database(Box::new(e)))?;
453        query(
454            r#"
455            SELECT
456                id,
457                unit,
458                amount,
459                request,
460                fee_reserve,
461                state,
462                expiry,
463                payment_proof,
464                payment_method,
465                estimated_blocks,
466                fee_index,
467                used_by_operation,
468                version,
469                mint_url
470            FROM
471                melt_quote
472            WHERE
473                id=:id
474            "#,
475        )?
476        .bind("id", quote_id.to_owned())
477        .fetch_one(&*conn)
478        .await?
479        .map(sql_row_to_melt_quote)
480        .transpose()
481    }
482
483    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
484    async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, database::Error> {
485        let conn = self
486            .pool
487            .get()
488            .await
489            .map_err(|e| Error::Database(Box::new(e)))?;
490        query(
491            r#"
492            SELECT
493                keys
494            FROM key
495            WHERE id = :id
496            "#,
497        )?
498        .bind("id", keyset_id.to_string())
499        .pluck(&*conn)
500        .await?
501        .map(|keys| {
502            let keys = column_as_string!(keys);
503            serde_json::from_str(&keys).map_err(Error::from)
504        })
505        .transpose()
506    }
507
508    #[instrument(skip(self, state, spending_conditions))]
509    async fn get_proofs(
510        &self,
511        mint_url: Option<MintUrl>,
512        unit: Option<CurrencyUnit>,
513        state: Option<Vec<State>>,
514        spending_conditions: Option<Vec<SpendingConditions>>,
515    ) -> Result<Vec<ProofInfo>, database::Error> {
516        let conn = self
517            .pool
518            .get()
519            .await
520            .map_err(|e| Error::Database(Box::new(e)))?;
521        Ok(query(
522            r#"
523            SELECT
524                amount,
525                unit,
526                keyset_id,
527                secret,
528                c,
529                witness,
530                dleq_e,
531                dleq_s,
532                dleq_r,
533                y,
534                mint_url,
535                state,
536                spending_condition,
537                used_by_operation,
538                created_by_operation,
539                p2pk_e
540            FROM proof
541            "#,
542        )?
543        .fetch_all(&*conn)
544        .await?
545        .into_iter()
546        .filter_map(|row| {
547            let row = sql_row_to_proof_info(row).ok()?;
548
549            if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) {
550                Some(row)
551            } else {
552                None
553            }
554        })
555        .collect::<Vec<_>>())
556    }
557
558    #[instrument(skip(self, ys))]
559    async fn get_proofs_by_ys(
560        &self,
561        ys: Vec<PublicKey>,
562    ) -> Result<Vec<ProofInfo>, database::Error> {
563        let conn = self
564            .pool
565            .get()
566            .await
567            .map_err(|e| Error::Database(Box::new(e)))?;
568        Ok(query(
569            r#"
570            SELECT
571                amount,
572                unit,
573                keyset_id,
574                secret,
575                c,
576                witness,
577                dleq_e,
578                dleq_s,
579                dleq_r,
580                y,
581                mint_url,
582                state,
583                spending_condition,
584                used_by_operation,
585                created_by_operation,
586                p2pk_e
587            FROM proof
588            WHERE y IN (:ys)
589        "#,
590        )?
591        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
592        .fetch_all(&*conn)
593        .await?
594        .into_iter()
595        .filter_map(|row| sql_row_to_proof_info(row).ok())
596        .collect::<Vec<_>>())
597    }
598
599    async fn get_balance(
600        &self,
601        mint_url: Option<MintUrl>,
602        unit: Option<CurrencyUnit>,
603        states: Option<Vec<State>>,
604    ) -> Result<u64, database::Error> {
605        let conn = self
606            .pool
607            .get()
608            .await
609            .map_err(|e| Error::Database(Box::new(e)))?;
610
611        let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string();
612        let mut where_clauses = Vec::new();
613        let states = states
614            .unwrap_or_default()
615            .into_iter()
616            .map(|x| x.to_string())
617            .collect::<Vec<_>>();
618
619        if mint_url.is_some() {
620            where_clauses.push("mint_url = :mint_url");
621        }
622        if unit.is_some() {
623            where_clauses.push("unit = :unit");
624        }
625        if !states.is_empty() {
626            where_clauses.push("state IN (:states)");
627        }
628
629        if !where_clauses.is_empty() {
630            query_str.push_str(" WHERE ");
631            query_str.push_str(&where_clauses.join(" AND "));
632        }
633
634        let mut q = query(&query_str)?;
635
636        if let Some(ref mint_url) = mint_url {
637            q = q.bind("mint_url", mint_url.to_string());
638        }
639        if let Some(ref unit) = unit {
640            q = q.bind("unit", unit.to_string());
641        }
642
643        if !states.is_empty() {
644            q = q.bind_vec("states", states)?;
645        }
646
647        let balance = q
648            .pluck(&*conn)
649            .await?
650            .map(|n| {
651                // SQLite SUM returns INTEGER which we need to convert to u64
652                match n {
653                    crate::stmt::Column::Integer(i) => Ok(i as u64),
654                    crate::stmt::Column::Real(f) => Ok(f as u64),
655                    _ => Err(Error::Database(Box::new(std::io::Error::new(
656                        std::io::ErrorKind::InvalidData,
657                        "Invalid balance type",
658                    )))),
659                }
660            })
661            .transpose()?
662            .unwrap_or(0);
663
664        Ok(balance)
665    }
666
667    #[instrument(skip(self))]
668    async fn get_transaction(
669        &self,
670        transaction_id: TransactionId,
671    ) -> Result<Option<Transaction>, database::Error> {
672        let conn = self
673            .pool
674            .get()
675            .await
676            .map_err(|e| Error::Database(Box::new(e)))?;
677        Ok(query(
678            r#"
679            SELECT
680                mint_url,
681                direction,
682                unit,
683                amount,
684                fee,
685                ys,
686                timestamp,
687                memo,
688                metadata,
689                quote_id,
690                payment_request,
691                payment_proof,
692                payment_method,
693                saga_id,
694                status
695            FROM
696                transactions
697            WHERE
698                id = :id
699            "#,
700        )?
701        .bind("id", transaction_id.as_slice().to_vec())
702        .fetch_one(&*conn)
703        .await?
704        .map(sql_row_to_transaction)
705        .transpose()?)
706    }
707
708    #[instrument(skip(self))]
709    async fn list_transactions(
710        &self,
711        mint_url: Option<MintUrl>,
712        direction: Option<TransactionDirection>,
713        unit: Option<CurrencyUnit>,
714    ) -> Result<Vec<Transaction>, database::Error> {
715        let conn = self
716            .pool
717            .get()
718            .await
719            .map_err(|e| Error::Database(Box::new(e)))?;
720
721        Ok(query(
722            r#"
723            SELECT
724                mint_url,
725                direction,
726                unit,
727                amount,
728                fee,
729                ys,
730                timestamp,
731                memo,
732                metadata,
733                quote_id,
734                payment_request,
735                payment_proof,
736                payment_method,
737                saga_id,
738                status
739            FROM
740                transactions
741            "#,
742        )?
743        .fetch_all(&*conn)
744        .await?
745        .into_iter()
746        .filter_map(|row| {
747            // TODO: Avoid a table scan by passing the heavy lifting of checking to the DB engine
748            let transaction = sql_row_to_transaction(row).ok()?;
749            if transaction.matches_conditions(&mint_url, &direction, &unit) {
750                Some(transaction)
751            } else {
752                None
753            }
754        })
755        .collect::<Vec<_>>())
756    }
757
758    async fn update_proofs(
759        &self,
760        added: Vec<ProofInfo>,
761        removed_ys: Vec<PublicKey>,
762    ) -> Result<(), database::Error> {
763        let conn = self
764            .pool
765            .get()
766            .await
767            .map_err(|e| Error::Database(Box::new(e)))?;
768        let tx = ConnectionWithTransaction::new(conn).await?;
769
770        for proof in added {
771            query(
772                r#"
773    INSERT INTO proof
774    (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness, dleq_e, dleq_s, dleq_r, used_by_operation, created_by_operation, p2pk_e)
775    VALUES
776    (:y, :mint_url, :state, :spending_condition, :unit, :amount, :keyset_id, :secret, :c, :witness, :dleq_e, :dleq_s, :dleq_r, :used_by_operation, :created_by_operation, :p2pk_e)
777    ON CONFLICT(y) DO UPDATE SET
778        mint_url = excluded.mint_url,
779        state = excluded.state,
780        spending_condition = excluded.spending_condition,
781        unit = excluded.unit,
782        amount = excluded.amount,
783        keyset_id = excluded.keyset_id,
784        secret = excluded.secret,
785        c = excluded.c,
786        witness = excluded.witness,
787        dleq_e = excluded.dleq_e,
788        dleq_s = excluded.dleq_s,
789        dleq_r = excluded.dleq_r,
790        used_by_operation = excluded.used_by_operation,
791        created_by_operation = excluded.created_by_operation,
792        p2pk_e = excluded.p2pk_e
793    ;
794            "#,
795            )?
796            .bind("y", proof.y.to_bytes().to_vec())
797            .bind("mint_url", proof.mint_url.to_string())
798            .bind("state", proof.state.to_string())
799            .bind(
800                "spending_condition",
801                proof
802                    .spending_condition
803                    .map(|s| serde_json::to_string(&s).ok()),
804            )
805            .bind("unit", proof.unit.to_string())
806            .bind("amount", u64::from(proof.proof.amount) as i64)
807            .bind("keyset_id", proof.proof.keyset_id.to_string())
808            .bind("secret", proof.proof.secret.to_string())
809            .bind("c", proof.proof.c.to_bytes().to_vec())
810            .bind(
811                "witness",
812                proof
813                    .proof
814                    .witness
815                    .and_then(|w| serde_json::to_string(&w).ok()),
816            )
817            .bind(
818                "dleq_e",
819                proof.proof.dleq.as_ref().map(|dleq| dleq.e.to_secret_bytes().to_vec()),
820            )
821            .bind(
822                "dleq_s",
823                proof.proof.dleq.as_ref().map(|dleq| dleq.s.to_secret_bytes().to_vec()),
824            )
825            .bind(
826                "dleq_r",
827                proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()),
828            )
829            .bind("used_by_operation", proof.used_by_operation.map(|id| id.to_string()))
830            .bind("created_by_operation", proof.created_by_operation.map(|id| id.to_string()))
831            .bind(
832                "p2pk_e",
833                proof
834                    .proof
835                    .p2pk_e
836                    .as_ref()
837                    .map(|pk| pk.to_bytes().to_vec()),
838            )
839            .execute(&tx)
840            .await?;
841        }
842
843        if !removed_ys.is_empty() {
844            query(r#"DELETE FROM proof WHERE y IN (:ys)"#)?
845                .bind_vec(
846                    "ys",
847                    removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(),
848                )?
849                .execute(&tx)
850                .await?;
851        }
852
853        tx.commit().await?;
854
855        Ok(())
856    }
857
858    #[instrument(skip(self))]
859    async fn update_proofs_state(
860        &self,
861        ys: Vec<PublicKey>,
862        state: State,
863    ) -> Result<(), database::Error> {
864        let conn = self
865            .pool
866            .get()
867            .await
868            .map_err(|e| Error::Database(Box::new(e)))?;
869
870        query("UPDATE proof SET state = :state WHERE y IN (:ys)")?
871            .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
872            .bind("state", state.to_string())
873            .execute(&*conn)
874            .await?;
875
876        Ok(())
877    }
878
879    #[instrument(
880        skip(self, transaction),
881        fields(
882            direction = %transaction.direction,
883            amount = %transaction.amount,
884            unit = %transaction.unit,
885            quote_id = ?transaction.quote_id,
886            saga_id = ?transaction.saga_id,
887        )
888    )]
889    async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> {
890        let conn = self
891            .pool
892            .get()
893            .await
894            .map_err(|e| Error::Database(Box::new(e)))?;
895
896        let mint_url = transaction.mint_url.to_string();
897        let direction = transaction.direction.to_string();
898        let unit = transaction.unit.to_string();
899        let amount = u64::from(transaction.amount) as i64;
900        let fee = u64::from(transaction.fee) as i64;
901        let ys = transaction
902            .ys
903            .iter()
904            .flat_map(|y| y.to_bytes().to_vec())
905            .collect::<Vec<_>>();
906
907        let id = transaction.id();
908
909        query(
910               r#"
911   INSERT INTO transactions
912   (id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata, quote_id, payment_request, payment_proof, payment_method, saga_id, status)
913   VALUES
914   (:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata, :quote_id, :payment_request, :payment_proof, :payment_method, :saga_id, :status)
915   ON CONFLICT(id) DO UPDATE SET
916       mint_url = excluded.mint_url,
917       direction = excluded.direction,
918       unit = excluded.unit,
919       amount = excluded.amount,
920       fee = excluded.fee,
921       timestamp = excluded.timestamp,
922       memo = excluded.memo,
923       metadata = excluded.metadata,
924       quote_id = excluded.quote_id,
925       payment_request = excluded.payment_request,
926       payment_proof = excluded.payment_proof,
927       payment_method = excluded.payment_method,
928       saga_id = excluded.saga_id,
929       status = excluded.status
930   ;
931           "#,
932           )?
933           .bind("id", id.as_slice().to_vec())
934           .bind("mint_url", mint_url)
935           .bind("direction", direction)
936           .bind("unit", unit)
937           .bind("amount", amount)
938           .bind("fee", fee)
939           .bind("ys", ys)
940           .bind("timestamp", transaction.timestamp as i64)
941           .bind("memo", transaction.memo)
942           .bind(
943               "metadata",
944               serde_json::to_string(&transaction.metadata).map_err(Error::from)?,
945           )
946           .bind("quote_id", transaction.quote_id)
947           .bind("payment_request", transaction.payment_request)
948           .bind("payment_proof", transaction.payment_proof)
949           .bind("payment_method", transaction.payment_method.map(|pm| pm.to_string()))
950           .bind("saga_id", transaction.saga_id.map(|id| id.to_string()))
951           .bind("status", transaction.status.to_string())
952           .execute(&*conn)
953           .await?;
954
955        Ok(())
956    }
957
958    #[instrument(skip(self))]
959    async fn update_mint_url(
960        &self,
961        old_mint_url: MintUrl,
962        new_mint_url: MintUrl,
963    ) -> Result<(), database::Error> {
964        let conn = self
965            .pool
966            .get()
967            .await
968            .map_err(|e| Error::Database(Box::new(e)))?;
969        let tx = ConnectionWithTransaction::new(conn).await?;
970        let tables = ["mint_quote", "proof"];
971
972        for table in &tables {
973            query(&format!(
974                r#"
975                UPDATE {table}
976                SET mint_url = :new_mint_url
977                WHERE mint_url = :old_mint_url
978            "#
979            ))?
980            .bind("new_mint_url", new_mint_url.to_string())
981            .bind("old_mint_url", old_mint_url.to_string())
982            .execute(&tx)
983            .await?;
984        }
985
986        tx.commit().await?;
987
988        Ok(())
989    }
990
991    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
992    async fn increment_keyset_counter(
993        &self,
994        keyset_id: &Id,
995        count: u32,
996    ) -> Result<u32, database::Error> {
997        let conn = self
998            .pool
999            .get()
1000            .await
1001            .map_err(|e| Error::Database(Box::new(e)))?;
1002
1003        let new_counter = query(
1004            r#"
1005            INSERT INTO keyset_counter (keyset_id, counter)
1006            VALUES (:keyset_id, :count)
1007            ON CONFLICT(keyset_id) DO UPDATE SET
1008                counter = keyset_counter.counter + :count
1009            RETURNING counter
1010            "#,
1011        )?
1012        .bind("keyset_id", keyset_id.to_string())
1013        .bind("count", count)
1014        .pluck(&*conn)
1015        .await?
1016        .map(|n| Ok::<_, Error>(column_as_number!(n)))
1017        .transpose()?
1018        .ok_or_else(|| Error::Internal("Counter update returned no value".to_owned()))?;
1019
1020        Ok(new_counter)
1021    }
1022
1023    #[instrument(skip(self))]
1024    async fn increment_derivation_counter(
1025        &self,
1026        namespace: &str,
1027        count: u32,
1028    ) -> Result<u32, database::Error> {
1029        let conn = self
1030            .pool
1031            .get()
1032            .await
1033            .map_err(|e| Error::Database(Box::new(e)))?;
1034
1035        let new_counter = query(
1036            r#"
1037            INSERT INTO derivation_counter (namespace, counter)
1038            VALUES (:namespace, :count)
1039            ON CONFLICT(namespace) DO UPDATE SET
1040                counter = derivation_counter.counter + :count
1041            RETURNING counter
1042            "#,
1043        )?
1044        .bind("namespace", namespace.to_owned())
1045        .bind("count", count)
1046        .pluck(&*conn)
1047        .await?
1048        .map(|n| Ok::<_, Error>(column_as_number!(n)))
1049        .transpose()?
1050        .ok_or_else(|| Error::Internal("Derivation counter update returned no value".to_owned()))?;
1051
1052        Ok(new_counter)
1053    }
1054
1055    #[instrument(skip(self, mint_info))]
1056    async fn add_mint(
1057        &self,
1058        mint_url: MintUrl,
1059        mint_info: Option<MintInfo>,
1060    ) -> Result<(), database::Error> {
1061        let conn = self
1062            .pool
1063            .get()
1064            .await
1065            .map_err(|e| Error::Database(Box::new(e)))?;
1066
1067        let (
1068            name,
1069            pubkey,
1070            version,
1071            description,
1072            description_long,
1073            contact,
1074            nuts,
1075            icon_url,
1076            urls,
1077            motd,
1078            time,
1079            tos_url,
1080        ) = match mint_info {
1081            Some(mint_info) => {
1082                let MintInfo {
1083                    name,
1084                    pubkey,
1085                    version,
1086                    description,
1087                    description_long,
1088                    contact,
1089                    nuts,
1090                    icon_url,
1091                    urls,
1092                    motd,
1093                    time,
1094                    tos_url,
1095                } = mint_info;
1096
1097                (
1098                    name,
1099                    pubkey.map(|p| p.to_bytes().to_vec()),
1100                    version.map(|v| serde_json::to_string(&v).ok()),
1101                    description,
1102                    description_long,
1103                    contact.map(|c| serde_json::to_string(&c).ok()),
1104                    serde_json::to_string(&nuts).ok(),
1105                    icon_url,
1106                    urls.map(|c| serde_json::to_string(&c).ok()),
1107                    motd,
1108                    time,
1109                    tos_url,
1110                )
1111            }
1112            None => (
1113                None, None, None, None, None, None, None, None, None, None, None, None,
1114            ),
1115        };
1116
1117        query(
1118            r#"
1119   INSERT INTO mint
1120   (
1121       mint_url, name, pubkey, version, description, description_long,
1122       contact, nuts, icon_url, urls, motd, mint_time, tos_url
1123   )
1124   VALUES
1125   (
1126       :mint_url, :name, :pubkey, :version, :description, :description_long,
1127       :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url
1128   )
1129   ON CONFLICT(mint_url) DO UPDATE SET
1130       name = excluded.name,
1131       pubkey = excluded.pubkey,
1132       version = excluded.version,
1133       description = excluded.description,
1134       description_long = excluded.description_long,
1135       contact = excluded.contact,
1136       nuts = excluded.nuts,
1137       icon_url = excluded.icon_url,
1138       urls = excluded.urls,
1139       motd = excluded.motd,
1140       mint_time = excluded.mint_time,
1141       tos_url = excluded.tos_url
1142   ;
1143           "#,
1144        )?
1145        .bind("mint_url", mint_url.to_string())
1146        .bind("name", name)
1147        .bind("pubkey", pubkey)
1148        .bind("version", version)
1149        .bind("description", description)
1150        .bind("description_long", description_long)
1151        .bind("contact", contact)
1152        .bind("nuts", nuts)
1153        .bind("icon_url", icon_url)
1154        .bind("urls", urls)
1155        .bind("motd", motd)
1156        .bind("mint_time", time.map(|v| v as i64))
1157        .bind("tos_url", tos_url)
1158        .execute(&*conn)
1159        .await?;
1160
1161        Ok(())
1162    }
1163
1164    #[instrument(skip(self))]
1165    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
1166        let conn = self
1167            .pool
1168            .get()
1169            .await
1170            .map_err(|e| Error::Database(Box::new(e)))?;
1171
1172        query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)?
1173            .bind("mint_url", mint_url.to_string())
1174            .execute(&*conn)
1175            .await?;
1176
1177        Ok(())
1178    }
1179
1180    #[instrument(skip(self, keysets))]
1181    async fn add_mint_keysets(
1182        &self,
1183        mint_url: MintUrl,
1184        keysets: Vec<KeySetInfo>,
1185    ) -> Result<(), database::Error> {
1186        let conn = self
1187            .pool
1188            .get()
1189            .await
1190            .map_err(|e| Error::Database(Box::new(e)))?;
1191        let tx = ConnectionWithTransaction::new(conn).await?;
1192
1193        for keyset in keysets {
1194            query(
1195                r#"
1196        INSERT INTO keyset
1197        (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32)
1198        VALUES
1199        (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32)
1200        ON CONFLICT(id) DO UPDATE SET
1201            active = excluded.active,
1202            input_fee_ppk = excluded.input_fee_ppk
1203        "#,
1204            )?
1205            .bind("mint_url", mint_url.to_string())
1206            .bind("id", keyset.id.to_string())
1207            .bind("unit", keyset.unit.to_string())
1208            .bind("active", keyset.active)
1209            .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
1210            .bind("final_expiry", keyset.final_expiry.map(|v| v as i64))
1211            .bind("keyset_u32", u32::from(keyset.id))
1212            .execute(&tx)
1213            .await?;
1214        }
1215
1216        tx.commit().await?;
1217
1218        Ok(())
1219    }
1220
1221    #[instrument(skip_all)]
1222    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
1223        let conn = self
1224            .pool
1225            .get()
1226            .await
1227            .map_err(|e| Error::Database(Box::new(e)))?;
1228
1229        let expected_version = quote.version;
1230        let new_version = expected_version.wrapping_add(1);
1231
1232        let rows_affected = query(
1233                r#"
1234    INSERT INTO mint_quote
1235    (id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid, updated_at, estimated_blocks, version, used_by_operation)
1236    VALUES
1237    (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid, :updated_at, :estimated_blocks, :version, :used_by_operation)
1238    ON CONFLICT(id) DO UPDATE SET
1239        mint_url = excluded.mint_url,
1240        amount = excluded.amount,
1241        unit = excluded.unit,
1242        request = excluded.request,
1243        state = excluded.state,
1244        expiry = excluded.expiry,
1245        secret_key = excluded.secret_key,
1246        payment_method = excluded.payment_method,
1247        amount_issued = excluded.amount_issued,
1248        amount_paid = excluded.amount_paid,
1249        updated_at = excluded.updated_at,
1250        estimated_blocks = excluded.estimated_blocks,
1251        version = :new_version,
1252        used_by_operation = excluded.used_by_operation
1253    WHERE mint_quote.version = :expected_version
1254    ;
1255            "#,
1256            )?
1257            .bind("id", quote.id.to_string())
1258            .bind("mint_url", quote.mint_url.to_string())
1259            .bind("amount", quote.amount.map(|a| a.to_i64()))
1260            .bind("unit", quote.unit.to_string())
1261            .bind("request", quote.request)
1262            .bind("state", quote.state.to_string())
1263            .bind("expiry", quote.expiry as i64)
1264            .bind(
1265                "secret_key",
1266                quote.secret_key.map(|key| key.to_secret_hex()),
1267            )
1268            .bind("payment_method", quote.payment_method.to_string())
1269            .bind("amount_issued", quote.amount_issued.to_i64())
1270            .bind("amount_paid", quote.amount_paid.to_i64())
1271            .bind("updated_at", quote.updated_at as i64)
1272            .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1273            .bind("version", quote.version as i64)
1274            .bind("new_version", new_version as i64)
1275            .bind("expected_version", expected_version as i64)
1276            .bind("used_by_operation", quote.used_by_operation)
1277            .execute(&*conn).await?;
1278
1279        if rows_affected == 0 {
1280            return Err(database::Error::ConcurrentUpdate);
1281        }
1282
1283        Ok(())
1284    }
1285
1286    #[instrument(skip(self))]
1287    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1288        let conn = self
1289            .pool
1290            .get()
1291            .await
1292            .map_err(|e| Error::Database(Box::new(e)))?;
1293
1294        query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
1295            .bind("id", quote_id.to_string())
1296            .execute(&*conn)
1297            .await?;
1298
1299        Ok(())
1300    }
1301
1302    #[instrument(skip_all)]
1303    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
1304        let conn = self
1305            .pool
1306            .get()
1307            .await
1308            .map_err(|e| Error::Database(Box::new(e)))?;
1309
1310        let expected_version = quote.version;
1311        let new_version = expected_version.wrapping_add(1);
1312
1313        let rows_affected = query(
1314            r#"
1315 INSERT INTO melt_quote
1316 (id, unit, amount, request, fee_reserve, state, expiry, payment_proof, payment_method, estimated_blocks, fee_index, version, mint_url, used_by_operation)
1317 VALUES
1318 (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_proof, :payment_method, :estimated_blocks, :fee_index, :version, :mint_url, :used_by_operation)
1319 ON CONFLICT(id) DO UPDATE SET
1320     unit = excluded.unit,
1321     amount = excluded.amount,
1322     request = excluded.request,
1323     fee_reserve = excluded.fee_reserve,
1324     state = excluded.state,
1325     expiry = excluded.expiry,
1326     payment_proof = COALESCE(excluded.payment_proof, melt_quote.payment_proof),
1327     payment_method = excluded.payment_method,
1328     estimated_blocks = excluded.estimated_blocks,
1329     fee_index = excluded.fee_index,
1330     version = :new_version,
1331     mint_url = excluded.mint_url,
1332     used_by_operation = excluded.used_by_operation
1333 WHERE melt_quote.version = :expected_version
1334 ;
1335         "#,
1336        )?
1337        .bind("id", quote.id.to_string())
1338        .bind("unit", quote.unit.to_string())
1339        .bind("amount", u64::from(quote.amount) as i64)
1340        .bind("request", quote.request)
1341        .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
1342        .bind("state", quote.state.to_string())
1343        .bind("expiry", quote.expiry as i64)
1344        .bind("payment_proof", quote.payment_proof)
1345        .bind("payment_method", quote.payment_method.to_string())
1346        .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1347        .bind("fee_index", quote.fee_index.map(i64::from))
1348        .bind("version", quote.version as i64)
1349        .bind("new_version", new_version as i64)
1350        .bind("expected_version", expected_version as i64)
1351        .bind("mint_url", quote.mint_url.map(|m| m.to_string()))
1352        .bind("used_by_operation", quote.used_by_operation)
1353        .execute(&*conn)
1354        .await?;
1355
1356        if rows_affected == 0 {
1357            return Err(database::Error::ConcurrentUpdate);
1358        }
1359
1360        Ok(())
1361    }
1362
1363    #[instrument(skip(self))]
1364    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1365        let conn = self
1366            .pool
1367            .get()
1368            .await
1369            .map_err(|e| Error::Database(Box::new(e)))?;
1370
1371        query(r#"DELETE FROM melt_quote WHERE id=:id"#)?
1372            .bind("id", quote_id.to_owned())
1373            .execute(&*conn)
1374            .await?;
1375
1376        Ok(())
1377    }
1378
1379    #[instrument(skip_all)]
1380    async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
1381        let conn = self
1382            .pool
1383            .get()
1384            .await
1385            .map_err(|e| Error::Database(Box::new(e)))?;
1386
1387        keyset.verify_id()?;
1388
1389        query(
1390            r#"
1391                INSERT INTO key
1392                (id, keys, keyset_u32)
1393                VALUES
1394                (:id, :keys, :keyset_u32)
1395            "#,
1396        )?
1397        .bind("id", keyset.id.to_string())
1398        .bind(
1399            "keys",
1400            serde_json::to_string(&keyset.keys).map_err(Error::from)?,
1401        )
1402        .bind("keyset_u32", u32::from(keyset.id))
1403        .execute(&*conn)
1404        .await?;
1405
1406        Ok(())
1407    }
1408
1409    #[instrument(skip(self))]
1410    async fn remove_keys(&self, id: &Id) -> Result<(), database::Error> {
1411        let conn = self
1412            .pool
1413            .get()
1414            .await
1415            .map_err(|e| Error::Database(Box::new(e)))?;
1416
1417        query(r#"DELETE FROM key WHERE id = :id"#)?
1418            .bind("id", id.to_string())
1419            .execute(&*conn)
1420            .await?;
1421
1422        Ok(())
1423    }
1424
1425    #[instrument(skip(self))]
1426    async fn remove_transaction(
1427        &self,
1428        transaction_id: TransactionId,
1429    ) -> Result<(), database::Error> {
1430        let conn = self
1431            .pool
1432            .get()
1433            .await
1434            .map_err(|e| Error::Database(Box::new(e)))?;
1435
1436        query(r#"DELETE FROM transactions WHERE id=:id"#)?
1437            .bind("id", transaction_id.as_slice().to_vec())
1438            .execute(&*conn)
1439            .await?;
1440
1441        Ok(())
1442    }
1443
1444    #[instrument(skip(self))]
1445    async fn add_saga(&self, saga: wallet::WalletSaga) -> Result<(), database::Error> {
1446        let conn = self
1447            .pool
1448            .get()
1449            .await
1450            .map_err(|e| Error::Database(Box::new(e)))?;
1451
1452        let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1453            Error::Database(Box::new(std::io::Error::new(
1454                std::io::ErrorKind::InvalidData,
1455                format!("Failed to serialize saga state: {}", e),
1456            )))
1457        })?;
1458
1459        let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1460            Error::Database(Box::new(std::io::Error::new(
1461                std::io::ErrorKind::InvalidData,
1462                format!("Failed to serialize saga data: {}", e),
1463            )))
1464        })?;
1465
1466        query(
1467            r#"
1468            INSERT INTO wallet_sagas
1469            (id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version)
1470            VALUES
1471            (:id, :kind, :state, :amount, :mint_url, :unit, :quote_id, :created_at, :updated_at, :data, :version)
1472            "#,
1473        )?
1474        .bind("id", saga.id.to_string())
1475        .bind("kind", saga.kind.to_string())
1476        .bind("state", state_json)
1477        .bind("amount", u64::from(saga.amount) as i64)
1478        .bind("mint_url", saga.mint_url.to_string())
1479        .bind("unit", saga.unit.to_string())
1480        .bind("quote_id", saga.quote_id)
1481        .bind("created_at", saga.created_at as i64)
1482        .bind("updated_at", saga.updated_at as i64)
1483        .bind("data", data_json)
1484        .bind("version", saga.version as i64)
1485        .execute(&*conn)
1486        .await?;
1487
1488        Ok(())
1489    }
1490
1491    #[instrument(skip(self))]
1492    async fn get_saga(
1493        &self,
1494        id: &uuid::Uuid,
1495    ) -> Result<Option<wallet::WalletSaga>, database::Error> {
1496        let conn = self
1497            .pool
1498            .get()
1499            .await
1500            .map_err(|e| Error::Database(Box::new(e)))?;
1501
1502        let rows = query(
1503            r#"
1504            SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1505            FROM wallet_sagas
1506            WHERE id = :id
1507            "#,
1508        )?
1509        .bind("id", id.to_string())
1510        .fetch_all(&*conn)
1511        .await?;
1512
1513        match rows.into_iter().next() {
1514            Some(row) => Ok(Some(sql_row_to_wallet_saga(row)?)),
1515            None => Ok(None),
1516        }
1517    }
1518
1519    #[instrument(skip(self))]
1520    async fn update_saga(&self, saga: wallet::WalletSaga) -> Result<bool, database::Error> {
1521        let conn = self
1522            .pool
1523            .get()
1524            .await
1525            .map_err(|e| Error::Database(Box::new(e)))?;
1526
1527        let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1528            Error::Database(Box::new(std::io::Error::new(
1529                std::io::ErrorKind::InvalidData,
1530                format!("Failed to serialize saga state: {}", e),
1531            )))
1532        })?;
1533
1534        let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1535            Error::Database(Box::new(std::io::Error::new(
1536                std::io::ErrorKind::InvalidData,
1537                format!("Failed to serialize saga data: {}", e),
1538            )))
1539        })?;
1540
1541        // Optimistic locking: only update if the version matches the expected value.
1542        // The saga.version has already been incremented by the caller, so we check
1543        // for (saga.version - 1) in the WHERE clause.
1544        let expected_version = saga.version.saturating_sub(1);
1545
1546        let rows_affected = query(
1547            r#"
1548            UPDATE wallet_sagas
1549            SET kind = :kind, state = :state, amount = :amount, mint_url = :mint_url,
1550                unit = :unit, quote_id = :quote_id, updated_at = :updated_at, data = :data,
1551                version = :new_version
1552            WHERE id = :id AND version = :expected_version
1553            "#,
1554        )?
1555        .bind("id", saga.id.to_string())
1556        .bind("kind", saga.kind.to_string())
1557        .bind("state", state_json)
1558        .bind("amount", u64::from(saga.amount) as i64)
1559        .bind("mint_url", saga.mint_url.to_string())
1560        .bind("unit", saga.unit.to_string())
1561        .bind("quote_id", saga.quote_id)
1562        .bind("updated_at", saga.updated_at as i64)
1563        .bind("data", data_json)
1564        .bind("new_version", saga.version as i64)
1565        .bind("expected_version", expected_version as i64)
1566        .execute(&*conn)
1567        .await?;
1568
1569        // Return true if the update succeeded (version matched), false if version mismatch
1570        Ok(rows_affected > 0)
1571    }
1572
1573    #[instrument(skip(self))]
1574    async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), database::Error> {
1575        let conn = self
1576            .pool
1577            .get()
1578            .await
1579            .map_err(|e| Error::Database(Box::new(e)))?;
1580
1581        query(r#"DELETE FROM wallet_sagas WHERE id = :id"#)?
1582            .bind("id", id.to_string())
1583            .execute(&*conn)
1584            .await?;
1585
1586        Ok(())
1587    }
1588
1589    #[instrument(skip(self))]
1590    async fn get_incomplete_sagas(&self) -> Result<Vec<wallet::WalletSaga>, database::Error> {
1591        let conn = self
1592            .pool
1593            .get()
1594            .await
1595            .map_err(|e| Error::Database(Box::new(e)))?;
1596
1597        let rows = query(
1598            r#"
1599            SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1600            FROM wallet_sagas
1601            ORDER BY created_at ASC
1602            "#,
1603        )?
1604        .fetch_all(&*conn)
1605        .await?;
1606
1607        rows.into_iter().map(sql_row_to_wallet_saga).collect()
1608    }
1609
1610    #[instrument(skip(self))]
1611    async fn reserve_proofs(
1612        &self,
1613        ys: Vec<PublicKey>,
1614        operation_id: &uuid::Uuid,
1615    ) -> Result<(), database::Error> {
1616        let conn = self
1617            .pool
1618            .get()
1619            .await
1620            .map_err(|e| Error::Database(Box::new(e)))?;
1621
1622        if ys.is_empty() {
1623            return Ok(());
1624        }
1625
1626        let expected = ys.len();
1627        let tx = ConnectionWithTransaction::new(conn).await?;
1628
1629        let rows_affected = query(
1630            r#"
1631            UPDATE proof
1632            SET state = 'RESERVED', used_by_operation = :operation_id
1633            WHERE y IN (:ys) AND state = 'UNSPENT'
1634            "#,
1635        )?
1636        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
1637        .bind("operation_id", operation_id.to_string())
1638        .execute(&tx)
1639        .await?;
1640
1641        // Reserving is all-or-nothing: a partial match must not leave some of
1642        // the proofs reserved.
1643        if rows_affected != expected {
1644            tx.rollback().await?;
1645            return Err(database::Error::ProofNotUnspent);
1646        }
1647
1648        tx.commit().await?;
1649
1650        Ok(())
1651    }
1652
1653    #[instrument(skip(self))]
1654    async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1655        let conn = self
1656            .pool
1657            .get()
1658            .await
1659            .map_err(|e| Error::Database(Box::new(e)))?;
1660
1661        query(
1662            r#"
1663            UPDATE proof
1664            SET state = 'UNSPENT', used_by_operation = NULL
1665            WHERE used_by_operation = :operation_id
1666              AND state IN ('RESERVED', 'PENDING')
1667            "#,
1668        )?
1669        .bind("operation_id", operation_id.to_string())
1670        .execute(&*conn)
1671        .await?;
1672
1673        Ok(())
1674    }
1675
1676    #[instrument(skip(self))]
1677    async fn get_reserved_proofs(
1678        &self,
1679        operation_id: &uuid::Uuid,
1680    ) -> Result<Vec<ProofInfo>, database::Error> {
1681        let conn = self
1682            .pool
1683            .get()
1684            .await
1685            .map_err(|e| Error::Database(Box::new(e)))?;
1686
1687        let rows = query(
1688            r#"
1689            SELECT
1690                amount,
1691                unit,
1692                keyset_id,
1693                secret,
1694                c,
1695                witness,
1696                dleq_e,
1697                dleq_s,
1698                dleq_r,
1699                y,
1700                mint_url,
1701                state,
1702                spending_condition,
1703                used_by_operation,
1704                created_by_operation,
1705                p2pk_e
1706            FROM proof
1707            WHERE used_by_operation = :operation_id
1708            "#,
1709        )?
1710        .bind("operation_id", operation_id.to_string())
1711        .fetch_all(&*conn)
1712        .await?;
1713
1714        rows.into_iter().map(sql_row_to_proof_info).collect()
1715    }
1716
1717    #[instrument(skip(self))]
1718    async fn reserve_melt_quote(
1719        &self,
1720        quote_id: &str,
1721        operation_id: &uuid::Uuid,
1722    ) -> Result<(), database::Error> {
1723        let conn = self
1724            .pool
1725            .get()
1726            .await
1727            .map_err(|e| Error::Database(Box::new(e)))?;
1728
1729        let rows_affected = query(
1730            r#"
1731            UPDATE melt_quote
1732            SET used_by_operation = :operation_id
1733            WHERE id = :quote_id AND used_by_operation IS NULL
1734            "#,
1735        )?
1736        .bind("operation_id", operation_id.to_string())
1737        .bind("quote_id", quote_id)
1738        .execute(&*conn)
1739        .await?;
1740
1741        if rows_affected == 0 {
1742            // Check if the quote exists
1743            let exists = query(
1744                r#"
1745                SELECT 1 FROM melt_quote WHERE id = :quote_id
1746                "#,
1747            )?
1748            .bind("quote_id", quote_id)
1749            .fetch_one(&*conn)
1750            .await?;
1751
1752            if exists.is_none() {
1753                return Err(database::Error::UnknownQuote);
1754            }
1755            return Err(database::Error::QuoteAlreadyInUse);
1756        }
1757
1758        Ok(())
1759    }
1760
1761    #[instrument(skip(self))]
1762    async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1763        let conn = self
1764            .pool
1765            .get()
1766            .await
1767            .map_err(|e| Error::Database(Box::new(e)))?;
1768
1769        query(
1770            r#"
1771            UPDATE melt_quote
1772            SET used_by_operation = NULL
1773            WHERE used_by_operation = :operation_id
1774            "#,
1775        )?
1776        .bind("operation_id", operation_id.to_string())
1777        .execute(&*conn)
1778        .await?;
1779
1780        Ok(())
1781    }
1782
1783    #[instrument(skip(self))]
1784    async fn reserve_mint_quote(
1785        &self,
1786        quote_id: &str,
1787        operation_id: &uuid::Uuid,
1788    ) -> Result<(), database::Error> {
1789        let conn = self
1790            .pool
1791            .get()
1792            .await
1793            .map_err(|e| Error::Database(Box::new(e)))?;
1794
1795        let rows_affected = query(
1796            r#"
1797            UPDATE mint_quote
1798            SET used_by_operation = :operation_id
1799            WHERE id = :quote_id AND used_by_operation IS NULL
1800            "#,
1801        )?
1802        .bind("operation_id", operation_id.to_string())
1803        .bind("quote_id", quote_id)
1804        .execute(&*conn)
1805        .await?;
1806
1807        if rows_affected == 0 {
1808            // Check if the quote exists
1809            let exists = query(
1810                r#"
1811                SELECT 1 FROM mint_quote WHERE id = :quote_id
1812                "#,
1813            )?
1814            .bind("quote_id", quote_id)
1815            .fetch_one(&*conn)
1816            .await?;
1817
1818            if exists.is_none() {
1819                return Err(database::Error::UnknownQuote);
1820            }
1821            return Err(database::Error::QuoteAlreadyInUse);
1822        }
1823
1824        Ok(())
1825    }
1826
1827    #[instrument(skip(self))]
1828    async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1829        let conn = self
1830            .pool
1831            .get()
1832            .await
1833            .map_err(|e| Error::Database(Box::new(e)))?;
1834
1835        query(
1836            r#"
1837            UPDATE mint_quote
1838            SET used_by_operation = NULL
1839            WHERE used_by_operation = :operation_id
1840            "#,
1841        )?
1842        .bind("operation_id", operation_id.to_string())
1843        .execute(&*conn)
1844        .await?;
1845
1846        Ok(())
1847    }
1848
1849    async fn kv_read(
1850        &self,
1851        primary_namespace: &str,
1852        secondary_namespace: &str,
1853        key: &str,
1854    ) -> Result<Option<Vec<u8>>, database::Error> {
1855        crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
1856    }
1857
1858    async fn kv_list(
1859        &self,
1860        primary_namespace: &str,
1861        secondary_namespace: &str,
1862    ) -> Result<Vec<String>, database::Error> {
1863        crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
1864    }
1865
1866    async fn kv_write(
1867        &self,
1868        primary_namespace: &str,
1869        secondary_namespace: &str,
1870        key: &str,
1871        value: &[u8],
1872    ) -> Result<(), database::Error> {
1873        let conn = self
1874            .pool
1875            .get()
1876            .await
1877            .map_err(|e| Error::Database(Box::new(e)))?;
1878        crate::keyvalue::kv_write_standalone(
1879            &*conn,
1880            primary_namespace,
1881            secondary_namespace,
1882            key,
1883            value,
1884        )
1885        .await?;
1886        Ok(())
1887    }
1888
1889    async fn kv_remove(
1890        &self,
1891        primary_namespace: &str,
1892        secondary_namespace: &str,
1893        key: &str,
1894    ) -> Result<(), database::Error> {
1895        let conn = self
1896            .pool
1897            .get()
1898            .await
1899            .map_err(|e| Error::Database(Box::new(e)))?;
1900        crate::keyvalue::kv_remove_standalone(&*conn, primary_namespace, secondary_namespace, key)
1901            .await?;
1902        Ok(())
1903    }
1904
1905    // P2PK methods
1906
1907    #[instrument(skip(self))]
1908    async fn add_p2pk_key(
1909        &self,
1910        pubkey: &PublicKey,
1911        derivation_path: DerivationPath,
1912        derivation_index: u32,
1913    ) -> Result<(), Error> {
1914        let conn = self
1915            .pool
1916            .get()
1917            .await
1918            .map_err(|e| Error::Database(Box::new(e)))?;
1919        let query_str = r#"
1920        INSERT INTO p2pk_signing_key (pubkey, derivation_index, derivation_path, created_time)
1921        VALUES (:pubkey, :derivation_index, :derivation_path, :created_time)
1922        "#
1923        .to_string();
1924
1925        query(&query_str)?
1926            .bind("pubkey", pubkey.to_bytes().to_vec())
1927            .bind("derivation_index", derivation_index)
1928            .bind("derivation_path", derivation_path.to_string())
1929            .bind("created_time", unix_time() as i64)
1930            .execute(&*conn)
1931            .await?;
1932
1933        Ok(())
1934    }
1935
1936    #[instrument(skip(self))]
1937    async fn get_p2pk_key(
1938        &self,
1939        pubkey: &PublicKey,
1940    ) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1941        let conn = self
1942            .pool
1943            .get()
1944            .await
1945            .map_err(|e| Error::Database(Box::new(e)))?;
1946        let query_str = r#"SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key WHERE pubkey = :pubkey"#.to_string();
1947
1948        query(&query_str)?
1949            .bind("pubkey", pubkey.to_bytes().to_vec())
1950            .fetch_one(&*conn)
1951            .await?
1952            .map(sql_row_to_p2pk_signing_key)
1953            .transpose()
1954    }
1955
1956    #[instrument(skip(self))]
1957    async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, Error> {
1958        let conn = self
1959            .pool
1960            .get()
1961            .await
1962            .map_err(|e| Error::Database(Box::new(e)))?;
1963        let query_str = r#"
1964        SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC
1965        "#.to_string();
1966
1967        Ok(query(&query_str)?
1968            .fetch_all(&*conn)
1969            .await?
1970            .into_iter()
1971            .filter_map(|row| {
1972                let row = sql_row_to_p2pk_signing_key(row).ok()?;
1973
1974                Some(row)
1975            })
1976            .collect::<Vec<wallet::P2PKSigningKey>>())
1977    }
1978
1979    #[instrument(skip(self))]
1980    async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1981        let conn = self
1982            .pool
1983            .get()
1984            .await
1985            .map_err(|e| Error::Database(Box::new(e)))?;
1986        let query_str = r#"
1987        SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC LIMIT 1
1988        "#.to_string();
1989
1990        query(&query_str)?
1991            .fetch_one(&*conn)
1992            .await?
1993            .map(sql_row_to_p2pk_signing_key)
1994            .transpose()
1995    }
1996}
1997
1998fn sql_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
1999    unpack_into!(
2000        let (
2001            name,
2002            pubkey,
2003            version,
2004            description,
2005            description_long,
2006            contact,
2007            nuts,
2008            icon_url,
2009            motd,
2010            urls,
2011            mint_time,
2012            tos_url
2013        ) = row
2014    );
2015
2016    Ok(MintInfo {
2017        name: column_as_nullable_string!(&name),
2018        pubkey: column_as_nullable_binary!(&pubkey)
2019            .map(|bytes| cdk_common::nuts::PublicKey::from_slice(&bytes))
2020            .transpose()?,
2021        version: column_as_nullable_string!(&version).and_then(|v| serde_json::from_str(&v).ok()),
2022        description: column_as_nullable_string!(description),
2023        description_long: column_as_nullable_string!(description_long),
2024        contact: column_as_nullable_string!(contact, |v| serde_json::from_str(&v).ok()),
2025        nuts: column_as_nullable_string!(nuts, |v| serde_json::from_str(&v).ok())
2026            .unwrap_or_default(),
2027        urls: column_as_nullable_string!(urls, |v| serde_json::from_str(&v).ok()),
2028        icon_url: column_as_nullable_string!(icon_url),
2029        motd: column_as_nullable_string!(motd),
2030        time: column_as_nullable_number!(mint_time).map(|t| t),
2031        tos_url: column_as_nullable_string!(tos_url),
2032    })
2033}
2034
2035#[instrument(skip_all)]
2036fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
2037    unpack_into!(
2038        let (
2039            id,
2040            unit,
2041            active,
2042            input_fee_ppk,
2043            final_expiry
2044        ) = row
2045    );
2046
2047    Ok(KeySetInfo {
2048        id: column_as_string!(id, Id::from_str, Id::from_bytes),
2049        unit: column_as_string!(unit, CurrencyUnit::from_str),
2050        active: matches!(active, Column::Integer(1)),
2051        input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or(0),
2052        final_expiry: column_as_nullable_number!(final_expiry),
2053    })
2054}
2055
2056fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
2057    unpack_into!(
2058        let (
2059            id,
2060            mint_url,
2061            amount,
2062            unit,
2063            request,
2064            state,
2065            expiry,
2066            secret_key,
2067            row_method,
2068            row_amount_minted,
2069            row_amount_paid,
2070            updated_at,
2071            estimated_blocks,
2072            used_by_operation,
2073            version
2074        ) = row
2075    );
2076
2077    let amount: Option<i64> = column_as_nullable_number!(amount);
2078
2079    let amount_paid: u64 = column_as_number!(row_amount_paid);
2080    let amount_minted: u64 = column_as_number!(row_amount_minted);
2081    let expiry_val: u64 = column_as_number!(expiry);
2082    let updated_at: u64 = column_as_number!(updated_at);
2083    let version_val: u32 = column_as_number!(version);
2084    let payment_method =
2085        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2086
2087    Ok(MintQuote {
2088        id: column_as_string!(id),
2089        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2090        amount: amount.and_then(Amount::from_i64),
2091        unit: column_as_string!(unit, CurrencyUnit::from_str),
2092        request: column_as_string!(request),
2093        state: column_as_string!(state, MintQuoteState::from_str),
2094        expiry: expiry_val,
2095        secret_key: column_as_nullable_string!(secret_key, |s| SecretKey::from_str(&s).ok()),
2096        payment_method,
2097        amount_issued: Amount::from(amount_minted),
2098        amount_paid: Amount::from(amount_paid),
2099        updated_at,
2100        estimated_blocks: column_as_nullable_number!(estimated_blocks),
2101        used_by_operation: column_as_nullable_string!(used_by_operation),
2102        version: version_val,
2103    })
2104}
2105
2106fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
2107    unpack_into!(
2108        let (
2109            id,
2110            unit,
2111            amount,
2112            request,
2113            fee_reserve,
2114            state,
2115            expiry,
2116            payment_proof,
2117            row_method,
2118            estimated_blocks,
2119            fee_index,
2120            used_by_operation,
2121            version,
2122            mint_url
2123        ) = row
2124    );
2125
2126    let payment_method =
2127        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2128
2129    let amount_val: u64 = column_as_number!(amount);
2130    let fee_reserve_val: u64 = column_as_number!(fee_reserve);
2131    let expiry_val: u64 = column_as_number!(expiry);
2132    let version_val: u32 = column_as_number!(version);
2133
2134    Ok(wallet::MeltQuote {
2135        id: column_as_string!(id),
2136        mint_url: column_as_nullable_string!(mint_url, |s| MintUrl::from_str(&s).ok()),
2137        unit: column_as_string!(unit, CurrencyUnit::from_str),
2138        amount: Amount::from(amount_val),
2139        request: column_as_string!(request),
2140        fee_reserve: Amount::from(fee_reserve_val),
2141        state: column_as_string!(state, MeltQuoteState::from_str),
2142        expiry: expiry_val,
2143        payment_proof: column_as_nullable_string!(payment_proof),
2144        estimated_blocks: column_as_nullable_number!(estimated_blocks),
2145        fee_index: column_as_nullable_number!(fee_index),
2146        payment_method,
2147        used_by_operation: column_as_nullable_string!(used_by_operation),
2148        version: version_val,
2149    })
2150}
2151
2152fn sql_row_to_proof_info(row: Vec<Column>) -> Result<ProofInfo, Error> {
2153    unpack_into!(
2154        let (
2155            amount,
2156            unit,
2157            keyset_id,
2158            secret,
2159            c,
2160            witness,
2161            dleq_e,
2162            dleq_s,
2163            dleq_r,
2164            y,
2165            mint_url,
2166            state,
2167            spending_condition,
2168            used_by_operation,
2169            created_by_operation,
2170            p2pk_e
2171        ) = row
2172    );
2173
2174    let dleq = match (
2175        column_as_nullable_binary!(dleq_e),
2176        column_as_nullable_binary!(dleq_s),
2177        column_as_nullable_binary!(dleq_r),
2178    ) {
2179        (Some(e), Some(s), Some(r)) => {
2180            let e_key = SecretKey::from_slice(&e)?;
2181            let s_key = SecretKey::from_slice(&s)?;
2182            let r_key = SecretKey::from_slice(&r)?;
2183
2184            Some(ProofDleq::new(e_key, s_key, r_key))
2185        }
2186        _ => None,
2187    };
2188
2189    let amount: u64 = column_as_number!(amount);
2190    let proof = Proof {
2191        amount: Amount::from(amount),
2192        keyset_id: column_as_string!(keyset_id, Id::from_str),
2193        secret: column_as_string!(secret, Secret::from_str),
2194        witness: column_as_nullable_string!(witness, |v| { serde_json::from_str(&v).ok() }, |v| {
2195            serde_json::from_slice(&v).ok()
2196        }),
2197        c: column_as_string!(c, PublicKey::from_str, PublicKey::from_slice),
2198        dleq,
2199        p2pk_e: column_as_nullable_binary!(p2pk_e)
2200            .map(|bytes| PublicKey::from_slice(&bytes))
2201            .transpose()?,
2202    };
2203
2204    let used_by_operation =
2205        column_as_nullable_string!(used_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2206    let created_by_operation =
2207        column_as_nullable_string!(created_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2208
2209    Ok(ProofInfo {
2210        proof,
2211        y: column_as_string!(y, PublicKey::from_str, PublicKey::from_slice),
2212        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2213        state: column_as_string!(state, State::from_str),
2214        spending_condition: column_as_nullable_string!(
2215            spending_condition,
2216            |r| { serde_json::from_str(&r).ok() },
2217            |r| { serde_json::from_slice(&r).ok() }
2218        ),
2219        unit: column_as_string!(unit, CurrencyUnit::from_str),
2220        used_by_operation,
2221        created_by_operation,
2222    })
2223}
2224
2225fn sql_row_to_wallet_saga(row: Vec<Column>) -> Result<wallet::WalletSaga, Error> {
2226    unpack_into!(
2227        let (
2228            id,
2229            kind,
2230            state,
2231            amount,
2232            mint_url,
2233            unit,
2234            quote_id,
2235            created_at,
2236            updated_at,
2237            data,
2238            version
2239        ) = row
2240    );
2241
2242    let id_str: String = column_as_string!(id);
2243    let id = uuid::Uuid::parse_str(&id_str).map_err(|e| {
2244        Error::Database(Box::new(std::io::Error::new(
2245            std::io::ErrorKind::InvalidData,
2246            format!("Invalid UUID: {}", e),
2247        )))
2248    })?;
2249    let kind_str: String = column_as_string!(kind);
2250    let state_json: String = column_as_string!(state);
2251    let amount: u64 = column_as_number!(amount);
2252    let mint_url: MintUrl = column_as_string!(mint_url, MintUrl::from_str);
2253    let unit: CurrencyUnit = column_as_string!(unit, CurrencyUnit::from_str);
2254    let quote_id: Option<String> = column_as_nullable_string!(quote_id);
2255    let created_at: u64 = column_as_number!(created_at);
2256    let updated_at: u64 = column_as_number!(updated_at);
2257    let data_json: String = column_as_string!(data);
2258    let version: u32 = column_as_number!(version);
2259
2260    let kind = wallet::OperationKind::from_str(&kind_str).map_err(|_| {
2261        Error::Database(Box::new(std::io::Error::new(
2262            std::io::ErrorKind::InvalidData,
2263            format!("Invalid operation kind: {}", kind_str),
2264        )))
2265    })?;
2266    let state: wallet::WalletSagaState = serde_json::from_str(&state_json).map_err(|e| {
2267        Error::Database(Box::new(std::io::Error::new(
2268            std::io::ErrorKind::InvalidData,
2269            format!("Failed to deserialize saga state: {}", e),
2270        )))
2271    })?;
2272    let data: wallet::OperationData = serde_json::from_str(&data_json).map_err(|e| {
2273        Error::Database(Box::new(std::io::Error::new(
2274            std::io::ErrorKind::InvalidData,
2275            format!("Failed to deserialize saga data: {}", e),
2276        )))
2277    })?;
2278
2279    Ok(wallet::WalletSaga {
2280        id,
2281        kind,
2282        state,
2283        amount: Amount::from(amount),
2284        mint_url,
2285        unit,
2286        quote_id,
2287        created_at,
2288        updated_at,
2289        data,
2290        version,
2291    })
2292}
2293
2294fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
2295    unpack_into!(
2296        let (
2297            mint_url,
2298            direction,
2299            unit,
2300            amount,
2301            fee,
2302            ys,
2303            timestamp,
2304            memo,
2305            metadata,
2306            quote_id,
2307            payment_request,
2308            payment_proof,
2309            payment_method,
2310            saga_id,
2311            status
2312        ) = row
2313    );
2314
2315    let amount: u64 = column_as_number!(amount);
2316    let fee: u64 = column_as_number!(fee);
2317
2318    let saga_id: Option<Uuid> = column_as_nullable_string!(saga_id)
2319        .map(|id| Uuid::from_str(&id).ok())
2320        .flatten();
2321
2322    Ok(Transaction {
2323        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2324        direction: column_as_string!(direction, TransactionDirection::from_str),
2325        unit: column_as_string!(unit, CurrencyUnit::from_str),
2326        amount: Amount::from(amount),
2327        fee: Amount::from(fee),
2328        ys: column_as_binary!(ys)
2329            .chunks(33)
2330            .map(PublicKey::from_slice)
2331            .collect::<Result<Vec<_>, _>>()?,
2332        timestamp: column_as_number!(timestamp),
2333        memo: column_as_nullable_string!(memo),
2334        metadata: column_as_nullable_string!(metadata, |v| serde_json::from_str(&v).ok(), |v| {
2335            serde_json::from_slice(&v).ok()
2336        })
2337        .unwrap_or_default(),
2338        quote_id: column_as_nullable_string!(quote_id),
2339        payment_request: column_as_nullable_string!(payment_request),
2340        payment_proof: column_as_nullable_string!(payment_proof),
2341        payment_method: column_as_nullable_string!(payment_method)
2342            .map(|v| PaymentMethod::from_str(&v))
2343            .transpose()
2344            .map_err(Error::from)?,
2345        saga_id,
2346        status: column_as_string!(status, TransactionStatus::from_str),
2347    })
2348}
2349
2350fn sql_row_to_p2pk_signing_key(row: Vec<Column>) -> Result<wallet::P2PKSigningKey, Error> {
2351    unpack_into!(
2352        let (
2353            pubkey,
2354            derivation_index,
2355            derivation_path,
2356            created_time
2357        ) = row
2358    );
2359
2360    Ok(wallet::P2PKSigningKey {
2361        pubkey: column_as_string!(pubkey, PublicKey::from_str, PublicKey::from_slice),
2362        derivation_index: column_as_number!(derivation_index),
2363        derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
2364        created_time: column_as_number!(created_time),
2365    })
2366}