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,
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 estimated_blocks,
351 used_by_operation,
352 version
353 FROM
354 mint_quote
355 WHERE
356 id = :id
357 "#,
358 )?
359 .bind("id", quote_id.to_string())
360 .fetch_one(&*conn)
361 .await?
362 .map(sql_row_to_mint_quote)
363 .transpose()
364 }
365
366 #[instrument(skip(self))]
367 async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
368 let conn = self
369 .pool
370 .get()
371 .await
372 .map_err(|e| Error::Database(Box::new(e)))?;
373 Ok(query(
374 r#"
375 SELECT
376 id,
377 mint_url,
378 amount,
379 unit,
380 request,
381 state,
382 expiry,
383 secret_key,
384 payment_method,
385 amount_issued,
386 amount_paid,
387 estimated_blocks,
388 used_by_operation,
389 version
390 FROM
391 mint_quote
392 "#,
393 )?
394 .fetch_all(&*conn)
395 .await?
396 .into_iter()
397 .map(sql_row_to_mint_quote)
398 .collect::<Result<_, _>>()?)
399 }
400
401 #[instrument(skip(self))]
402 async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
403 let conn = self
404 .pool
405 .get()
406 .await
407 .map_err(|e| Error::Database(Box::new(e)))?;
408 Ok(query(
409 r#"
410 SELECT
411 id,
412 mint_url,
413 amount,
414 unit,
415 request,
416 state,
417 expiry,
418 secret_key,
419 payment_method,
420 amount_issued,
421 amount_paid,
422 estimated_blocks,
423 used_by_operation,
424 version
425 FROM
426 mint_quote
427 WHERE
428 amount_issued = 0
429 OR
430 payment_method = 'bolt12'
431 "#,
432 )?
433 .fetch_all(&*conn)
434 .await?
435 .into_iter()
436 .map(sql_row_to_mint_quote)
437 .collect::<Result<_, _>>()?)
438 }
439
440 #[instrument(skip(self))]
441 async fn get_melt_quote(
442 &self,
443 quote_id: &str,
444 ) -> Result<Option<wallet::MeltQuote>, database::Error> {
445 let conn = self
446 .pool
447 .get()
448 .await
449 .map_err(|e| Error::Database(Box::new(e)))?;
450 query(
451 r#"
452 SELECT
453 id,
454 unit,
455 amount,
456 request,
457 fee_reserve,
458 state,
459 expiry,
460 payment_proof,
461 payment_method,
462 estimated_blocks,
463 fee_index,
464 used_by_operation,
465 version,
466 mint_url
467 FROM
468 melt_quote
469 WHERE
470 id=:id
471 "#,
472 )?
473 .bind("id", quote_id.to_owned())
474 .fetch_one(&*conn)
475 .await?
476 .map(sql_row_to_melt_quote)
477 .transpose()
478 }
479
480 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
481 async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, database::Error> {
482 let conn = self
483 .pool
484 .get()
485 .await
486 .map_err(|e| Error::Database(Box::new(e)))?;
487 query(
488 r#"
489 SELECT
490 keys
491 FROM key
492 WHERE id = :id
493 "#,
494 )?
495 .bind("id", keyset_id.to_string())
496 .pluck(&*conn)
497 .await?
498 .map(|keys| {
499 let keys = column_as_string!(keys);
500 serde_json::from_str(&keys).map_err(Error::from)
501 })
502 .transpose()
503 }
504
505 #[instrument(skip(self, state, spending_conditions))]
506 async fn get_proofs(
507 &self,
508 mint_url: Option<MintUrl>,
509 unit: Option<CurrencyUnit>,
510 state: Option<Vec<State>>,
511 spending_conditions: Option<Vec<SpendingConditions>>,
512 ) -> Result<Vec<ProofInfo>, database::Error> {
513 let conn = self
514 .pool
515 .get()
516 .await
517 .map_err(|e| Error::Database(Box::new(e)))?;
518 Ok(query(
519 r#"
520 SELECT
521 amount,
522 unit,
523 keyset_id,
524 secret,
525 c,
526 witness,
527 dleq_e,
528 dleq_s,
529 dleq_r,
530 y,
531 mint_url,
532 state,
533 spending_condition,
534 used_by_operation,
535 created_by_operation,
536 p2pk_e
537 FROM proof
538 "#,
539 )?
540 .fetch_all(&*conn)
541 .await?
542 .into_iter()
543 .filter_map(|row| {
544 let row = sql_row_to_proof_info(row).ok()?;
545
546 if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) {
547 Some(row)
548 } else {
549 None
550 }
551 })
552 .collect::<Vec<_>>())
553 }
554
555 #[instrument(skip(self, ys))]
556 async fn get_proofs_by_ys(
557 &self,
558 ys: Vec<PublicKey>,
559 ) -> Result<Vec<ProofInfo>, database::Error> {
560 let conn = self
561 .pool
562 .get()
563 .await
564 .map_err(|e| Error::Database(Box::new(e)))?;
565 Ok(query(
566 r#"
567 SELECT
568 amount,
569 unit,
570 keyset_id,
571 secret,
572 c,
573 witness,
574 dleq_e,
575 dleq_s,
576 dleq_r,
577 y,
578 mint_url,
579 state,
580 spending_condition,
581 used_by_operation,
582 created_by_operation,
583 p2pk_e
584 FROM proof
585 WHERE y IN (:ys)
586 "#,
587 )?
588 .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
589 .fetch_all(&*conn)
590 .await?
591 .into_iter()
592 .filter_map(|row| sql_row_to_proof_info(row).ok())
593 .collect::<Vec<_>>())
594 }
595
596 async fn get_balance(
597 &self,
598 mint_url: Option<MintUrl>,
599 unit: Option<CurrencyUnit>,
600 states: Option<Vec<State>>,
601 ) -> Result<u64, database::Error> {
602 let conn = self
603 .pool
604 .get()
605 .await
606 .map_err(|e| Error::Database(Box::new(e)))?;
607
608 let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string();
609 let mut where_clauses = Vec::new();
610 let states = states
611 .unwrap_or_default()
612 .into_iter()
613 .map(|x| x.to_string())
614 .collect::<Vec<_>>();
615
616 if mint_url.is_some() {
617 where_clauses.push("mint_url = :mint_url");
618 }
619 if unit.is_some() {
620 where_clauses.push("unit = :unit");
621 }
622 if !states.is_empty() {
623 where_clauses.push("state IN (:states)");
624 }
625
626 if !where_clauses.is_empty() {
627 query_str.push_str(" WHERE ");
628 query_str.push_str(&where_clauses.join(" AND "));
629 }
630
631 let mut q = query(&query_str)?;
632
633 if let Some(ref mint_url) = mint_url {
634 q = q.bind("mint_url", mint_url.to_string());
635 }
636 if let Some(ref unit) = unit {
637 q = q.bind("unit", unit.to_string());
638 }
639
640 if !states.is_empty() {
641 q = q.bind_vec("states", states)?;
642 }
643
644 let balance = q
645 .pluck(&*conn)
646 .await?
647 .map(|n| {
648 match n {
650 crate::stmt::Column::Integer(i) => Ok(i as u64),
651 crate::stmt::Column::Real(f) => Ok(f as u64),
652 _ => Err(Error::Database(Box::new(std::io::Error::new(
653 std::io::ErrorKind::InvalidData,
654 "Invalid balance type",
655 )))),
656 }
657 })
658 .transpose()?
659 .unwrap_or(0);
660
661 Ok(balance)
662 }
663
664 #[instrument(skip(self))]
665 async fn get_transaction(
666 &self,
667 transaction_id: TransactionId,
668 ) -> Result<Option<Transaction>, database::Error> {
669 let conn = self
670 .pool
671 .get()
672 .await
673 .map_err(|e| Error::Database(Box::new(e)))?;
674 Ok(query(
675 r#"
676 SELECT
677 mint_url,
678 direction,
679 unit,
680 amount,
681 fee,
682 ys,
683 timestamp,
684 memo,
685 metadata,
686 quote_id,
687 payment_request,
688 payment_proof,
689 payment_method,
690 saga_id
691 FROM
692 transactions
693 WHERE
694 id = :id
695 "#,
696 )?
697 .bind("id", transaction_id.as_slice().to_vec())
698 .fetch_one(&*conn)
699 .await?
700 .map(sql_row_to_transaction)
701 .transpose()?)
702 }
703
704 #[instrument(skip(self))]
705 async fn list_transactions(
706 &self,
707 mint_url: Option<MintUrl>,
708 direction: Option<TransactionDirection>,
709 unit: Option<CurrencyUnit>,
710 ) -> Result<Vec<Transaction>, database::Error> {
711 let conn = self
712 .pool
713 .get()
714 .await
715 .map_err(|e| Error::Database(Box::new(e)))?;
716
717 Ok(query(
718 r#"
719 SELECT
720 mint_url,
721 direction,
722 unit,
723 amount,
724 fee,
725 ys,
726 timestamp,
727 memo,
728 metadata,
729 quote_id,
730 payment_request,
731 payment_proof,
732 payment_method,
733 saga_id
734 FROM
735 transactions
736 "#,
737 )?
738 .fetch_all(&*conn)
739 .await?
740 .into_iter()
741 .filter_map(|row| {
742 let transaction = sql_row_to_transaction(row).ok()?;
744 if transaction.matches_conditions(&mint_url, &direction, &unit) {
745 Some(transaction)
746 } else {
747 None
748 }
749 })
750 .collect::<Vec<_>>())
751 }
752
753 async fn update_proofs(
754 &self,
755 added: Vec<ProofInfo>,
756 removed_ys: Vec<PublicKey>,
757 ) -> Result<(), database::Error> {
758 let conn = self
759 .pool
760 .get()
761 .await
762 .map_err(|e| Error::Database(Box::new(e)))?;
763 let tx = ConnectionWithTransaction::new(conn).await?;
764
765 for proof in added {
766 query(
767 r#"
768 INSERT INTO proof
769 (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)
770 VALUES
771 (: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)
772 ON CONFLICT(y) DO UPDATE SET
773 mint_url = excluded.mint_url,
774 state = excluded.state,
775 spending_condition = excluded.spending_condition,
776 unit = excluded.unit,
777 amount = excluded.amount,
778 keyset_id = excluded.keyset_id,
779 secret = excluded.secret,
780 c = excluded.c,
781 witness = excluded.witness,
782 dleq_e = excluded.dleq_e,
783 dleq_s = excluded.dleq_s,
784 dleq_r = excluded.dleq_r,
785 used_by_operation = excluded.used_by_operation,
786 created_by_operation = excluded.created_by_operation,
787 p2pk_e = excluded.p2pk_e
788 ;
789 "#,
790 )?
791 .bind("y", proof.y.to_bytes().to_vec())
792 .bind("mint_url", proof.mint_url.to_string())
793 .bind("state", proof.state.to_string())
794 .bind(
795 "spending_condition",
796 proof
797 .spending_condition
798 .map(|s| serde_json::to_string(&s).ok()),
799 )
800 .bind("unit", proof.unit.to_string())
801 .bind("amount", u64::from(proof.proof.amount) as i64)
802 .bind("keyset_id", proof.proof.keyset_id.to_string())
803 .bind("secret", proof.proof.secret.to_string())
804 .bind("c", proof.proof.c.to_bytes().to_vec())
805 .bind(
806 "witness",
807 proof
808 .proof
809 .witness
810 .and_then(|w| serde_json::to_string(&w).ok()),
811 )
812 .bind(
813 "dleq_e",
814 proof.proof.dleq.as_ref().map(|dleq| dleq.e.to_secret_bytes().to_vec()),
815 )
816 .bind(
817 "dleq_s",
818 proof.proof.dleq.as_ref().map(|dleq| dleq.s.to_secret_bytes().to_vec()),
819 )
820 .bind(
821 "dleq_r",
822 proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()),
823 )
824 .bind("used_by_operation", proof.used_by_operation.map(|id| id.to_string()))
825 .bind("created_by_operation", proof.created_by_operation.map(|id| id.to_string()))
826 .bind(
827 "p2pk_e",
828 proof
829 .proof
830 .p2pk_e
831 .as_ref()
832 .map(|pk| pk.to_bytes().to_vec()),
833 )
834 .execute(&tx)
835 .await?;
836 }
837
838 if !removed_ys.is_empty() {
839 query(r#"DELETE FROM proof WHERE y IN (:ys)"#)?
840 .bind_vec(
841 "ys",
842 removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(),
843 )?
844 .execute(&tx)
845 .await?;
846 }
847
848 tx.commit().await?;
849
850 Ok(())
851 }
852
853 #[instrument(skip(self))]
854 async fn update_proofs_state(
855 &self,
856 ys: Vec<PublicKey>,
857 state: State,
858 ) -> Result<(), database::Error> {
859 let conn = self
860 .pool
861 .get()
862 .await
863 .map_err(|e| Error::Database(Box::new(e)))?;
864
865 query("UPDATE proof SET state = :state WHERE y IN (:ys)")?
866 .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())?
867 .bind("state", state.to_string())
868 .execute(&*conn)
869 .await?;
870
871 Ok(())
872 }
873
874 #[instrument(skip(self))]
875 async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> {
876 let conn = self
877 .pool
878 .get()
879 .await
880 .map_err(|e| Error::Database(Box::new(e)))?;
881
882 let mint_url = transaction.mint_url.to_string();
883 let direction = transaction.direction.to_string();
884 let unit = transaction.unit.to_string();
885 let amount = u64::from(transaction.amount) as i64;
886 let fee = u64::from(transaction.fee) as i64;
887 let ys = transaction
888 .ys
889 .iter()
890 .flat_map(|y| y.to_bytes().to_vec())
891 .collect::<Vec<_>>();
892
893 let id = transaction.id();
894
895 query(
896 r#"
897 INSERT INTO transactions
898 (id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata, quote_id, payment_request, payment_proof, payment_method, saga_id)
899 VALUES
900 (:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata, :quote_id, :payment_request, :payment_proof, :payment_method, :saga_id)
901 ON CONFLICT(id) DO UPDATE SET
902 mint_url = excluded.mint_url,
903 direction = excluded.direction,
904 unit = excluded.unit,
905 amount = excluded.amount,
906 fee = excluded.fee,
907 timestamp = excluded.timestamp,
908 memo = excluded.memo,
909 metadata = excluded.metadata,
910 quote_id = excluded.quote_id,
911 payment_request = excluded.payment_request,
912 payment_proof = excluded.payment_proof,
913 payment_method = excluded.payment_method,
914 saga_id = excluded.saga_id
915 ;
916 "#,
917 )?
918 .bind("id", id.as_slice().to_vec())
919 .bind("mint_url", mint_url)
920 .bind("direction", direction)
921 .bind("unit", unit)
922 .bind("amount", amount)
923 .bind("fee", fee)
924 .bind("ys", ys)
925 .bind("timestamp", transaction.timestamp as i64)
926 .bind("memo", transaction.memo)
927 .bind(
928 "metadata",
929 serde_json::to_string(&transaction.metadata).map_err(Error::from)?,
930 )
931 .bind("quote_id", transaction.quote_id)
932 .bind("payment_request", transaction.payment_request)
933 .bind("payment_proof", transaction.payment_proof)
934 .bind("payment_method", transaction.payment_method.map(|pm| pm.to_string()))
935 .bind("saga_id", transaction.saga_id.map(|id| id.to_string()))
936 .execute(&*conn)
937 .await?;
938
939 Ok(())
940 }
941
942 #[instrument(skip(self))]
943 async fn update_mint_url(
944 &self,
945 old_mint_url: MintUrl,
946 new_mint_url: MintUrl,
947 ) -> Result<(), database::Error> {
948 let conn = self
949 .pool
950 .get()
951 .await
952 .map_err(|e| Error::Database(Box::new(e)))?;
953 let tx = ConnectionWithTransaction::new(conn).await?;
954 let tables = ["mint_quote", "proof"];
955
956 for table in &tables {
957 query(&format!(
958 r#"
959 UPDATE {table}
960 SET mint_url = :new_mint_url
961 WHERE mint_url = :old_mint_url
962 "#
963 ))?
964 .bind("new_mint_url", new_mint_url.to_string())
965 .bind("old_mint_url", old_mint_url.to_string())
966 .execute(&tx)
967 .await?;
968 }
969
970 tx.commit().await?;
971
972 Ok(())
973 }
974
975 #[instrument(skip(self), fields(keyset_id = %keyset_id))]
976 async fn increment_keyset_counter(
977 &self,
978 keyset_id: &Id,
979 count: u32,
980 ) -> Result<u32, database::Error> {
981 let conn = self
982 .pool
983 .get()
984 .await
985 .map_err(|e| Error::Database(Box::new(e)))?;
986
987 let new_counter = query(
988 r#"
989 INSERT INTO keyset_counter (keyset_id, counter)
990 VALUES (:keyset_id, :count)
991 ON CONFLICT(keyset_id) DO UPDATE SET
992 counter = keyset_counter.counter + :count
993 RETURNING counter
994 "#,
995 )?
996 .bind("keyset_id", keyset_id.to_string())
997 .bind("count", count)
998 .pluck(&*conn)
999 .await?
1000 .map(|n| Ok::<_, Error>(column_as_number!(n)))
1001 .transpose()?
1002 .ok_or_else(|| Error::Internal("Counter update returned no value".to_owned()))?;
1003
1004 Ok(new_counter)
1005 }
1006
1007 #[instrument(skip(self, mint_info))]
1008 async fn add_mint(
1009 &self,
1010 mint_url: MintUrl,
1011 mint_info: Option<MintInfo>,
1012 ) -> Result<(), database::Error> {
1013 let conn = self
1014 .pool
1015 .get()
1016 .await
1017 .map_err(|e| Error::Database(Box::new(e)))?;
1018
1019 let (
1020 name,
1021 pubkey,
1022 version,
1023 description,
1024 description_long,
1025 contact,
1026 nuts,
1027 icon_url,
1028 urls,
1029 motd,
1030 time,
1031 tos_url,
1032 ) = match mint_info {
1033 Some(mint_info) => {
1034 let MintInfo {
1035 name,
1036 pubkey,
1037 version,
1038 description,
1039 description_long,
1040 contact,
1041 nuts,
1042 icon_url,
1043 urls,
1044 motd,
1045 time,
1046 tos_url,
1047 } = mint_info;
1048
1049 (
1050 name,
1051 pubkey.map(|p| p.to_bytes().to_vec()),
1052 version.map(|v| serde_json::to_string(&v).ok()),
1053 description,
1054 description_long,
1055 contact.map(|c| serde_json::to_string(&c).ok()),
1056 serde_json::to_string(&nuts).ok(),
1057 icon_url,
1058 urls.map(|c| serde_json::to_string(&c).ok()),
1059 motd,
1060 time,
1061 tos_url,
1062 )
1063 }
1064 None => (
1065 None, None, None, None, None, None, None, None, None, None, None, None,
1066 ),
1067 };
1068
1069 query(
1070 r#"
1071 INSERT INTO mint
1072 (
1073 mint_url, name, pubkey, version, description, description_long,
1074 contact, nuts, icon_url, urls, motd, mint_time, tos_url
1075 )
1076 VALUES
1077 (
1078 :mint_url, :name, :pubkey, :version, :description, :description_long,
1079 :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url
1080 )
1081 ON CONFLICT(mint_url) DO UPDATE SET
1082 name = excluded.name,
1083 pubkey = excluded.pubkey,
1084 version = excluded.version,
1085 description = excluded.description,
1086 description_long = excluded.description_long,
1087 contact = excluded.contact,
1088 nuts = excluded.nuts,
1089 icon_url = excluded.icon_url,
1090 urls = excluded.urls,
1091 motd = excluded.motd,
1092 mint_time = excluded.mint_time,
1093 tos_url = excluded.tos_url
1094 ;
1095 "#,
1096 )?
1097 .bind("mint_url", mint_url.to_string())
1098 .bind("name", name)
1099 .bind("pubkey", pubkey)
1100 .bind("version", version)
1101 .bind("description", description)
1102 .bind("description_long", description_long)
1103 .bind("contact", contact)
1104 .bind("nuts", nuts)
1105 .bind("icon_url", icon_url)
1106 .bind("urls", urls)
1107 .bind("motd", motd)
1108 .bind("mint_time", time.map(|v| v as i64))
1109 .bind("tos_url", tos_url)
1110 .execute(&*conn)
1111 .await?;
1112
1113 Ok(())
1114 }
1115
1116 #[instrument(skip(self))]
1117 async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
1118 let conn = self
1119 .pool
1120 .get()
1121 .await
1122 .map_err(|e| Error::Database(Box::new(e)))?;
1123
1124 query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)?
1125 .bind("mint_url", mint_url.to_string())
1126 .execute(&*conn)
1127 .await?;
1128
1129 Ok(())
1130 }
1131
1132 #[instrument(skip(self, keysets))]
1133 async fn add_mint_keysets(
1134 &self,
1135 mint_url: MintUrl,
1136 keysets: Vec<KeySetInfo>,
1137 ) -> Result<(), database::Error> {
1138 let conn = self
1139 .pool
1140 .get()
1141 .await
1142 .map_err(|e| Error::Database(Box::new(e)))?;
1143 let tx = ConnectionWithTransaction::new(conn).await?;
1144
1145 for keyset in keysets {
1146 query(
1147 r#"
1148 INSERT INTO keyset
1149 (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32)
1150 VALUES
1151 (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32)
1152 ON CONFLICT(id) DO UPDATE SET
1153 active = excluded.active,
1154 input_fee_ppk = excluded.input_fee_ppk
1155 "#,
1156 )?
1157 .bind("mint_url", mint_url.to_string())
1158 .bind("id", keyset.id.to_string())
1159 .bind("unit", keyset.unit.to_string())
1160 .bind("active", keyset.active)
1161 .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
1162 .bind("final_expiry", keyset.final_expiry.map(|v| v as i64))
1163 .bind("keyset_u32", u32::from(keyset.id))
1164 .execute(&tx)
1165 .await?;
1166 }
1167
1168 tx.commit().await?;
1169
1170 Ok(())
1171 }
1172
1173 #[instrument(skip_all)]
1174 async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
1175 let conn = self
1176 .pool
1177 .get()
1178 .await
1179 .map_err(|e| Error::Database(Box::new(e)))?;
1180
1181 let expected_version = quote.version;
1182 let new_version = expected_version.wrapping_add(1);
1183
1184 let rows_affected = query(
1185 r#"
1186 INSERT INTO mint_quote
1187 (id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid, estimated_blocks, version, used_by_operation)
1188 VALUES
1189 (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid, :estimated_blocks, :version, :used_by_operation)
1190 ON CONFLICT(id) DO UPDATE SET
1191 mint_url = excluded.mint_url,
1192 amount = excluded.amount,
1193 unit = excluded.unit,
1194 request = excluded.request,
1195 state = excluded.state,
1196 expiry = excluded.expiry,
1197 secret_key = excluded.secret_key,
1198 payment_method = excluded.payment_method,
1199 amount_issued = excluded.amount_issued,
1200 amount_paid = excluded.amount_paid,
1201 estimated_blocks = excluded.estimated_blocks,
1202 version = :new_version,
1203 used_by_operation = excluded.used_by_operation
1204 WHERE mint_quote.version = :expected_version
1205 ;
1206 "#,
1207 )?
1208 .bind("id", quote.id.to_string())
1209 .bind("mint_url", quote.mint_url.to_string())
1210 .bind("amount", quote.amount.map(|a| a.to_i64()))
1211 .bind("unit", quote.unit.to_string())
1212 .bind("request", quote.request)
1213 .bind("state", quote.state.to_string())
1214 .bind("expiry", quote.expiry as i64)
1215 .bind("secret_key", quote.secret_key.map(|p| p.to_string()))
1216 .bind("payment_method", quote.payment_method.to_string())
1217 .bind("amount_issued", quote.amount_issued.to_i64())
1218 .bind("amount_paid", quote.amount_paid.to_i64())
1219 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1220 .bind("version", quote.version as i64)
1221 .bind("new_version", new_version as i64)
1222 .bind("expected_version", expected_version as i64)
1223 .bind("used_by_operation", quote.used_by_operation)
1224 .execute(&*conn).await?;
1225
1226 if rows_affected == 0 {
1227 return Err(database::Error::ConcurrentUpdate);
1228 }
1229
1230 Ok(())
1231 }
1232
1233 #[instrument(skip(self))]
1234 async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1235 let conn = self
1236 .pool
1237 .get()
1238 .await
1239 .map_err(|e| Error::Database(Box::new(e)))?;
1240
1241 query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
1242 .bind("id", quote_id.to_string())
1243 .execute(&*conn)
1244 .await?;
1245
1246 Ok(())
1247 }
1248
1249 #[instrument(skip_all)]
1250 async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
1251 let conn = self
1252 .pool
1253 .get()
1254 .await
1255 .map_err(|e| Error::Database(Box::new(e)))?;
1256
1257 let expected_version = quote.version;
1258 let new_version = expected_version.wrapping_add(1);
1259
1260 let rows_affected = query(
1261 r#"
1262 INSERT INTO melt_quote
1263 (id, unit, amount, request, fee_reserve, state, expiry, payment_proof, payment_method, estimated_blocks, fee_index, version, mint_url, used_by_operation)
1264 VALUES
1265 (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_proof, :payment_method, :estimated_blocks, :fee_index, :version, :mint_url, :used_by_operation)
1266 ON CONFLICT(id) DO UPDATE SET
1267 unit = excluded.unit,
1268 amount = excluded.amount,
1269 request = excluded.request,
1270 fee_reserve = excluded.fee_reserve,
1271 state = excluded.state,
1272 expiry = excluded.expiry,
1273 payment_proof = COALESCE(excluded.payment_proof, melt_quote.payment_proof),
1274 payment_method = excluded.payment_method,
1275 estimated_blocks = excluded.estimated_blocks,
1276 fee_index = excluded.fee_index,
1277 version = :new_version,
1278 mint_url = excluded.mint_url,
1279 used_by_operation = excluded.used_by_operation
1280 WHERE melt_quote.version = :expected_version
1281 ;
1282 "#,
1283 )?
1284 .bind("id", quote.id.to_string())
1285 .bind("unit", quote.unit.to_string())
1286 .bind("amount", u64::from(quote.amount) as i64)
1287 .bind("request", quote.request)
1288 .bind("fee_reserve", u64::from(quote.fee_reserve) as i64)
1289 .bind("state", quote.state.to_string())
1290 .bind("expiry", quote.expiry as i64)
1291 .bind("payment_proof", quote.payment_proof)
1292 .bind("payment_method", quote.payment_method.to_string())
1293 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1294 .bind("fee_index", quote.fee_index.map(i64::from))
1295 .bind("version", quote.version as i64)
1296 .bind("new_version", new_version as i64)
1297 .bind("expected_version", expected_version as i64)
1298 .bind("mint_url", quote.mint_url.map(|m| m.to_string()))
1299 .bind("used_by_operation", quote.used_by_operation)
1300 .execute(&*conn)
1301 .await?;
1302
1303 if rows_affected == 0 {
1304 return Err(database::Error::ConcurrentUpdate);
1305 }
1306
1307 Ok(())
1308 }
1309
1310 #[instrument(skip(self))]
1311 async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
1312 let conn = self
1313 .pool
1314 .get()
1315 .await
1316 .map_err(|e| Error::Database(Box::new(e)))?;
1317
1318 query(r#"DELETE FROM melt_quote WHERE id=:id"#)?
1319 .bind("id", quote_id.to_owned())
1320 .execute(&*conn)
1321 .await?;
1322
1323 Ok(())
1324 }
1325
1326 #[instrument(skip_all)]
1327 async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
1328 let conn = self
1329 .pool
1330 .get()
1331 .await
1332 .map_err(|e| Error::Database(Box::new(e)))?;
1333
1334 keyset.verify_id()?;
1335
1336 query(
1337 r#"
1338 INSERT INTO key
1339 (id, keys, keyset_u32)
1340 VALUES
1341 (:id, :keys, :keyset_u32)
1342 "#,
1343 )?
1344 .bind("id", keyset.id.to_string())
1345 .bind(
1346 "keys",
1347 serde_json::to_string(&keyset.keys).map_err(Error::from)?,
1348 )
1349 .bind("keyset_u32", u32::from(keyset.id))
1350 .execute(&*conn)
1351 .await?;
1352
1353 Ok(())
1354 }
1355
1356 #[instrument(skip(self))]
1357 async fn remove_keys(&self, id: &Id) -> Result<(), database::Error> {
1358 let conn = self
1359 .pool
1360 .get()
1361 .await
1362 .map_err(|e| Error::Database(Box::new(e)))?;
1363
1364 query(r#"DELETE FROM key WHERE id = :id"#)?
1365 .bind("id", id.to_string())
1366 .execute(&*conn)
1367 .await?;
1368
1369 Ok(())
1370 }
1371
1372 #[instrument(skip(self))]
1373 async fn remove_transaction(
1374 &self,
1375 transaction_id: TransactionId,
1376 ) -> Result<(), database::Error> {
1377 let conn = self
1378 .pool
1379 .get()
1380 .await
1381 .map_err(|e| Error::Database(Box::new(e)))?;
1382
1383 query(r#"DELETE FROM transactions WHERE id=:id"#)?
1384 .bind("id", transaction_id.as_slice().to_vec())
1385 .execute(&*conn)
1386 .await?;
1387
1388 Ok(())
1389 }
1390
1391 #[instrument(skip(self))]
1392 async fn add_saga(&self, saga: wallet::WalletSaga) -> Result<(), database::Error> {
1393 let conn = self
1394 .pool
1395 .get()
1396 .await
1397 .map_err(|e| Error::Database(Box::new(e)))?;
1398
1399 let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1400 Error::Database(Box::new(std::io::Error::new(
1401 std::io::ErrorKind::InvalidData,
1402 format!("Failed to serialize saga state: {}", e),
1403 )))
1404 })?;
1405
1406 let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1407 Error::Database(Box::new(std::io::Error::new(
1408 std::io::ErrorKind::InvalidData,
1409 format!("Failed to serialize saga data: {}", e),
1410 )))
1411 })?;
1412
1413 query(
1414 r#"
1415 INSERT INTO wallet_sagas
1416 (id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version)
1417 VALUES
1418 (:id, :kind, :state, :amount, :mint_url, :unit, :quote_id, :created_at, :updated_at, :data, :version)
1419 "#,
1420 )?
1421 .bind("id", saga.id.to_string())
1422 .bind("kind", saga.kind.to_string())
1423 .bind("state", state_json)
1424 .bind("amount", u64::from(saga.amount) as i64)
1425 .bind("mint_url", saga.mint_url.to_string())
1426 .bind("unit", saga.unit.to_string())
1427 .bind("quote_id", saga.quote_id)
1428 .bind("created_at", saga.created_at as i64)
1429 .bind("updated_at", saga.updated_at as i64)
1430 .bind("data", data_json)
1431 .bind("version", saga.version as i64)
1432 .execute(&*conn)
1433 .await?;
1434
1435 Ok(())
1436 }
1437
1438 #[instrument(skip(self))]
1439 async fn get_saga(
1440 &self,
1441 id: &uuid::Uuid,
1442 ) -> Result<Option<wallet::WalletSaga>, database::Error> {
1443 let conn = self
1444 .pool
1445 .get()
1446 .await
1447 .map_err(|e| Error::Database(Box::new(e)))?;
1448
1449 let rows = query(
1450 r#"
1451 SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1452 FROM wallet_sagas
1453 WHERE id = :id
1454 "#,
1455 )?
1456 .bind("id", id.to_string())
1457 .fetch_all(&*conn)
1458 .await?;
1459
1460 match rows.into_iter().next() {
1461 Some(row) => Ok(Some(sql_row_to_wallet_saga(row)?)),
1462 None => Ok(None),
1463 }
1464 }
1465
1466 #[instrument(skip(self))]
1467 async fn update_saga(&self, saga: wallet::WalletSaga) -> Result<bool, database::Error> {
1468 let conn = self
1469 .pool
1470 .get()
1471 .await
1472 .map_err(|e| Error::Database(Box::new(e)))?;
1473
1474 let state_json = serde_json::to_string(&saga.state).map_err(|e| {
1475 Error::Database(Box::new(std::io::Error::new(
1476 std::io::ErrorKind::InvalidData,
1477 format!("Failed to serialize saga state: {}", e),
1478 )))
1479 })?;
1480
1481 let data_json = serde_json::to_string(&saga.data).map_err(|e| {
1482 Error::Database(Box::new(std::io::Error::new(
1483 std::io::ErrorKind::InvalidData,
1484 format!("Failed to serialize saga data: {}", e),
1485 )))
1486 })?;
1487
1488 let expected_version = saga.version.saturating_sub(1);
1492
1493 let rows_affected = query(
1494 r#"
1495 UPDATE wallet_sagas
1496 SET kind = :kind, state = :state, amount = :amount, mint_url = :mint_url,
1497 unit = :unit, quote_id = :quote_id, updated_at = :updated_at, data = :data,
1498 version = :new_version
1499 WHERE id = :id AND version = :expected_version
1500 "#,
1501 )?
1502 .bind("id", saga.id.to_string())
1503 .bind("kind", saga.kind.to_string())
1504 .bind("state", state_json)
1505 .bind("amount", u64::from(saga.amount) as i64)
1506 .bind("mint_url", saga.mint_url.to_string())
1507 .bind("unit", saga.unit.to_string())
1508 .bind("quote_id", saga.quote_id)
1509 .bind("updated_at", saga.updated_at as i64)
1510 .bind("data", data_json)
1511 .bind("new_version", saga.version as i64)
1512 .bind("expected_version", expected_version as i64)
1513 .execute(&*conn)
1514 .await?;
1515
1516 Ok(rows_affected > 0)
1518 }
1519
1520 #[instrument(skip(self))]
1521 async fn delete_saga(&self, id: &uuid::Uuid) -> Result<(), database::Error> {
1522 let conn = self
1523 .pool
1524 .get()
1525 .await
1526 .map_err(|e| Error::Database(Box::new(e)))?;
1527
1528 query(r#"DELETE FROM wallet_sagas WHERE id = :id"#)?
1529 .bind("id", id.to_string())
1530 .execute(&*conn)
1531 .await?;
1532
1533 Ok(())
1534 }
1535
1536 #[instrument(skip(self))]
1537 async fn get_incomplete_sagas(&self) -> Result<Vec<wallet::WalletSaga>, database::Error> {
1538 let conn = self
1539 .pool
1540 .get()
1541 .await
1542 .map_err(|e| Error::Database(Box::new(e)))?;
1543
1544 let rows = query(
1545 r#"
1546 SELECT id, kind, state, amount, mint_url, unit, quote_id, created_at, updated_at, data, version
1547 FROM wallet_sagas
1548 ORDER BY created_at ASC
1549 "#,
1550 )?
1551 .fetch_all(&*conn)
1552 .await?;
1553
1554 rows.into_iter().map(sql_row_to_wallet_saga).collect()
1555 }
1556
1557 #[instrument(skip(self))]
1558 async fn reserve_proofs(
1559 &self,
1560 ys: Vec<PublicKey>,
1561 operation_id: &uuid::Uuid,
1562 ) -> Result<(), database::Error> {
1563 let conn = self
1564 .pool
1565 .get()
1566 .await
1567 .map_err(|e| Error::Database(Box::new(e)))?;
1568
1569 for y in ys {
1570 let rows_affected = query(
1571 r#"
1572 UPDATE proof
1573 SET state = 'RESERVED', used_by_operation = :operation_id
1574 WHERE y = :y AND state = 'UNSPENT'
1575 "#,
1576 )?
1577 .bind("y", y.to_bytes().to_vec())
1578 .bind("operation_id", operation_id.to_string())
1579 .execute(&*conn)
1580 .await?;
1581
1582 if rows_affected == 0 {
1583 return Err(database::Error::ProofNotUnspent);
1584 }
1585 }
1586
1587 Ok(())
1588 }
1589
1590 #[instrument(skip(self))]
1591 async fn release_proofs(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1592 let conn = self
1593 .pool
1594 .get()
1595 .await
1596 .map_err(|e| Error::Database(Box::new(e)))?;
1597
1598 query(
1599 r#"
1600 UPDATE proof
1601 SET state = 'UNSPENT', used_by_operation = NULL
1602 WHERE used_by_operation = :operation_id
1603 "#,
1604 )?
1605 .bind("operation_id", operation_id.to_string())
1606 .execute(&*conn)
1607 .await?;
1608
1609 Ok(())
1610 }
1611
1612 #[instrument(skip(self))]
1613 async fn get_reserved_proofs(
1614 &self,
1615 operation_id: &uuid::Uuid,
1616 ) -> Result<Vec<ProofInfo>, database::Error> {
1617 let conn = self
1618 .pool
1619 .get()
1620 .await
1621 .map_err(|e| Error::Database(Box::new(e)))?;
1622
1623 let rows = query(
1624 r#"
1625 SELECT
1626 amount,
1627 unit,
1628 keyset_id,
1629 secret,
1630 c,
1631 witness,
1632 dleq_e,
1633 dleq_s,
1634 dleq_r,
1635 y,
1636 mint_url,
1637 state,
1638 spending_condition,
1639 used_by_operation,
1640 created_by_operation,
1641 p2pk_e
1642 FROM proof
1643 WHERE used_by_operation = :operation_id
1644 "#,
1645 )?
1646 .bind("operation_id", operation_id.to_string())
1647 .fetch_all(&*conn)
1648 .await?;
1649
1650 rows.into_iter().map(sql_row_to_proof_info).collect()
1651 }
1652
1653 #[instrument(skip(self))]
1654 async fn reserve_melt_quote(
1655 &self,
1656 quote_id: &str,
1657 operation_id: &uuid::Uuid,
1658 ) -> Result<(), database::Error> {
1659 let conn = self
1660 .pool
1661 .get()
1662 .await
1663 .map_err(|e| Error::Database(Box::new(e)))?;
1664
1665 let rows_affected = query(
1666 r#"
1667 UPDATE melt_quote
1668 SET used_by_operation = :operation_id
1669 WHERE id = :quote_id AND used_by_operation IS NULL
1670 "#,
1671 )?
1672 .bind("operation_id", operation_id.to_string())
1673 .bind("quote_id", quote_id)
1674 .execute(&*conn)
1675 .await?;
1676
1677 if rows_affected == 0 {
1678 let exists = query(
1680 r#"
1681 SELECT 1 FROM melt_quote WHERE id = :quote_id
1682 "#,
1683 )?
1684 .bind("quote_id", quote_id)
1685 .fetch_one(&*conn)
1686 .await?;
1687
1688 if exists.is_none() {
1689 return Err(database::Error::UnknownQuote);
1690 }
1691 return Err(database::Error::QuoteAlreadyInUse);
1692 }
1693
1694 Ok(())
1695 }
1696
1697 #[instrument(skip(self))]
1698 async fn release_melt_quote(&self, operation_id: &uuid::Uuid) -> Result<(), database::Error> {
1699 let conn = self
1700 .pool
1701 .get()
1702 .await
1703 .map_err(|e| Error::Database(Box::new(e)))?;
1704
1705 query(
1706 r#"
1707 UPDATE melt_quote
1708 SET used_by_operation = NULL
1709 WHERE used_by_operation = :operation_id
1710 "#,
1711 )?
1712 .bind("operation_id", operation_id.to_string())
1713 .execute(&*conn)
1714 .await?;
1715
1716 Ok(())
1717 }
1718
1719 #[instrument(skip(self))]
1720 async fn reserve_mint_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 mint_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 mint_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_mint_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 mint_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 async fn kv_read(
1786 &self,
1787 primary_namespace: &str,
1788 secondary_namespace: &str,
1789 key: &str,
1790 ) -> Result<Option<Vec<u8>>, database::Error> {
1791 crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
1792 }
1793
1794 async fn kv_list(
1795 &self,
1796 primary_namespace: &str,
1797 secondary_namespace: &str,
1798 ) -> Result<Vec<String>, database::Error> {
1799 crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
1800 }
1801
1802 async fn kv_write(
1803 &self,
1804 primary_namespace: &str,
1805 secondary_namespace: &str,
1806 key: &str,
1807 value: &[u8],
1808 ) -> Result<(), database::Error> {
1809 let conn = self
1810 .pool
1811 .get()
1812 .await
1813 .map_err(|e| Error::Database(Box::new(e)))?;
1814 crate::keyvalue::kv_write_standalone(
1815 &*conn,
1816 primary_namespace,
1817 secondary_namespace,
1818 key,
1819 value,
1820 )
1821 .await?;
1822 Ok(())
1823 }
1824
1825 async fn kv_remove(
1826 &self,
1827 primary_namespace: &str,
1828 secondary_namespace: &str,
1829 key: &str,
1830 ) -> Result<(), database::Error> {
1831 let conn = self
1832 .pool
1833 .get()
1834 .await
1835 .map_err(|e| Error::Database(Box::new(e)))?;
1836 crate::keyvalue::kv_remove_standalone(&*conn, primary_namespace, secondary_namespace, key)
1837 .await?;
1838 Ok(())
1839 }
1840
1841 #[instrument(skip(self))]
1844 async fn add_p2pk_key(
1845 &self,
1846 pubkey: &PublicKey,
1847 derivation_path: DerivationPath,
1848 derivation_index: u32,
1849 ) -> Result<(), Error> {
1850 let conn = self
1851 .pool
1852 .get()
1853 .await
1854 .map_err(|e| Error::Database(Box::new(e)))?;
1855 let query_str = r#"
1856 INSERT INTO p2pk_signing_key (pubkey, derivation_index, derivation_path, created_time)
1857 VALUES (:pubkey, :derivation_index, :derivation_path, :created_time)
1858 "#
1859 .to_string();
1860
1861 query(&query_str)?
1862 .bind("pubkey", pubkey.to_bytes().to_vec())
1863 .bind("derivation_index", derivation_index)
1864 .bind("derivation_path", derivation_path.to_string())
1865 .bind("created_time", unix_time() as i64)
1866 .execute(&*conn)
1867 .await?;
1868
1869 Ok(())
1870 }
1871
1872 #[instrument(skip(self))]
1873 async fn get_p2pk_key(
1874 &self,
1875 pubkey: &PublicKey,
1876 ) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1877 let conn = self
1878 .pool
1879 .get()
1880 .await
1881 .map_err(|e| Error::Database(Box::new(e)))?;
1882 let query_str = r#"SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key WHERE pubkey = :pubkey"#.to_string();
1883
1884 query(&query_str)?
1885 .bind("pubkey", pubkey.to_bytes().to_vec())
1886 .fetch_one(&*conn)
1887 .await?
1888 .map(sql_row_to_p2pk_signing_key)
1889 .transpose()
1890 }
1891
1892 #[instrument(skip(self))]
1893 async fn list_p2pk_keys(&self) -> Result<Vec<wallet::P2PKSigningKey>, Error> {
1894 let conn = self
1895 .pool
1896 .get()
1897 .await
1898 .map_err(|e| Error::Database(Box::new(e)))?;
1899 let query_str = r#"
1900 SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC
1901 "#.to_string();
1902
1903 Ok(query(&query_str)?
1904 .fetch_all(&*conn)
1905 .await?
1906 .into_iter()
1907 .filter_map(|row| {
1908 let row = sql_row_to_p2pk_signing_key(row).ok()?;
1909
1910 Some(row)
1911 })
1912 .collect::<Vec<wallet::P2PKSigningKey>>())
1913 }
1914
1915 #[instrument(skip(self))]
1916 async fn latest_p2pk(&self) -> Result<Option<wallet::P2PKSigningKey>, Error> {
1917 let conn = self
1918 .pool
1919 .get()
1920 .await
1921 .map_err(|e| Error::Database(Box::new(e)))?;
1922 let query_str = r#"
1923 SELECT pubkey, derivation_index, derivation_path, created_time FROM p2pk_signing_key ORDER BY derivation_index DESC LIMIT 1
1924 "#.to_string();
1925
1926 query(&query_str)?
1927 .fetch_one(&*conn)
1928 .await?
1929 .map(sql_row_to_p2pk_signing_key)
1930 .transpose()
1931 }
1932}
1933
1934fn sql_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
1935 unpack_into!(
1936 let (
1937 name,
1938 pubkey,
1939 version,
1940 description,
1941 description_long,
1942 contact,
1943 nuts,
1944 icon_url,
1945 motd,
1946 urls,
1947 mint_time,
1948 tos_url
1949 ) = row
1950 );
1951
1952 Ok(MintInfo {
1953 name: column_as_nullable_string!(&name),
1954 pubkey: column_as_nullable_string!(&pubkey, |v| serde_json::from_str(v).ok(), |v| {
1955 serde_json::from_slice(v).ok()
1956 }),
1957 version: column_as_nullable_string!(&version).and_then(|v| serde_json::from_str(&v).ok()),
1958 description: column_as_nullable_string!(description),
1959 description_long: column_as_nullable_string!(description_long),
1960 contact: column_as_nullable_string!(contact, |v| serde_json::from_str(&v).ok()),
1961 nuts: column_as_nullable_string!(nuts, |v| serde_json::from_str(&v).ok())
1962 .unwrap_or_default(),
1963 urls: column_as_nullable_string!(urls, |v| serde_json::from_str(&v).ok()),
1964 icon_url: column_as_nullable_string!(icon_url),
1965 motd: column_as_nullable_string!(motd),
1966 time: column_as_nullable_number!(mint_time).map(|t| t),
1967 tos_url: column_as_nullable_string!(tos_url),
1968 })
1969}
1970
1971#[instrument(skip_all)]
1972fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
1973 unpack_into!(
1974 let (
1975 id,
1976 unit,
1977 active,
1978 input_fee_ppk,
1979 final_expiry
1980 ) = row
1981 );
1982
1983 Ok(KeySetInfo {
1984 id: column_as_string!(id, Id::from_str, Id::from_bytes),
1985 unit: column_as_string!(unit, CurrencyUnit::from_str),
1986 active: matches!(active, Column::Integer(1)),
1987 input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or(0),
1988 final_expiry: column_as_nullable_number!(final_expiry),
1989 })
1990}
1991
1992fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
1993 unpack_into!(
1994 let (
1995 id,
1996 mint_url,
1997 amount,
1998 unit,
1999 request,
2000 state,
2001 expiry,
2002 secret_key,
2003 row_method,
2004 row_amount_minted,
2005 row_amount_paid,
2006 estimated_blocks,
2007 used_by_operation,
2008 version
2009 ) = row
2010 );
2011
2012 let amount: Option<i64> = column_as_nullable_number!(amount);
2013
2014 let amount_paid: u64 = column_as_number!(row_amount_paid);
2015 let amount_minted: u64 = column_as_number!(row_amount_minted);
2016 let expiry_val: u64 = column_as_number!(expiry);
2017 let version_val: u32 = column_as_number!(version);
2018 let payment_method =
2019 PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2020
2021 Ok(MintQuote {
2022 id: column_as_string!(id),
2023 mint_url: column_as_string!(mint_url, MintUrl::from_str),
2024 amount: amount.and_then(Amount::from_i64),
2025 unit: column_as_string!(unit, CurrencyUnit::from_str),
2026 request: column_as_string!(request),
2027 state: column_as_string!(state, MintQuoteState::from_str),
2028 expiry: expiry_val,
2029 secret_key: column_as_nullable_string!(secret_key, |s| SecretKey::from_str(&s).ok()),
2030 payment_method,
2031 amount_issued: Amount::from(amount_minted),
2032 amount_paid: Amount::from(amount_paid),
2033 estimated_blocks: column_as_nullable_number!(estimated_blocks),
2034 used_by_operation: column_as_nullable_string!(used_by_operation),
2035 version: version_val,
2036 })
2037}
2038
2039fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
2040 unpack_into!(
2041 let (
2042 id,
2043 unit,
2044 amount,
2045 request,
2046 fee_reserve,
2047 state,
2048 expiry,
2049 payment_proof,
2050 row_method,
2051 estimated_blocks,
2052 fee_index,
2053 used_by_operation,
2054 version,
2055 mint_url
2056 ) = row
2057 );
2058
2059 let payment_method =
2060 PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
2061
2062 let amount_val: u64 = column_as_number!(amount);
2063 let fee_reserve_val: u64 = column_as_number!(fee_reserve);
2064 let expiry_val: u64 = column_as_number!(expiry);
2065 let version_val: u32 = column_as_number!(version);
2066
2067 Ok(wallet::MeltQuote {
2068 id: column_as_string!(id),
2069 mint_url: column_as_nullable_string!(mint_url, |s| MintUrl::from_str(&s).ok()),
2070 unit: column_as_string!(unit, CurrencyUnit::from_str),
2071 amount: Amount::from(amount_val),
2072 request: column_as_string!(request),
2073 fee_reserve: Amount::from(fee_reserve_val),
2074 state: column_as_string!(state, MeltQuoteState::from_str),
2075 expiry: expiry_val,
2076 payment_proof: column_as_nullable_string!(payment_proof),
2077 estimated_blocks: column_as_nullable_number!(estimated_blocks),
2078 fee_index: column_as_nullable_number!(fee_index),
2079 payment_method,
2080 used_by_operation: column_as_nullable_string!(used_by_operation),
2081 version: version_val,
2082 })
2083}
2084
2085fn sql_row_to_proof_info(row: Vec<Column>) -> Result<ProofInfo, Error> {
2086 unpack_into!(
2087 let (
2088 amount,
2089 unit,
2090 keyset_id,
2091 secret,
2092 c,
2093 witness,
2094 dleq_e,
2095 dleq_s,
2096 dleq_r,
2097 y,
2098 mint_url,
2099 state,
2100 spending_condition,
2101 used_by_operation,
2102 created_by_operation,
2103 p2pk_e
2104 ) = row
2105 );
2106
2107 let dleq = match (
2108 column_as_nullable_binary!(dleq_e),
2109 column_as_nullable_binary!(dleq_s),
2110 column_as_nullable_binary!(dleq_r),
2111 ) {
2112 (Some(e), Some(s), Some(r)) => {
2113 let e_key = SecretKey::from_slice(&e)?;
2114 let s_key = SecretKey::from_slice(&s)?;
2115 let r_key = SecretKey::from_slice(&r)?;
2116
2117 Some(ProofDleq::new(e_key, s_key, r_key))
2118 }
2119 _ => None,
2120 };
2121
2122 let amount: u64 = column_as_number!(amount);
2123 let proof = Proof {
2124 amount: Amount::from(amount),
2125 keyset_id: column_as_string!(keyset_id, Id::from_str),
2126 secret: column_as_string!(secret, Secret::from_str),
2127 witness: column_as_nullable_string!(witness, |v| { serde_json::from_str(&v).ok() }, |v| {
2128 serde_json::from_slice(&v).ok()
2129 }),
2130 c: column_as_string!(c, PublicKey::from_str, PublicKey::from_slice),
2131 dleq,
2132 p2pk_e: column_as_nullable_binary!(p2pk_e)
2133 .map(|bytes| PublicKey::from_slice(&bytes))
2134 .transpose()?,
2135 };
2136
2137 let used_by_operation =
2138 column_as_nullable_string!(used_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2139 let created_by_operation =
2140 column_as_nullable_string!(created_by_operation).and_then(|id| Uuid::from_str(&id).ok());
2141
2142 Ok(ProofInfo {
2143 proof,
2144 y: column_as_string!(y, PublicKey::from_str, PublicKey::from_slice),
2145 mint_url: column_as_string!(mint_url, MintUrl::from_str),
2146 state: column_as_string!(state, State::from_str),
2147 spending_condition: column_as_nullable_string!(
2148 spending_condition,
2149 |r| { serde_json::from_str(&r).ok() },
2150 |r| { serde_json::from_slice(&r).ok() }
2151 ),
2152 unit: column_as_string!(unit, CurrencyUnit::from_str),
2153 used_by_operation,
2154 created_by_operation,
2155 })
2156}
2157
2158fn sql_row_to_wallet_saga(row: Vec<Column>) -> Result<wallet::WalletSaga, Error> {
2159 unpack_into!(
2160 let (
2161 id,
2162 kind,
2163 state,
2164 amount,
2165 mint_url,
2166 unit,
2167 quote_id,
2168 created_at,
2169 updated_at,
2170 data,
2171 version
2172 ) = row
2173 );
2174
2175 let id_str: String = column_as_string!(id);
2176 let id = uuid::Uuid::parse_str(&id_str).map_err(|e| {
2177 Error::Database(Box::new(std::io::Error::new(
2178 std::io::ErrorKind::InvalidData,
2179 format!("Invalid UUID: {}", e),
2180 )))
2181 })?;
2182 let kind_str: String = column_as_string!(kind);
2183 let state_json: String = column_as_string!(state);
2184 let amount: u64 = column_as_number!(amount);
2185 let mint_url: MintUrl = column_as_string!(mint_url, MintUrl::from_str);
2186 let unit: CurrencyUnit = column_as_string!(unit, CurrencyUnit::from_str);
2187 let quote_id: Option<String> = column_as_nullable_string!(quote_id);
2188 let created_at: u64 = column_as_number!(created_at);
2189 let updated_at: u64 = column_as_number!(updated_at);
2190 let data_json: String = column_as_string!(data);
2191 let version: u32 = column_as_number!(version);
2192
2193 let kind = wallet::OperationKind::from_str(&kind_str).map_err(|_| {
2194 Error::Database(Box::new(std::io::Error::new(
2195 std::io::ErrorKind::InvalidData,
2196 format!("Invalid operation kind: {}", kind_str),
2197 )))
2198 })?;
2199 let state: wallet::WalletSagaState = serde_json::from_str(&state_json).map_err(|e| {
2200 Error::Database(Box::new(std::io::Error::new(
2201 std::io::ErrorKind::InvalidData,
2202 format!("Failed to deserialize saga state: {}", e),
2203 )))
2204 })?;
2205 let data: wallet::OperationData = serde_json::from_str(&data_json).map_err(|e| {
2206 Error::Database(Box::new(std::io::Error::new(
2207 std::io::ErrorKind::InvalidData,
2208 format!("Failed to deserialize saga data: {}", e),
2209 )))
2210 })?;
2211
2212 Ok(wallet::WalletSaga {
2213 id,
2214 kind,
2215 state,
2216 amount: Amount::from(amount),
2217 mint_url,
2218 unit,
2219 quote_id,
2220 created_at,
2221 updated_at,
2222 data,
2223 version,
2224 })
2225}
2226
2227fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
2228 unpack_into!(
2229 let (
2230 mint_url,
2231 direction,
2232 unit,
2233 amount,
2234 fee,
2235 ys,
2236 timestamp,
2237 memo,
2238 metadata,
2239 quote_id,
2240 payment_request,
2241 payment_proof,
2242 payment_method,
2243 saga_id
2244 ) = row
2245 );
2246
2247 let amount: u64 = column_as_number!(amount);
2248 let fee: u64 = column_as_number!(fee);
2249
2250 let saga_id: Option<Uuid> = column_as_nullable_string!(saga_id)
2251 .map(|id| Uuid::from_str(&id).ok())
2252 .flatten();
2253
2254 Ok(Transaction {
2255 mint_url: column_as_string!(mint_url, MintUrl::from_str),
2256 direction: column_as_string!(direction, TransactionDirection::from_str),
2257 unit: column_as_string!(unit, CurrencyUnit::from_str),
2258 amount: Amount::from(amount),
2259 fee: Amount::from(fee),
2260 ys: column_as_binary!(ys)
2261 .chunks(33)
2262 .map(PublicKey::from_slice)
2263 .collect::<Result<Vec<_>, _>>()?,
2264 timestamp: column_as_number!(timestamp),
2265 memo: column_as_nullable_string!(memo),
2266 metadata: column_as_nullable_string!(metadata, |v| serde_json::from_str(&v).ok(), |v| {
2267 serde_json::from_slice(&v).ok()
2268 })
2269 .unwrap_or_default(),
2270 quote_id: column_as_nullable_string!(quote_id),
2271 payment_request: column_as_nullable_string!(payment_request),
2272 payment_proof: column_as_nullable_string!(payment_proof),
2273 payment_method: column_as_nullable_string!(payment_method)
2274 .map(|v| PaymentMethod::from_str(&v))
2275 .transpose()
2276 .map_err(Error::from)?,
2277 saga_id,
2278 })
2279}
2280
2281fn sql_row_to_p2pk_signing_key(row: Vec<Column>) -> Result<wallet::P2PKSigningKey, Error> {
2282 unpack_into!(
2283 let (
2284 pubkey,
2285 derivation_index,
2286 derivation_path,
2287 created_time
2288 ) = row
2289 );
2290
2291 Ok(wallet::P2PKSigningKey {
2292 pubkey: column_as_string!(pubkey, PublicKey::from_str, PublicKey::from_slice),
2293 derivation_index: column_as_number!(derivation_index),
2294 derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
2295 created_time: column_as_number!(created_time),
2296 })
2297}