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