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