1use 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#[derive(Debug, Clone)]
30pub struct WalletSqliteDatabase {
31 pool: Pool<Sqlite>,
32}
33
34impl WalletSqliteDatabase {
35 #[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 #[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 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 let mint_result = MintUrl::from_str(&mint_url).ok();
224 let mint_info = sqlite_row_to_mint_info(&row).ok();
225
226 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 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 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 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 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 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 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 proof.dleq = Some(dleq);
1013
1014 let proof_info =
1016 ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
1017
1018 db.update_proofs(vec![proof_info.clone()], vec![])
1020 .await
1021 .unwrap();
1022
1023 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 assert_eq!(retrieved_proofs.len(), 1);
1036
1037 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 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}