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