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