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