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