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