1use std::collections::HashMap;
4use std::fmt::Debug;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use bitcoin::bip32::DerivationPath;
10use cdk_common::database::{ConversionError, Error, WalletDatabase};
11use cdk_common::mint_url::MintUrl;
12use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
13use cdk_common::secret::Secret;
14use cdk_common::util::unix_time;
15use cdk_common::wallet::{
16 self, MintQuote, ProofInfo, Transaction, TransactionDirection, TransactionId, TransactionStatus,
17};
18use cdk_common::{
19 database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod, Proof,
20 ProofDleq, PublicKey, SecretKey, SpendingConditions, State,
21};
22use tracing::instrument;
23use uuid::Uuid;
24
25use crate::common::migrate;
26use crate::database::{ConnectionWithTransaction, DatabaseExecutor};
27use crate::pool::{DatabasePool, Pool, PooledResource};
28use crate::stmt::{query, Column};
29use crate::{
30 column_as_binary, column_as_nullable_binary, column_as_nullable_number,
31 column_as_nullable_string, column_as_number, column_as_string, unpack_into,
32};
33
34#[rustfmt::skip]
35mod migrations {
36 include!(concat!(env!("OUT_DIR"), "/migrations_wallet.rs"));
37}
38
39#[derive(Debug, Clone)]
41pub struct SQLWalletDatabase<RM>
42where
43 RM: DatabasePool + 'static,
44{
45 pool: Arc<Pool<RM>>,
46}
47
48impl<RM> SQLWalletDatabase<RM>
49where
50 RM: DatabasePool + 'static,
51{
52 pub async fn new<X>(db: X) -> Result<Self, Error>
54 where
55 X: Into<RM::Config>,
56 {
57 let pool = Pool::new(db.into());
58 Self::migrate(pool.get().await.map_err(|e| Error::Database(Box::new(e)))?).await?;
59
60 Ok(Self { pool })
61 }
62
63 async fn migrate(conn: PooledResource<RM>) -> Result<(), Error> {
65 let tx = ConnectionWithTransaction::new(conn).await?;
66 migrate(&tx, RM::Connection::name(), migrations::MIGRATIONS).await?;
67 Self::add_keyset_u32(&tx).await?;
69 tx.commit().await?;
70
71 Ok(())
72 }
73
74 async fn add_keyset_u32<T>(conn: &T) -> Result<(), Error>
75 where
76 T: DatabaseExecutor,
77 {
78 let keys_without_u32: Vec<Vec<Column>> = query(
80 r#"
81 SELECT
82 id
83 FROM key
84 WHERE keyset_u32 IS NULL
85 "#,
86 )?
87 .fetch_all(conn)
88 .await?;
89
90 for row in keys_without_u32 {
91 unpack_into!(let (id) = row);
92 let id = column_as_string!(id);
93
94 if let Ok(id) = Id::from_str(&id) {
95 query(
96 r#"
97 UPDATE
98 key
99 SET keyset_u32 = :u32_keyset
100 WHERE id = :keyset_id
101 "#,
102 )?
103 .bind("u32_keyset", u32::from(id))
104 .bind("keyset_id", id.to_string())
105 .execute(conn)
106 .await?;
107 }
108 }
109
110 let keysets_without_u32: Vec<Vec<Column>> = query(
112 r#"
113 SELECT
114 id
115 FROM keyset
116 WHERE keyset_u32 IS NULL
117 "#,
118 )?
119 .fetch_all(conn)
120 .await?;
121
122 for row in keysets_without_u32 {
123 unpack_into!(let (id) = row);
124 let id = column_as_string!(id);
125
126 if let Ok(id) = Id::from_str(&id) {
127 query(
128 r#"
129 UPDATE
130 keyset
131 SET keyset_u32 = :u32_keyset
132 WHERE id = :keyset_id
133 "#,
134 )?
135 .bind("u32_keyset", u32::from(id))
136 .bind("keyset_id", id.to_string())
137 .execute(conn)
138 .await?;
139 }
140 }
141
142 Ok(())
143 }
144}
145
146#[async_trait]
147impl<RM> WalletDatabase<database::Error> for SQLWalletDatabase<RM>
148where
149 RM: DatabasePool + 'static,
150{
151 #[instrument(skip(self))]
152 async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, database::Error> {
153 let conn = self
154 .pool
155 .get()
156 .await
157 .map_err(|e| Error::Database(Box::new(e)))?;
158
159 Ok(query(
160 r#"
161 SELECT
162 id,
163 unit,
164 amount,
165 request,
166 fee_reserve,
167 state,
168 expiry,
169 payment_proof,
170 payment_method,
171 estimated_blocks,
172 fee_index,
173 used_by_operation,
174 version,
175 mint_url
176 FROM
177 melt_quote
178 "#,
179 )?
180 .fetch_all(&*conn)
181 .await?
182 .into_iter()
183 .map(sql_row_to_melt_quote)
184 .collect::<Result<_, _>>()?)
185 }
186
187 #[instrument(skip(self))]
188 async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, database::Error> {
189 let conn = self
190 .pool
191 .get()
192 .await
193 .map_err(|e| Error::Database(Box::new(e)))?;
194 Ok(query(
195 r#"
196 SELECT
197 name,
198 pubkey,
199 version,
200 description,
201 description_long,
202 contact,
203 nuts,
204 icon_url,
205 motd,
206 urls,
207 mint_time,
208 tos_url
209 FROM
210 mint
211 WHERE mint_url = :mint_url
212 "#,
213 )?
214 .bind("mint_url", mint_url.to_string())
215 .fetch_one(&*conn)
216 .await?
217 .map(sql_row_to_mint_info)
218 .transpose()?)
219 }
220
221 #[instrument(skip(self))]
222 async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, database::Error> {
223 let conn = self
224 .pool
225 .get()
226 .await
227 .map_err(|e| Error::Database(Box::new(e)))?;
228 Ok(query(
229 r#"
230 SELECT
231 name,
232 pubkey,
233 version,
234 description,
235 description_long,
236 contact,
237 nuts,
238 icon_url,
239 motd,
240 urls,
241 mint_time,
242 tos_url,
243 mint_url
244 FROM
245 mint
246 "#,
247 )?
248 .fetch_all(&*conn)
249 .await?
250 .into_iter()
251 .map(|mut row| {
252 let url = column_as_string!(
253 row.pop().ok_or(ConversionError::MissingColumn(0, 1))?,
254 MintUrl::from_str
255 );
256
257 Ok((url, sql_row_to_mint_info(row).ok()))
258 })
259 .collect::<Result<HashMap<_, _>, Error>>()?)
260 }
261
262 #[instrument(skip(self))]
263 async fn get_mint_keysets(
264 &self,
265 mint_url: MintUrl,
266 ) -> Result<Option<Vec<KeySetInfo>>, database::Error> {
267 let conn = self
268 .pool
269 .get()
270 .await
271 .map_err(|e| Error::Database(Box::new(e)))?;
272
273 let keysets = query(
274 r#"
275 SELECT
276 id,
277 unit,
278 active,
279 input_fee_ppk,
280 final_expiry
281 FROM
282 keyset
283 WHERE mint_url = :mint_url
284 "#,
285 )?
286 .bind("mint_url", mint_url.to_string())
287 .fetch_all(&*conn)
288 .await?
289 .into_iter()
290 .map(sql_row_to_keyset)
291 .collect::<Result<Vec<_>, Error>>()?;
292
293 match keysets.is_empty() {
294 false => Ok(Some(keysets)),
295 true => Ok(None),
296 }
297 }
298
299 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
300 async fn get_keyset_by_id(
301 &self,
302 keyset_id: &Id,
303 ) -> Result<Option<KeySetInfo>, database::Error> {
304 let conn = self
305 .pool
306 .get()
307 .await
308 .map_err(|e| Error::Database(Box::new(e)))?;
309 query(
310 r#"
311 SELECT
312 id,
313 unit,
314 active,
315 input_fee_ppk,
316 final_expiry
317 FROM
318 keyset
319 WHERE id = :id
320 "#,
321 )?
322 .bind("id", keyset_id.to_string())
323 .fetch_one(&*conn)
324 .await?
325 .map(sql_row_to_keyset)
326 .transpose()
327 }
328
329 #[instrument(skip(self))]
330 async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, database::Error> {
331 let conn = self
332 .pool
333 .get()
334 .await
335 .map_err(|e| Error::Database(Box::new(e)))?;
336 query(
337 r#"
338 SELECT
339 id,
340 mint_url,
341 amount,
342 unit,
343 request,
344 state,
345 expiry,
346 secret_key,
347 payment_method,
348 amount_issued,
349 amount_paid,
350 updated_at,
351 estimated_blocks,
352 used_by_operation,
353 version
354 FROM
355 mint_quote
356 WHERE
357 id = :id
358 "#,
359 )?
360 .bind("id", quote_id.to_string())
361 .fetch_one(&*conn)
362 .await?
363 .map(sql_row_to_mint_quote)
364 .transpose()
365 }
366
367 #[instrument(skip(self))]
368 async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
369 let conn = self
370 .pool
371 .get()
372 .await
373 .map_err(|e| Error::Database(Box::new(e)))?;
374 Ok(query(
375 r#"
376 SELECT
377 id,
378 mint_url,
379 amount,
380 unit,
381 request,
382 state,
383 expiry,
384 secret_key,
385 payment_method,
386 amount_issued,
387 amount_paid,
388 updated_at,
389 estimated_blocks,
390 used_by_operation,
391 version
392 FROM
393 mint_quote
394 "#,
395 )?
396 .fetch_all(&*conn)
397 .await?
398 .into_iter()
399 .map(sql_row_to_mint_quote)
400 .collect::<Result<_, _>>()?)
401 }
402
403 #[instrument(skip(self))]
404 async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
405 let conn = self
406 .pool
407 .get()
408 .await
409 .map_err(|e| Error::Database(Box::new(e)))?;
410 Ok(query(
411 r#"
412 SELECT
413 id,
414 mint_url,
415 amount,
416 unit,
417 request,
418 state,
419 expiry,
420 secret_key,
421 payment_method,
422 amount_issued,
423 amount_paid,
424 updated_at,
425 estimated_blocks,
426 used_by_operation,
427 version
428 FROM
429 mint_quote
430 WHERE
431 amount_issued = 0
432 OR
433 payment_method = 'bolt12'
434 "#,
435 )?
436 .fetch_all(&*conn)
437 .await?
438 .into_iter()
439 .map(sql_row_to_mint_quote)
440 .collect::<Result<_, _>>()?)
441 }
442
443 #[instrument(skip(self))]
444 async fn get_melt_quote(
445 &self,
446 quote_id: &str,
447 ) -> Result<Option<wallet::MeltQuote>, database::Error> {
448 let conn = self
449 .pool
450 .get()
451 .await
452 .map_err(|e| Error::Database(Box::new(e)))?;
453 query(
454 r#"
455 SELECT
456 id,
457 unit,
458 amount,
459 request,
460 fee_reserve,
461 state,
462 expiry,
463 payment_proof,
464 payment_method,
465 estimated_blocks,
466 fee_index,
467 used_by_operation,
468 version,
469 mint_url
470 FROM
471 melt_quote
472 WHERE
473 id=:id
474 "#,
475 )?
476 .bind("id", quote_id.to_owned())
477 .fetch_one(&*conn)
478 .await?
479 .map(sql_row_to_melt_quote)
480 .transpose()
481 }
482
483 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
484 async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, database::Error> {
485 let conn = self
486 .pool
487 .get()
488 .await
489 .map_err(|e| Error::Database(Box::new(e)))?;
490 query(
491 r#"
492 SELECT
493 keys
494 FROM key
495 WHERE id = :id
496 "#,
497 )?
498 .bind("id", keyset_id.to_string())
499 .pluck(&*conn)
500 .await?
501 .map(|keys| {
502 let keys = column_as_string!(keys);
503 serde_json::from_str(&keys).map_err(Error::from)
504 })
505 .transpose()
506 }
507
508 #[instrument(skip(self, state, spending_conditions))]
509 async fn get_proofs(
510 &self,
511 mint_url: Option<MintUrl>,
512 unit: Option<CurrencyUnit>,
513 state: Option<Vec<State>>,
514 spending_conditions: Option<Vec<SpendingConditions>>,
515 ) -> Result<Vec<ProofInfo>, database::Error> {
516 let conn = self
517 .pool
518 .get()
519 .await
520 .map_err(|e| Error::Database(Box::new(e)))?;
521 Ok(query(
522 r#"
523 SELECT
524 amount,
525 unit,
526 keyset_id,
527 secret,
528 c,
529 witness,
530 dleq_e,
531 dleq_s,
532 dleq_r,
533 y,
534 mint_url,
535 state,
536 spending_condition,
537 used_by_operation,
538 created_by_operation,
539 p2pk_e
540 FROM proof
541 "#,
542 )?
543 .fetch_all(&*conn)
544 .await?
545 .into_iter()
546 .filter_map(|row| {
547 let row = sql_row_to_proof_info(row).ok()?;
548
549 if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) {
550 Some(row)
551 } else {
552 None
553 }
554 })
555 .collect::<Vec<_>>())
556 }
557
558 #[instrument(skip(self, ys))]
559 async fn get_proofs_by_ys(
560 &self,
561 ys: Vec<PublicKey>,
562 ) -> Result<Vec<ProofInfo>, database::Error> {
563 let conn = self
564 .pool
565 .get()
566 .await
567 .map_err(|e| Error::Database(Box::new(e)))?;
568 Ok(query(
569 r#"
570 SELECT
571 amount,
572 unit,
573 keyset_id,
574 secret,
575 c,
576 witness,
577 dleq_e,
578 dleq_s,
579 dleq_r,
580 y,
581 mint_url,
582 state,
583 spending_condition,
584 used_by_operation,
585 created_by_operation,
586 p2pk_e
587 FROM proof
588 WHERE y IN (:ys)
589 "#,
590 )?
591 .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
592 .fetch_all(&*conn)
593 .await?
594 .into_iter()
595 .filter_map(|row| sql_row_to_proof_info(row).ok())
596 .collect::<Vec<_>>())
597 }
598
599 async fn get_balance(
600 &self,
601 mint_url: Option<MintUrl>,
602 unit: Option<CurrencyUnit>,
603 states: Option<Vec<State>>,
604 ) -> Result<u64, database::Error> {
605 let conn = self
606 .pool
607 .get()
608 .await
609 .map_err(|e| Error::Database(Box::new(e)))?;
610
611 let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string();
612 let mut where_clauses = Vec::new();
613 let states = states
614 .unwrap_or_default()
615 .into_iter()
616 .map(|x| x.to_string())
617 .collect::<Vec<_>>();
618
619 if mint_url.is_some() {
620 where_clauses.push("mint_url = :mint_url");
621 }
622 if unit.is_some() {
623 where_clauses.push("unit = :unit");
624 }
625 if !states.is_empty() {
626 where_clauses.push("state IN (:states)");
627 }
628
629 if !where_clauses.is_empty() {
630 query_str.push_str(" WHERE ");
631 query_str.push_str(&where_clauses.join(" AND "));
632 }
633
634 let mut q = query(&query_str)?;
635
636 if let Some(ref mint_url) = mint_url {
637 q = q.bind("mint_url", mint_url.to_string());
638 }
639 if let Some(ref unit) = unit {
640 q = q.bind("unit", unit.to_string());
641 }
642
643 if !states.is_empty() {
644 q = q.bind_vec("states", states)?;
645 }
646
647 let balance = q
648 .pluck(&*conn)
649 .await?
650 .map(|n| {
651 match n {
653 crate::stmt::Column::Integer(i) => Ok(i as u64),
654 crate::stmt::Column::Real(f) => Ok(f as u64),
655 _ => Err(Error::Database(Box::new(std::io::Error::new(
656 std::io::ErrorKind::InvalidData,
657 "Invalid balance type",
658 )))),
659 }
660 })
661 .transpose()?
662 .unwrap_or(0);
663
664 Ok(balance)
665 }
666
667 #[instrument(skip(self))]
668 async fn get_transaction(
669 &self,
670 transaction_id: TransactionId,
671 ) -> Result<Option<Transaction>, database::Error> {
672 let conn = self
673 .pool
674 .get()
675 .await
676 .map_err(|e| Error::Database(Box::new(e)))?;
677 Ok(query(
678 r#"
679 SELECT
680 mint_url,
681 direction,
682 unit,
683 amount,
684 fee,
685 ys,
686 timestamp,
687 memo,
688 metadata,
689 quote_id,
690 payment_request,
691 payment_proof,
692 payment_method,
693 saga_id,
694 status
695 FROM
696 transactions
697 WHERE
698 id = :id
699 "#,
700 )?
701 .bind("id", transaction_id.as_slice().to_vec())
702 .fetch_one(&*conn)
703 .await?
704 .map(sql_row_to_transaction)
705 .transpose()?)
706 }
707
708 #[instrument(skip(self))]
709 async fn list_transactions(
710 &self,
711 mint_url: Option<MintUrl>,
712 direction: Option<TransactionDirection>,
713 unit: Option<CurrencyUnit>,
714 ) -> Result<Vec<Transaction>, database::Error> {
715 let conn = self
716 .pool
717 .get()
718 .await
719 .map_err(|e| Error::Database(Box::new(e)))?;
720
721 Ok(query(
722 r#"
723 SELECT
724 mint_url,
725 direction,
726 unit,
727 amount,
728 fee,
729 ys,
730 timestamp,
731 memo,
732 metadata,
733 quote_id,
734 payment_request,
735 payment_proof,
736 payment_method,
737 saga_id,
738 status
739 FROM
740 transactions
741 "#,
742 )?
743 .fetch_all(&*conn)
744 .await?
745 .into_iter()
746 .filter_map(|row| {
747 let transaction = sql_row_to_transaction(row).ok()?;
749 if transaction.matches_conditions(&mint_url, &direction, &unit) {
750 Some(transaction)
751 } else {
752 None
753 }
754 })
755 .collect::<Vec<_>>())
756 }
757
758 async fn update_proofs(
759 &self,
760 added: Vec<ProofInfo>,
761 removed_ys: Vec<PublicKey>,
762 ) -> Result<(), database::Error> {
763 let conn = self
764 .pool
765 .get()
766 .await
767 .map_err(|e| Error::Database(Box::new(e)))?;
768 let tx = ConnectionWithTransaction::new(conn).await?;
769
770 for proof in added {
771 query(
772 r#"
773 INSERT INTO proof
774 (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness, dleq_e, dleq_s, dleq_r, used_by_operation, created_by_operation, p2pk_e)
775 VALUES
776 (:y, :mint_url, :state, :spending_condition, :unit, :amount, :keyset_id, :secret, :c, :witness, :dleq_e, :dleq_s, :dleq_r, :used_by_operation, :created_by_operation, :p2pk_e)
777 ON CONFLICT(y) DO UPDATE SET
778 mint_url = excluded.mint_url,
779 state = excluded.state,
780 spending_condition = excluded.spending_condition,
781 unit = excluded.unit,
782 amount = excluded.amount,
783 keyset_id = excluded.keyset_id,
784 secret = excluded.secret,
785 c = excluded.c,
786 witness = excluded.witness,
787 dleq_e = excluded.dleq_e,
788 dleq_s = excluded.dleq_s,
789 dleq_r = excluded.dleq_r,
790 used_by_operation = excluded.used_by_operation,
791 created_by_operation = excluded.created_by_operation,
792 p2pk_e = excluded.p2pk_e
793 ;
794 "#,
795 )?
796 .bind("y", proof.y.to_bytes().to_vec())
797 .bind("mint_url", proof.mint_url.to_string())
798 .bind("state", proof.state.to_string())
799 .bind(
800 "spending_condition",
801 proof
802 .spending_condition
803 .map(|s| serde_json::to_string(&s).ok()),
804 )
805 .bind("unit", proof.unit.to_string())
806 .bind("amount", u64::from(proof.proof.amount) as i64)
807 .bind("keyset_id", proof.proof.keyset_id.to_string())
808 .bind("secret", proof.proof.secret.to_string())
809 .bind("c", proof.proof.c.to_bytes().to_vec())
810 .bind(
811 "witness",
812 proof
813 .proof
814 .witness
815 .and_then(|w| serde_json::to_string(&w).ok()),
816 )
817 .bind(
818 "dleq_e",
819 proof.proof.dleq.as_ref().map(|dleq| dleq.e.to_secret_bytes().to_vec()),
820 )
821 .bind(
822 "dleq_s",
823 proof.proof.dleq.as_ref().map(|dleq| dleq.s.to_secret_bytes().to_vec()),
824 )
825 .bind(
826 "dleq_r",
827 proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()),
828 )
829 .bind("used_by_operation", proof.used_by_operation.map(|id| id.to_string()))
830 .bind("created_by_operation", proof.created_by_operation.map(|id| id.to_string()))
831 .bind(
832 "p2pk_e",
833 proof
834 .proof
835 .p2pk_e
836 .as_ref()
837 .map(|pk| pk.to_bytes().to_vec()),
838 )
839 .execute(&tx)
840 .await?;
841 }
842
843 if !removed_ys.is_empty() {
844 query(r#"DELETE FROM proof WHERE y IN (:ys)"#)?
845 .bind_vec(
846 "ys",
847 removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(),
848 )?
849 .execute(&tx)
850 .await?;
851 }
852
853 tx.commit().await?;
854
855 Ok(())
856 }
857
858 #[instrument(skip(self))]
859 async fn update_proofs_state(
860 &self,
861 ys: Vec<PublicKey>,
862 state: State,
863 ) -> Result<(), database::Error> {
864 let conn = self
865 .pool
866 .get()
867 .await
868 .map_err(|e| Error::Database(Box::new(e)))?;
869
870 query("UPDATE proof SET state = :state WHERE y IN (:ys)")?
871 .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
872 .bind("state", state.to_string())
873 .execute(&*conn)
874 .await?;
875
876 Ok(())
877 }
878
879 #[instrument(
880 skip(self, transaction),
881 fields(
882 direction = %transaction.direction,
883 amount = %transaction.amount,
884 unit = %transaction.unit,
885 quote_id = ?transaction.quote_id,
886 saga_id = ?transaction.saga_id,
887 )
888 )]
889 async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> {
890 let conn = self
891 .pool
892 .get()
893 .await
894 .map_err(|e| Error::Database(Box::new(e)))?;
895
896 let mint_url = transaction.mint_url.to_string();
897 let direction = transaction.direction.to_string();
898 let unit = transaction.unit.to_string();
899 let amount = u64::from(transaction.amount) as i64;
900 let fee = u64::from(transaction.fee) as i64;
901 let ys = transaction
902 .ys
903 .iter()
904 .flat_map(|y| y.to_bytes().to_vec())
905 .collect::<Vec<_>>();
906
907 let id = transaction.id();
908
909 query(
910 r#"
911 INSERT INTO transactions
912 (id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata, quote_id, payment_request, payment_proof, payment_method, saga_id, status)
913 VALUES
914 (:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata, :quote_id, :payment_request, :payment_proof, :payment_method, :saga_id, :status)
915 ON CONFLICT(id) DO UPDATE SET
916 mint_url = excluded.mint_url,
917 direction = excluded.direction,
918 unit = excluded.unit,
919 amount = excluded.amount,
920 fee = excluded.fee,
921 timestamp = excluded.timestamp,
922 memo = excluded.memo,
923 metadata = excluded.metadata,
924 quote_id = excluded.quote_id,
925 payment_request = excluded.payment_request,
926 payment_proof = excluded.payment_proof,
927 payment_method = excluded.payment_method,
928 saga_id = excluded.saga_id,
929 status = excluded.status
930 ;
931 "#,
932 )?
933 .bind("id", id.as_slice().to_vec())
934 .bind("mint_url", mint_url)
935 .bind("direction", direction)
936 .bind("unit", unit)
937 .bind("amount", amount)
938 .bind("fee", fee)
939 .bind("ys", ys)
940 .bind("timestamp", transaction.timestamp as i64)
941 .bind("memo", transaction.memo)
942 .bind(
943 "metadata",
944 serde_json::to_string(&transaction.metadata).map_err(Error::from)?,
945 )
946 .bind("quote_id", transaction.quote_id)
947 .bind("payment_request", transaction.payment_request)
948 .bind("payment_proof", transaction.payment_proof)
949 .bind("payment_method", transaction.payment_method.map(|pm| pm.to_string()))
950 .bind("saga_id", transaction.saga_id.map(|id| id.to_string()))
951 .bind("status", transaction.status.to_string())
952 .execute(&*conn)
953 .await?;
954
955 Ok(())
956 }
957
958 #[instrument(skip(self))]
959 async fn update_mint_url(
960 &self,
961 old_mint_url: MintUrl,
962 new_mint_url: MintUrl,
963 ) -> Result<(), database::Error> {
964 let conn = self
965 .pool
966 .get()
967 .await
968 .map_err(|e| Error::Database(Box::new(e)))?;
969 let tx = ConnectionWithTransaction::new(conn).await?;
970 let tables = ["mint_quote", "proof"];
971
972 for table in &tables {
973 query(&format!(
974 r#"
975 UPDATE {table}
976 SET mint_url = :new_mint_url
977 WHERE mint_url = :old_mint_url
978 "#
979 ))?
980 .bind("new_mint_url", new_mint_url.to_string())
981 .bind("old_mint_url", old_mint_url.to_string())
982 .execute(&tx)
983 .await?;
984 }
985
986 tx.commit().await?;
987
988 Ok(())
989 }
990
991 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
992 async fn increment_keyset_counter(
993 &self,
994 keyset_id: &Id,
995 count: u32,
996 ) -> Result<u32, database::Error> {
997 let conn = self
998 .pool
999 .get()
1000 .await
1001 .map_err(|e| Error::Database(Box::new(e)))?;
1002
1003 let new_counter = query(
1004 r#"
1005 INSERT INTO keyset_counter (keyset_id, counter)
1006 VALUES (:keyset_id, :count)
1007 ON CONFLICT(keyset_id) DO UPDATE SET
1008 counter = keyset_counter.counter + :count
1009 RETURNING counter
1010 "#,
1011 )?
1012 .bind("keyset_id", keyset_id.to_string())
1013 .bind("count", count)
1014 .pluck(&*conn)
1015 .await?
1016 .map(|n| Ok::<_, Error>(column_as_number!(n)))
1017 .transpose()?
1018 .ok_or_else(|| Error::Internal("Counter update returned no value".to_owned()))?;
1019
1020 Ok(new_counter)
1021 }
1022
1023 #[instrument(skip(self))]
1024 async fn increment_derivation_counter(
1025 &self,
1026 namespace: &str,
1027 count: u32,
1028 ) -> Result<u32, database::Error> {
1029 let conn = self
1030 .pool
1031 .get()
1032 .await
1033 .map_err(|e| Error::Database(Box::new(e)))?;
1034
1035 let new_counter = query(
1036 r#"
1037 INSERT INTO derivation_counter (namespace, counter)
1038 VALUES (:namespace, :count)
1039 ON CONFLICT(namespace) DO UPDATE SET
1040 counter = derivation_counter.counter + :count
1041 RETURNING counter
1042 "#,
1043 )?
1044 .bind("namespace", namespace.to_owned())
1045 .bind("count", count)
1046 .pluck(&*conn)
1047 .await?
1048 .map(|n| Ok::<_, Error>(column_as_number!(n)))
1049 .transpose()?
1050 .ok_or_else(|| Error::Internal("Derivation counter update returned no value".to_owned()))?;
1051
1052 Ok(new_counter)
1053 }
1054
1055 #[instrument(skip(self, mint_info))]
1056 async fn add_mint(
1057 &self,
1058 mint_url: MintUrl,
1059 mint_info: Option<MintInfo>,
1060 ) -> Result<(), database::Error> {
1061 let conn = self
1062 .pool
1063 .get()
1064 .await
1065 .map_err(|e| Error::Database(Box::new(e)))?;
1066
1067 let (
1068 name,
1069 pubkey,
1070 version,
1071 description,
1072 description_long,
1073 contact,
1074 nuts,
1075 icon_url,
1076 urls,
1077 motd,
1078 time,
1079 tos_url,
1080 ) = match mint_info {
1081 Some(mint_info) => {
1082 let MintInfo {
1083 name,
1084 pubkey,
1085 version,
1086 description,
1087 description_long,
1088 contact,
1089 nuts,
1090 icon_url,
1091 urls,
1092 motd,
1093 time,
1094 tos_url,
1095 max_array_length: _,
1097 } = mint_info;
1098
1099 (
1100 name,
1101 pubkey.map(|p| p.to_bytes().to_vec()),
1102 version.map(|v| serde_json::to_string(&v).ok()),
1103 description,
1104 description_long,
1105 contact.map(|c| serde_json::to_string(&c).ok()),
1106 serde_json::to_string(&nuts).ok(),
1107 icon_url,
1108 urls.map(|c| serde_json::to_string(&c).ok()),
1109 motd,
1110 time,
1111 tos_url,
1112 )
1113 }
1114 None => (
1115 None, None, None, None, None, None, None, None, None, None, None, None,
1116 ),
1117 };
1118
1119 query(
1120 r#"
1121 INSERT INTO mint
1122 (
1123 mint_url, name, pubkey, version, description, description_long,
1124 contact, nuts, icon_url, urls, motd, mint_time, tos_url
1125 )
1126 VALUES
1127 (
1128 :mint_url, :name, :pubkey, :version, :description, :description_long,
1129 :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url
1130 )
1131 ON CONFLICT(mint_url) DO UPDATE SET
1132 name = excluded.name,
1133 pubkey = excluded.pubkey,
1134 version = excluded.version,
1135 description = excluded.description,
1136 description_long = excluded.description_long,
1137 contact = excluded.contact,
1138 nuts = excluded.nuts,
1139 icon_url = excluded.icon_url,
1140 urls = excluded.urls,
1141 motd = excluded.motd,
1142 mint_time = excluded.mint_time,
1143 tos_url = excluded.tos_url
1144 ;
1145 "#,
1146 )?
1147 .bind("mint_url", mint_url.to_string())
1148 .bind("name", name)
1149 .bind("pubkey", pubkey)
1150 .bind("version", version)
1151 .bind("description", description)
1152 .bind("description_long", description_long)
1153 .bind("contact", contact)
1154 .bind("nuts", nuts)
1155 .bind("icon_url", icon_url)
1156 .bind("urls", urls)
1157 .bind("motd", motd)
1158 .bind("mint_time", time.map(|v| v as i64))
1159 .bind("tos_url", tos_url)
1160 .execute(&*conn)
1161 .await?;
1162
1163 Ok(())
1164 }
1165
1166 #[instrument(skip(self))]
1167 async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
1168 let conn = self
1169 .pool
1170 .get()
1171 .await
1172 .map_err(|e| Error::Database(Box::new(e)))?;
1173
1174 query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)?
1175 .bind("mint_url", mint_url.to_string())
1176 .execute(&*conn)
1177 .await?;
1178
1179 Ok(())
1180 }
1181
1182 #[instrument(skip(self, keysets))]
1183 async fn add_mint_keysets(
1184 &self,
1185 mint_url: MintUrl,
1186 keysets: Vec<KeySetInfo>,
1187 ) -> Result<(), database::Error> {
1188 let conn = self
1189 .pool
1190 .get()
1191 .await
1192 .map_err(|e| Error::Database(Box::new(e)))?;
1193 let tx = ConnectionWithTransaction::new(conn).await?;
1194
1195 for keyset in keysets {
1196 query(
1197 r#"
1198 INSERT INTO keyset
1199 (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32)
1200 VALUES
1201 (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32)
1202 ON CONFLICT(id) DO UPDATE SET
1203 active = excluded.active,
1204 input_fee_ppk = excluded.input_fee_ppk
1205 "#,
1206 )?
1207 .bind("mint_url", mint_url.to_string())
1208 .bind("id", keyset.id.to_string())
1209 .bind("unit", keyset.unit.to_string())
1210 .bind("active", keyset.active)
1211 .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
1212 .bind("final_expiry", keyset.final_expiry.map(|v| v as i64))
1213 .bind("keyset_u32", u32::from(keyset.id))
1214 .execute(&tx)
1215 .await?;
1216 }
1217
1218 tx.commit().await?;
1219
1220 Ok(())
1221 }
1222
1223 #[instrument(skip_all)]
1224 async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
1225 let conn = self
1226 .pool
1227 .get()
1228 .await
1229 .map_err(|e| Error::Database(Box::new(e)))?;
1230
1231 let expected_version = quote.version;
1232 let new_version = expected_version.wrapping_add(1);
1233
1234 let rows_affected = query(
1235 r#"
1236 INSERT INTO mint_quote
1237 (id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid, updated_at, estimated_blocks, version, used_by_operation)
1238 VALUES
1239 (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid, :updated_at, :estimated_blocks, :version, :used_by_operation)
1240 ON CONFLICT(id) DO UPDATE SET
1241 mint_url = excluded.mint_url,
1242 amount = excluded.amount,
1243 unit = excluded.unit,
1244 request = excluded.request,
1245 state = excluded.state,
1246 expiry = excluded.expiry,
1247 secret_key = excluded.secret_key,
1248 payment_method = excluded.payment_method,
1249 amount_issued = excluded.amount_issued,
1250 amount_paid = excluded.amount_paid,
1251 updated_at = excluded.updated_at,
1252 estimated_blocks = excluded.estimated_blocks,
1253 version = :new_version,
1254 used_by_operation = excluded.used_by_operation
1255 WHERE mint_quote.version = :expected_version
1256 ;
1257 "#,
1258 )?
1259 .bind("id", quote.id.to_string())
1260 .bind("mint_url", quote.mint_url.to_string())
1261 .bind("amount", quote.amount.map(|a| a.to_i64()))
1262 .bind("unit", quote.unit.to_string())
1263 .bind("request", quote.request)
1264 .bind("state", quote.state.to_string())
1265 .bind("expiry", quote.expiry as i64)
1266 .bind(
1267 "secret_key",
1268 quote.secret_key.map(|key| key.to_secret_hex()),
1269 )
1270 .bind("payment_method", quote.payment_method.to_string())
1271 .bind("amount_issued", quote.amount_issued.to_i64())
1272 .bind("amount_paid", quote.amount_paid.to_i64())
1273 .bind("updated_at", quote.updated_at as i64)
1274 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1275 .bind("version", quote.version as i64)
1276 .bind("new_version", new_version as i64)
1277 .bind("expected_version", expected_version as i64)
1278 .bind("used_by_operation", quote.used_by_operation)
1279 .execute(&*conn).await?;
1280
1281 if rows_affected == 0 {
1282 return Err(database::Error::ConcurrentUpdate);
1283 }
1284
1285 Ok(())
1286 }
1287
1288 #[instrument(skip(self))]
1289 async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1290 let conn = self
1291 .pool
1292 .get()
1293 .await
1294 .map_err(|e| Error::Database(Box::new(e)))?;
1295
1296 query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
1297 .bind("id", quote_id.to_string())
1298 .execute(&*conn)
1299 .await?;
1300
1301 Ok(())
1302 }
1303
1304 #[instrument(skip_all)]
1305 async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
1306 let conn = self
1307 .pool
1308 .get()
1309 .await
1310 .map_err(|e| Error::Database(Box::new(e)))?;
1311
1312 let expected_version = quote.version;
1313 let new_version = expected_version.wrapping_add(1);
1314
1315 let rows_affected = query(
1316 r#"
1317 INSERT INTO melt_quote
1318 (id, unit, amount, request, fee_reserve, state, expiry, payment_proof, payment_method, estimated_blocks, fee_index, version, mint_url, used_by_operation)
1319 VALUES
1320 (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_proof, :payment_method, :estimated_blocks, :fee_index, :version, :mint_url, :used_by_operation)
1321 ON CONFLICT(id) DO UPDATE SET
1322 unit = excluded.unit,
1323 amount = excluded.amount,
1324 request = excluded.request,
1325 fee_reserve = excluded.fee_reserve,
1326 state = excluded.state,
1327 expiry = excluded.expiry,
1328 payment_proof = COALESCE(excluded.payment_proof, melt_quote.payment_proof),
1329 payment_method = excluded.payment_method,
1330 estimated_blocks = excluded.estimated_blocks,
1331 fee_index = excluded.fee_index,
1332 version = :new_version,
1333 mint_url = excluded.mint_url,
1334 used_by_operation = excluded.used_by_operation
1335 WHERE melt_quote.version = :expected_version
1336 ;
1337 "#,
1338 )?
1339 .bind("id", quote.id.to_string())
1340 .bind("unit", quote.unit.to_string())
1341 .bind("amount", u64::from(quote.amount) as i64)
1342 .bind("request", quote.request)
1343 .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
1344 .bind("state", quote.state.to_string())
1345 .bind("expiry", quote.expiry as i64)
1346 .bind("payment_proof", quote.payment_proof)
1347 .bind("payment_method", quote.payment_method.to_string())
1348 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1349 .bind("fee_index", quote.fee_index.map(i64::from))
1350 .bind("version", quote.version as i64)
1351 .bind("new_version", new_version as i64)
1352 .bind("expected_version", expected_version as i64)
1353 .bind("mint_url", quote.mint_url.map(|m| m.to_string()))
1354 .bind("used_by_operation", quote.used_by_operation)
1355 .execute(&*conn)
1356 .await?;
1357
1358 if rows_affected == 0 {
1359 return Err(database::Error::ConcurrentUpdate);
1360 }
1361
1362 Ok(())
1363 }
1364
1365 #[instrument(skip(self))]
1366 async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1367 let conn = self
1368 .pool
1369 .get()
1370 .await
1371 .map_err(|e| Error::Database(Box::new(e)))?;
1372
1373 query(r#"DELETE FROM melt_quote WHERE id=:id"#)?
1374 .bind("id", quote_id.to_owned())
1375 .execute(&*conn)
1376 .await?;
1377
1378 Ok(())
1379 }
1380
1381 #[instrument(skip_all)]
1382 async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
1383 let conn = self
1384 .pool
1385 .get()
1386 .await
1387 .map_err(|e| Error::Database(Box::new(e)))?;
1388
1389 keyset.verify_id()?;
1390
1391 query(
1392 r#"
1393 INSERT INTO key
1394 (id, keys, keyset_u32)
1395 VALUES
1396 (:id, :keys, :keyset_u32)
1397 "#,
1398 )?
1399 .bind("id", keyset.id.to_string())
1400 .bind(
1401 "keys",
1402 serde_json::to_string(&keyset.keys).map_err(Error::from)?,
1403 )
1404 .bind("keyset_u32", u32::from(keyset.id))
1405 .execute(&*conn)
1406 .await?;
1407
1408 Ok(())
1409 }
1410
1411 #[instrument(skip(self))]
1412 async fn remove_keys(&self, id: &Id) -> Result<(), database::Error> {
1413 let conn = self
1414 .pool
1415 .get()
1416 .await
1417 .map_err(|e| Error::Database(Box::new(e)))?;
1418
1419 query(r#"DELETE FROM key WHERE id = :id"#)?
1420 .bind("id", id.to_string())
1421 .execute(&*conn)
1422 .await?;
1423
1424 Ok(())
1425 }
1426
1427 #[instrument(skip(self))]
1428 async fn remove_transaction(
1429 &self,
1430 transaction_id: TransactionId,
1431 ) -> Result<(), database::Error> {
1432 let conn = self
1433 .pool
1434 .get()
1435 .await
1436 .map_err(|e| Error::Database(Box::new(e)))?;
1437
1438 query(r#"DELETE FROM transactions WHERE id=:id"#)?
1439 .bind("id", transaction_id.as_slice().to_vec())
1440 .execute(&*conn)
1441 .await?;
1442
1443 Ok(())
1444 }
1445
1446 #[instrument(skip(self))]
1447 async fn add_saga(&self, saga: wallet::WalletSaga) -> Result<(), database::Error> {
1448 let conn = self
1449 .pool
1450 .get()
1451 .await
1452 .map_err(|e| Error::Database(Box::new(e)))?;
1453
1454 let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1455 Error::Database(Box::new(std::io::Error::new(
1456 std::io::ErrorKind::InvalidData,
1457 format!("Failed to serialize saga state: {}", e),
1458 )))
1459 })?;
1460
1461 let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1462 Error::Database(Box::new(std::io::Error::new(
1463 std::io::ErrorKind::InvalidData,
1464 format!("Failed to serialize saga data: {}", e),
1465 )))
1466 })?;
1467
1468 query(
1469 r#"
1470 INSERT INTO wallet_sagas
1471 (id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version)
1472 VALUES
1473 (:id, :kind, :state, :amount, :mint_url, :unit, :quote_id, :created_at, :updated_at, :data, :version)
1474 "#,
1475 )?
1476 .bind("id", saga.id.to_string())
1477 .bind("kind", saga.kind.to_string())
1478 .bind("state", state_json)
1479 .bind("amount", u64::from(saga.amount) as i64)
1480 .bind("mint_url", saga.mint_url.to_string())
1481 .bind("unit", saga.unit.to_string())
1482 .bind("quote_id", saga.quote_id)
1483 .bind("created_at", saga.created_at as i64)
1484 .bind("updated_at", saga.updated_at as i64)
1485 .bind("data", data_json)
1486 .bind("version", saga.version as i64)
1487 .execute(&*conn)
1488 .await?;
1489
1490 Ok(())
1491 }
1492
1493 #[instrument(skip(self))]
1494 async fn get_saga(
1495 &self,
1496 id: &uuid::Uuid,
1497 ) -> Result<Option<wallet::WalletSaga>, database::Error> {
1498 let conn = self
1499 .pool
1500 .get()
1501 .await
1502 .map_err(|e| Error::Database(Box::new(e)))?;
1503
1504 let rows = query(
1505 r#"
1506 SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1507 FROM wallet_sagas
1508 WHERE id = :id
1509 "#,
1510 )?
1511 .bind("id", id.to_string())
1512 .fetch_all(&*conn)
1513 .await?;
1514
1515 match rows.into_iter().next() {
1516 Some(row) => Ok(Some(sql_row_to_wallet_saga(row)?)),
1517 None => Ok(None),
1518 }
1519 }
1520
1521 #[instrument(skip(self))]
1522 async fn update_saga(&self, saga: wallet::WalletSaga) -> Result<bool, database::Error> {
1523 let conn = self
1524 .pool
1525 .get()
1526 .await
1527 .map_err(|e| Error::Database(Box::new(e)))?;
1528
1529 let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1530 Error::Database(Box::new(std::io::Error::new(
1531 std::io::ErrorKind::InvalidData,
1532 format!("Failed to serialize saga state: {}", e),
1533 )))
1534 })?;
1535
1536 let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1537 Error::Database(Box::new(std::io::Error::new(
1538 std::io::ErrorKind::InvalidData,
1539 format!("Failed to serialize saga data: {}", e),
1540 )))
1541 })?;
1542
1543 let expected_version = saga.version.saturating_sub(1);
1547
1548 let rows_affected = query(
1549 r#"
1550 UPDATE wallet_sagas
1551 SET kind = :kind, state = :state, amount = :amount, mint_url = :mint_url,
1552 unit = :unit, quote_id = :quote_id, updated_at = :updated_at, data = :data,
1553 version = :new_version
1554 WHERE id = :id AND version = :expected_version
1555 "#,
1556 )?
1557 .bind("id", saga.id.to_string())
1558 .bind("kind", saga.kind.to_string())
1559 .bind("state", state_json)
1560 .bind("amount", u64::from(saga.amount) as i64)
1561 .bind("mint_url", saga.mint_url.to_string())
1562 .bind("unit", saga.unit.to_string())
1563 .bind("quote_id", saga.quote_id)
1564 .bind("updated_at", saga.updated_at as i64)
1565 .bind("data", data_json)
1566 .bind("new_version", saga.version as i64)
1567 .bind("expected_version", expected_version as i64)
1568 .execute(&*conn)
1569 .await?;
1570
1571 Ok(rows_affected > 0)
1573 }
1574
1575 #[instrument(skip(self))]
1576 async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), database::Error> {
1577 let conn = self
1578 .pool
1579 .get()
1580 .await
1581 .map_err(|e| Error::Database(Box::new(e)))?;
1582
1583 query(r#"DELETE FROM wallet_sagas WHERE id = :id"#)?
1584 .bind("id", id.to_string())
1585 .execute(&*conn)
1586 .await?;
1587
1588 Ok(())
1589 }
1590
1591 #[instrument(skip(self))]
1592 async fn get_incomplete_sagas(&self) -> Result<Vec<wallet::WalletSaga>, database::Error> {
1593 let conn = self
1594 .pool
1595 .get()
1596 .await
1597 .map_err(|e| Error::Database(Box::new(e)))?;
1598
1599 let rows = query(
1600 r#"
1601 SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1602 FROM wallet_sagas
1603 ORDER BY created_at ASC
1604 "#,
1605 )?
1606 .fetch_all(&*conn)
1607 .await?;
1608
1609 rows.into_iter().map(sql_row_to_wallet_saga).collect()
1610 }
1611
1612 #[instrument(skip(self))]
1613 async fn reserve_proofs(
1614 &self,
1615 ys: Vec<PublicKey>,
1616 operation_id: &uuid::Uuid,
1617 ) -> Result<(), database::Error> {
1618 let conn = self
1619 .pool
1620 .get()
1621 .await
1622 .map_err(|e| Error::Database(Box::new(e)))?;
1623
1624 if ys.is_empty() {
1625 return Ok(());
1626 }
1627
1628 let expected = ys.len();
1629 let tx = ConnectionWithTransaction::new(conn).await?;
1630
1631 let rows_affected = query(
1632 r#"
1633 UPDATE proof
1634 SET state = 'RESERVED', used_by_operation = :operation_id
1635 WHERE y IN (:ys) AND state = 'UNSPENT'
1636 "#,
1637 )?
1638 .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
1639 .bind("operation_id", operation_id.to_string())
1640 .execute(&tx)
1641 .await?;
1642
1643 if rows_affected != expected {
1646 tx.rollback().await?;
1647 return Err(database::Error::ProofNotUnspent);
1648 }
1649
1650 tx.commit().await?;
1651
1652 Ok(())
1653 }
1654
1655 #[instrument(skip(self))]
1656 async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1657 let conn = self
1658 .pool
1659 .get()
1660 .await
1661 .map_err(|e| Error::Database(Box::new(e)))?;
1662
1663 query(
1664 r#"
1665 UPDATE proof
1666 SET state = 'UNSPENT', used_by_operation = NULL
1667 WHERE used_by_operation = :operation_id
1668 AND state IN ('RESERVED', 'PENDING')
1669 "#,
1670 )?
1671 .bind("operation_id", operation_id.to_string())
1672 .execute(&*conn)
1673 .await?;
1674
1675 Ok(())
1676 }
1677
1678 #[instrument(skip(self))]
1679 async fn get_reserved_proofs(
1680 &self,
1681 operation_id: &uuid::Uuid,
1682 ) -> Result<Vec<ProofInfo>, database::Error> {
1683 let conn = self
1684 .pool
1685 .get()
1686 .await
1687 .map_err(|e| Error::Database(Box::new(e)))?;
1688
1689 let rows = query(
1690 r#"
1691 SELECT
1692 amount,
1693 unit,
1694 keyset_id,
1695 secret,
1696 c,
1697 witness,
1698 dleq_e,
1699 dleq_s,
1700 dleq_r,
1701 y,
1702 mint_url,
1703 state,
1704 spending_condition,
1705 used_by_operation,
1706 created_by_operation,
1707 p2pk_e
1708 FROM proof
1709 WHERE used_by_operation = :operation_id
1710 "#,
1711 )?
1712 .bind("operation_id", operation_id.to_string())
1713 .fetch_all(&*conn)
1714 .await?;
1715
1716 rows.into_iter().map(sql_row_to_proof_info).collect()
1717 }
1718
1719 #[instrument(skip(self))]
1720 async fn reserve_melt_quote(
1721 &self,
1722 quote_id: &str,
1723 operation_id: &uuid::Uuid,
1724 ) -> Result<(), database::Error> {
1725 let conn = self
1726 .pool
1727 .get()
1728 .await
1729 .map_err(|e| Error::Database(Box::new(e)))?;
1730
1731 let rows_affected = query(
1732 r#"
1733 UPDATE melt_quote
1734 SET used_by_operation = :operation_id
1735 WHERE id = :quote_id AND used_by_operation IS NULL
1736 "#,
1737 )?
1738 .bind("operation_id", operation_id.to_string())
1739 .bind("quote_id", quote_id)
1740 .execute(&*conn)
1741 .await?;
1742
1743 if rows_affected == 0 {
1744 let exists = query(
1746 r#"
1747 SELECT 1 FROM melt_quote WHERE id = :quote_id
1748 "#,
1749 )?
1750 .bind("quote_id", quote_id)
1751 .fetch_one(&*conn)
1752 .await?;
1753
1754 if exists.is_none() {
1755 return Err(database::Error::UnknownQuote);
1756 }
1757 return Err(database::Error::QuoteAlreadyInUse);
1758 }
1759
1760 Ok(())
1761 }
1762
1763 #[instrument(skip(self))]
1764 async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1765 let conn = self
1766 .pool
1767 .get()
1768 .await
1769 .map_err(|e| Error::Database(Box::new(e)))?;
1770
1771 query(
1772 r#"
1773 UPDATE melt_quote
1774 SET used_by_operation = NULL
1775 WHERE used_by_operation = :operation_id
1776 "#,
1777 )?
1778 .bind("operation_id", operation_id.to_string())
1779 .execute(&*conn)
1780 .await?;
1781
1782 Ok(())
1783 }
1784
1785 #[instrument(skip(self))]
1786 async fn reserve_mint_quote(
1787 &self,
1788 quote_id: &str,
1789 operation_id: &uuid::Uuid,
1790 ) -> Result<(), database::Error> {
1791 let conn = self
1792 .pool
1793 .get()
1794 .await
1795 .map_err(|e| Error::Database(Box::new(e)))?;
1796
1797 let rows_affected = query(
1798 r#"
1799 UPDATE mint_quote
1800 SET used_by_operation = :operation_id
1801 WHERE id = :quote_id AND used_by_operation IS NULL
1802 "#,
1803 )?
1804 .bind("operation_id", operation_id.to_string())
1805 .bind("quote_id", quote_id)
1806 .execute(&*conn)
1807 .await?;
1808
1809 if rows_affected == 0 {
1810 let exists = query(
1812 r#"
1813 SELECT 1 FROM mint_quote WHERE id = :quote_id
1814 "#,
1815 )?
1816 .bind("quote_id", quote_id)
1817 .fetch_one(&*conn)
1818 .await?;
1819
1820 if exists.is_none() {
1821 return Err(database::Error::UnknownQuote);
1822 }
1823 return Err(database::Error::QuoteAlreadyInUse);
1824 }
1825
1826 Ok(())
1827 }
1828
1829 #[instrument(skip(self))]
1830 async fn release_mint_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1831 let conn = self
1832 .pool
1833 .get()
1834 .await
1835 .map_err(|e| Error::Database(Box::new(e)))?;
1836
1837 query(
1838 r#"
1839 UPDATE mint_quote
1840 SET used_by_operation = NULL
1841 WHERE used_by_operation = :operation_id
1842 "#,
1843 )?
1844 .bind("operation_id", operation_id.to_string())
1845 .execute(&*conn)
1846 .await?;
1847
1848 Ok(())
1849 }
1850
1851 async fn kv_read(
1852 &self,
1853 primary_namespace: &str,
1854 secondary_namespace: &str,
1855 key: &str,
1856 ) -> Result<Option<Vec<u8>>, database::Error> {
1857 crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
1858 }
1859
1860 async fn kv_list(
1861 &self,
1862 primary_namespace: &str,
1863 secondary_namespace: &str,
1864 ) -> Result<Vec<String>, database::Error> {
1865 crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
1866 }
1867
1868 async fn kv_write(
1869 &self,
1870 primary_namespace: &str,
1871 secondary_namespace: &str,
1872 key: &str,
1873 value: &[u8],
1874 ) -> Result<(), database::Error> {
1875 let conn = self
1876 .pool
1877 .get()
1878 .await
1879 .map_err(|e| Error::Database(Box::new(e)))?;
1880 crate::keyvalue::kv_write_standalone(
1881 &*conn,
1882 primary_namespace,
1883 secondary_namespace,
1884 key,
1885 value,
1886 )
1887 .await?;
1888 Ok(())
1889 }
1890
1891 async fn kv_remove(
1892 &self,
1893 primary_namespace: &str,
1894 secondary_namespace: &str,
1895 key: &str,
1896 ) -> Result<(), database::Error> {
1897 let conn = self
1898 .pool
1899 .get()
1900 .await
1901 .map_err(|e| Error::Database(Box::new(e)))?;
1902 crate::keyvalue::kv_remove_standalone(&*conn, primary_namespace, secondary_namespace, key)
1903 .await?;
1904 Ok(())
1905 }
1906
1907 #[instrument(skip(self))]
1910 async fn add_p2pk_key(
1911 &self,
1912 pubkey: &PublicKey,
1913 derivation_path: DerivationPath,
1914 derivation_index: u32,
1915 ) -> Result<(), Error> {
1916 let conn = self
1917 .pool
1918 .get()
1919 .await
1920 .map_err(|e| Error::Database(Box::new(e)))?;
1921 let query_str = r#"
1922 INSERT INTO p2pk_signing_key (pubkey, derivation_index, derivation_path, created_time)
1923 VALUES (:pubkey, :derivation_index, :derivation_path, :created_time)
1924 "#
1925 .to_string();
1926
1927 query(&query_str)?
1928 .bind("pubkey", pubkey.to_bytes().to_vec())
1929 .bind("derivation_index", derivation_index)
1930 .bind("derivation_path", derivation_path.to_string())
1931 .bind("created_time", unix_time() as i64)
1932 .execute(&*conn)
1933 .await?;
1934
1935 Ok(())
1936 }
1937
1938 #[instrument(skip(self))]
1939 async fn get_p2pk_key(
1940 &self,
1941 pubkey: &PublicKey,
1942 ) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1943 let conn = self
1944 .pool
1945 .get()
1946 .await
1947 .map_err(|e| Error::Database(Box::new(e)))?;
1948 let query_str = r#"SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key WHERE pubkey = :pubkey"#.to_string();
1949
1950 query(&query_str)?
1951 .bind("pubkey", pubkey.to_bytes().to_vec())
1952 .fetch_one(&*conn)
1953 .await?
1954 .map(sql_row_to_p2pk_signing_key)
1955 .transpose()
1956 }
1957
1958 #[instrument(skip(self))]
1959 async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, Error> {
1960 let conn = self
1961 .pool
1962 .get()
1963 .await
1964 .map_err(|e| Error::Database(Box::new(e)))?;
1965 let query_str = r#"
1966 SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC
1967 "#.to_string();
1968
1969 Ok(query(&query_str)?
1970 .fetch_all(&*conn)
1971 .await?
1972 .into_iter()
1973 .filter_map(|row| {
1974 let row = sql_row_to_p2pk_signing_key(row).ok()?;
1975
1976 Some(row)
1977 })
1978 .collect::<Vec<wallet::P2PKSigningKey>>())
1979 }
1980
1981 #[instrument(skip(self))]
1982 async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1983 let conn = self
1984 .pool
1985 .get()
1986 .await
1987 .map_err(|e| Error::Database(Box::new(e)))?;
1988 let query_str = r#"
1989 SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC LIMIT 1
1990 "#.to_string();
1991
1992 query(&query_str)?
1993 .fetch_one(&*conn)
1994 .await?
1995 .map(sql_row_to_p2pk_signing_key)
1996 .transpose()
1997 }
1998}
1999
2000fn sql_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
2001 unpack_into!(
2002 let (
2003 name,
2004 pubkey,
2005 version,
2006 description,
2007 description_long,
2008 contact,
2009 nuts,
2010 icon_url,
2011 motd,
2012 urls,
2013 mint_time,
2014 tos_url
2015 ) = row
2016 );
2017
2018 Ok(MintInfo {
2019 name: column_as_nullable_string!(&name),
2020 pubkey: column_as_nullable_binary!(&pubkey)
2021 .map(|bytes| cdk_common::nuts::PublicKey::from_slice(&bytes))
2022 .transpose()?,
2023 version: column_as_nullable_string!(&version).and_then(|v| serde_json::from_str(&v).ok()),
2024 description: column_as_nullable_string!(description),
2025 description_long: column_as_nullable_string!(description_long),
2026 contact: column_as_nullable_string!(contact, |v| serde_json::from_str(&v).ok()),
2027 nuts: column_as_nullable_string!(nuts, |v| serde_json::from_str(&v).ok())
2028 .unwrap_or_default(),
2029 urls: column_as_nullable_string!(urls, |v| serde_json::from_str(&v).ok()),
2030 icon_url: column_as_nullable_string!(icon_url),
2031 motd: column_as_nullable_string!(motd),
2032 time: column_as_nullable_number!(mint_time).map(|t| t),
2033 tos_url: column_as_nullable_string!(tos_url),
2034 max_array_length: None,
2035 })
2036}
2037
2038#[instrument(skip_all)]
2039fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
2040 unpack_into!(
2041 let (
2042 id,
2043 unit,
2044 active,
2045 input_fee_ppk,
2046 final_expiry
2047 ) = row
2048 );
2049
2050 Ok(KeySetInfo {
2051 id: column_as_string!(id, Id::from_str, Id::from_bytes),
2052 unit: column_as_string!(unit, CurrencyUnit::from_str),
2053 active: matches!(active, Column::Integer(1)),
2054 input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or(0),
2055 final_expiry: column_as_nullable_number!(final_expiry),
2056 })
2057}
2058
2059fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
2060 unpack_into!(
2061 let (
2062 id,
2063 mint_url,
2064 amount,
2065 unit,
2066 request,
2067 state,
2068 expiry,
2069 secret_key,
2070 row_method,
2071 row_amount_minted,
2072 row_amount_paid,
2073 updated_at,
2074 estimated_blocks,
2075 used_by_operation,
2076 version
2077 ) = row
2078 );
2079
2080 let amount: Option<i64> = column_as_nullable_number!(amount);
2081
2082 let amount_paid: u64 = column_as_number!(row_amount_paid);
2083 let amount_minted: u64 = column_as_number!(row_amount_minted);
2084 let expiry_val: u64 = column_as_number!(expiry);
2085 let updated_at: u64 = column_as_number!(updated_at);
2086 let version_val: u32 = column_as_number!(version);
2087 let payment_method =
2088 PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2089
2090 Ok(MintQuote {
2091 id: column_as_string!(id),
2092 mint_url: column_as_string!(mint_url, MintUrl::from_str),
2093 amount: amount.and_then(Amount::from_i64),
2094 unit: column_as_string!(unit, CurrencyUnit::from_str),
2095 request: column_as_string!(request),
2096 state: column_as_string!(state, MintQuoteState::from_str),
2097 expiry: expiry_val,
2098 secret_key: column_as_nullable_string!(secret_key, |s| SecretKey::from_str(&s).ok()),
2099 payment_method,
2100 amount_issued: Amount::from(amount_minted),
2101 amount_paid: Amount::from(amount_paid),
2102 updated_at,
2103 estimated_blocks: column_as_nullable_number!(estimated_blocks),
2104 used_by_operation: column_as_nullable_string!(used_by_operation),
2105 version: version_val,
2106 })
2107}
2108
2109fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
2110 unpack_into!(
2111 let (
2112 id,
2113 unit,
2114 amount,
2115 request,
2116 fee_reserve,
2117 state,
2118 expiry,
2119 payment_proof,
2120 row_method,
2121 estimated_blocks,
2122 fee_index,
2123 used_by_operation,
2124 version,
2125 mint_url
2126 ) = row
2127 );
2128
2129 let payment_method =
2130 PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2131
2132 let amount_val: u64 = column_as_number!(amount);
2133 let fee_reserve_val: u64 = column_as_number!(fee_reserve);
2134 let expiry_val: u64 = column_as_number!(expiry);
2135 let version_val: u32 = column_as_number!(version);
2136
2137 Ok(wallet::MeltQuote {
2138 id: column_as_string!(id),
2139 mint_url: column_as_nullable_string!(mint_url, |s| MintUrl::from_str(&s).ok()),
2140 unit: column_as_string!(unit, CurrencyUnit::from_str),
2141 amount: Amount::from(amount_val),
2142 request: column_as_string!(request),
2143 fee_reserve: Amount::from(fee_reserve_val),
2144 state: column_as_string!(state, MeltQuoteState::from_str),
2145 expiry: expiry_val,
2146 payment_proof: column_as_nullable_string!(payment_proof),
2147 estimated_blocks: column_as_nullable_number!(estimated_blocks),
2148 fee_index: column_as_nullable_number!(fee_index),
2149 payment_method,
2150 used_by_operation: column_as_nullable_string!(used_by_operation),
2151 version: version_val,
2152 })
2153}
2154
2155fn sql_row_to_proof_info(row: Vec<Column>) -> Result<ProofInfo, Error> {
2156 unpack_into!(
2157 let (
2158 amount,
2159 unit,
2160 keyset_id,
2161 secret,
2162 c,
2163 witness,
2164 dleq_e,
2165 dleq_s,
2166 dleq_r,
2167 y,
2168 mint_url,
2169 state,
2170 spending_condition,
2171 used_by_operation,
2172 created_by_operation,
2173 p2pk_e
2174 ) = row
2175 );
2176
2177 let dleq = match (
2178 column_as_nullable_binary!(dleq_e),
2179 column_as_nullable_binary!(dleq_s),
2180 column_as_nullable_binary!(dleq_r),
2181 ) {
2182 (Some(e), Some(s), Some(r)) => {
2183 let e_key = SecretKey::from_slice(&e)?;
2184 let s_key = SecretKey::from_slice(&s)?;
2185 let r_key = SecretKey::from_slice(&r)?;
2186
2187 Some(ProofDleq::new(e_key, s_key, r_key))
2188 }
2189 _ => None,
2190 };
2191
2192 let amount: u64 = column_as_number!(amount);
2193 let proof = Proof {
2194 amount: Amount::from(amount),
2195 keyset_id: column_as_string!(keyset_id, Id::from_str),
2196 secret: column_as_string!(secret, Secret::from_str),
2197 witness: column_as_nullable_string!(witness, |v| { serde_json::from_str(&v).ok() }, |v| {
2198 serde_json::from_slice(&v).ok()
2199 }),
2200 c: column_as_string!(c, PublicKey::from_str, PublicKey::from_slice),
2201 dleq,
2202 p2pk_e: column_as_nullable_binary!(p2pk_e)
2203 .map(|bytes| PublicKey::from_slice(&bytes))
2204 .transpose()?,
2205 };
2206
2207 let used_by_operation =
2208 column_as_nullable_string!(used_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2209 let created_by_operation =
2210 column_as_nullable_string!(created_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2211
2212 Ok(ProofInfo {
2213 proof,
2214 y: column_as_string!(y, PublicKey::from_str, PublicKey::from_slice),
2215 mint_url: column_as_string!(mint_url, MintUrl::from_str),
2216 state: column_as_string!(state, State::from_str),
2217 spending_condition: column_as_nullable_string!(
2218 spending_condition,
2219 |r| { serde_json::from_str(&r).ok() },
2220 |r| { serde_json::from_slice(&r).ok() }
2221 ),
2222 unit: column_as_string!(unit, CurrencyUnit::from_str),
2223 used_by_operation,
2224 created_by_operation,
2225 })
2226}
2227
2228fn sql_row_to_wallet_saga(row: Vec<Column>) -> Result<wallet::WalletSaga, Error> {
2229 unpack_into!(
2230 let (
2231 id,
2232 kind,
2233 state,
2234 amount,
2235 mint_url,
2236 unit,
2237 quote_id,
2238 created_at,
2239 updated_at,
2240 data,
2241 version
2242 ) = row
2243 );
2244
2245 let id_str: String = column_as_string!(id);
2246 let id = uuid::Uuid::parse_str(&id_str).map_err(|e| {
2247 Error::Database(Box::new(std::io::Error::new(
2248 std::io::ErrorKind::InvalidData,
2249 format!("Invalid UUID: {}", e),
2250 )))
2251 })?;
2252 let kind_str: String = column_as_string!(kind);
2253 let state_json: String = column_as_string!(state);
2254 let amount: u64 = column_as_number!(amount);
2255 let mint_url: MintUrl = column_as_string!(mint_url, MintUrl::from_str);
2256 let unit: CurrencyUnit = column_as_string!(unit, CurrencyUnit::from_str);
2257 let quote_id: Option<String> = column_as_nullable_string!(quote_id);
2258 let created_at: u64 = column_as_number!(created_at);
2259 let updated_at: u64 = column_as_number!(updated_at);
2260 let data_json: String = column_as_string!(data);
2261 let version: u32 = column_as_number!(version);
2262
2263 let kind = wallet::OperationKind::from_str(&kind_str).map_err(|_| {
2264 Error::Database(Box::new(std::io::Error::new(
2265 std::io::ErrorKind::InvalidData,
2266 format!("Invalid operation kind: {}", kind_str),
2267 )))
2268 })?;
2269 let state: wallet::WalletSagaState = serde_json::from_str(&state_json).map_err(|e| {
2270 Error::Database(Box::new(std::io::Error::new(
2271 std::io::ErrorKind::InvalidData,
2272 format!("Failed to deserialize saga state: {}", e),
2273 )))
2274 })?;
2275 let data: wallet::OperationData = serde_json::from_str(&data_json).map_err(|e| {
2276 Error::Database(Box::new(std::io::Error::new(
2277 std::io::ErrorKind::InvalidData,
2278 format!("Failed to deserialize saga data: {}", e),
2279 )))
2280 })?;
2281
2282 Ok(wallet::WalletSaga {
2283 id,
2284 kind,
2285 state,
2286 amount: Amount::from(amount),
2287 mint_url,
2288 unit,
2289 quote_id,
2290 created_at,
2291 updated_at,
2292 data,
2293 version,
2294 })
2295}
2296
2297fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
2298 unpack_into!(
2299 let (
2300 mint_url,
2301 direction,
2302 unit,
2303 amount,
2304 fee,
2305 ys,
2306 timestamp,
2307 memo,
2308 metadata,
2309 quote_id,
2310 payment_request,
2311 payment_proof,
2312 payment_method,
2313 saga_id,
2314 status
2315 ) = row
2316 );
2317
2318 let amount: u64 = column_as_number!(amount);
2319 let fee: u64 = column_as_number!(fee);
2320
2321 let saga_id: Option<Uuid> = column_as_nullable_string!(saga_id)
2322 .map(|id| Uuid::from_str(&id).ok())
2323 .flatten();
2324
2325 Ok(Transaction {
2326 mint_url: column_as_string!(mint_url, MintUrl::from_str),
2327 direction: column_as_string!(direction, TransactionDirection::from_str),
2328 unit: column_as_string!(unit, CurrencyUnit::from_str),
2329 amount: Amount::from(amount),
2330 fee: Amount::from(fee),
2331 ys: column_as_binary!(ys)
2332 .chunks(33)
2333 .map(PublicKey::from_slice)
2334 .collect::<Result<Vec<_>, _>>()?,
2335 timestamp: column_as_number!(timestamp),
2336 memo: column_as_nullable_string!(memo),
2337 metadata: column_as_nullable_string!(metadata, |v| serde_json::from_str(&v).ok(), |v| {
2338 serde_json::from_slice(&v).ok()
2339 })
2340 .unwrap_or_default(),
2341 quote_id: column_as_nullable_string!(quote_id),
2342 payment_request: column_as_nullable_string!(payment_request),
2343 payment_proof: column_as_nullable_string!(payment_proof),
2344 payment_method: column_as_nullable_string!(payment_method)
2345 .map(|v| PaymentMethod::from_str(&v))
2346 .transpose()
2347 .map_err(Error::from)?,
2348 saga_id,
2349 status: column_as_string!(status, TransactionStatus::from_str),
2350 })
2351}
2352
2353fn sql_row_to_p2pk_signing_key(row: Vec<Column>) -> Result<wallet::P2PKSigningKey, Error> {
2354 unpack_into!(
2355 let (
2356 pubkey,
2357 derivation_index,
2358 derivation_path,
2359 created_time
2360 ) = row
2361 );
2362
2363 Ok(wallet::P2PKSigningKey {
2364 pubkey: column_as_string!(pubkey, PublicKey::from_str, PublicKey::from_slice),
2365 derivation_index: column_as_number!(derivation_index),
2366 derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
2367 created_time: column_as_number!(created_time),
2368 })
2369}