cdk_sqlite/wallet/
mod.rs

1//! SQLite Wallet Database
2
3use std::collections::HashMap;
4use std::path::Path;
5use std::str::FromStr;
6
7use async_trait::async_trait;
8use cdk_common::common::ProofInfo;
9use cdk_common::database::WalletDatabase;
10use cdk_common::mint_url::MintUrl;
11use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
12use cdk_common::secret::Secret;
13use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
14use cdk_common::{
15    database, nut01, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
16    PublicKey, SecretKey, SpendingConditions, State,
17};
18use error::Error;
19use sqlx::sqlite::SqliteRow;
20use sqlx::{Pool, Row, Sqlite};
21use tracing::instrument;
22
23use crate::common::create_sqlite_pool;
24
25pub mod error;
26pub mod memory;
27
28/// Wallet SQLite Database
29#[derive(Debug, Clone)]
30pub struct WalletSqliteDatabase {
31    pool: Pool<Sqlite>,
32}
33
34impl WalletSqliteDatabase {
35    /// Create new [`WalletSqliteDatabase`]
36    #[cfg(not(feature = "sqlcipher"))]
37    pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
38        let db = Self {
39            pool: create_sqlite_pool(path.as_ref().to_str().ok_or(Error::InvalidDbPath)?).await?,
40        };
41        db.migrate().await?;
42        Ok(db)
43    }
44
45    /// Create new [`WalletSqliteDatabase`]
46    #[cfg(feature = "sqlcipher")]
47    pub async fn new<P: AsRef<Path>>(path: P, password: String) -> Result<Self, Error> {
48        let db = Self {
49            pool: create_sqlite_pool(
50                path.as_ref().to_str().ok_or(Error::InvalidDbPath)?,
51                password,
52            )
53            .await?,
54        };
55        db.migrate().await?;
56        Ok(db)
57    }
58
59    /// Migrate [`WalletSqliteDatabase`]
60    async fn migrate(&self) -> Result<(), Error> {
61        sqlx::migrate!("./src/wallet/migrations")
62            .run(&self.pool)
63            .await
64            .map_err(|_| Error::CouldNotInitialize)?;
65        Ok(())
66    }
67}
68
69#[async_trait]
70impl WalletDatabase for WalletSqliteDatabase {
71    type Err = database::Error;
72
73    #[instrument(skip(self, mint_info))]
74    async fn add_mint(
75        &self,
76        mint_url: MintUrl,
77        mint_info: Option<MintInfo>,
78    ) -> Result<(), Self::Err> {
79        let (
80            name,
81            pubkey,
82            version,
83            description,
84            description_long,
85            contact,
86            nuts,
87            icon_url,
88            urls,
89            motd,
90            time,
91            tos_url,
92        ) = match mint_info {
93            Some(mint_info) => {
94                let MintInfo {
95                    name,
96                    pubkey,
97                    version,
98                    description,
99                    description_long,
100                    contact,
101                    nuts,
102                    icon_url,
103                    urls,
104                    motd,
105                    time,
106                    tos_url,
107                } = mint_info;
108
109                (
110                    name,
111                    pubkey.map(|p| p.to_bytes().to_vec()),
112                    version.map(|v| serde_json::to_string(&v).ok()),
113                    description,
114                    description_long,
115                    contact.map(|c| serde_json::to_string(&c).ok()),
116                    serde_json::to_string(&nuts).ok(),
117                    icon_url,
118                    urls.map(|c| serde_json::to_string(&c).ok()),
119                    motd,
120                    time,
121                    tos_url,
122                )
123            }
124            None => (
125                None, None, None, None, None, None, None, None, None, None, None, None,
126            ),
127        };
128
129        sqlx::query(
130            r#"
131INSERT INTO mint
132(mint_url, name, pubkey, version, description, description_long, contact, nuts, icon_url, urls, motd, mint_time, tos_url)
133VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
134ON CONFLICT(mint_url) DO UPDATE SET
135    name = excluded.name,
136    pubkey = excluded.pubkey,
137    version = excluded.version,
138    description = excluded.description,
139    description_long = excluded.description_long,
140    contact = excluded.contact,
141    nuts = excluded.nuts,
142    icon_url = excluded.icon_url,
143    urls = excluded.urls,
144    motd = excluded.motd,
145    mint_time = excluded.mint_time,
146    tos_url = excluded.tos_url
147;
148        "#,
149        )
150        .bind(mint_url.to_string())
151        .bind(name)
152        .bind(pubkey)
153        .bind(version)
154        .bind(description)
155        .bind(description_long)
156        .bind(contact)
157        .bind(nuts)
158        .bind(icon_url)
159        .bind(urls)
160        .bind(motd)
161        .bind(time.map(|v| v as i64))
162        .bind(tos_url)
163        .execute(&self.pool)
164        .await
165        .map_err(Error::from)?;
166
167        Ok(())
168    }
169
170    #[instrument(skip(self))]
171    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
172        sqlx::query(
173            r#"
174DELETE FROM mint
175WHERE mint_url=?
176        "#,
177        )
178        .bind(mint_url.to_string())
179        .execute(&self.pool)
180        .await
181        .map_err(Error::from)?;
182
183        Ok(())
184    }
185
186    #[instrument(skip(self))]
187    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
188        let rec = sqlx::query(
189            r#"
190SELECT *
191FROM mint
192WHERE mint_url=?;
193        "#,
194        )
195        .bind(mint_url.to_string())
196        .fetch_one(&self.pool)
197        .await;
198
199        let rec = match rec {
200            Ok(rec) => rec,
201            Err(err) => match err {
202                sqlx::Error::RowNotFound => return Ok(None),
203                _ => return Err(Error::SQLX(err).into()),
204            },
205        };
206
207        Ok(Some(sqlite_row_to_mint_info(&rec)?))
208    }
209
210    #[instrument(skip(self))]
211    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
212        let rec = sqlx::query(
213            r#"
214SELECT *
215FROM mint
216        "#,
217        )
218        .fetch_all(&self.pool)
219        .await
220        .map_err(Error::from)?;
221
222        let mints = rec
223            .into_iter()
224            .flat_map(|row| {
225                let mint_url: String = row.get("mint_url");
226
227                // Attempt to parse mint_url and convert mint_info
228                let mint_result = MintUrl::from_str(&mint_url).ok();
229                let mint_info = sqlite_row_to_mint_info(&row).ok();
230
231                // Combine mint_result and mint_info into an Option tuple
232                mint_result.map(|mint| (mint, mint_info))
233            })
234            .collect();
235
236        Ok(mints)
237    }
238
239    #[instrument(skip(self))]
240    async fn update_mint_url(
241        &self,
242        old_mint_url: MintUrl,
243        new_mint_url: MintUrl,
244    ) -> Result<(), Self::Err> {
245        let tables = ["mint_quote", "proof"];
246        for table in &tables {
247            let query = format!(
248                r#"
249            UPDATE {}
250            SET mint_url = ?
251            WHERE mint_url = ?;
252            "#,
253                table
254            );
255
256            sqlx::query(&query)
257                .bind(new_mint_url.to_string())
258                .bind(old_mint_url.to_string())
259                .execute(&self.pool)
260                .await
261                .map_err(Error::from)?;
262        }
263        Ok(())
264    }
265
266    #[instrument(skip(self, keysets))]
267    async fn add_mint_keysets(
268        &self,
269        mint_url: MintUrl,
270        keysets: Vec<KeySetInfo>,
271    ) -> Result<(), Self::Err> {
272        for keyset in keysets {
273            sqlx::query(
274                r#"
275    INSERT INTO keyset
276    (mint_url, id, unit, active, input_fee_ppk)
277    VALUES (?, ?, ?, ?, ?)
278    ON CONFLICT(id) DO UPDATE SET
279        mint_url = excluded.mint_url,
280        unit = excluded.unit,
281        active = excluded.active,
282        input_fee_ppk = excluded.input_fee_ppk;
283    "#,
284            )
285            .bind(mint_url.to_string())
286            .bind(keyset.id.to_string())
287            .bind(keyset.unit.to_string())
288            .bind(keyset.active)
289            .bind(keyset.input_fee_ppk as i64)
290            .execute(&self.pool)
291            .await
292            .map_err(Error::from)?;
293        }
294
295        Ok(())
296    }
297
298    #[instrument(skip(self))]
299    async fn get_mint_keysets(
300        &self,
301        mint_url: MintUrl,
302    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
303        let recs = sqlx::query(
304            r#"
305SELECT *
306FROM keyset
307WHERE mint_url=?
308        "#,
309        )
310        .bind(mint_url.to_string())
311        .fetch_all(&self.pool)
312        .await;
313
314        let recs = match recs {
315            Ok(recs) => recs,
316            Err(err) => match err {
317                sqlx::Error::RowNotFound => return Ok(None),
318                _ => return Err(Error::SQLX(err).into()),
319            },
320        };
321
322        let keysets = recs
323            .iter()
324            .map(sqlite_row_to_keyset)
325            .collect::<Result<Vec<KeySetInfo>, _>>()?;
326
327        match keysets.is_empty() {
328            false => Ok(Some(keysets)),
329            true => Ok(None),
330        }
331    }
332
333    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
334    async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err> {
335        let rec = sqlx::query(
336            r#"
337SELECT *
338FROM keyset
339WHERE id=?
340        "#,
341        )
342        .bind(keyset_id.to_string())
343        .fetch_one(&self.pool)
344        .await;
345
346        let rec = match rec {
347            Ok(recs) => recs,
348            Err(err) => match err {
349                sqlx::Error::RowNotFound => return Ok(None),
350                _ => return Err(Error::SQLX(err).into()),
351            },
352        };
353
354        Ok(Some(sqlite_row_to_keyset(&rec)?))
355    }
356
357    #[instrument(skip_all)]
358    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
359        sqlx::query(
360            r#"
361INSERT INTO mint_quote
362(id, mint_url, amount, unit, request, state, expiry, secret_key)
363VALUES (?, ?, ?, ?, ?, ?, ?, ?)
364ON CONFLICT(id) DO UPDATE SET
365    mint_url = excluded.mint_url,
366    amount = excluded.amount,
367    unit = excluded.unit,
368    request = excluded.request,
369    state = excluded.state,
370    expiry = excluded.expiry,
371    secret_key = excluded.secret_key
372;
373        "#,
374        )
375        .bind(quote.id.to_string())
376        .bind(quote.mint_url.to_string())
377        .bind(u64::from(quote.amount) as i64)
378        .bind(quote.unit.to_string())
379        .bind(quote.request)
380        .bind(quote.state.to_string())
381        .bind(quote.expiry as i64)
382        .bind(quote.secret_key.map(|p| p.to_string()))
383        .execute(&self.pool)
384        .await
385        .map_err(Error::from)?;
386
387        Ok(())
388    }
389
390    #[instrument(skip(self))]
391    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
392        let rec = sqlx::query(
393            r#"
394SELECT *
395FROM mint_quote
396WHERE id=?;
397        "#,
398        )
399        .bind(quote_id)
400        .fetch_one(&self.pool)
401        .await;
402
403        let rec = match rec {
404            Ok(rec) => rec,
405            Err(err) => match err {
406                sqlx::Error::RowNotFound => return Ok(None),
407                _ => return Err(Error::SQLX(err).into()),
408            },
409        };
410
411        Ok(Some(sqlite_row_to_mint_quote(&rec)?))
412    }
413
414    #[instrument(skip(self))]
415    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
416        let rec = sqlx::query(
417            r#"
418SELECT *
419FROM mint_quote
420        "#,
421        )
422        .fetch_all(&self.pool)
423        .await
424        .map_err(Error::from)?;
425
426        let mint_quotes = rec
427            .iter()
428            .map(sqlite_row_to_mint_quote)
429            .collect::<Result<_, _>>()?;
430
431        Ok(mint_quotes)
432    }
433
434    #[instrument(skip(self))]
435    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
436        sqlx::query(
437            r#"
438DELETE FROM mint_quote
439WHERE id=?
440        "#,
441        )
442        .bind(quote_id)
443        .execute(&self.pool)
444        .await
445        .map_err(Error::from)?;
446
447        Ok(())
448    }
449
450    #[instrument(skip_all)]
451    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
452        sqlx::query(
453            r#"
454INSERT INTO melt_quote
455(id, unit, amount, request, fee_reserve, state, expiry)
456VALUES (?, ?, ?, ?, ?, ?, ?)
457ON CONFLICT(id) DO UPDATE SET
458    unit = excluded.unit,
459    amount = excluded.amount,
460    request = excluded.request,
461    fee_reserve = excluded.fee_reserve,
462    state = excluded.state,
463    expiry = excluded.expiry
464;
465        "#,
466        )
467        .bind(quote.id.to_string())
468        .bind(quote.unit.to_string())
469        .bind(u64::from(quote.amount) as i64)
470        .bind(quote.request)
471        .bind(u64::from(quote.fee_reserve) as i64)
472        .bind(quote.state.to_string())
473        .bind(quote.expiry as i64)
474        .execute(&self.pool)
475        .await
476        .map_err(Error::from)?;
477
478        Ok(())
479    }
480
481    #[instrument(skip(self))]
482    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
483        let rec = sqlx::query(
484            r#"
485SELECT *
486FROM melt_quote
487WHERE id=?;
488        "#,
489        )
490        .bind(quote_id)
491        .fetch_one(&self.pool)
492        .await;
493
494        let rec = match rec {
495            Ok(rec) => rec,
496            Err(err) => match err {
497                sqlx::Error::RowNotFound => return Ok(None),
498                _ => return Err(Error::SQLX(err).into()),
499            },
500        };
501
502        Ok(Some(sqlite_row_to_melt_quote(&rec)?))
503    }
504
505    #[instrument(skip(self))]
506    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
507        sqlx::query(
508            r#"
509DELETE FROM melt_quote
510WHERE id=?
511        "#,
512        )
513        .bind(quote_id)
514        .execute(&self.pool)
515        .await
516        .map_err(Error::from)?;
517
518        Ok(())
519    }
520
521    #[instrument(skip_all)]
522    async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
523        sqlx::query(
524            r#"
525INSERT INTO key
526(id, keys)
527VALUES (?, ?)
528ON CONFLICT(id) DO UPDATE SET
529    keys = excluded.keys
530;
531        "#,
532        )
533        .bind(Id::from(&keys).to_string())
534        .bind(serde_json::to_string(&keys).map_err(Error::from)?)
535        .execute(&self.pool)
536        .await
537        .map_err(Error::from)?;
538
539        Ok(())
540    }
541
542    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
543    async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, Self::Err> {
544        let rec = sqlx::query(
545            r#"
546SELECT *
547FROM key
548WHERE id=?;
549        "#,
550        )
551        .bind(keyset_id.to_string())
552        .fetch_one(&self.pool)
553        .await;
554
555        let rec = match rec {
556            Ok(rec) => rec,
557            Err(err) => match err {
558                sqlx::Error::RowNotFound => return Ok(None),
559                _ => return Err(Error::SQLX(err).into()),
560            },
561        };
562
563        let keys: String = rec.get("keys");
564
565        Ok(serde_json::from_str(&keys).map_err(Error::from)?)
566    }
567
568    #[instrument(skip(self))]
569    async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> {
570        sqlx::query(
571            r#"
572DELETE FROM key
573WHERE id=?
574        "#,
575        )
576        .bind(id.to_string())
577        .execute(&self.pool)
578        .await
579        .map_err(Error::from)?;
580
581        Ok(())
582    }
583
584    async fn update_proofs(
585        &self,
586        added: Vec<ProofInfo>,
587        removed_ys: Vec<PublicKey>,
588    ) -> Result<(), Self::Err> {
589        for proof in added {
590            sqlx::query(
591                r#"
592    INSERT INTO proof
593    (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness, dleq_e, dleq_s, dleq_r)
594    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
595    ON CONFLICT(y) DO UPDATE SET
596        mint_url = excluded.mint_url,
597        state = excluded.state,
598        spending_condition = excluded.spending_condition,
599        unit = excluded.unit,
600        amount = excluded.amount,
601        keyset_id = excluded.keyset_id,
602        secret = excluded.secret,
603        c = excluded.c,
604        witness = excluded.witness,
605        dleq_e = excluded.dleq_e,
606        dleq_s = excluded.dleq_s,
607        dleq_r = excluded.dleq_r
608    ;
609            "#,
610            )
611            .bind(proof.y.to_bytes().to_vec())
612            .bind(proof.mint_url.to_string())
613            .bind(proof.state.to_string())
614            .bind(
615                proof
616                    .spending_condition
617                    .map(|s| serde_json::to_string(&s).ok()),
618            )
619            .bind(proof.unit.to_string())
620            .bind(u64::from(proof.proof.amount) as i64)
621            .bind(proof.proof.keyset_id.to_string())
622            .bind(proof.proof.secret.to_string())
623            .bind(proof.proof.c.to_bytes().to_vec())
624            .bind(
625                proof
626                    .proof
627                    .witness
628                    .map(|w| serde_json::to_string(&w).unwrap()),
629            )
630            .bind(
631                proof.proof.dleq.as_ref().map(|dleq| dleq.e.to_secret_bytes().to_vec()),
632            )
633            .bind(
634                proof.proof.dleq.as_ref().map(|dleq| dleq.s.to_secret_bytes().to_vec()),
635            )
636            .bind(
637                proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()),
638            )
639            .execute(&self.pool)
640            .await
641            .map_err(Error::from)?;
642        }
643
644        // TODO: Generate a IN clause
645        for y in removed_ys {
646            sqlx::query(
647                r#"
648    DELETE FROM proof
649    WHERE y = ?
650            "#,
651            )
652            .bind(y.to_bytes().to_vec())
653            .execute(&self.pool)
654            .await
655            .map_err(Error::from)?;
656        }
657
658        Ok(())
659    }
660
661    #[instrument(skip(self, state, spending_conditions))]
662    async fn get_proofs(
663        &self,
664        mint_url: Option<MintUrl>,
665        unit: Option<CurrencyUnit>,
666        state: Option<Vec<State>>,
667        spending_conditions: Option<Vec<SpendingConditions>>,
668    ) -> Result<Vec<ProofInfo>, Self::Err> {
669        let recs = sqlx::query(
670            r#"
671SELECT *
672FROM proof;
673        "#,
674        )
675        .fetch_all(&self.pool)
676        .await;
677
678        let recs = match recs {
679            Ok(rec) => rec,
680            Err(err) => match err {
681                sqlx::Error::RowNotFound => return Ok(vec![]),
682                _ => return Err(Error::SQLX(err).into()),
683            },
684        };
685
686        let proofs: Vec<ProofInfo> = recs
687            .iter()
688            .filter_map(|p| match sqlite_row_to_proof_info(p) {
689                Ok(proof_info) => {
690                    match proof_info.matches_conditions(
691                        &mint_url,
692                        &unit,
693                        &state,
694                        &spending_conditions,
695                    ) {
696                        true => Some(proof_info),
697                        false => None,
698                    }
699                }
700                Err(err) => {
701                    tracing::error!("Could not deserialize proof row: {}", err);
702                    None
703                }
704            })
705            .collect();
706
707        match proofs.is_empty() {
708            false => Ok(proofs),
709            true => return Ok(vec![]),
710        }
711    }
712
713    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err> {
714        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
715
716        let update_sql = format!(
717            "UPDATE proof SET state = ? WHERE y IN ({})",
718            "?,".repeat(ys.len()).trim_end_matches(',')
719        );
720
721        ys.iter()
722            .fold(
723                sqlx::query(&update_sql).bind(state.to_string()),
724                |query, y| query.bind(y.to_bytes().to_vec()),
725            )
726            .execute(&mut *transaction)
727            .await
728            .map_err(|err| {
729                tracing::error!("SQLite could not update proof state: {err:?}");
730                Error::SQLX(err)
731            })?;
732
733        transaction.commit().await.map_err(Error::from)?;
734
735        Ok(())
736    }
737
738    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
739    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
740        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
741
742        sqlx::query(
743            r#"
744UPDATE keyset
745SET counter=counter+?
746WHERE id=?;
747        "#,
748        )
749        .bind(count as i64)
750        .bind(keyset_id.to_string())
751        .execute(&mut *transaction)
752        .await
753        .map_err(Error::from)?;
754
755        transaction.commit().await.map_err(Error::from)?;
756
757        Ok(())
758    }
759
760    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
761    async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
762        let rec = sqlx::query(
763            r#"
764SELECT counter
765FROM keyset
766WHERE id=?;
767        "#,
768        )
769        .bind(keyset_id.to_string())
770        .fetch_one(&self.pool)
771        .await;
772
773        let count = match rec {
774            Ok(rec) => {
775                let count: Option<u32> = rec.try_get("counter").map_err(Error::from)?;
776                count
777            }
778            Err(err) => match err {
779                sqlx::Error::RowNotFound => return Ok(None),
780                _ => return Err(Error::SQLX(err).into()),
781            },
782        };
783
784        Ok(count)
785    }
786
787    #[instrument(skip(self))]
788    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err> {
789        let mint_url = transaction.mint_url.to_string();
790        let direction = transaction.direction.to_string();
791        let unit = transaction.unit.to_string();
792        let amount = u64::from(transaction.amount) as i64;
793        let fee = u64::from(transaction.fee) as i64;
794        let ys = transaction
795            .ys
796            .iter()
797            .flat_map(|y| y.to_bytes().to_vec())
798            .collect::<Vec<_>>();
799
800        sqlx::query(
801            r#"
802INSERT INTO transactions
803(id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata)
804VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
805ON CONFLICT(id) DO UPDATE SET
806    mint_url = excluded.mint_url,
807    direction = excluded.direction,
808    unit = excluded.unit,
809    amount = excluded.amount,
810    fee = excluded.fee,
811    ys = excluded.ys,
812    timestamp = excluded.timestamp,
813    memo = excluded.memo,
814    metadata = excluded.metadata
815;
816        "#,
817        )
818        .bind(transaction.id().as_slice())
819        .bind(mint_url)
820        .bind(direction)
821        .bind(unit)
822        .bind(amount)
823        .bind(fee)
824        .bind(ys)
825        .bind(transaction.timestamp as i64)
826        .bind(transaction.memo)
827        .bind(serde_json::to_string(&transaction.metadata).map_err(Error::from)?)
828        .execute(&self.pool)
829        .await
830        .map_err(Error::from)?;
831
832        Ok(())
833    }
834
835    #[instrument(skip(self))]
836    async fn get_transaction(
837        &self,
838        transaction_id: TransactionId,
839    ) -> Result<Option<Transaction>, Self::Err> {
840        let rec = sqlx::query(
841            r#"
842SELECT *
843FROM transactions
844WHERE id=?;
845        "#,
846        )
847        .bind(transaction_id.as_slice())
848        .fetch_one(&self.pool)
849        .await;
850
851        let rec = match rec {
852            Ok(rec) => rec,
853            Err(err) => match err {
854                sqlx::Error::RowNotFound => return Ok(None),
855                _ => return Err(Error::SQLX(err).into()),
856            },
857        };
858
859        let transaction = sqlite_row_to_transaction(&rec)?;
860
861        Ok(Some(transaction))
862    }
863
864    #[instrument(skip(self))]
865    async fn list_transactions(
866        &self,
867        mint_url: Option<MintUrl>,
868        direction: Option<TransactionDirection>,
869        unit: Option<CurrencyUnit>,
870    ) -> Result<Vec<Transaction>, Self::Err> {
871        let recs = sqlx::query(
872            r#"
873SELECT *
874FROM transactions;
875        "#,
876        )
877        .fetch_all(&self.pool)
878        .await;
879
880        let recs = match recs {
881            Ok(rec) => rec,
882            Err(err) => match err {
883                sqlx::Error::RowNotFound => return Ok(vec![]),
884                _ => return Err(Error::SQLX(err).into()),
885            },
886        };
887
888        let transactions = recs
889            .iter()
890            .filter_map(|p| {
891                let transaction = sqlite_row_to_transaction(p).ok()?;
892                if transaction.matches_conditions(&mint_url, &direction, &unit) {
893                    Some(transaction)
894                } else {
895                    None
896                }
897            })
898            .collect();
899
900        Ok(transactions)
901    }
902
903    #[instrument(skip(self))]
904    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err> {
905        sqlx::query(
906            r#"
907DELETE FROM transactions
908WHERE id=?
909        "#,
910        )
911        .bind(transaction_id.as_slice())
912        .execute(&self.pool)
913        .await
914        .map_err(Error::from)?;
915
916        Ok(())
917    }
918}
919
920fn sqlite_row_to_mint_info(row: &SqliteRow) -> Result<MintInfo, Error> {
921    let name: Option<String> = row.try_get("name").map_err(Error::from)?;
922    let row_pubkey: Option<Vec<u8>> = row.try_get("pubkey").map_err(Error::from)?;
923    let row_version: Option<String> = row.try_get("version").map_err(Error::from)?;
924    let description: Option<String> = row.try_get("description").map_err(Error::from)?;
925    let description_long: Option<String> = row.try_get("description_long").map_err(Error::from)?;
926    let row_contact: Option<String> = row.try_get("contact").map_err(Error::from)?;
927    let row_nuts: Option<String> = row.try_get("nuts").map_err(Error::from)?;
928    let icon_url: Option<String> = row.try_get("icon_url").map_err(Error::from)?;
929    let motd: Option<String> = row.try_get("motd").map_err(Error::from)?;
930    let row_urls: Option<String> = row.try_get("urls").map_err(Error::from)?;
931    let time: Option<i64> = row.try_get("mint_time").map_err(Error::from)?;
932    let tos_url: Option<String> = row.try_get("tos_url").map_err(Error::from)?;
933    Ok(MintInfo {
934        name,
935        pubkey: row_pubkey.and_then(|p| PublicKey::from_slice(&p).ok()),
936        version: row_version.and_then(|v| serde_json::from_str(&v).ok()),
937        description,
938        description_long,
939        contact: row_contact.and_then(|c| serde_json::from_str(&c).ok()),
940        nuts: row_nuts
941            .and_then(|n| serde_json::from_str(&n).ok())
942            .unwrap_or_default(),
943        icon_url,
944        urls: row_urls.and_then(|c| serde_json::from_str(&c).ok()),
945        motd,
946        time: time.map(|t| t as u64),
947        tos_url,
948    })
949}
950
951fn sqlite_row_to_keyset(row: &SqliteRow) -> Result<KeySetInfo, Error> {
952    let row_id: String = row.try_get("id").map_err(Error::from)?;
953    let row_unit: String = row.try_get("unit").map_err(Error::from)?;
954    let active: bool = row.try_get("active").map_err(Error::from)?;
955    let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").map_err(Error::from)?;
956
957    Ok(KeySetInfo {
958        id: Id::from_str(&row_id)?,
959        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
960        active,
961        input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64,
962    })
963}
964
965fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
966    let row_id: String = row.try_get("id").map_err(Error::from)?;
967    let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?;
968    let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
969    let row_unit: String = row.try_get("unit").map_err(Error::from)?;
970    let row_request: String = row.try_get("request").map_err(Error::from)?;
971    let row_state: String = row.try_get("state").map_err(Error::from)?;
972    let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
973    let row_secret: Option<String> = row.try_get("secret_key").map_err(Error::from)?;
974
975    let state = MintQuoteState::from_str(&row_state)?;
976
977    let secret_key = row_secret
978        .map(|key| SecretKey::from_str(&key))
979        .transpose()?;
980
981    Ok(MintQuote {
982        id: row_id,
983        mint_url: MintUrl::from_str(&row_mint_url)?,
984        amount: Amount::from(row_amount as u64),
985        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
986        request: row_request,
987        state,
988        expiry: row_expiry as u64,
989        secret_key,
990    })
991}
992
993fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<wallet::MeltQuote, Error> {
994    let row_id: String = row.try_get("id").map_err(Error::from)?;
995    let row_unit: String = row.try_get("unit").map_err(Error::from)?;
996    let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
997    let row_request: String = row.try_get("request").map_err(Error::from)?;
998    let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?;
999    let row_state: String = row.try_get("state").map_err(Error::from)?;
1000    let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
1001    let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
1002
1003    let state = MeltQuoteState::from_str(&row_state)?;
1004    Ok(wallet::MeltQuote {
1005        id: row_id,
1006        amount: Amount::from(row_amount as u64),
1007        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
1008        request: row_request,
1009        fee_reserve: Amount::from(row_fee_reserve as u64),
1010        state,
1011        expiry: row_expiry as u64,
1012        payment_preimage: row_preimage,
1013    })
1014}
1015
1016fn sqlite_row_to_proof_info(row: &SqliteRow) -> Result<ProofInfo, Error> {
1017    let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
1018    let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?;
1019    let row_secret: String = row.try_get("secret").map_err(Error::from)?;
1020    let row_c: Vec<u8> = row.try_get("c").map_err(Error::from)?;
1021    let row_witness: Option<String> = row.try_get("witness").map_err(Error::from)?;
1022
1023    // Get DLEQ fields
1024    let row_dleq_e: Option<Vec<u8>> = row.try_get("dleq_e").map_err(Error::from)?;
1025    let row_dleq_s: Option<Vec<u8>> = row.try_get("dleq_s").map_err(Error::from)?;
1026    let row_dleq_r: Option<Vec<u8>> = row.try_get("dleq_r").map_err(Error::from)?;
1027
1028    let y: Vec<u8> = row.try_get("y").map_err(Error::from)?;
1029    let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?;
1030    let row_state: String = row.try_get("state").map_err(Error::from)?;
1031    let row_spending_condition: Option<String> =
1032        row.try_get("spending_condition").map_err(Error::from)?;
1033    let row_unit: String = row.try_get("unit").map_err(Error::from)?;
1034
1035    // Create DLEQ proof if all fields are present
1036    let dleq = match (row_dleq_e, row_dleq_s, row_dleq_r) {
1037        (Some(e), Some(s), Some(r)) => {
1038            let e_key = SecretKey::from_slice(&e)?;
1039            let s_key = SecretKey::from_slice(&s)?;
1040            let r_key = SecretKey::from_slice(&r)?;
1041
1042            Some(ProofDleq::new(e_key, s_key, r_key))
1043        }
1044        _ => None,
1045    };
1046
1047    let proof = Proof {
1048        amount: Amount::from(row_amount as u64),
1049        keyset_id: Id::from_str(&keyset_id)?,
1050        secret: Secret::from_str(&row_secret)?,
1051        c: PublicKey::from_slice(&row_c)?,
1052        witness: row_witness.and_then(|w| serde_json::from_str(&w).ok()),
1053        dleq,
1054    };
1055
1056    Ok(ProofInfo {
1057        proof,
1058        y: PublicKey::from_slice(&y)?,
1059        mint_url: MintUrl::from_str(&row_mint_url)?,
1060        state: State::from_str(&row_state)?,
1061        spending_condition: row_spending_condition.and_then(|r| serde_json::from_str(&r).ok()),
1062        unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
1063    })
1064}
1065
1066fn sqlite_row_to_transaction(row: &SqliteRow) -> Result<Transaction, Error> {
1067    let mint_url: String = row.try_get("mint_url").map_err(Error::from)?;
1068    let direction: String = row.try_get("direction").map_err(Error::from)?;
1069    let unit: String = row.try_get("unit").map_err(Error::from)?;
1070    let amount: i64 = row.try_get("amount").map_err(Error::from)?;
1071    let fee: i64 = row.try_get("fee").map_err(Error::from)?;
1072    let ys: Vec<u8> = row.try_get("ys").map_err(Error::from)?;
1073    let timestamp: i64 = row.try_get("timestamp").map_err(Error::from)?;
1074    let memo: Option<String> = row.try_get("memo").map_err(Error::from)?;
1075    let row_metadata: Option<String> = row.try_get("metadata").map_err(Error::from)?;
1076
1077    let metadata: HashMap<String, String> = row_metadata
1078        .and_then(|m| serde_json::from_str(&m).ok())
1079        .unwrap_or_default();
1080
1081    let ys: Result<Vec<PublicKey>, nut01::Error> =
1082        ys.chunks(33).map(PublicKey::from_slice).collect();
1083
1084    Ok(Transaction {
1085        mint_url: MintUrl::from_str(&mint_url)?,
1086        direction: TransactionDirection::from_str(&direction)?,
1087        unit: CurrencyUnit::from_str(&unit)?,
1088        amount: Amount::from(amount as u64),
1089        fee: Amount::from(fee as u64),
1090        ys: ys?,
1091        timestamp: timestamp as u64,
1092        memo,
1093        metadata,
1094    })
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099    use cdk_common::database::WalletDatabase;
1100    use cdk_common::nuts::{ProofDleq, State};
1101    use cdk_common::secret::Secret;
1102
1103    use crate::WalletSqliteDatabase;
1104
1105    #[tokio::test]
1106    #[cfg(feature = "sqlcipher")]
1107    async fn test_sqlcipher() {
1108        use cdk_common::mint_url::MintUrl;
1109        use cdk_common::MintInfo;
1110
1111        use super::*;
1112        let path = std::env::temp_dir()
1113            .to_path_buf()
1114            .join(format!("cdk-test-{}.sqlite", uuid::Uuid::new_v4()));
1115        let db = WalletSqliteDatabase::new(path, "password".to_string())
1116            .await
1117            .unwrap();
1118
1119        db.migrate().await;
1120
1121        let mint_info = MintInfo::new().description("test");
1122        let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
1123
1124        db.add_mint(mint_url.clone(), Some(mint_info.clone()))
1125            .await
1126            .unwrap();
1127
1128        let res = db.get_mint(mint_url).await.unwrap();
1129        assert_eq!(mint_info, res.clone().unwrap());
1130        assert_eq!("test", &res.unwrap().description.unwrap());
1131    }
1132
1133    #[tokio::test]
1134    async fn test_proof_with_dleq() {
1135        use std::str::FromStr;
1136
1137        use cdk_common::common::ProofInfo;
1138        use cdk_common::mint_url::MintUrl;
1139        use cdk_common::nuts::{CurrencyUnit, Id, Proof, PublicKey, SecretKey};
1140        use cdk_common::Amount;
1141
1142        // Create a temporary database
1143        let path = std::env::temp_dir()
1144            .to_path_buf()
1145            .join(format!("cdk-test-dleq-{}.sqlite", uuid::Uuid::new_v4()));
1146
1147        #[cfg(feature = "sqlcipher")]
1148        let db = WalletSqliteDatabase::new(path, "password".to_string())
1149            .await
1150            .unwrap();
1151
1152        #[cfg(not(feature = "sqlcipher"))]
1153        let db = WalletSqliteDatabase::new(path).await.unwrap();
1154
1155        // Create a proof with DLEQ
1156        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
1157        let mint_url = MintUrl::from_str("https://example.com").unwrap();
1158        let secret = Secret::new("test_secret_for_dleq");
1159
1160        // Create DLEQ components
1161        let e = SecretKey::generate();
1162        let s = SecretKey::generate();
1163        let r = SecretKey::generate();
1164
1165        let dleq = ProofDleq::new(e.clone(), s.clone(), r.clone());
1166
1167        let mut proof = Proof::new(
1168            Amount::from(64),
1169            keyset_id,
1170            secret,
1171            PublicKey::from_hex(
1172                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1173            )
1174            .unwrap(),
1175        );
1176
1177        // Add DLEQ to the proof
1178        proof.dleq = Some(dleq);
1179
1180        // Create ProofInfo
1181        let proof_info =
1182            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1183
1184        // Store the proof in the database
1185        db.update_proofs(vec![proof_info.clone()], vec![])
1186            .await
1187            .unwrap();
1188
1189        // Retrieve the proof from the database
1190        let retrieved_proofs = db
1191            .get_proofs(
1192                Some(mint_url),
1193                Some(CurrencyUnit::Sat),
1194                Some(vec![State::Unspent]),
1195                None,
1196            )
1197            .await
1198            .unwrap();
1199
1200        // Verify we got back exactly one proof
1201        assert_eq!(retrieved_proofs.len(), 1);
1202
1203        // Verify the DLEQ data was preserved
1204        let retrieved_proof = &retrieved_proofs[0];
1205        assert!(retrieved_proof.proof.dleq.is_some());
1206
1207        let retrieved_dleq = retrieved_proof.proof.dleq.as_ref().unwrap();
1208
1209        // Verify DLEQ components match what we stored
1210        assert_eq!(retrieved_dleq.e.to_string(), e.to_string());
1211        assert_eq!(retrieved_dleq.s.to_string(), s.to_string());
1212        assert_eq!(retrieved_dleq.r.to_string(), r.to_string());
1213    }
1214}