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                    // Not persisted: a runtime hint the mint recomputes, refreshed with /v1/info.
1096                    max_array_length: _,
1097                } = mint_info;
1098
1099                (
1100                    name,
1101                    pubkey.map(|p| p.to_bytes().to_vec()),
1102                    version.map(|v| serde_json::to_string(&v).ok()),
1103                    description,
1104                    description_long,
1105                    contact.map(|c| serde_json::to_string(&c).ok()),
1106                    serde_json::to_string(&nuts).ok(),
1107                    icon_url,
1108                    urls.map(|c| serde_json::to_string(&c).ok()),
1109                    motd,
1110                    time,
1111                    tos_url,
1112                )
1113            }
1114            None => (
1115                None, None, None, None, None, None, None, None, None, None, None, None,
1116            ),
1117        };
1118
1119        query(
1120            r#"
1121   INSERT INTO mint
1122   (
1123       mint_url, name, pubkey, version, description, description_long,
1124       contact, nuts, icon_url, urls, motd, mint_time, tos_url
1125   )
1126   VALUES
1127   (
1128       :mint_url, :name, :pubkey, :version, :description, :description_long,
1129       :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url
1130   )
1131   ON CONFLICT(mint_url) DO UPDATE SET
1132       name = excluded.name,
1133       pubkey = excluded.pubkey,
1134       version = excluded.version,
1135       description = excluded.description,
1136       description_long = excluded.description_long,
1137       contact = excluded.contact,
1138       nuts = excluded.nuts,
1139       icon_url = excluded.icon_url,
1140       urls = excluded.urls,
1141       motd = excluded.motd,
1142       mint_time = excluded.mint_time,
1143       tos_url = excluded.tos_url
1144   ;
1145           "#,
1146        )?
1147        .bind("mint_url", mint_url.to_string())
1148        .bind("name", name)
1149        .bind("pubkey", pubkey)
1150        .bind("version", version)
1151        .bind("description", description)
1152        .bind("description_long", description_long)
1153        .bind("contact", contact)
1154        .bind("nuts", nuts)
1155        .bind("icon_url", icon_url)
1156        .bind("urls", urls)
1157        .bind("motd", motd)
1158        .bind("mint_time", time.map(|v| v as i64))
1159        .bind("tos_url", tos_url)
1160        .execute(&*conn)
1161        .await?;
1162
1163        Ok(())
1164    }
1165
1166    #[instrument(skip(self))]
1167    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
1168        let conn = self
1169            .pool
1170            .get()
1171            .await
1172            .map_err(|e| Error::Database(Box::new(e)))?;
1173
1174        query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)?
1175            .bind("mint_url", mint_url.to_string())
1176            .execute(&*conn)
1177            .await?;
1178
1179        Ok(())
1180    }
1181
1182    #[instrument(skip(self, keysets))]
1183    async fn add_mint_keysets(
1184        &self,
1185        mint_url: MintUrl,
1186        keysets: Vec<KeySetInfo>,
1187    ) -> Result<(), database::Error> {
1188        let conn = self
1189            .pool
1190            .get()
1191            .await
1192            .map_err(|e| Error::Database(Box::new(e)))?;
1193        let tx = ConnectionWithTransaction::new(conn).await?;
1194
1195        for keyset in keysets {
1196            query(
1197                r#"
1198        INSERT INTO keyset
1199        (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32)
1200        VALUES
1201        (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32)
1202        ON CONFLICT(id) DO UPDATE SET
1203            active = excluded.active,
1204            input_fee_ppk = excluded.input_fee_ppk
1205        "#,
1206            )?
1207            .bind("mint_url", mint_url.to_string())
1208            .bind("id", keyset.id.to_string())
1209            .bind("unit", keyset.unit.to_string())
1210            .bind("active", keyset.active)
1211            .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
1212            .bind("final_expiry", keyset.final_expiry.map(|v| v as i64))
1213            .bind("keyset_u32", u32::from(keyset.id))
1214            .execute(&tx)
1215            .await?;
1216        }
1217
1218        tx.commit().await?;
1219
1220        Ok(())
1221    }
1222
1223    #[instrument(skip_all)]
1224    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
1225        let conn = self
1226            .pool
1227            .get()
1228            .await
1229            .map_err(|e| Error::Database(Box::new(e)))?;
1230
1231        let expected_version = quote.version;
1232        let new_version = expected_version.wrapping_add(1);
1233
1234        let rows_affected = query(
1235                r#"
1236    INSERT INTO mint_quote
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    VALUES
1239    (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid, :updated_at, :estimated_blocks, :version, :used_by_operation)
1240    ON CONFLICT(id) DO UPDATE SET
1241        mint_url = excluded.mint_url,
1242        amount = excluded.amount,
1243        unit = excluded.unit,
1244        request = excluded.request,
1245        state = excluded.state,
1246        expiry = excluded.expiry,
1247        secret_key = excluded.secret_key,
1248        payment_method = excluded.payment_method,
1249        amount_issued = excluded.amount_issued,
1250        amount_paid = excluded.amount_paid,
1251        updated_at = excluded.updated_at,
1252        estimated_blocks = excluded.estimated_blocks,
1253        version = :new_version,
1254        used_by_operation = excluded.used_by_operation
1255    WHERE mint_quote.version = :expected_version
1256    ;
1257            "#,
1258            )?
1259            .bind("id", quote.id.to_string())
1260            .bind("mint_url", quote.mint_url.to_string())
1261            .bind("amount", quote.amount.map(|a| a.to_i64()))
1262            .bind("unit", quote.unit.to_string())
1263            .bind("request", quote.request)
1264            .bind("state", quote.state.to_string())
1265            .bind("expiry", quote.expiry as i64)
1266            .bind(
1267                "secret_key",
1268                quote.secret_key.map(|key| key.to_secret_hex()),
1269            )
1270            .bind("payment_method", quote.payment_method.to_string())
1271            .bind("amount_issued", quote.amount_issued.to_i64())
1272            .bind("amount_paid", quote.amount_paid.to_i64())
1273            .bind("updated_at", quote.updated_at as i64)
1274            .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1275            .bind("version", quote.version as i64)
1276            .bind("new_version", new_version as i64)
1277            .bind("expected_version", expected_version as i64)
1278            .bind("used_by_operation", quote.used_by_operation)
1279            .execute(&*conn).await?;
1280
1281        if rows_affected == 0 {
1282            return Err(database::Error::ConcurrentUpdate);
1283        }
1284
1285        Ok(())
1286    }
1287
1288    #[instrument(skip(self))]
1289    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1290        let conn = self
1291            .pool
1292            .get()
1293            .await
1294            .map_err(|e| Error::Database(Box::new(e)))?;
1295
1296        query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
1297            .bind("id", quote_id.to_string())
1298            .execute(&*conn)
1299            .await?;
1300
1301        Ok(())
1302    }
1303
1304    #[instrument(skip_all)]
1305    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
1306        let conn = self
1307            .pool
1308            .get()
1309            .await
1310            .map_err(|e| Error::Database(Box::new(e)))?;
1311
1312        let expected_version = quote.version;
1313        let new_version = expected_version.wrapping_add(1);
1314
1315        let rows_affected = query(
1316            r#"
1317 INSERT INTO melt_quote
1318 (id, unit, amount, request, fee_reserve, state, expiry, payment_proof, payment_method, estimated_blocks, fee_index, version, mint_url, used_by_operation)
1319 VALUES
1320 (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_proof, :payment_method, :estimated_blocks, :fee_index, :version, :mint_url, :used_by_operation)
1321 ON CONFLICT(id) DO UPDATE SET
1322     unit = excluded.unit,
1323     amount = excluded.amount,
1324     request = excluded.request,
1325     fee_reserve = excluded.fee_reserve,
1326     state = excluded.state,
1327     expiry = excluded.expiry,
1328     payment_proof = COALESCE(excluded.payment_proof, melt_quote.payment_proof),
1329     payment_method = excluded.payment_method,
1330     estimated_blocks = excluded.estimated_blocks,
1331     fee_index = excluded.fee_index,
1332     version = :new_version,
1333     mint_url = excluded.mint_url,
1334     used_by_operation = excluded.used_by_operation
1335 WHERE melt_quote.version = :expected_version
1336 ;
1337         "#,
1338        )?
1339        .bind("id", quote.id.to_string())
1340        .bind("unit", quote.unit.to_string())
1341        .bind("amount", u64::from(quote.amount) as i64)
1342        .bind("request", quote.request)
1343        .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
1344        .bind("state", quote.state.to_string())
1345        .bind("expiry", quote.expiry as i64)
1346        .bind("payment_proof", quote.payment_proof)
1347        .bind("payment_method", quote.payment_method.to_string())
1348        .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1349        .bind("fee_index", quote.fee_index.map(i64::from))
1350        .bind("version", quote.version as i64)
1351        .bind("new_version", new_version as i64)
1352        .bind("expected_version", expected_version as i64)
1353        .bind("mint_url", quote.mint_url.map(|m| m.to_string()))
1354        .bind("used_by_operation", quote.used_by_operation)
1355        .execute(&*conn)
1356        .await?;
1357
1358        if rows_affected == 0 {
1359            return Err(database::Error::ConcurrentUpdate);
1360        }
1361
1362        Ok(())
1363    }
1364
1365    #[instrument(skip(self))]
1366    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1367        let conn = self
1368            .pool
1369            .get()
1370            .await
1371            .map_err(|e| Error::Database(Box::new(e)))?;
1372
1373        query(r#"DELETE FROM melt_quote WHERE id=:id"#)?
1374            .bind("id", quote_id.to_owned())
1375            .execute(&*conn)
1376            .await?;
1377
1378        Ok(())
1379    }
1380
1381    #[instrument(skip_all)]
1382    async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
1383        let conn = self
1384            .pool
1385            .get()
1386            .await
1387            .map_err(|e| Error::Database(Box::new(e)))?;
1388
1389        keyset.verify_id()?;
1390
1391        query(
1392            r#"
1393                INSERT INTO key
1394                (id, keys, keyset_u32)
1395                VALUES
1396                (:id, :keys, :keyset_u32)
1397            "#,
1398        )?
1399        .bind("id", keyset.id.to_string())
1400        .bind(
1401            "keys",
1402            serde_json::to_string(&keyset.keys).map_err(Error::from)?,
1403        )
1404        .bind("keyset_u32", u32::from(keyset.id))
1405        .execute(&*conn)
1406        .await?;
1407
1408        Ok(())
1409    }
1410
1411    #[instrument(skip(self))]
1412    async fn remove_keys(&self, id: &Id) -> Result<(), database::Error> {
1413        let conn = self
1414            .pool
1415            .get()
1416            .await
1417            .map_err(|e| Error::Database(Box::new(e)))?;
1418
1419        query(r#"DELETE FROM key WHERE id = :id"#)?
1420            .bind("id", id.to_string())
1421            .execute(&*conn)
1422            .await?;
1423
1424        Ok(())
1425    }
1426
1427    #[instrument(skip(self))]
1428    async fn remove_transaction(
1429        &self,
1430        transaction_id: TransactionId,
1431    ) -> Result<(), database::Error> {
1432        let conn = self
1433            .pool
1434            .get()
1435            .await
1436            .map_err(|e| Error::Database(Box::new(e)))?;
1437
1438        query(r#"DELETE FROM transactions WHERE id=:id"#)?
1439            .bind("id", transaction_id.as_slice().to_vec())
1440            .execute(&*conn)
1441            .await?;
1442
1443        Ok(())
1444    }
1445
1446    #[instrument(skip(self))]
1447    async fn add_saga(&self, saga: wallet::WalletSaga) -> Result<(), database::Error> {
1448        let conn = self
1449            .pool
1450            .get()
1451            .await
1452            .map_err(|e| Error::Database(Box::new(e)))?;
1453
1454        let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1455            Error::Database(Box::new(std::io::Error::new(
1456                std::io::ErrorKind::InvalidData,
1457                format!("Failed to serialize saga state: {}", e),
1458            )))
1459        })?;
1460
1461        let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1462            Error::Database(Box::new(std::io::Error::new(
1463                std::io::ErrorKind::InvalidData,
1464                format!("Failed to serialize saga data: {}", e),
1465            )))
1466        })?;
1467
1468        query(
1469            r#"
1470            INSERT INTO wallet_sagas
1471            (id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version)
1472            VALUES
1473            (:id, :kind, :state, :amount, :mint_url, :unit, :quote_id, :created_at, :updated_at, :data, :version)
1474            "#,
1475        )?
1476        .bind("id", saga.id.to_string())
1477        .bind("kind", saga.kind.to_string())
1478        .bind("state", state_json)
1479        .bind("amount", u64::from(saga.amount) as i64)
1480        .bind("mint_url", saga.mint_url.to_string())
1481        .bind("unit", saga.unit.to_string())
1482        .bind("quote_id", saga.quote_id)
1483        .bind("created_at", saga.created_at as i64)
1484        .bind("updated_at", saga.updated_at as i64)
1485        .bind("data", data_json)
1486        .bind("version", saga.version as i64)
1487        .execute(&*conn)
1488        .await?;
1489
1490        Ok(())
1491    }
1492
1493    #[instrument(skip(self))]
1494    async fn get_saga(
1495        &self,
1496        id: &uuid::Uuid,
1497    ) -> Result<Option<wallet::WalletSaga>, database::Error> {
1498        let conn = self
1499            .pool
1500            .get()
1501            .await
1502            .map_err(|e| Error::Database(Box::new(e)))?;
1503
1504        let rows = query(
1505            r#"
1506            SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1507            FROM wallet_sagas
1508            WHERE id = :id
1509            "#,
1510        )?
1511        .bind("id", id.to_string())
1512        .fetch_all(&*conn)
1513        .await?;
1514
1515        match rows.into_iter().next() {
1516            Some(row) => Ok(Some(sql_row_to_wallet_saga(row)?)),
1517            None => Ok(None),
1518        }
1519    }
1520
1521    #[instrument(skip(self))]
1522    async fn update_saga(&self, saga: wallet::WalletSaga) -> Result<bool, database::Error> {
1523        let conn = self
1524            .pool
1525            .get()
1526            .await
1527            .map_err(|e| Error::Database(Box::new(e)))?;
1528
1529        let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1530            Error::Database(Box::new(std::io::Error::new(
1531                std::io::ErrorKind::InvalidData,
1532                format!("Failed to serialize saga state: {}", e),
1533            )))
1534        })?;
1535
1536        let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1537            Error::Database(Box::new(std::io::Error::new(
1538                std::io::ErrorKind::InvalidData,
1539                format!("Failed to serialize saga data: {}", e),
1540            )))
1541        })?;
1542
1543        // Optimistic locking: only update if the version matches the expected value.
1544        // The saga.version has already been incremented by the caller, so we check
1545        // for (saga.version - 1) in the WHERE clause.
1546        let expected_version = saga.version.saturating_sub(1);
1547
1548        let rows_affected = query(
1549            r#"
1550            UPDATE wallet_sagas
1551            SET kind = :kind, state = :state, amount = :amount, mint_url = :mint_url,
1552                unit = :unit, quote_id = :quote_id, updated_at = :updated_at, data = :data,
1553                version = :new_version
1554            WHERE id = :id AND version = :expected_version
1555            "#,
1556        )?
1557        .bind("id", saga.id.to_string())
1558        .bind("kind", saga.kind.to_string())
1559        .bind("state", state_json)
1560        .bind("amount", u64::from(saga.amount) as i64)
1561        .bind("mint_url", saga.mint_url.to_string())
1562        .bind("unit", saga.unit.to_string())
1563        .bind("quote_id", saga.quote_id)
1564        .bind("updated_at", saga.updated_at as i64)
1565        .bind("data", data_json)
1566        .bind("new_version", saga.version as i64)
1567        .bind("expected_version", expected_version as i64)
1568        .execute(&*conn)
1569        .await?;
1570
1571        // Return true if the update succeeded (version matched), false if version mismatch
1572        Ok(rows_affected > 0)
1573    }
1574
1575    #[instrument(skip(self))]
1576    async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), database::Error> {
1577        let conn = self
1578            .pool
1579            .get()
1580            .await
1581            .map_err(|e| Error::Database(Box::new(e)))?;
1582
1583        query(r#"DELETE FROM wallet_sagas WHERE id = :id"#)?
1584            .bind("id", id.to_string())
1585            .execute(&*conn)
1586            .await?;
1587
1588        Ok(())
1589    }
1590
1591    #[instrument(skip(self))]
1592    async fn get_incomplete_sagas(&self) -> Result<Vec<wallet::WalletSaga>, database::Error> {
1593        let conn = self
1594            .pool
1595            .get()
1596            .await
1597            .map_err(|e| Error::Database(Box::new(e)))?;
1598
1599        let rows = query(
1600            r#"
1601            SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1602            FROM wallet_sagas
1603            ORDER BY created_at ASC
1604            "#,
1605        )?
1606        .fetch_all(&*conn)
1607        .await?;
1608
1609        rows.into_iter().map(sql_row_to_wallet_saga).collect()
1610    }
1611
1612    #[instrument(skip(self))]
1613    async fn reserve_proofs(
1614        &self,
1615        ys: Vec<PublicKey>,
1616        operation_id: &uuid::Uuid,
1617    ) -> Result<(), database::Error> {
1618        let conn = self
1619            .pool
1620            .get()
1621            .await
1622            .map_err(|e| Error::Database(Box::new(e)))?;
1623
1624        if ys.is_empty() {
1625            return Ok(());
1626        }
1627
1628        let expected = ys.len();
1629        let tx = ConnectionWithTransaction::new(conn).await?;
1630
1631        let rows_affected = query(
1632            r#"
1633            UPDATE proof
1634            SET state = 'RESERVED', used_by_operation = :operation_id
1635            WHERE y IN (:ys) AND state = 'UNSPENT'
1636            "#,
1637        )?
1638        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
1639        .bind("operation_id", operation_id.to_string())
1640        .execute(&tx)
1641        .await?;
1642
1643        // Reserving is all-or-nothing: a partial match must not leave some of
1644        // the proofs reserved.
1645        if rows_affected != expected {
1646            tx.rollback().await?;
1647            return Err(database::Error::ProofNotUnspent);
1648        }
1649
1650        tx.commit().await?;
1651
1652        Ok(())
1653    }
1654
1655    #[instrument(skip(self))]
1656    async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1657        let conn = self
1658            .pool
1659            .get()
1660            .await
1661            .map_err(|e| Error::Database(Box::new(e)))?;
1662
1663        query(
1664            r#"
1665            UPDATE proof
1666            SET state = 'UNSPENT', used_by_operation = NULL
1667            WHERE used_by_operation = :operation_id
1668              AND state IN ('RESERVED', 'PENDING')
1669            "#,
1670        )?
1671        .bind("operation_id", operation_id.to_string())
1672        .execute(&*conn)
1673        .await?;
1674
1675        Ok(())
1676    }
1677
1678    #[instrument(skip(self))]
1679    async fn get_reserved_proofs(
1680        &self,
1681        operation_id: &uuid::Uuid,
1682    ) -> Result<Vec<ProofInfo>, database::Error> {
1683        let conn = self
1684            .pool
1685            .get()
1686            .await
1687            .map_err(|e| Error::Database(Box::new(e)))?;
1688
1689        let rows = query(
1690            r#"
1691            SELECT
1692                amount,
1693                unit,
1694                keyset_id,
1695                secret,
1696                c,
1697                witness,
1698                dleq_e,
1699                dleq_s,
1700                dleq_r,
1701                y,
1702                mint_url,
1703                state,
1704                spending_condition,
1705                used_by_operation,
1706                created_by_operation,
1707                p2pk_e
1708            FROM proof
1709            WHERE used_by_operation = :operation_id
1710            "#,
1711        )?
1712        .bind("operation_id", operation_id.to_string())
1713        .fetch_all(&*conn)
1714        .await?;
1715
1716        rows.into_iter().map(sql_row_to_proof_info).collect()
1717    }
1718
1719    #[instrument(skip(self))]
1720    async fn reserve_melt_quote(
1721        &self,
1722        quote_id: &str,
1723        operation_id: &uuid::Uuid,
1724    ) -> Result<(), database::Error> {
1725        let conn = self
1726            .pool
1727            .get()
1728            .await
1729            .map_err(|e| Error::Database(Box::new(e)))?;
1730
1731        let rows_affected = query(
1732            r#"
1733            UPDATE melt_quote
1734            SET used_by_operation = :operation_id
1735            WHERE id = :quote_id AND used_by_operation IS NULL
1736            "#,
1737        )?
1738        .bind("operation_id", operation_id.to_string())
1739        .bind("quote_id", quote_id)
1740        .execute(&*conn)
1741        .await?;
1742
1743        if rows_affected == 0 {
1744            // Check if the quote exists
1745            let exists = query(
1746                r#"
1747                SELECT 1 FROM melt_quote WHERE id = :quote_id
1748                "#,
1749            )?
1750            .bind("quote_id", quote_id)
1751            .fetch_one(&*conn)
1752            .await?;
1753
1754            if exists.is_none() {
1755                return Err(database::Error::UnknownQuote);
1756            }
1757            return Err(database::Error::QuoteAlreadyInUse);
1758        }
1759
1760        Ok(())
1761    }
1762
1763    #[instrument(skip(self))]
1764    async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1765        let conn = self
1766            .pool
1767            .get()
1768            .await
1769            .map_err(|e| Error::Database(Box::new(e)))?;
1770
1771        query(
1772            r#"
1773            UPDATE melt_quote
1774            SET used_by_operation = NULL
1775            WHERE used_by_operation = :operation_id
1776            "#,
1777        )?
1778        .bind("operation_id", operation_id.to_string())
1779        .execute(&*conn)
1780        .await?;
1781
1782        Ok(())
1783    }
1784
1785    #[instrument(skip(self))]
1786    async fn reserve_mint_quote(
1787        &self,
1788        quote_id: &str,
1789        operation_id: &uuid::Uuid,
1790    ) -> Result<(), database::Error> {
1791        let conn = self
1792            .pool
1793            .get()
1794            .await
1795            .map_err(|e| Error::Database(Box::new(e)))?;
1796
1797        let rows_affected = query(
1798            r#"
1799            UPDATE mint_quote
1800            SET used_by_operation = :operation_id
1801            WHERE id = :quote_id AND used_by_operation IS NULL
1802            "#,
1803        )?
1804        .bind("operation_id", operation_id.to_string())
1805        .bind("quote_id", quote_id)
1806        .execute(&*conn)
1807        .await?;
1808
1809        if rows_affected == 0 {
1810            // Check if the quote exists
1811            let exists = query(
1812                r#"
1813                SELECT 1 FROM mint_quote WHERE id = :quote_id
1814                "#,
1815            )?
1816            .bind("quote_id", quote_id)
1817            .fetch_one(&*conn)
1818            .await?;
1819
1820            if exists.is_none() {
1821                return Err(database::Error::UnknownQuote);
1822            }
1823            return Err(database::Error::QuoteAlreadyInUse);
1824        }
1825
1826        Ok(())
1827    }
1828
1829    #[instrument(skip(self))]
1830    async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1831        let conn = self
1832            .pool
1833            .get()
1834            .await
1835            .map_err(|e| Error::Database(Box::new(e)))?;
1836
1837        query(
1838            r#"
1839            UPDATE mint_quote
1840            SET used_by_operation = NULL
1841            WHERE used_by_operation = :operation_id
1842            "#,
1843        )?
1844        .bind("operation_id", operation_id.to_string())
1845        .execute(&*conn)
1846        .await?;
1847
1848        Ok(())
1849    }
1850
1851    async fn kv_read(
1852        &self,
1853        primary_namespace: &str,
1854        secondary_namespace: &str,
1855        key: &str,
1856    ) -> Result<Option<Vec<u8>>, database::Error> {
1857        crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
1858    }
1859
1860    async fn kv_list(
1861        &self,
1862        primary_namespace: &str,
1863        secondary_namespace: &str,
1864    ) -> Result<Vec<String>, database::Error> {
1865        crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
1866    }
1867
1868    async fn kv_write(
1869        &self,
1870        primary_namespace: &str,
1871        secondary_namespace: &str,
1872        key: &str,
1873        value: &[u8],
1874    ) -> Result<(), database::Error> {
1875        let conn = self
1876            .pool
1877            .get()
1878            .await
1879            .map_err(|e| Error::Database(Box::new(e)))?;
1880        crate::keyvalue::kv_write_standalone(
1881            &*conn,
1882            primary_namespace,
1883            secondary_namespace,
1884            key,
1885            value,
1886        )
1887        .await?;
1888        Ok(())
1889    }
1890
1891    async fn kv_remove(
1892        &self,
1893        primary_namespace: &str,
1894        secondary_namespace: &str,
1895        key: &str,
1896    ) -> Result<(), database::Error> {
1897        let conn = self
1898            .pool
1899            .get()
1900            .await
1901            .map_err(|e| Error::Database(Box::new(e)))?;
1902        crate::keyvalue::kv_remove_standalone(&*conn, primary_namespace, secondary_namespace, key)
1903            .await?;
1904        Ok(())
1905    }
1906
1907    // P2PK methods
1908
1909    #[instrument(skip(self))]
1910    async fn add_p2pk_key(
1911        &self,
1912        pubkey: &PublicKey,
1913        derivation_path: DerivationPath,
1914        derivation_index: u32,
1915    ) -> Result<(), Error> {
1916        let conn = self
1917            .pool
1918            .get()
1919            .await
1920            .map_err(|e| Error::Database(Box::new(e)))?;
1921        let query_str = r#"
1922        INSERT INTO p2pk_signing_key (pubkey, derivation_index, derivation_path, created_time)
1923        VALUES (:pubkey, :derivation_index, :derivation_path, :created_time)
1924        "#
1925        .to_string();
1926
1927        query(&query_str)?
1928            .bind("pubkey", pubkey.to_bytes().to_vec())
1929            .bind("derivation_index", derivation_index)
1930            .bind("derivation_path", derivation_path.to_string())
1931            .bind("created_time", unix_time() as i64)
1932            .execute(&*conn)
1933            .await?;
1934
1935        Ok(())
1936    }
1937
1938    #[instrument(skip(self))]
1939    async fn get_p2pk_key(
1940        &self,
1941        pubkey: &PublicKey,
1942    ) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1943        let conn = self
1944            .pool
1945            .get()
1946            .await
1947            .map_err(|e| Error::Database(Box::new(e)))?;
1948        let query_str = r#"SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key WHERE pubkey = :pubkey"#.to_string();
1949
1950        query(&query_str)?
1951            .bind("pubkey", pubkey.to_bytes().to_vec())
1952            .fetch_one(&*conn)
1953            .await?
1954            .map(sql_row_to_p2pk_signing_key)
1955            .transpose()
1956    }
1957
1958    #[instrument(skip(self))]
1959    async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, Error> {
1960        let conn = self
1961            .pool
1962            .get()
1963            .await
1964            .map_err(|e| Error::Database(Box::new(e)))?;
1965        let query_str = r#"
1966        SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC
1967        "#.to_string();
1968
1969        Ok(query(&query_str)?
1970            .fetch_all(&*conn)
1971            .await?
1972            .into_iter()
1973            .filter_map(|row| {
1974                let row = sql_row_to_p2pk_signing_key(row).ok()?;
1975
1976                Some(row)
1977            })
1978            .collect::<Vec<wallet::P2PKSigningKey>>())
1979    }
1980
1981    #[instrument(skip(self))]
1982    async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1983        let conn = self
1984            .pool
1985            .get()
1986            .await
1987            .map_err(|e| Error::Database(Box::new(e)))?;
1988        let query_str = r#"
1989        SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC LIMIT 1
1990        "#.to_string();
1991
1992        query(&query_str)?
1993            .fetch_one(&*conn)
1994            .await?
1995            .map(sql_row_to_p2pk_signing_key)
1996            .transpose()
1997    }
1998}
1999
2000fn sql_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
2001    unpack_into!(
2002        let (
2003            name,
2004            pubkey,
2005            version,
2006            description,
2007            description_long,
2008            contact,
2009            nuts,
2010            icon_url,
2011            motd,
2012            urls,
2013            mint_time,
2014            tos_url
2015        ) = row
2016    );
2017
2018    Ok(MintInfo {
2019        name: column_as_nullable_string!(&name),
2020        pubkey: column_as_nullable_binary!(&pubkey)
2021            .map(|bytes| cdk_common::nuts::PublicKey::from_slice(&bytes))
2022            .transpose()?,
2023        version: column_as_nullable_string!(&version).and_then(|v| serde_json::from_str(&v).ok()),
2024        description: column_as_nullable_string!(description),
2025        description_long: column_as_nullable_string!(description_long),
2026        contact: column_as_nullable_string!(contact, |v| serde_json::from_str(&v).ok()),
2027        nuts: column_as_nullable_string!(nuts, |v| serde_json::from_str(&v).ok())
2028            .unwrap_or_default(),
2029        urls: column_as_nullable_string!(urls, |v| serde_json::from_str(&v).ok()),
2030        icon_url: column_as_nullable_string!(icon_url),
2031        motd: column_as_nullable_string!(motd),
2032        time: column_as_nullable_number!(mint_time).map(|t| t),
2033        tos_url: column_as_nullable_string!(tos_url),
2034        max_array_length: None,
2035    })
2036}
2037
2038#[instrument(skip_all)]
2039fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
2040    unpack_into!(
2041        let (
2042            id,
2043            unit,
2044            active,
2045            input_fee_ppk,
2046            final_expiry
2047        ) = row
2048    );
2049
2050    Ok(KeySetInfo {
2051        id: column_as_string!(id, Id::from_str, Id::from_bytes),
2052        unit: column_as_string!(unit, CurrencyUnit::from_str),
2053        active: matches!(active, Column::Integer(1)),
2054        input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or(0),
2055        final_expiry: column_as_nullable_number!(final_expiry),
2056    })
2057}
2058
2059fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
2060    unpack_into!(
2061        let (
2062            id,
2063            mint_url,
2064            amount,
2065            unit,
2066            request,
2067            state,
2068            expiry,
2069            secret_key,
2070            row_method,
2071            row_amount_minted,
2072            row_amount_paid,
2073            updated_at,
2074            estimated_blocks,
2075            used_by_operation,
2076            version
2077        ) = row
2078    );
2079
2080    let amount: Option<i64> = column_as_nullable_number!(amount);
2081
2082    let amount_paid: u64 = column_as_number!(row_amount_paid);
2083    let amount_minted: u64 = column_as_number!(row_amount_minted);
2084    let expiry_val: u64 = column_as_number!(expiry);
2085    let updated_at: u64 = column_as_number!(updated_at);
2086    let version_val: u32 = column_as_number!(version);
2087    let payment_method =
2088        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2089
2090    Ok(MintQuote {
2091        id: column_as_string!(id),
2092        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2093        amount: amount.and_then(Amount::from_i64),
2094        unit: column_as_string!(unit, CurrencyUnit::from_str),
2095        request: column_as_string!(request),
2096        state: column_as_string!(state, MintQuoteState::from_str),
2097        expiry: expiry_val,
2098        secret_key: column_as_nullable_string!(secret_key, |s| SecretKey::from_str(&s).ok()),
2099        payment_method,
2100        amount_issued: Amount::from(amount_minted),
2101        amount_paid: Amount::from(amount_paid),
2102        updated_at,
2103        estimated_blocks: column_as_nullable_number!(estimated_blocks),
2104        used_by_operation: column_as_nullable_string!(used_by_operation),
2105        version: version_val,
2106    })
2107}
2108
2109fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
2110    unpack_into!(
2111        let (
2112            id,
2113            unit,
2114            amount,
2115            request,
2116            fee_reserve,
2117            state,
2118            expiry,
2119            payment_proof,
2120            row_method,
2121            estimated_blocks,
2122            fee_index,
2123            used_by_operation,
2124            version,
2125            mint_url
2126        ) = row
2127    );
2128
2129    let payment_method =
2130        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2131
2132    let amount_val: u64 = column_as_number!(amount);
2133    let fee_reserve_val: u64 = column_as_number!(fee_reserve);
2134    let expiry_val: u64 = column_as_number!(expiry);
2135    let version_val: u32 = column_as_number!(version);
2136
2137    Ok(wallet::MeltQuote {
2138        id: column_as_string!(id),
2139        mint_url: column_as_nullable_string!(mint_url, |s| MintUrl::from_str(&s).ok()),
2140        unit: column_as_string!(unit, CurrencyUnit::from_str),
2141        amount: Amount::from(amount_val),
2142        request: column_as_string!(request),
2143        fee_reserve: Amount::from(fee_reserve_val),
2144        state: column_as_string!(state, MeltQuoteState::from_str),
2145        expiry: expiry_val,
2146        payment_proof: column_as_nullable_string!(payment_proof),
2147        estimated_blocks: column_as_nullable_number!(estimated_blocks),
2148        fee_index: column_as_nullable_number!(fee_index),
2149        payment_method,
2150        used_by_operation: column_as_nullable_string!(used_by_operation),
2151        version: version_val,
2152    })
2153}
2154
2155fn sql_row_to_proof_info(row: Vec<Column>) -> Result<ProofInfo, Error> {
2156    unpack_into!(
2157        let (
2158            amount,
2159            unit,
2160            keyset_id,
2161            secret,
2162            c,
2163            witness,
2164            dleq_e,
2165            dleq_s,
2166            dleq_r,
2167            y,
2168            mint_url,
2169            state,
2170            spending_condition,
2171            used_by_operation,
2172            created_by_operation,
2173            p2pk_e
2174        ) = row
2175    );
2176
2177    let dleq = match (
2178        column_as_nullable_binary!(dleq_e),
2179        column_as_nullable_binary!(dleq_s),
2180        column_as_nullable_binary!(dleq_r),
2181    ) {
2182        (Some(e), Some(s), Some(r)) => {
2183            let e_key = SecretKey::from_slice(&e)?;
2184            let s_key = SecretKey::from_slice(&s)?;
2185            let r_key = SecretKey::from_slice(&r)?;
2186
2187            Some(ProofDleq::new(e_key, s_key, r_key))
2188        }
2189        _ => None,
2190    };
2191
2192    let amount: u64 = column_as_number!(amount);
2193    let proof = Proof {
2194        amount: Amount::from(amount),
2195        keyset_id: column_as_string!(keyset_id, Id::from_str),
2196        secret: column_as_string!(secret, Secret::from_str),
2197        witness: column_as_nullable_string!(witness, |v| { serde_json::from_str(&v).ok() }, |v| {
2198            serde_json::from_slice(&v).ok()
2199        }),
2200        c: column_as_string!(c, PublicKey::from_str, PublicKey::from_slice),
2201        dleq,
2202        p2pk_e: column_as_nullable_binary!(p2pk_e)
2203            .map(|bytes| PublicKey::from_slice(&bytes))
2204            .transpose()?,
2205    };
2206
2207    let used_by_operation =
2208        column_as_nullable_string!(used_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2209    let created_by_operation =
2210        column_as_nullable_string!(created_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2211
2212    Ok(ProofInfo {
2213        proof,
2214        y: column_as_string!(y, PublicKey::from_str, PublicKey::from_slice),
2215        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2216        state: column_as_string!(state, State::from_str),
2217        spending_condition: column_as_nullable_string!(
2218            spending_condition,
2219            |r| { serde_json::from_str(&r).ok() },
2220            |r| { serde_json::from_slice(&r).ok() }
2221        ),
2222        unit: column_as_string!(unit, CurrencyUnit::from_str),
2223        used_by_operation,
2224        created_by_operation,
2225    })
2226}
2227
2228fn sql_row_to_wallet_saga(row: Vec<Column>) -> Result<wallet::WalletSaga, Error> {
2229    unpack_into!(
2230        let (
2231            id,
2232            kind,
2233            state,
2234            amount,
2235            mint_url,
2236            unit,
2237            quote_id,
2238            created_at,
2239            updated_at,
2240            data,
2241            version
2242        ) = row
2243    );
2244
2245    let id_str: String = column_as_string!(id);
2246    let id = uuid::Uuid::parse_str(&id_str).map_err(|e| {
2247        Error::Database(Box::new(std::io::Error::new(
2248            std::io::ErrorKind::InvalidData,
2249            format!("Invalid UUID: {}", e),
2250        )))
2251    })?;
2252    let kind_str: String = column_as_string!(kind);
2253    let state_json: String = column_as_string!(state);
2254    let amount: u64 = column_as_number!(amount);
2255    let mint_url: MintUrl = column_as_string!(mint_url, MintUrl::from_str);
2256    let unit: CurrencyUnit = column_as_string!(unit, CurrencyUnit::from_str);
2257    let quote_id: Option<String> = column_as_nullable_string!(quote_id);
2258    let created_at: u64 = column_as_number!(created_at);
2259    let updated_at: u64 = column_as_number!(updated_at);
2260    let data_json: String = column_as_string!(data);
2261    let version: u32 = column_as_number!(version);
2262
2263    let kind = wallet::OperationKind::from_str(&kind_str).map_err(|_| {
2264        Error::Database(Box::new(std::io::Error::new(
2265            std::io::ErrorKind::InvalidData,
2266            format!("Invalid operation kind: {}", kind_str),
2267        )))
2268    })?;
2269    let state: wallet::WalletSagaState = serde_json::from_str(&state_json).map_err(|e| {
2270        Error::Database(Box::new(std::io::Error::new(
2271            std::io::ErrorKind::InvalidData,
2272            format!("Failed to deserialize saga state: {}", e),
2273        )))
2274    })?;
2275    let data: wallet::OperationData = serde_json::from_str(&data_json).map_err(|e| {
2276        Error::Database(Box::new(std::io::Error::new(
2277            std::io::ErrorKind::InvalidData,
2278            format!("Failed to deserialize saga data: {}", e),
2279        )))
2280    })?;
2281
2282    Ok(wallet::WalletSaga {
2283        id,
2284        kind,
2285        state,
2286        amount: Amount::from(amount),
2287        mint_url,
2288        unit,
2289        quote_id,
2290        created_at,
2291        updated_at,
2292        data,
2293        version,
2294    })
2295}
2296
2297fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
2298    unpack_into!(
2299        let (
2300            mint_url,
2301            direction,
2302            unit,
2303            amount,
2304            fee,
2305            ys,
2306            timestamp,
2307            memo,
2308            metadata,
2309            quote_id,
2310            payment_request,
2311            payment_proof,
2312            payment_method,
2313            saga_id,
2314            status
2315        ) = row
2316    );
2317
2318    let amount: u64 = column_as_number!(amount);
2319    let fee: u64 = column_as_number!(fee);
2320
2321    let saga_id: Option<Uuid> = column_as_nullable_string!(saga_id)
2322        .map(|id| Uuid::from_str(&id).ok())
2323        .flatten();
2324
2325    Ok(Transaction {
2326        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2327        direction: column_as_string!(direction, TransactionDirection::from_str),
2328        unit: column_as_string!(unit, CurrencyUnit::from_str),
2329        amount: Amount::from(amount),
2330        fee: Amount::from(fee),
2331        ys: column_as_binary!(ys)
2332            .chunks(33)
2333            .map(PublicKey::from_slice)
2334            .collect::<Result<Vec<_>, _>>()?,
2335        timestamp: column_as_number!(timestamp),
2336        memo: column_as_nullable_string!(memo),
2337        metadata: column_as_nullable_string!(metadata, |v| serde_json::from_str(&v).ok(), |v| {
2338            serde_json::from_slice(&v).ok()
2339        })
2340        .unwrap_or_default(),
2341        quote_id: column_as_nullable_string!(quote_id),
2342        payment_request: column_as_nullable_string!(payment_request),
2343        payment_proof: column_as_nullable_string!(payment_proof),
2344        payment_method: column_as_nullable_string!(payment_method)
2345            .map(|v| PaymentMethod::from_str(&v))
2346            .transpose()
2347            .map_err(Error::from)?,
2348        saga_id,
2349        status: column_as_string!(status, TransactionStatus::from_str),
2350    })
2351}
2352
2353fn sql_row_to_p2pk_signing_key(row: Vec<Column>) -> Result<wallet::P2PKSigningKey, Error> {
2354    unpack_into!(
2355        let (
2356            pubkey,
2357            derivation_index,
2358            derivation_path,
2359            created_time
2360        ) = row
2361    );
2362
2363    Ok(wallet::P2PKSigningKey {
2364        pubkey: column_as_string!(pubkey, PublicKey::from_str, PublicKey::from_slice),
2365        derivation_index: column_as_number!(derivation_index),
2366        derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
2367        created_time: column_as_number!(created_time),
2368    })
2369}