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                derivation_index,
540                p2pk_e
541            FROM proof
542            "#,
543        )?
544        .fetch_all(&*conn)
545        .await?
546        .into_iter()
547        .filter_map(|row| {
548            let row = sql_row_to_proof_info(row).ok()?;
549
550            if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) {
551                Some(row)
552            } else {
553                None
554            }
555        })
556        .collect::<Vec<_>>())
557    }
558
559    #[instrument(skip(self, ys))]
560    async fn get_proofs_by_ys(
561        &self,
562        ys: Vec<PublicKey>,
563    ) -> Result<Vec<ProofInfo>, database::Error> {
564        let conn = self
565            .pool
566            .get()
567            .await
568            .map_err(|e| Error::Database(Box::new(e)))?;
569        Ok(query(
570            r#"
571            SELECT
572                amount,
573                unit,
574                keyset_id,
575                secret,
576                c,
577                witness,
578                dleq_e,
579                dleq_s,
580                dleq_r,
581                y,
582                mint_url,
583                state,
584                spending_condition,
585                used_by_operation,
586                created_by_operation,
587                derivation_index,
588                p2pk_e
589            FROM proof
590            WHERE y IN (:ys)
591        "#,
592        )?
593        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
594        .fetch_all(&*conn)
595        .await?
596        .into_iter()
597        .filter_map(|row| sql_row_to_proof_info(row).ok())
598        .collect::<Vec<_>>())
599    }
600
601    async fn get_balance(
602        &self,
603        mint_url: Option<MintUrl>,
604        unit: Option<CurrencyUnit>,
605        states: Option<Vec<State>>,
606    ) -> Result<u64, database::Error> {
607        let conn = self
608            .pool
609            .get()
610            .await
611            .map_err(|e| Error::Database(Box::new(e)))?;
612
613        let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string();
614        let mut where_clauses = Vec::new();
615        let states = states
616            .unwrap_or_default()
617            .into_iter()
618            .map(|x| x.to_string())
619            .collect::<Vec<_>>();
620
621        if mint_url.is_some() {
622            where_clauses.push("mint_url = :mint_url");
623        }
624        if unit.is_some() {
625            where_clauses.push("unit = :unit");
626        }
627        if !states.is_empty() {
628            where_clauses.push("state IN (:states)");
629        }
630
631        if !where_clauses.is_empty() {
632            query_str.push_str(" WHERE ");
633            query_str.push_str(&where_clauses.join(" AND "));
634        }
635
636        let mut q = query(&query_str)?;
637
638        if let Some(ref mint_url) = mint_url {
639            q = q.bind("mint_url", mint_url.to_string());
640        }
641        if let Some(ref unit) = unit {
642            q = q.bind("unit", unit.to_string());
643        }
644
645        if !states.is_empty() {
646            q = q.bind_vec("states", states)?;
647        }
648
649        let balance = q
650            .pluck(&*conn)
651            .await?
652            .map(|n| {
653                // SQLite SUM returns INTEGER which we need to convert to u64
654                match n {
655                    crate::stmt::Column::Integer(i) => Ok(i as u64),
656                    crate::stmt::Column::Real(f) => Ok(f as u64),
657                    _ => Err(Error::Database(Box::new(std::io::Error::new(
658                        std::io::ErrorKind::InvalidData,
659                        "Invalid balance type",
660                    )))),
661                }
662            })
663            .transpose()?
664            .unwrap_or(0);
665
666        Ok(balance)
667    }
668
669    #[instrument(skip(self))]
670    async fn get_transaction(
671        &self,
672        transaction_id: TransactionId,
673    ) -> Result<Option<Transaction>, database::Error> {
674        let conn = self
675            .pool
676            .get()
677            .await
678            .map_err(|e| Error::Database(Box::new(e)))?;
679        Ok(query(
680            r#"
681            SELECT
682                mint_url,
683                direction,
684                unit,
685                amount,
686                fee,
687                ys,
688                timestamp,
689                memo,
690                metadata,
691                quote_id,
692                payment_request,
693                payment_proof,
694                payment_method,
695                saga_id,
696                status
697            FROM
698                transactions
699            WHERE
700                id = :id
701            "#,
702        )?
703        .bind("id", transaction_id.as_slice().to_vec())
704        .fetch_one(&*conn)
705        .await?
706        .map(sql_row_to_transaction)
707        .transpose()?)
708    }
709
710    #[instrument(skip(self))]
711    async fn list_transactions(
712        &self,
713        mint_url: Option<MintUrl>,
714        direction: Option<TransactionDirection>,
715        unit: Option<CurrencyUnit>,
716    ) -> Result<Vec<Transaction>, database::Error> {
717        let conn = self
718            .pool
719            .get()
720            .await
721            .map_err(|e| Error::Database(Box::new(e)))?;
722
723        Ok(query(
724            r#"
725            SELECT
726                mint_url,
727                direction,
728                unit,
729                amount,
730                fee,
731                ys,
732                timestamp,
733                memo,
734                metadata,
735                quote_id,
736                payment_request,
737                payment_proof,
738                payment_method,
739                saga_id,
740                status
741            FROM
742                transactions
743            "#,
744        )?
745        .fetch_all(&*conn)
746        .await?
747        .into_iter()
748        .filter_map(|row| {
749            // TODO: Avoid a table scan by passing the heavy lifting of checking to the DB engine
750            let transaction = sql_row_to_transaction(row).ok()?;
751            if transaction.matches_conditions(&mint_url, &direction, &unit) {
752                Some(transaction)
753            } else {
754                None
755            }
756        })
757        .collect::<Vec<_>>())
758    }
759
760    async fn update_proofs(
761        &self,
762        added: Vec<ProofInfo>,
763        removed_ys: Vec<PublicKey>,
764    ) -> Result<(), database::Error> {
765        let conn = self
766            .pool
767            .get()
768            .await
769            .map_err(|e| Error::Database(Box::new(e)))?;
770        let tx = ConnectionWithTransaction::new(conn).await?;
771
772        for proof in added {
773            query(
774                r#"
775    INSERT INTO proof
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, derivation_index, p2pk_e)
777    VALUES
778    (: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, :derivation_index, :p2pk_e)
779    ON CONFLICT(y) DO UPDATE SET
780        mint_url = excluded.mint_url,
781        state = excluded.state,
782        spending_condition = excluded.spending_condition,
783        unit = excluded.unit,
784        amount = excluded.amount,
785        keyset_id = excluded.keyset_id,
786        secret = excluded.secret,
787        c = excluded.c,
788        witness = excluded.witness,
789        dleq_e = excluded.dleq_e,
790        dleq_s = excluded.dleq_s,
791        dleq_r = excluded.dleq_r,
792        used_by_operation = excluded.used_by_operation,
793        created_by_operation = excluded.created_by_operation,
794        derivation_index = COALESCE(excluded.derivation_index, proof.derivation_index),
795        p2pk_e = excluded.p2pk_e
796    ;
797            "#,
798            )?
799            .bind("y", proof.y.to_bytes().to_vec())
800            .bind("mint_url", proof.mint_url.to_string())
801            .bind("state", proof.state.to_string())
802            .bind(
803                "spending_condition",
804                proof
805                    .spending_condition
806                    .map(|s| serde_json::to_string(&s).ok()),
807            )
808            .bind("unit", proof.unit.to_string())
809            .bind("amount", u64::from(proof.proof.amount) as i64)
810            .bind("keyset_id", proof.proof.keyset_id.to_string())
811            .bind("secret", proof.proof.secret.to_string())
812            .bind("c", proof.proof.c.to_bytes().to_vec())
813            .bind(
814                "witness",
815                proof
816                    .proof
817                    .witness
818                    .and_then(|w| serde_json::to_string(&w).ok()),
819            )
820            .bind(
821                "dleq_e",
822                proof.proof.dleq.as_ref().map(|dleq| dleq.e.to_secret_bytes().to_vec()),
823            )
824            .bind(
825                "dleq_s",
826                proof.proof.dleq.as_ref().map(|dleq| dleq.s.to_secret_bytes().to_vec()),
827            )
828            .bind(
829                "dleq_r",
830                proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()),
831            )
832            .bind("used_by_operation", proof.used_by_operation.map(|id| id.to_string()))
833            .bind("created_by_operation", proof.created_by_operation.map(|id| id.to_string()))
834            .bind("derivation_index", proof.derivation_index.map(i64::from))
835            .bind(
836                "p2pk_e",
837                proof
838                    .proof
839                    .p2pk_e
840                    .as_ref()
841                    .map(|pk| pk.to_bytes().to_vec()),
842            )
843            .execute(&tx)
844            .await?;
845        }
846
847        if !removed_ys.is_empty() {
848            query(r#"DELETE FROM proof WHERE y IN (:ys)"#)?
849                .bind_vec(
850                    "ys",
851                    removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(),
852                )?
853                .execute(&tx)
854                .await?;
855        }
856
857        tx.commit().await?;
858
859        Ok(())
860    }
861
862    #[instrument(skip(self))]
863    async fn update_proofs_state(
864        &self,
865        ys: Vec<PublicKey>,
866        state: State,
867    ) -> Result<(), database::Error> {
868        let conn = self
869            .pool
870            .get()
871            .await
872            .map_err(|e| Error::Database(Box::new(e)))?;
873
874        query("UPDATE proof SET state = :state WHERE y IN (:ys)")?
875            .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
876            .bind("state", state.to_string())
877            .execute(&*conn)
878            .await?;
879
880        Ok(())
881    }
882
883    #[instrument(
884        skip(self, transaction),
885        fields(
886            direction = %transaction.direction,
887            amount = %transaction.amount,
888            unit = %transaction.unit,
889            quote_id = ?transaction.quote_id,
890            saga_id = ?transaction.saga_id,
891        )
892    )]
893    async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> {
894        let conn = self
895            .pool
896            .get()
897            .await
898            .map_err(|e| Error::Database(Box::new(e)))?;
899
900        let mint_url = transaction.mint_url.to_string();
901        let direction = transaction.direction.to_string();
902        let unit = transaction.unit.to_string();
903        let amount = u64::from(transaction.amount) as i64;
904        let fee = u64::from(transaction.fee) as i64;
905        let ys = transaction
906            .ys
907            .iter()
908            .flat_map(|y| y.to_bytes().to_vec())
909            .collect::<Vec<_>>();
910
911        let id = transaction.id();
912
913        query(
914               r#"
915   INSERT INTO transactions
916   (id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata, quote_id, payment_request, payment_proof, payment_method, saga_id, status)
917   VALUES
918   (:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata, :quote_id, :payment_request, :payment_proof, :payment_method, :saga_id, :status)
919   ON CONFLICT(id) DO UPDATE SET
920       mint_url = excluded.mint_url,
921       direction = excluded.direction,
922       unit = excluded.unit,
923       amount = excluded.amount,
924       fee = excluded.fee,
925       timestamp = excluded.timestamp,
926       memo = excluded.memo,
927       metadata = excluded.metadata,
928       quote_id = excluded.quote_id,
929       payment_request = excluded.payment_request,
930       payment_proof = excluded.payment_proof,
931       payment_method = excluded.payment_method,
932       saga_id = excluded.saga_id,
933       status = excluded.status
934   ;
935           "#,
936           )?
937           .bind("id", id.as_slice().to_vec())
938           .bind("mint_url", mint_url)
939           .bind("direction", direction)
940           .bind("unit", unit)
941           .bind("amount", amount)
942           .bind("fee", fee)
943           .bind("ys", ys)
944           .bind("timestamp", transaction.timestamp as i64)
945           .bind("memo", transaction.memo)
946           .bind(
947               "metadata",
948               serde_json::to_string(&transaction.metadata).map_err(Error::from)?,
949           )
950           .bind("quote_id", transaction.quote_id)
951           .bind("payment_request", transaction.payment_request)
952           .bind("payment_proof", transaction.payment_proof)
953           .bind("payment_method", transaction.payment_method.map(|pm| pm.to_string()))
954           .bind("saga_id", transaction.saga_id.map(|id| id.to_string()))
955           .bind("status", transaction.status.to_string())
956           .execute(&*conn)
957           .await?;
958
959        Ok(())
960    }
961
962    #[instrument(skip(self))]
963    async fn update_mint_url(
964        &self,
965        old_mint_url: MintUrl,
966        new_mint_url: MintUrl,
967    ) -> Result<(), database::Error> {
968        let conn = self
969            .pool
970            .get()
971            .await
972            .map_err(|e| Error::Database(Box::new(e)))?;
973        let tx = ConnectionWithTransaction::new(conn).await?;
974        let tables = ["mint_quote", "proof"];
975
976        for table in &tables {
977            query(&format!(
978                r#"
979                UPDATE {table}
980                SET mint_url = :new_mint_url
981                WHERE mint_url = :old_mint_url
982            "#
983            ))?
984            .bind("new_mint_url", new_mint_url.to_string())
985            .bind("old_mint_url", old_mint_url.to_string())
986            .execute(&tx)
987            .await?;
988        }
989
990        tx.commit().await?;
991
992        Ok(())
993    }
994
995    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
996    async fn increment_keyset_counter(
997        &self,
998        keyset_id: &Id,
999        count: u32,
1000    ) -> Result<u32, database::Error> {
1001        let conn = self
1002            .pool
1003            .get()
1004            .await
1005            .map_err(|e| Error::Database(Box::new(e)))?;
1006
1007        let new_counter = query(
1008            r#"
1009            INSERT INTO keyset_counter (keyset_id, counter)
1010            VALUES (:keyset_id, :count)
1011            ON CONFLICT(keyset_id) DO UPDATE SET
1012                counter = keyset_counter.counter + :count
1013            RETURNING counter
1014            "#,
1015        )?
1016        .bind("keyset_id", keyset_id.to_string())
1017        .bind("count", count)
1018        .pluck(&*conn)
1019        .await?
1020        .map(|n| Ok::<_, Error>(column_as_number!(n)))
1021        .transpose()?
1022        .ok_or_else(|| Error::Internal("Counter update returned no value".to_owned()))?;
1023
1024        Ok(new_counter)
1025    }
1026
1027    #[instrument(skip(self))]
1028    async fn increment_derivation_counter(
1029        &self,
1030        namespace: &str,
1031        count: u32,
1032    ) -> Result<u32, database::Error> {
1033        let conn = self
1034            .pool
1035            .get()
1036            .await
1037            .map_err(|e| Error::Database(Box::new(e)))?;
1038
1039        let new_counter = query(
1040            r#"
1041            INSERT INTO derivation_counter (namespace, counter)
1042            VALUES (:namespace, :count)
1043            ON CONFLICT(namespace) DO UPDATE SET
1044                counter = derivation_counter.counter + :count
1045            RETURNING counter
1046            "#,
1047        )?
1048        .bind("namespace", namespace.to_owned())
1049        .bind("count", count)
1050        .pluck(&*conn)
1051        .await?
1052        .map(|n| Ok::<_, Error>(column_as_number!(n)))
1053        .transpose()?
1054        .ok_or_else(|| Error::Internal("Derivation counter update returned no value".to_owned()))?;
1055
1056        Ok(new_counter)
1057    }
1058
1059    #[instrument(skip(self, mint_info))]
1060    async fn add_mint(
1061        &self,
1062        mint_url: MintUrl,
1063        mint_info: Option<MintInfo>,
1064    ) -> Result<(), database::Error> {
1065        let conn = self
1066            .pool
1067            .get()
1068            .await
1069            .map_err(|e| Error::Database(Box::new(e)))?;
1070
1071        let (
1072            name,
1073            pubkey,
1074            version,
1075            description,
1076            description_long,
1077            contact,
1078            nuts,
1079            icon_url,
1080            urls,
1081            motd,
1082            time,
1083            tos_url,
1084        ) = match mint_info {
1085            Some(mint_info) => {
1086                let MintInfo {
1087                    name,
1088                    pubkey,
1089                    version,
1090                    description,
1091                    description_long,
1092                    contact,
1093                    nuts,
1094                    icon_url,
1095                    urls,
1096                    motd,
1097                    time,
1098                    tos_url,
1099                    // Not persisted: a runtime hint the mint recomputes, refreshed with /v1/info.
1100                    max_array_length: _,
1101                } = mint_info;
1102
1103                (
1104                    name,
1105                    pubkey.map(|p| p.to_bytes().to_vec()),
1106                    version.map(|v| serde_json::to_string(&v).ok()),
1107                    description,
1108                    description_long,
1109                    contact.map(|c| serde_json::to_string(&c).ok()),
1110                    serde_json::to_string(&nuts).ok(),
1111                    icon_url,
1112                    urls.map(|c| serde_json::to_string(&c).ok()),
1113                    motd,
1114                    time,
1115                    tos_url,
1116                )
1117            }
1118            None => (
1119                None, None, None, None, None, None, None, None, None, None, None, None,
1120            ),
1121        };
1122
1123        query(
1124            r#"
1125   INSERT INTO mint
1126   (
1127       mint_url, name, pubkey, version, description, description_long,
1128       contact, nuts, icon_url, urls, motd, mint_time, tos_url
1129   )
1130   VALUES
1131   (
1132       :mint_url, :name, :pubkey, :version, :description, :description_long,
1133       :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url
1134   )
1135   ON CONFLICT(mint_url) DO UPDATE SET
1136       name = excluded.name,
1137       pubkey = excluded.pubkey,
1138       version = excluded.version,
1139       description = excluded.description,
1140       description_long = excluded.description_long,
1141       contact = excluded.contact,
1142       nuts = excluded.nuts,
1143       icon_url = excluded.icon_url,
1144       urls = excluded.urls,
1145       motd = excluded.motd,
1146       mint_time = excluded.mint_time,
1147       tos_url = excluded.tos_url
1148   ;
1149           "#,
1150        )?
1151        .bind("mint_url", mint_url.to_string())
1152        .bind("name", name)
1153        .bind("pubkey", pubkey)
1154        .bind("version", version)
1155        .bind("description", description)
1156        .bind("description_long", description_long)
1157        .bind("contact", contact)
1158        .bind("nuts", nuts)
1159        .bind("icon_url", icon_url)
1160        .bind("urls", urls)
1161        .bind("motd", motd)
1162        .bind("mint_time", time.map(|v| v as i64))
1163        .bind("tos_url", tos_url)
1164        .execute(&*conn)
1165        .await?;
1166
1167        Ok(())
1168    }
1169
1170    #[instrument(skip(self))]
1171    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
1172        let conn = self
1173            .pool
1174            .get()
1175            .await
1176            .map_err(|e| Error::Database(Box::new(e)))?;
1177
1178        query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)?
1179            .bind("mint_url", mint_url.to_string())
1180            .execute(&*conn)
1181            .await?;
1182
1183        Ok(())
1184    }
1185
1186    #[instrument(skip(self, keysets))]
1187    async fn add_mint_keysets(
1188        &self,
1189        mint_url: MintUrl,
1190        keysets: Vec<KeySetInfo>,
1191    ) -> Result<(), database::Error> {
1192        let conn = self
1193            .pool
1194            .get()
1195            .await
1196            .map_err(|e| Error::Database(Box::new(e)))?;
1197        let tx = ConnectionWithTransaction::new(conn).await?;
1198
1199        for keyset in keysets {
1200            query(
1201                r#"
1202        INSERT INTO keyset
1203        (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32)
1204        VALUES
1205        (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32)
1206        ON CONFLICT(id) DO UPDATE SET
1207            active = excluded.active,
1208            input_fee_ppk = excluded.input_fee_ppk
1209        "#,
1210            )?
1211            .bind("mint_url", mint_url.to_string())
1212            .bind("id", keyset.id.to_string())
1213            .bind("unit", keyset.unit.to_string())
1214            .bind("active", keyset.active)
1215            .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
1216            .bind("final_expiry", keyset.final_expiry.map(|v| v as i64))
1217            .bind("keyset_u32", u32::from(keyset.id))
1218            .execute(&tx)
1219            .await?;
1220        }
1221
1222        tx.commit().await?;
1223
1224        Ok(())
1225    }
1226
1227    #[instrument(skip_all)]
1228    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
1229        let conn = self
1230            .pool
1231            .get()
1232            .await
1233            .map_err(|e| Error::Database(Box::new(e)))?;
1234
1235        let expected_version = quote.version;
1236        let new_version = expected_version.wrapping_add(1);
1237
1238        let rows_affected = query(
1239                r#"
1240    INSERT INTO mint_quote
1241    (id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid, updated_at, estimated_blocks, version, used_by_operation)
1242    VALUES
1243    (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid, :updated_at, :estimated_blocks, :version, :used_by_operation)
1244    ON CONFLICT(id) DO UPDATE SET
1245        mint_url = excluded.mint_url,
1246        amount = excluded.amount,
1247        unit = excluded.unit,
1248        request = excluded.request,
1249        state = excluded.state,
1250        expiry = excluded.expiry,
1251        secret_key = excluded.secret_key,
1252        payment_method = excluded.payment_method,
1253        amount_issued = excluded.amount_issued,
1254        amount_paid = excluded.amount_paid,
1255        updated_at = excluded.updated_at,
1256        estimated_blocks = excluded.estimated_blocks,
1257        version = :new_version,
1258        used_by_operation = excluded.used_by_operation
1259    WHERE mint_quote.version = :expected_version
1260    ;
1261            "#,
1262            )?
1263            .bind("id", quote.id.to_string())
1264            .bind("mint_url", quote.mint_url.to_string())
1265            .bind("amount", quote.amount.map(|a| a.to_i64()))
1266            .bind("unit", quote.unit.to_string())
1267            .bind("request", quote.request)
1268            .bind("state", quote.state.to_string())
1269            .bind("expiry", quote.expiry as i64)
1270            .bind(
1271                "secret_key",
1272                quote.secret_key.map(|key| key.to_secret_hex()),
1273            )
1274            .bind("payment_method", quote.payment_method.to_string())
1275            .bind("amount_issued", quote.amount_issued.to_i64())
1276            .bind("amount_paid", quote.amount_paid.to_i64())
1277            .bind("updated_at", quote.updated_at as i64)
1278            .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1279            .bind("version", quote.version as i64)
1280            .bind("new_version", new_version as i64)
1281            .bind("expected_version", expected_version as i64)
1282            .bind("used_by_operation", quote.used_by_operation)
1283            .execute(&*conn).await?;
1284
1285        if rows_affected == 0 {
1286            return Err(database::Error::ConcurrentUpdate);
1287        }
1288
1289        Ok(())
1290    }
1291
1292    #[instrument(skip(self))]
1293    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1294        let conn = self
1295            .pool
1296            .get()
1297            .await
1298            .map_err(|e| Error::Database(Box::new(e)))?;
1299
1300        query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
1301            .bind("id", quote_id.to_string())
1302            .execute(&*conn)
1303            .await?;
1304
1305        Ok(())
1306    }
1307
1308    #[instrument(skip_all)]
1309    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
1310        let conn = self
1311            .pool
1312            .get()
1313            .await
1314            .map_err(|e| Error::Database(Box::new(e)))?;
1315
1316        let expected_version = quote.version;
1317        let new_version = expected_version.wrapping_add(1);
1318
1319        let rows_affected = query(
1320            r#"
1321 INSERT INTO melt_quote
1322 (id, unit, amount, request, fee_reserve, state, expiry, payment_proof, payment_method, estimated_blocks, fee_index, version, mint_url, used_by_operation)
1323 VALUES
1324 (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_proof, :payment_method, :estimated_blocks, :fee_index, :version, :mint_url, :used_by_operation)
1325 ON CONFLICT(id) DO UPDATE SET
1326     unit = excluded.unit,
1327     amount = excluded.amount,
1328     request = excluded.request,
1329     fee_reserve = excluded.fee_reserve,
1330     state = excluded.state,
1331     expiry = excluded.expiry,
1332     payment_proof = COALESCE(excluded.payment_proof, melt_quote.payment_proof),
1333     payment_method = excluded.payment_method,
1334     estimated_blocks = excluded.estimated_blocks,
1335     fee_index = excluded.fee_index,
1336     version = :new_version,
1337     mint_url = excluded.mint_url,
1338     used_by_operation = excluded.used_by_operation
1339 WHERE melt_quote.version = :expected_version
1340 ;
1341         "#,
1342        )?
1343        .bind("id", quote.id.to_string())
1344        .bind("unit", quote.unit.to_string())
1345        .bind("amount", u64::from(quote.amount) as i64)
1346        .bind("request", quote.request)
1347        .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
1348        .bind("state", quote.state.to_string())
1349        .bind("expiry", quote.expiry as i64)
1350        .bind("payment_proof", quote.payment_proof)
1351        .bind("payment_method", quote.payment_method.to_string())
1352        .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1353        .bind("fee_index", quote.fee_index.map(i64::from))
1354        .bind("version", quote.version as i64)
1355        .bind("new_version", new_version as i64)
1356        .bind("expected_version", expected_version as i64)
1357        .bind("mint_url", quote.mint_url.map(|m| m.to_string()))
1358        .bind("used_by_operation", quote.used_by_operation)
1359        .execute(&*conn)
1360        .await?;
1361
1362        if rows_affected == 0 {
1363            return Err(database::Error::ConcurrentUpdate);
1364        }
1365
1366        Ok(())
1367    }
1368
1369    #[instrument(skip(self))]
1370    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1371        let conn = self
1372            .pool
1373            .get()
1374            .await
1375            .map_err(|e| Error::Database(Box::new(e)))?;
1376
1377        query(r#"DELETE FROM melt_quote WHERE id=:id"#)?
1378            .bind("id", quote_id.to_owned())
1379            .execute(&*conn)
1380            .await?;
1381
1382        Ok(())
1383    }
1384
1385    #[instrument(skip_all)]
1386    async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
1387        let conn = self
1388            .pool
1389            .get()
1390            .await
1391            .map_err(|e| Error::Database(Box::new(e)))?;
1392
1393        keyset.verify_id()?;
1394
1395        query(
1396            r#"
1397                INSERT INTO key
1398                (id, keys, keyset_u32)
1399                VALUES
1400                (:id, :keys, :keyset_u32)
1401            "#,
1402        )?
1403        .bind("id", keyset.id.to_string())
1404        .bind(
1405            "keys",
1406            serde_json::to_string(&keyset.keys).map_err(Error::from)?,
1407        )
1408        .bind("keyset_u32", u32::from(keyset.id))
1409        .execute(&*conn)
1410        .await?;
1411
1412        Ok(())
1413    }
1414
1415    #[instrument(skip(self))]
1416    async fn remove_keys(&self, id: &Id) -> Result<(), database::Error> {
1417        let conn = self
1418            .pool
1419            .get()
1420            .await
1421            .map_err(|e| Error::Database(Box::new(e)))?;
1422
1423        query(r#"DELETE FROM key WHERE id = :id"#)?
1424            .bind("id", id.to_string())
1425            .execute(&*conn)
1426            .await?;
1427
1428        Ok(())
1429    }
1430
1431    #[instrument(skip(self))]
1432    async fn remove_transaction(
1433        &self,
1434        transaction_id: TransactionId,
1435    ) -> Result<(), database::Error> {
1436        let conn = self
1437            .pool
1438            .get()
1439            .await
1440            .map_err(|e| Error::Database(Box::new(e)))?;
1441
1442        query(r#"DELETE FROM transactions WHERE id=:id"#)?
1443            .bind("id", transaction_id.as_slice().to_vec())
1444            .execute(&*conn)
1445            .await?;
1446
1447        Ok(())
1448    }
1449
1450    #[instrument(skip(self))]
1451    async fn add_saga(&self, saga: wallet::WalletSaga) -> Result<(), database::Error> {
1452        let conn = self
1453            .pool
1454            .get()
1455            .await
1456            .map_err(|e| Error::Database(Box::new(e)))?;
1457
1458        let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1459            Error::Database(Box::new(std::io::Error::new(
1460                std::io::ErrorKind::InvalidData,
1461                format!("Failed to serialize saga state: {}", e),
1462            )))
1463        })?;
1464
1465        let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1466            Error::Database(Box::new(std::io::Error::new(
1467                std::io::ErrorKind::InvalidData,
1468                format!("Failed to serialize saga data: {}", e),
1469            )))
1470        })?;
1471
1472        query(
1473            r#"
1474            INSERT INTO wallet_sagas
1475            (id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version)
1476            VALUES
1477            (:id, :kind, :state, :amount, :mint_url, :unit, :quote_id, :created_at, :updated_at, :data, :version)
1478            "#,
1479        )?
1480        .bind("id", saga.id.to_string())
1481        .bind("kind", saga.kind.to_string())
1482        .bind("state", state_json)
1483        .bind("amount", u64::from(saga.amount) as i64)
1484        .bind("mint_url", saga.mint_url.to_string())
1485        .bind("unit", saga.unit.to_string())
1486        .bind("quote_id", saga.quote_id)
1487        .bind("created_at", saga.created_at as i64)
1488        .bind("updated_at", saga.updated_at as i64)
1489        .bind("data", data_json)
1490        .bind("version", saga.version as i64)
1491        .execute(&*conn)
1492        .await?;
1493
1494        Ok(())
1495    }
1496
1497    #[instrument(skip(self))]
1498    async fn get_saga(
1499        &self,
1500        id: &uuid::Uuid,
1501    ) -> Result<Option<wallet::WalletSaga>, database::Error> {
1502        let conn = self
1503            .pool
1504            .get()
1505            .await
1506            .map_err(|e| Error::Database(Box::new(e)))?;
1507
1508        let rows = query(
1509            r#"
1510            SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1511            FROM wallet_sagas
1512            WHERE id = :id
1513            "#,
1514        )?
1515        .bind("id", id.to_string())
1516        .fetch_all(&*conn)
1517        .await?;
1518
1519        match rows.into_iter().next() {
1520            Some(row) => Ok(Some(sql_row_to_wallet_saga(row)?)),
1521            None => Ok(None),
1522        }
1523    }
1524
1525    #[instrument(skip(self))]
1526    async fn update_saga(&self, saga: wallet::WalletSaga) -> Result<bool, database::Error> {
1527        let conn = self
1528            .pool
1529            .get()
1530            .await
1531            .map_err(|e| Error::Database(Box::new(e)))?;
1532
1533        let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1534            Error::Database(Box::new(std::io::Error::new(
1535                std::io::ErrorKind::InvalidData,
1536                format!("Failed to serialize saga state: {}", e),
1537            )))
1538        })?;
1539
1540        let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1541            Error::Database(Box::new(std::io::Error::new(
1542                std::io::ErrorKind::InvalidData,
1543                format!("Failed to serialize saga data: {}", e),
1544            )))
1545        })?;
1546
1547        // Optimistic locking: only update if the version matches the expected value.
1548        // The saga.version has already been incremented by the caller, so we check
1549        // for (saga.version - 1) in the WHERE clause.
1550        let expected_version = saga.version.saturating_sub(1);
1551
1552        let rows_affected = query(
1553            r#"
1554            UPDATE wallet_sagas
1555            SET kind = :kind, state = :state, amount = :amount, mint_url = :mint_url,
1556                unit = :unit, quote_id = :quote_id, updated_at = :updated_at, data = :data,
1557                version = :new_version
1558            WHERE id = :id AND version = :expected_version
1559            "#,
1560        )?
1561        .bind("id", saga.id.to_string())
1562        .bind("kind", saga.kind.to_string())
1563        .bind("state", state_json)
1564        .bind("amount", u64::from(saga.amount) as i64)
1565        .bind("mint_url", saga.mint_url.to_string())
1566        .bind("unit", saga.unit.to_string())
1567        .bind("quote_id", saga.quote_id)
1568        .bind("updated_at", saga.updated_at as i64)
1569        .bind("data", data_json)
1570        .bind("new_version", saga.version as i64)
1571        .bind("expected_version", expected_version as i64)
1572        .execute(&*conn)
1573        .await?;
1574
1575        // Return true if the update succeeded (version matched), false if version mismatch
1576        Ok(rows_affected > 0)
1577    }
1578
1579    #[instrument(skip(self))]
1580    async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), database::Error> {
1581        let conn = self
1582            .pool
1583            .get()
1584            .await
1585            .map_err(|e| Error::Database(Box::new(e)))?;
1586
1587        query(r#"DELETE FROM wallet_sagas WHERE id = :id"#)?
1588            .bind("id", id.to_string())
1589            .execute(&*conn)
1590            .await?;
1591
1592        Ok(())
1593    }
1594
1595    #[instrument(skip(self))]
1596    async fn get_incomplete_sagas(&self) -> Result<Vec<wallet::WalletSaga>, database::Error> {
1597        let conn = self
1598            .pool
1599            .get()
1600            .await
1601            .map_err(|e| Error::Database(Box::new(e)))?;
1602
1603        let rows = query(
1604            r#"
1605            SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1606            FROM wallet_sagas
1607            ORDER BY created_at ASC
1608            "#,
1609        )?
1610        .fetch_all(&*conn)
1611        .await?;
1612
1613        rows.into_iter().map(sql_row_to_wallet_saga).collect()
1614    }
1615
1616    #[instrument(skip(self))]
1617    async fn reserve_proofs(
1618        &self,
1619        ys: Vec<PublicKey>,
1620        operation_id: &uuid::Uuid,
1621    ) -> Result<(), database::Error> {
1622        let conn = self
1623            .pool
1624            .get()
1625            .await
1626            .map_err(|e| Error::Database(Box::new(e)))?;
1627
1628        if ys.is_empty() {
1629            return Ok(());
1630        }
1631
1632        let expected = ys.len();
1633        let tx = ConnectionWithTransaction::new(conn).await?;
1634
1635        let rows_affected = query(
1636            r#"
1637            UPDATE proof
1638            SET state = 'RESERVED', used_by_operation = :operation_id
1639            WHERE y IN (:ys) AND state = 'UNSPENT'
1640            "#,
1641        )?
1642        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
1643        .bind("operation_id", operation_id.to_string())
1644        .execute(&tx)
1645        .await?;
1646
1647        // Reserving is all-or-nothing: a partial match must not leave some of
1648        // the proofs reserved.
1649        if rows_affected != expected {
1650            tx.rollback().await?;
1651            return Err(database::Error::ProofNotUnspent);
1652        }
1653
1654        tx.commit().await?;
1655
1656        Ok(())
1657    }
1658
1659    #[instrument(skip(self))]
1660    async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1661        let conn = self
1662            .pool
1663            .get()
1664            .await
1665            .map_err(|e| Error::Database(Box::new(e)))?;
1666
1667        query(
1668            r#"
1669            UPDATE proof
1670            SET state = 'UNSPENT', used_by_operation = NULL
1671            WHERE used_by_operation = :operation_id
1672              AND state IN ('RESERVED', 'PENDING')
1673            "#,
1674        )?
1675        .bind("operation_id", operation_id.to_string())
1676        .execute(&*conn)
1677        .await?;
1678
1679        Ok(())
1680    }
1681
1682    #[instrument(skip(self))]
1683    async fn get_reserved_proofs(
1684        &self,
1685        operation_id: &uuid::Uuid,
1686    ) -> Result<Vec<ProofInfo>, database::Error> {
1687        let conn = self
1688            .pool
1689            .get()
1690            .await
1691            .map_err(|e| Error::Database(Box::new(e)))?;
1692
1693        let rows = query(
1694            r#"
1695            SELECT
1696                amount,
1697                unit,
1698                keyset_id,
1699                secret,
1700                c,
1701                witness,
1702                dleq_e,
1703                dleq_s,
1704                dleq_r,
1705                y,
1706                mint_url,
1707                state,
1708                spending_condition,
1709                used_by_operation,
1710                created_by_operation,
1711                derivation_index,
1712                p2pk_e
1713            FROM proof
1714            WHERE used_by_operation = :operation_id
1715            "#,
1716        )?
1717        .bind("operation_id", operation_id.to_string())
1718        .fetch_all(&*conn)
1719        .await?;
1720
1721        rows.into_iter().map(sql_row_to_proof_info).collect()
1722    }
1723
1724    #[instrument(skip(self))]
1725    async fn reserve_melt_quote(
1726        &self,
1727        quote_id: &str,
1728        operation_id: &uuid::Uuid,
1729    ) -> Result<(), database::Error> {
1730        let conn = self
1731            .pool
1732            .get()
1733            .await
1734            .map_err(|e| Error::Database(Box::new(e)))?;
1735
1736        let rows_affected = query(
1737            r#"
1738            UPDATE melt_quote
1739            SET used_by_operation = :operation_id
1740            WHERE id = :quote_id AND used_by_operation IS NULL
1741            "#,
1742        )?
1743        .bind("operation_id", operation_id.to_string())
1744        .bind("quote_id", quote_id)
1745        .execute(&*conn)
1746        .await?;
1747
1748        if rows_affected == 0 {
1749            // Check if the quote exists
1750            let exists = query(
1751                r#"
1752                SELECT 1 FROM melt_quote WHERE id = :quote_id
1753                "#,
1754            )?
1755            .bind("quote_id", quote_id)
1756            .fetch_one(&*conn)
1757            .await?;
1758
1759            if exists.is_none() {
1760                return Err(database::Error::UnknownQuote);
1761            }
1762            return Err(database::Error::QuoteAlreadyInUse);
1763        }
1764
1765        Ok(())
1766    }
1767
1768    #[instrument(skip(self))]
1769    async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1770        let conn = self
1771            .pool
1772            .get()
1773            .await
1774            .map_err(|e| Error::Database(Box::new(e)))?;
1775
1776        query(
1777            r#"
1778            UPDATE melt_quote
1779            SET used_by_operation = NULL
1780            WHERE used_by_operation = :operation_id
1781            "#,
1782        )?
1783        .bind("operation_id", operation_id.to_string())
1784        .execute(&*conn)
1785        .await?;
1786
1787        Ok(())
1788    }
1789
1790    #[instrument(skip(self))]
1791    async fn reserve_mint_quote(
1792        &self,
1793        quote_id: &str,
1794        operation_id: &uuid::Uuid,
1795    ) -> Result<(), database::Error> {
1796        let conn = self
1797            .pool
1798            .get()
1799            .await
1800            .map_err(|e| Error::Database(Box::new(e)))?;
1801
1802        let rows_affected = query(
1803            r#"
1804            UPDATE mint_quote
1805            SET used_by_operation = :operation_id
1806            WHERE id = :quote_id AND used_by_operation IS NULL
1807            "#,
1808        )?
1809        .bind("operation_id", operation_id.to_string())
1810        .bind("quote_id", quote_id)
1811        .execute(&*conn)
1812        .await?;
1813
1814        if rows_affected == 0 {
1815            // Check if the quote exists
1816            let exists = query(
1817                r#"
1818                SELECT 1 FROM mint_quote WHERE id = :quote_id
1819                "#,
1820            )?
1821            .bind("quote_id", quote_id)
1822            .fetch_one(&*conn)
1823            .await?;
1824
1825            if exists.is_none() {
1826                return Err(database::Error::UnknownQuote);
1827            }
1828            return Err(database::Error::QuoteAlreadyInUse);
1829        }
1830
1831        Ok(())
1832    }
1833
1834    #[instrument(skip(self))]
1835    async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1836        let conn = self
1837            .pool
1838            .get()
1839            .await
1840            .map_err(|e| Error::Database(Box::new(e)))?;
1841
1842        query(
1843            r#"
1844            UPDATE mint_quote
1845            SET used_by_operation = NULL
1846            WHERE used_by_operation = :operation_id
1847            "#,
1848        )?
1849        .bind("operation_id", operation_id.to_string())
1850        .execute(&*conn)
1851        .await?;
1852
1853        Ok(())
1854    }
1855
1856    async fn kv_read(
1857        &self,
1858        primary_namespace: &str,
1859        secondary_namespace: &str,
1860        key: &str,
1861    ) -> Result<Option<Vec<u8>>, database::Error> {
1862        crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
1863    }
1864
1865    async fn kv_list(
1866        &self,
1867        primary_namespace: &str,
1868        secondary_namespace: &str,
1869    ) -> Result<Vec<String>, database::Error> {
1870        crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
1871    }
1872
1873    async fn kv_write(
1874        &self,
1875        primary_namespace: &str,
1876        secondary_namespace: &str,
1877        key: &str,
1878        value: &[u8],
1879    ) -> Result<(), database::Error> {
1880        let conn = self
1881            .pool
1882            .get()
1883            .await
1884            .map_err(|e| Error::Database(Box::new(e)))?;
1885        crate::keyvalue::kv_write_standalone(
1886            &*conn,
1887            primary_namespace,
1888            secondary_namespace,
1889            key,
1890            value,
1891        )
1892        .await?;
1893        Ok(())
1894    }
1895
1896    async fn kv_remove(
1897        &self,
1898        primary_namespace: &str,
1899        secondary_namespace: &str,
1900        key: &str,
1901    ) -> Result<(), database::Error> {
1902        let conn = self
1903            .pool
1904            .get()
1905            .await
1906            .map_err(|e| Error::Database(Box::new(e)))?;
1907        crate::keyvalue::kv_remove_standalone(&*conn, primary_namespace, secondary_namespace, key)
1908            .await?;
1909        Ok(())
1910    }
1911
1912    // P2PK methods
1913
1914    #[instrument(skip(self))]
1915    async fn add_p2pk_key(
1916        &self,
1917        pubkey: &PublicKey,
1918        derivation_path: DerivationPath,
1919        derivation_index: u32,
1920    ) -> Result<(), Error> {
1921        let conn = self
1922            .pool
1923            .get()
1924            .await
1925            .map_err(|e| Error::Database(Box::new(e)))?;
1926        let query_str = r#"
1927        INSERT INTO p2pk_signing_key (pubkey, derivation_index, derivation_path, created_time)
1928        VALUES (:pubkey, :derivation_index, :derivation_path, :created_time)
1929        "#
1930        .to_string();
1931
1932        query(&query_str)?
1933            .bind("pubkey", pubkey.to_bytes().to_vec())
1934            .bind("derivation_index", derivation_index)
1935            .bind("derivation_path", derivation_path.to_string())
1936            .bind("created_time", unix_time() as i64)
1937            .execute(&*conn)
1938            .await?;
1939
1940        Ok(())
1941    }
1942
1943    #[instrument(skip(self))]
1944    async fn get_p2pk_key(
1945        &self,
1946        pubkey: &PublicKey,
1947    ) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1948        let conn = self
1949            .pool
1950            .get()
1951            .await
1952            .map_err(|e| Error::Database(Box::new(e)))?;
1953        let query_str = r#"SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key WHERE pubkey = :pubkey"#.to_string();
1954
1955        query(&query_str)?
1956            .bind("pubkey", pubkey.to_bytes().to_vec())
1957            .fetch_one(&*conn)
1958            .await?
1959            .map(sql_row_to_p2pk_signing_key)
1960            .transpose()
1961    }
1962
1963    #[instrument(skip(self))]
1964    async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, Error> {
1965        let conn = self
1966            .pool
1967            .get()
1968            .await
1969            .map_err(|e| Error::Database(Box::new(e)))?;
1970        let query_str = r#"
1971        SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC
1972        "#.to_string();
1973
1974        Ok(query(&query_str)?
1975            .fetch_all(&*conn)
1976            .await?
1977            .into_iter()
1978            .filter_map(|row| {
1979                let row = sql_row_to_p2pk_signing_key(row).ok()?;
1980
1981                Some(row)
1982            })
1983            .collect::<Vec<wallet::P2PKSigningKey>>())
1984    }
1985
1986    #[instrument(skip(self))]
1987    async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1988        let conn = self
1989            .pool
1990            .get()
1991            .await
1992            .map_err(|e| Error::Database(Box::new(e)))?;
1993        let query_str = r#"
1994        SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC LIMIT 1
1995        "#.to_string();
1996
1997        query(&query_str)?
1998            .fetch_one(&*conn)
1999            .await?
2000            .map(sql_row_to_p2pk_signing_key)
2001            .transpose()
2002    }
2003}
2004
2005fn sql_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
2006    unpack_into!(
2007        let (
2008            name,
2009            pubkey,
2010            version,
2011            description,
2012            description_long,
2013            contact,
2014            nuts,
2015            icon_url,
2016            motd,
2017            urls,
2018            mint_time,
2019            tos_url
2020        ) = row
2021    );
2022
2023    Ok(MintInfo {
2024        name: column_as_nullable_string!(&name),
2025        pubkey: column_as_nullable_binary!(&pubkey)
2026            .map(|bytes| cdk_common::nuts::PublicKey::from_slice(&bytes))
2027            .transpose()?,
2028        version: column_as_nullable_string!(&version).and_then(|v| serde_json::from_str(&v).ok()),
2029        description: column_as_nullable_string!(description),
2030        description_long: column_as_nullable_string!(description_long),
2031        contact: column_as_nullable_string!(contact, |v| serde_json::from_str(&v).ok()),
2032        nuts: column_as_nullable_string!(nuts, |v| serde_json::from_str(&v).ok())
2033            .unwrap_or_default(),
2034        urls: column_as_nullable_string!(urls, |v| serde_json::from_str(&v).ok()),
2035        icon_url: column_as_nullable_string!(icon_url),
2036        motd: column_as_nullable_string!(motd),
2037        time: column_as_nullable_number!(mint_time).map(|t| t),
2038        tos_url: column_as_nullable_string!(tos_url),
2039        max_array_length: None,
2040    })
2041}
2042
2043#[instrument(skip_all)]
2044fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
2045    unpack_into!(
2046        let (
2047            id,
2048            unit,
2049            active,
2050            input_fee_ppk,
2051            final_expiry
2052        ) = row
2053    );
2054
2055    Ok(KeySetInfo {
2056        id: column_as_string!(id, Id::from_str, Id::from_bytes),
2057        unit: column_as_string!(unit, CurrencyUnit::from_str),
2058        active: matches!(active, Column::Integer(1)),
2059        input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or(0),
2060        final_expiry: column_as_nullable_number!(final_expiry),
2061    })
2062}
2063
2064fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
2065    unpack_into!(
2066        let (
2067            id,
2068            mint_url,
2069            amount,
2070            unit,
2071            request,
2072            state,
2073            expiry,
2074            secret_key,
2075            row_method,
2076            row_amount_minted,
2077            row_amount_paid,
2078            updated_at,
2079            estimated_blocks,
2080            used_by_operation,
2081            version
2082        ) = row
2083    );
2084
2085    let amount: Option<i64> = column_as_nullable_number!(amount);
2086
2087    let amount_paid: u64 = column_as_number!(row_amount_paid);
2088    let amount_minted: u64 = column_as_number!(row_amount_minted);
2089    let expiry_val: u64 = column_as_number!(expiry);
2090    let updated_at: u64 = column_as_number!(updated_at);
2091    let version_val: u32 = column_as_number!(version);
2092    let payment_method =
2093        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2094
2095    Ok(MintQuote {
2096        id: column_as_string!(id),
2097        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2098        amount: amount.and_then(Amount::from_i64),
2099        unit: column_as_string!(unit, CurrencyUnit::from_str),
2100        request: column_as_string!(request),
2101        state: column_as_string!(state, MintQuoteState::from_str),
2102        expiry: expiry_val,
2103        secret_key: column_as_nullable_string!(secret_key, |s| SecretKey::from_str(&s).ok()),
2104        payment_method,
2105        amount_issued: Amount::from(amount_minted),
2106        amount_paid: Amount::from(amount_paid),
2107        updated_at,
2108        estimated_blocks: column_as_nullable_number!(estimated_blocks),
2109        used_by_operation: column_as_nullable_string!(used_by_operation),
2110        version: version_val,
2111    })
2112}
2113
2114fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
2115    unpack_into!(
2116        let (
2117            id,
2118            unit,
2119            amount,
2120            request,
2121            fee_reserve,
2122            state,
2123            expiry,
2124            payment_proof,
2125            row_method,
2126            estimated_blocks,
2127            fee_index,
2128            used_by_operation,
2129            version,
2130            mint_url
2131        ) = row
2132    );
2133
2134    let payment_method =
2135        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2136
2137    let amount_val: u64 = column_as_number!(amount);
2138    let fee_reserve_val: u64 = column_as_number!(fee_reserve);
2139    let expiry_val: u64 = column_as_number!(expiry);
2140    let version_val: u32 = column_as_number!(version);
2141
2142    Ok(wallet::MeltQuote {
2143        id: column_as_string!(id),
2144        mint_url: column_as_nullable_string!(mint_url, |s| MintUrl::from_str(&s).ok()),
2145        unit: column_as_string!(unit, CurrencyUnit::from_str),
2146        amount: Amount::from(amount_val),
2147        request: column_as_string!(request),
2148        fee_reserve: Amount::from(fee_reserve_val),
2149        state: column_as_string!(state, MeltQuoteState::from_str),
2150        expiry: expiry_val,
2151        payment_proof: column_as_nullable_string!(payment_proof),
2152        estimated_blocks: column_as_nullable_number!(estimated_blocks),
2153        fee_index: column_as_nullable_number!(fee_index),
2154        payment_method,
2155        used_by_operation: column_as_nullable_string!(used_by_operation),
2156        version: version_val,
2157    })
2158}
2159
2160fn sql_row_to_proof_info(row: Vec<Column>) -> Result<ProofInfo, Error> {
2161    unpack_into!(
2162        let (
2163            amount,
2164            unit,
2165            keyset_id,
2166            secret,
2167            c,
2168            witness,
2169            dleq_e,
2170            dleq_s,
2171            dleq_r,
2172            y,
2173            mint_url,
2174            state,
2175            spending_condition,
2176            used_by_operation,
2177            created_by_operation,
2178            derivation_index,
2179            p2pk_e
2180        ) = row
2181    );
2182
2183    let dleq = match (
2184        column_as_nullable_binary!(dleq_e),
2185        column_as_nullable_binary!(dleq_s),
2186        column_as_nullable_binary!(dleq_r),
2187    ) {
2188        (Some(e), Some(s), Some(r)) => {
2189            let e_key = SecretKey::from_slice(&e)?;
2190            let s_key = SecretKey::from_slice(&s)?;
2191            let r_key = SecretKey::from_slice(&r)?;
2192
2193            Some(ProofDleq::new(e_key, s_key, r_key))
2194        }
2195        _ => None,
2196    };
2197
2198    let amount: u64 = column_as_number!(amount);
2199    let proof = Proof {
2200        amount: Amount::from(amount),
2201        keyset_id: column_as_string!(keyset_id, Id::from_str),
2202        secret: column_as_string!(secret, Secret::from_str),
2203        witness: column_as_nullable_string!(witness, |v| { serde_json::from_str(&v).ok() }, |v| {
2204            serde_json::from_slice(&v).ok()
2205        }),
2206        c: column_as_string!(c, PublicKey::from_str, PublicKey::from_slice),
2207        dleq,
2208        p2pk_e: column_as_nullable_binary!(p2pk_e)
2209            .map(|bytes| PublicKey::from_slice(&bytes))
2210            .transpose()?,
2211    };
2212
2213    let used_by_operation =
2214        column_as_nullable_string!(used_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2215    let created_by_operation =
2216        column_as_nullable_string!(created_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2217    let derivation_index = column_as_nullable_number!(derivation_index);
2218
2219    Ok(ProofInfo {
2220        proof,
2221        y: column_as_string!(y, PublicKey::from_str, PublicKey::from_slice),
2222        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2223        state: column_as_string!(state, State::from_str),
2224        spending_condition: column_as_nullable_string!(
2225            spending_condition,
2226            |r| { serde_json::from_str(&r).ok() },
2227            |r| { serde_json::from_slice(&r).ok() }
2228        ),
2229        unit: column_as_string!(unit, CurrencyUnit::from_str),
2230        used_by_operation,
2231        created_by_operation,
2232        derivation_index,
2233    })
2234}
2235
2236fn sql_row_to_wallet_saga(row: Vec<Column>) -> Result<wallet::WalletSaga, Error> {
2237    unpack_into!(
2238        let (
2239            id,
2240            kind,
2241            state,
2242            amount,
2243            mint_url,
2244            unit,
2245            quote_id,
2246            created_at,
2247            updated_at,
2248            data,
2249            version
2250        ) = row
2251    );
2252
2253    let id_str: String = column_as_string!(id);
2254    let id = uuid::Uuid::parse_str(&id_str).map_err(|e| {
2255        Error::Database(Box::new(std::io::Error::new(
2256            std::io::ErrorKind::InvalidData,
2257            format!("Invalid UUID: {}", e),
2258        )))
2259    })?;
2260    let kind_str: String = column_as_string!(kind);
2261    let state_json: String = column_as_string!(state);
2262    let amount: u64 = column_as_number!(amount);
2263    let mint_url: MintUrl = column_as_string!(mint_url, MintUrl::from_str);
2264    let unit: CurrencyUnit = column_as_string!(unit, CurrencyUnit::from_str);
2265    let quote_id: Option<String> = column_as_nullable_string!(quote_id);
2266    let created_at: u64 = column_as_number!(created_at);
2267    let updated_at: u64 = column_as_number!(updated_at);
2268    let data_json: String = column_as_string!(data);
2269    let version: u32 = column_as_number!(version);
2270
2271    let kind = wallet::OperationKind::from_str(&kind_str).map_err(|_| {
2272        Error::Database(Box::new(std::io::Error::new(
2273            std::io::ErrorKind::InvalidData,
2274            format!("Invalid operation kind: {}", kind_str),
2275        )))
2276    })?;
2277    let state: wallet::WalletSagaState = serde_json::from_str(&state_json).map_err(|e| {
2278        Error::Database(Box::new(std::io::Error::new(
2279            std::io::ErrorKind::InvalidData,
2280            format!("Failed to deserialize saga state: {}", e),
2281        )))
2282    })?;
2283    let data: wallet::OperationData = serde_json::from_str(&data_json).map_err(|e| {
2284        Error::Database(Box::new(std::io::Error::new(
2285            std::io::ErrorKind::InvalidData,
2286            format!("Failed to deserialize saga data: {}", e),
2287        )))
2288    })?;
2289
2290    Ok(wallet::WalletSaga {
2291        id,
2292        kind,
2293        state,
2294        amount: Amount::from(amount),
2295        mint_url,
2296        unit,
2297        quote_id,
2298        created_at,
2299        updated_at,
2300        data,
2301        version,
2302    })
2303}
2304
2305fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
2306    unpack_into!(
2307        let (
2308            mint_url,
2309            direction,
2310            unit,
2311            amount,
2312            fee,
2313            ys,
2314            timestamp,
2315            memo,
2316            metadata,
2317            quote_id,
2318            payment_request,
2319            payment_proof,
2320            payment_method,
2321            saga_id,
2322            status
2323        ) = row
2324    );
2325
2326    let amount: u64 = column_as_number!(amount);
2327    let fee: u64 = column_as_number!(fee);
2328
2329    let saga_id: Option<Uuid> = column_as_nullable_string!(saga_id)
2330        .map(|id| Uuid::from_str(&id).ok())
2331        .flatten();
2332
2333    Ok(Transaction {
2334        mint_url: column_as_string!(mint_url, MintUrl::from_str),
2335        direction: column_as_string!(direction, TransactionDirection::from_str),
2336        unit: column_as_string!(unit, CurrencyUnit::from_str),
2337        amount: Amount::from(amount),
2338        fee: Amount::from(fee),
2339        ys: column_as_binary!(ys)
2340            .chunks(33)
2341            .map(PublicKey::from_slice)
2342            .collect::<Result<Vec<_>, _>>()?,
2343        timestamp: column_as_number!(timestamp),
2344        memo: column_as_nullable_string!(memo),
2345        metadata: column_as_nullable_string!(metadata, |v| serde_json::from_str(&v).ok(), |v| {
2346            serde_json::from_slice(&v).ok()
2347        })
2348        .unwrap_or_default(),
2349        quote_id: column_as_nullable_string!(quote_id),
2350        payment_request: column_as_nullable_string!(payment_request),
2351        payment_proof: column_as_nullable_string!(payment_proof),
2352        payment_method: column_as_nullable_string!(payment_method)
2353            .map(|v| PaymentMethod::from_str(&v))
2354            .transpose()
2355            .map_err(Error::from)?,
2356        saga_id,
2357        status: column_as_string!(status, TransactionStatus::from_str),
2358    })
2359}
2360
2361fn sql_row_to_p2pk_signing_key(row: Vec<Column>) -> Result<wallet::P2PKSigningKey, Error> {
2362    unpack_into!(
2363        let (
2364            pubkey,
2365            derivation_index,
2366            derivation_path,
2367            created_time
2368        ) = row
2369    );
2370
2371    Ok(wallet::P2PKSigningKey {
2372        pubkey: column_as_string!(pubkey, PublicKey::from_str, PublicKey::from_slice),
2373        derivation_index: column_as_number!(derivation_index),
2374        derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
2375        created_time: column_as_number!(created_time),
2376    })
2377}