1use std::collections::HashMap;
4use std::str::FromStr;
5
6use async_trait::async_trait;
7use cdk_common::database::mint::{Acquired, LockedMeltQuotes};
8use cdk_common::database::{
9 self, ConversionError, Error, MintQuotesDatabase, MintQuotesTransaction,
10};
11use cdk_common::mint::{
12 self, IncomingPayment, Issuance, MeltPaymentRequest, MeltQuote, MintQuote, Operation,
13};
14use cdk_common::nuts::nut30::MeltQuoteOnchainFeeOption;
15use cdk_common::payment::PaymentIdentifier;
16use cdk_common::quote_id::QuoteId;
17use cdk_common::state::check_melt_quote_state_transition;
18use cdk_common::util::unix_time;
19use cdk_common::{
20 Amount, BlindedMessage, CurrencyUnit, Id, MeltQuoteState, PaymentMethod, PublicKey,
21};
22#[cfg(feature = "prometheus")]
23use cdk_prometheus::MintMetricGuard;
24use lightning_invoice::Bolt11Invoice;
25use tracing::instrument;
26
27use super::{SQLMintDatabase, SQLTransaction};
28use crate::database::DatabaseExecutor;
29use crate::pool::DatabasePool;
30use crate::stmt::{query, Column};
31use crate::{
32 column_as_nullable_number, column_as_nullable_string, column_as_number, column_as_string,
33 unpack_into,
34};
35
36async fn get_mint_quote_payments<C>(
37 conn: &C,
38 quote_id: &QuoteId,
39) -> Result<Vec<IncomingPayment>, Error>
40where
41 C: DatabaseExecutor + Send + Sync,
42{
43 query(
45 r#"
46 SELECT
47 p.payment_id,
48 p.timestamp,
49 p.amount,
50 q.unit
51 FROM
52 mint_quote_payments p
53 JOIN mint_quote q ON p.quote_id = q.id
54 WHERE
55 p.quote_id=:quote_id
56 "#,
57 )?
58 .bind("quote_id", quote_id.to_string())
59 .fetch_all(conn)
60 .await?
61 .into_iter()
62 .map(|row| {
63 let amount: u64 = column_as_number!(row[2].clone());
64 let time: u64 = column_as_number!(row[1].clone());
65 let unit = column_as_string!(&row[3], CurrencyUnit::from_str);
66 Ok(IncomingPayment::new(
67 Amount::from(amount).with_unit(unit),
68 column_as_string!(&row[0]),
69 time,
70 ))
71 })
72 .collect()
73}
74
75async fn get_mint_quote_issuance<C>(conn: &C, quote_id: &QuoteId) -> Result<Vec<Issuance>, Error>
76where
77 C: DatabaseExecutor + Send + Sync,
78{
79 query(
81 r#"
82SELECT i.amount, i.timestamp, q.unit
83FROM mint_quote_issued i
84JOIN mint_quote q ON i.quote_id = q.id
85WHERE i.quote_id=:quote_id
86 "#,
87 )?
88 .bind("quote_id", quote_id.to_string())
89 .fetch_all(conn)
90 .await?
91 .into_iter()
92 .map(|row| {
93 let time: u64 = column_as_number!(row[1].clone());
94 let unit = column_as_string!(&row[2], CurrencyUnit::from_str);
95 Ok(Issuance::new(
96 Amount::from_i64(column_as_number!(row[0].clone()))
97 .expect("Is amount when put into db")
98 .with_unit(unit),
99 time,
100 ))
101 })
102 .collect()
103}
104
105pub(super) async fn get_mint_quote_inner<T>(
107 executor: &T,
108 quote_id: &QuoteId,
109 for_update: bool,
110) -> Result<Option<MintQuote>, Error>
111where
112 T: DatabaseExecutor,
113{
114 let for_update_clause = if for_update { "FOR UPDATE" } else { "" };
115 let query_str = format!(
116 r#"
117 SELECT
118 id,
119 amount,
120 unit,
121 request,
122 expiry,
123 request_lookup_id,
124 pubkey,
125 created_time,
126 amount_paid,
127 amount_issued,
128 updated_at,
129 last_checked,
130 payment_method,
131 request_lookup_id_kind,
132 extra_json
133 FROM
134 mint_quote
135 WHERE id = :id
136 {for_update_clause}
137 "#
138 );
139
140 let mut mint_quote = query(&query_str)?
141 .bind("id", quote_id.to_string())
142 .fetch_one(executor)
143 .await?
144 .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
145 .transpose()?;
146
147 if let Some(quote) = mint_quote.as_mut() {
151 let payments = get_mint_quote_payments(executor, quote_id).await?;
152 let issuance = get_mint_quote_issuance(executor, quote_id).await?;
153 quote.payments = payments;
154 quote.issuance = issuance;
155 }
156
157 Ok(mint_quote)
158}
159
160pub(super) async fn get_mint_quote_by_request_inner<T>(
161 executor: &T,
162 request: &str,
163 for_update: bool,
164) -> Result<Option<MintQuote>, Error>
165where
166 T: DatabaseExecutor,
167{
168 let for_update_clause = if for_update { "FOR UPDATE" } else { "" };
169 let query_str = format!(
170 r#"
171 SELECT
172 id,
173 amount,
174 unit,
175 request,
176 expiry,
177 request_lookup_id,
178 pubkey,
179 created_time,
180 amount_paid,
181 amount_issued,
182 updated_at,
183 last_checked,
184 payment_method,
185 request_lookup_id_kind,
186 extra_json
187 FROM
188 mint_quote
189 WHERE request = :request
190 {for_update_clause}
191 "#
192 );
193
194 let mut mint_quote = query(&query_str)?
195 .bind("request", request.to_string())
196 .fetch_one(executor)
197 .await?
198 .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
199 .transpose()?;
200
201 if let Some(quote) = mint_quote.as_mut() {
202 let payments = get_mint_quote_payments(executor, "e.id).await?;
203 let issuance = get_mint_quote_issuance(executor, "e.id).await?;
204 quote.issuance = issuance;
205 quote.payments = payments;
206 }
207
208 Ok(mint_quote)
209}
210
211pub(super) async fn get_mint_quote_by_request_lookup_id_inner<T>(
212 executor: &T,
213 request_lookup_id: &PaymentIdentifier,
214 for_update: bool,
215) -> Result<Option<MintQuote>, Error>
216where
217 T: DatabaseExecutor,
218{
219 let for_update_clause = if for_update { "FOR UPDATE" } else { "" };
220 let query_str = format!(
221 r#"
222 SELECT
223 id,
224 amount,
225 unit,
226 request,
227 expiry,
228 request_lookup_id,
229 pubkey,
230 created_time,
231 amount_paid,
232 amount_issued,
233 updated_at,
234 last_checked,
235 payment_method,
236 request_lookup_id_kind,
237 extra_json
238 FROM
239 mint_quote
240 WHERE request_lookup_id = :request_lookup_id
241 AND request_lookup_id_kind = :request_lookup_id_kind
242 {for_update_clause}
243 "#
244 );
245
246 let mut mint_quote = query(&query_str)?
247 .bind("request_lookup_id", request_lookup_id.to_string())
248 .bind("request_lookup_id_kind", request_lookup_id.kind())
249 .fetch_one(executor)
250 .await?
251 .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
252 .transpose()?;
253
254 if let Some(quote) = mint_quote.as_mut() {
255 let payments = get_mint_quote_payments(executor, "e.id).await?;
256 let issuance = get_mint_quote_issuance(executor, "e.id).await?;
257 quote.issuance = issuance;
258 quote.payments = payments;
259 }
260
261 Ok(mint_quote)
262}
263
264pub(super) async fn get_melt_quote_inner<T>(
265 executor: &T,
266 quote_id: &QuoteId,
267 for_update: bool,
268) -> Result<Option<mint::MeltQuote>, Error>
269where
270 T: DatabaseExecutor,
271{
272 let for_update_clause = if for_update { "FOR UPDATE" } else { "" };
273 let query_str = format!(
274 r#"
275 SELECT
276 id,
277 unit,
278 amount,
279 request,
280 fee_reserve,
281 expiry,
282 state,
283 payment_proof,
284 estimated_blocks,
285 request_lookup_id,
286 created_time,
287 paid_time,
288 payment_method,
289 options,
290 request_lookup_id_kind,
291 extra_json,
292 fee_options,
293 selected_fee_index
294 FROM
295 melt_quote
296 WHERE
297 id=:id
298 {for_update_clause}
299 "#
300 );
301
302 query(&query_str)?
303 .bind("id", quote_id.to_string())
304 .fetch_one(executor)
305 .await?
306 .map(sql_row_to_melt_quote)
307 .transpose()
308}
309
310pub(super) async fn get_mint_quotes_inner<T>(
311 executor: &T,
312 quote_ids: &[QuoteId],
313 for_update: bool,
314) -> Result<Vec<Option<MintQuote>>, Error>
315where
316 T: DatabaseExecutor,
317{
318 let for_update_clause = if for_update { "FOR UPDATE" } else { "" };
319 let query_str = format!(
320 r#"
321 SELECT
322 id,
323 amount,
324 unit,
325 request,
326 expiry,
327 request_lookup_id,
328 pubkey,
329 created_time,
330 amount_paid,
331 amount_issued,
332 updated_at,
333 last_checked,
334 payment_method,
335 request_lookup_id_kind,
336 extra_json
337 FROM
338 mint_quote
339 WHERE id IN (:quote_ids)
340 ORDER BY id
341 {for_update_clause}
342 "#
343 );
344
345 let rows = query(&query_str)?
346 .bind_vec(
347 "quote_ids",
348 quote_ids.iter().map(|x| x.to_string()).collect(),
349 )?
350 .fetch_all(executor)
351 .await?;
352
353 let mut quote_map: HashMap<String, MintQuote> = HashMap::with_capacity(rows.len());
355
356 for row in rows {
357 let quote = sql_row_to_mint_quote(row, vec![], vec![])?;
358 quote_map.insert(quote.id.to_string(), quote);
359 }
360
361 for quote in quote_map.values_mut() {
363 let payments = get_mint_quote_payments(executor, "e.id).await?;
364 let issuance = get_mint_quote_issuance(executor, "e.id).await?;
365 quote.payments = payments;
366 quote.issuance = issuance;
367 }
368
369 let result: Vec<Option<MintQuote>> = quote_ids
371 .iter()
372 .map(|id| quote_map.remove(&id.to_string()))
373 .collect();
374
375 Ok(result)
376}
377
378pub(super) async fn get_melt_quotes_by_request_lookup_id_inner<T>(
379 executor: &T,
380 request_lookup_id: &PaymentIdentifier,
381 for_update: bool,
382) -> Result<Vec<mint::MeltQuote>, Error>
383where
384 T: DatabaseExecutor,
385{
386 let for_update_clause = if for_update { "FOR UPDATE" } else { "" };
387 let query_str = format!(
388 r#"
389 SELECT
390 id,
391 unit,
392 amount,
393 request,
394 fee_reserve,
395 expiry,
396 state,
397 payment_proof,
398 estimated_blocks,
399 request_lookup_id,
400 created_time,
401 paid_time,
402 payment_method,
403 options,
404 request_lookup_id_kind,
405 extra_json,
406 fee_options,
407 selected_fee_index
408 FROM
409 melt_quote
410 WHERE
411 request_lookup_id = :request_lookup_id
412 AND request_lookup_id_kind = :request_lookup_id_kind
413 ORDER BY id
414 {for_update_clause}
415 "#
416 );
417
418 query(&query_str)?
419 .bind("request_lookup_id", request_lookup_id.to_string())
420 .bind("request_lookup_id_kind", request_lookup_id.kind())
421 .fetch_all(executor)
422 .await?
423 .into_iter()
424 .map(sql_row_to_melt_quote)
425 .collect::<Result<Vec<_>, _>>()
426}
427
428async fn lock_melt_quote_and_related_inner<T>(
434 executor: &T,
435 quote_id: &QuoteId,
436) -> Result<LockedMeltQuotes, Error>
437where
438 T: DatabaseExecutor,
439{
440 let query_str = r#"
447 SELECT
448 id,
449 unit,
450 amount,
451 request,
452 fee_reserve,
453 expiry,
454 state,
455 payment_proof,
456 estimated_blocks,
457 request_lookup_id,
458 created_time,
459 paid_time,
460 payment_method,
461 options,
462 request_lookup_id_kind,
463 extra_json,
464 fee_options,
465 selected_fee_index
466 FROM
467 melt_quote
468 WHERE
469 (
470 request_lookup_id IS NOT NULL
471 AND request_lookup_id = (SELECT request_lookup_id FROM melt_quote WHERE id = :quote_id)
472 AND request_lookup_id_kind = (SELECT request_lookup_id_kind FROM melt_quote WHERE id = :quote_id)
473 )
474 OR
475 (
476 id = :quote_id
477 AND (SELECT request_lookup_id FROM melt_quote WHERE id = :quote_id) IS NULL
478 )
479 ORDER BY id
480 FOR UPDATE
481 "#;
482
483 let all_quotes: Vec<mint::MeltQuote> = query(query_str)?
484 .bind("quote_id", quote_id.to_string())
485 .fetch_all(executor)
486 .await?
487 .into_iter()
488 .map(sql_row_to_melt_quote)
489 .collect::<Result<Vec<_>, _>>()?;
490
491 let target_quote = all_quotes.iter().find(|q| &q.id == quote_id).cloned();
493
494 Ok(LockedMeltQuotes {
495 target: target_quote.map(|q| q.into()),
496 all_related: all_quotes.into_iter().map(|q| q.into()).collect(),
497 })
498}
499
500#[instrument(skip_all)]
501fn sql_row_to_mint_quote(
502 row: Vec<Column>,
503 payments: Vec<IncomingPayment>,
504 issueances: Vec<Issuance>,
505) -> Result<MintQuote, Error> {
506 unpack_into!(
507 let (
508 id, amount, unit, request, expiry, request_lookup_id,
509 pubkey, created_time, amount_paid, amount_issued, updated_at, last_checked,
510 payment_method,
511 request_lookup_id_kind, extra_json
512 ) = row
513 );
514
515 let request_str = column_as_string!(&request);
516 let request_lookup_id = column_as_nullable_string!(&request_lookup_id).unwrap_or_else(|| {
517 Bolt11Invoice::from_str(&request_str)
518 .map(|invoice| invoice.payment_hash().to_string())
519 .unwrap_or_else(|_| request_str.clone())
520 });
521 let request_lookup_id_kind = column_as_string!(request_lookup_id_kind);
522
523 let pubkey = column_as_nullable_string!(&pubkey)
524 .map(|pk| PublicKey::from_hex(&pk))
525 .transpose()?;
526
527 let id = column_as_string!(id);
528 let amount: Option<u64> = column_as_nullable_number!(amount);
529 let amount_paid: u64 = column_as_number!(amount_paid);
530 let amount_issued: u64 = column_as_number!(amount_issued);
531 let updated_at: u64 = column_as_number!(updated_at);
532 let last_checked: u64 = column_as_number!(last_checked);
533 let payment_method = column_as_string!(payment_method, PaymentMethod::from_str);
534 let unit = column_as_string!(unit, CurrencyUnit::from_str);
535 let extra_json = column_as_nullable_string!(&extra_json)
536 .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
537
538 let mut quote = MintQuote::new(
539 Some(QuoteId::from_str(&id)?),
540 request_str,
541 unit.clone(),
542 amount.map(|a| Amount::from(a).with_unit(unit.clone())),
543 column_as_number!(expiry),
544 PaymentIdentifier::new(&request_lookup_id_kind, &request_lookup_id)
545 .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?,
546 pubkey,
547 Amount::from(amount_paid).with_unit(unit.clone()),
548 Amount::from(amount_issued).with_unit(unit),
549 payment_method,
550 column_as_number!(created_time),
551 updated_at,
552 payments,
553 issueances,
554 extra_json,
555 );
556 quote.set_last_checked(last_checked);
557
558 Ok(quote)
559}
560
561fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<mint::MeltQuote, Error> {
563 unpack_into!(
564 let (
565 id,
566 unit,
567 amount,
568 request,
569 fee_reserve,
570 expiry,
571 state,
572 payment_proof,
573 estimated_blocks,
574 request_lookup_id,
575 created_time,
576 paid_time,
577 payment_method,
578 options,
579 request_lookup_id_kind,
580 extra_json,
581 fee_options,
582 selected_fee_index
583 ) = row
584 );
585
586 let id = column_as_string!(id);
587 let amount: u64 = column_as_number!(amount);
588 let fee_reserve: u64 = column_as_number!(fee_reserve);
589
590 let expiry = column_as_number!(expiry);
591 let payment_proof = column_as_nullable_string!(payment_proof);
592 let options = column_as_nullable_string!(options);
593 let options = options.and_then(|o| serde_json::from_str(&o).ok());
594 let created_time: i64 = column_as_number!(created_time);
595 let paid_time = column_as_nullable_number!(paid_time);
596 let payment_method = PaymentMethod::from_str(&column_as_string!(payment_method))?;
597 let extra_json = column_as_nullable_string!(&extra_json)
598 .and_then(|value| serde_json::from_str::<serde_json::Value>(&value).ok());
599 let fee_options = column_as_nullable_string!(&fee_options)
600 .and_then(|value| serde_json::from_str::<Vec<MeltQuoteOnchainFeeOption>>(&value).ok())
601 .unwrap_or_default();
602 let selected_fee_index: Option<u32> = column_as_nullable_number!(selected_fee_index);
603
604 let state =
605 MeltQuoteState::from_str(&column_as_string!(&state)).map_err(ConversionError::from)?;
606 let estimated_blocks: Option<u32> = column_as_nullable_number!(estimated_blocks);
607
608 let unit = column_as_string!(unit);
609 let request = column_as_string!(request);
610
611 let request_lookup_id_kind = column_as_nullable_string!(request_lookup_id_kind);
612
613 let request_lookup_id = column_as_nullable_string!(&request_lookup_id).or_else(|| {
614 Bolt11Invoice::from_str(&request)
615 .ok()
616 .map(|invoice| invoice.payment_hash().to_string())
617 });
618
619 let request_lookup_id = if let (Some(id_kind), Some(request_lookup_id)) =
620 (request_lookup_id_kind, request_lookup_id)
621 {
622 Some(
623 PaymentIdentifier::new(&id_kind, &request_lookup_id)
624 .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?,
625 )
626 } else {
627 None
628 };
629
630 let request = match serde_json::from_str(&request) {
631 Ok(req) => req,
632 Err(err) => {
633 tracing::debug!(
634 "Melt quote from pre migrations defaulting to bolt11 {}.",
635 err
636 );
637 let bolt11 = Bolt11Invoice::from_str(&request)
638 .map_err(|e| Error::Internal(format!("Could not parse invoice: {e}")))?;
639 MeltPaymentRequest::Bolt11 { bolt11 }
640 }
641 };
642
643 let unit = CurrencyUnit::from_str(&unit)?;
644 MeltQuote::from_db(
645 QuoteId::from_str(&id)?,
646 unit,
647 request,
648 amount,
649 fee_reserve,
650 state,
651 expiry,
652 payment_proof,
653 request_lookup_id,
654 options,
655 created_time as u64,
656 paid_time,
657 payment_method,
658 extra_json,
659 estimated_blocks,
660 fee_options,
661 selected_fee_index,
662 )
663 .map_err(|e| Error::Internal(format!("Invalid onchain melt quote row: {e}")))
664}
665
666#[async_trait]
667impl<RM> MintQuotesTransaction for SQLTransaction<RM>
668where
669 RM: DatabasePool + 'static,
670{
671 type Err = Error;
672
673 async fn add_melt_request(
674 &mut self,
675 quote_id: &QuoteId,
676 inputs_amount: Amount<CurrencyUnit>,
677 inputs_fee: Amount<CurrencyUnit>,
678 ) -> Result<(), Self::Err> {
679 query(
681 r#"
682 INSERT INTO melt_request
683 (quote_id, inputs_amount, inputs_fee)
684 VALUES
685 (:quote_id, :inputs_amount, :inputs_fee)
686 "#,
687 )?
688 .bind("quote_id", quote_id.to_string())
689 .bind("inputs_amount", inputs_amount.to_i64())
690 .bind("inputs_fee", inputs_fee.to_i64())
691 .execute(&self.inner)
692 .await?;
693
694 Ok(())
695 }
696
697 async fn add_blinded_messages(
698 &mut self,
699 quote_id: Option<&QuoteId>,
700 blinded_messages: &[BlindedMessage],
701 operation: &Operation,
702 ) -> Result<(), Self::Err> {
703 let current_time = unix_time();
704
705 let mut ordered_messages = blinded_messages.iter().enumerate().collect::<Vec<_>>();
706 ordered_messages.sort_unstable_by(|(_, left), (_, right)| {
707 left.blinded_secret
708 .to_bytes()
709 .cmp(&right.blinded_secret.to_bytes())
710 });
711
712 for (i, message) in ordered_messages {
715 match query(
716 r#"
717 INSERT INTO blind_signature
718 (blinded_message, amount, keyset_id, c, quote_id, created_time, operation_kind, operation_id, order_index)
719 VALUES
720 (:blinded_message, :amount, :keyset_id, NULL, :quote_id, :created_time, :operation_kind, :operation_id, :order_index)
721 "#,
722 )?
723 .bind(
724 "blinded_message",
725 message.blinded_secret.to_bytes().to_vec(),
726 )
727 .bind("amount", message.amount.to_i64())
728 .bind("keyset_id", message.keyset_id.to_string())
729 .bind("quote_id", quote_id.map(|q| q.to_string()))
730 .bind("created_time", current_time as i64)
731 .bind("operation_kind", operation.kind().to_string())
732 .bind("operation_id", operation.id().to_string())
733 .bind("order_index", i as i64)
734 .execute(&self.inner)
735 .await
736 {
737 Ok(_) => continue,
738 Err(database::Error::Duplicate) => {
739 return Err(database::Error::Duplicate);
744 }
745 Err(err) => return Err(err),
746 }
747 }
748
749 Ok(())
750 }
751
752 async fn delete_blinded_messages(
753 &mut self,
754 blinded_secrets: &[PublicKey],
755 ) -> Result<(), Self::Err> {
756 query(
758 r#"
759 SELECT blinded_message
760 FROM blind_signature
761 WHERE blinded_message IN (:blinded_secrets) AND c IS NULL
762 ORDER BY blinded_message
763 FOR UPDATE
764 "#,
765 )?
766 .bind_vec(
767 "blinded_secrets",
768 blinded_secrets
769 .iter()
770 .map(|secret| secret.to_bytes().to_vec())
771 .collect(),
772 )?
773 .fetch_all(&self.inner)
774 .await?;
775
776 query(
779 r#"
780 DELETE FROM blind_signature
781 WHERE blinded_message IN (:blinded_secrets) AND c IS NULL
782 "#,
783 )?
784 .bind_vec(
785 "blinded_secrets",
786 blinded_secrets
787 .iter()
788 .map(|secret| secret.to_bytes().to_vec())
789 .collect(),
790 )?
791 .execute(&self.inner)
792 .await?;
793
794 Ok(())
795 }
796
797 async fn get_melt_request_and_blinded_messages(
798 &mut self,
799 quote_id: &QuoteId,
800 ) -> Result<Option<database::mint::MeltRequestInfo>, Self::Err> {
801 let melt_request_row = query(
802 r#"
803 SELECT mr.inputs_amount, mr.inputs_fee, mq.unit
804 FROM melt_request mr
805 JOIN melt_quote mq ON mr.quote_id = mq.id
806 WHERE mr.quote_id = :quote_id
807 FOR UPDATE
808 "#,
809 )?
810 .bind("quote_id", quote_id.to_string())
811 .fetch_one(&self.inner)
812 .await?;
813
814 if let Some(row) = melt_request_row {
815 let inputs_amount: u64 = column_as_number!(row[0].clone());
816 let inputs_fee: u64 = column_as_number!(row[1].clone());
817 let unit_str = column_as_string!(&row[2]);
818 let unit = CurrencyUnit::from_str(&unit_str)?;
819
820 let blinded_messages_rows = query(
821 r#"
822 SELECT blinded_message, keyset_id, amount
823 FROM blind_signature
824 WHERE quote_id = :quote_id AND c IS NULL
825 ORDER BY order_index ASC, blinded_message ASC
826 FOR UPDATE
827 "#,
828 )?
829 .bind("quote_id", quote_id.to_string())
830 .fetch_all(&self.inner)
831 .await?;
832
833 let blinded_messages: Result<Vec<BlindedMessage>, Error> = blinded_messages_rows
834 .into_iter()
835 .map(|row| -> Result<BlindedMessage, Error> {
836 let blinded_message_key =
837 column_as_string!(&row[0], PublicKey::from_hex, PublicKey::from_slice);
838 let keyset_id = column_as_string!(&row[1], Id::from_str, Id::from_bytes);
839 let amount: u64 = column_as_number!(row[2].clone());
840
841 Ok(BlindedMessage {
842 blinded_secret: blinded_message_key,
843 keyset_id,
844 amount: Amount::from(amount),
845 witness: None, })
847 })
848 .collect();
849 let blinded_messages = blinded_messages?;
850
851 Ok(Some(database::mint::MeltRequestInfo {
852 inputs_amount: Amount::from(inputs_amount).with_unit(unit.clone()),
853 inputs_fee: Amount::from(inputs_fee).with_unit(unit),
854 change_outputs: blinded_messages,
855 }))
856 } else {
857 Ok(None)
858 }
859 }
860
861 async fn delete_melt_request(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> {
862 query(
864 r#"
865 DELETE FROM melt_request
866 WHERE quote_id = :quote_id
867 "#,
868 )?
869 .bind("quote_id", quote_id.to_string())
870 .execute(&self.inner)
871 .await?;
872
873 query(
876 r#"
877 SELECT blinded_message
878 FROM blind_signature
879 WHERE quote_id = :quote_id AND c IS NULL
880 ORDER BY order_index ASC, blinded_message ASC
881 FOR UPDATE
882 "#,
883 )?
884 .bind("quote_id", quote_id.to_string())
885 .fetch_all(&self.inner)
886 .await?;
887
888 query(
890 r#"
891 DELETE FROM blind_signature
892 WHERE quote_id = :quote_id AND c IS NULL
893 "#,
894 )?
895 .bind("quote_id", quote_id.to_string())
896 .execute(&self.inner)
897 .await?;
898
899 Ok(())
900 }
901
902 async fn update_mint_quote(
903 &mut self,
904 quote: &mut Acquired<mint::MintQuote>,
905 ) -> Result<(), Self::Err> {
906 let mut changes = if let Some(changes) = quote.take_changes() {
907 changes
908 } else {
909 return Ok(());
910 };
911
912 if changes.issuances.is_none() && changes.payments.is_none() {
913 return Ok(());
914 }
915
916 for payment in changes.payments.take().unwrap_or_default() {
917 query(
918 r#"
919 INSERT INTO mint_quote_payments
920 (quote_id, payment_id, amount, timestamp)
921 VALUES (:quote_id, :payment_id, :amount, :timestamp)
922 "#,
923 )?
924 .bind("quote_id", quote.id.to_string())
925 .bind("payment_id", payment.payment_id)
926 .bind("amount", payment.amount.to_i64())
927 .bind("timestamp", payment.time as i64)
928 .execute(&self.inner)
929 .await
930 .map_err(|err| {
931 tracing::error!("SQLite could not insert payment ID: {}", err);
932 err
933 })?;
934 }
935
936 let current_time = unix_time();
937
938 for amount_issued in changes.issuances.take().unwrap_or_default() {
939 query(
940 r#"
941 INSERT INTO mint_quote_issued
942 (quote_id, amount, timestamp)
943 VALUES (:quote_id, :amount, :timestamp);
944 "#,
945 )?
946 .bind("quote_id", quote.id.to_string())
947 .bind("amount", amount_issued.to_i64())
948 .bind("timestamp", current_time as i64)
949 .execute(&self.inner)
950 .await?;
951 }
952
953 query(
954 r#"
955 UPDATE
956 mint_quote
957 SET
958 amount_issued = :amount_issued,
959 amount_paid = :amount_paid,
960 updated_at = CASE
961 WHEN :current_time > updated_at + 1 THEN :current_time
962 ELSE updated_at + 1
963 END
964 WHERE
965 id = :quote_id
966 "#,
967 )?
968 .bind("quote_id", quote.id.to_string())
969 .bind("amount_issued", quote.amount_issued().to_i64())
970 .bind("amount_paid", quote.amount_paid().to_i64())
971 .bind("current_time", current_time as i64)
972 .execute(&self.inner)
973 .await
974 .inspect_err(|err| {
975 tracing::error!("SQL could not update mint quote accounting: {}", err);
976 })?;
977
978 let updated_at = query(
979 r#"
980 SELECT updated_at
981 FROM mint_quote
982 WHERE id = :quote_id
983 "#,
984 )?
985 .bind("quote_id", quote.id.to_string())
986 .pluck(&self.inner)
987 .await?
988 .map(|updated_at| Ok::<_, Error>(column_as_number!(updated_at)))
989 .transpose()?
990 .ok_or_else(|| {
991 Error::Internal(format!(
992 "mint quote {} missing after accounting update",
993 quote.id
994 ))
995 })?;
996
997 quote.set_updated_at(updated_at);
998
999 Ok(())
1000 }
1001
1002 #[instrument(skip_all)]
1003 async fn add_mint_quote(&mut self, quote: MintQuote) -> Result<Acquired<MintQuote>, Self::Err> {
1004 query(
1005 r#"
1006 INSERT INTO mint_quote (
1007 id, amount, unit, request, expiry, request_lookup_id, pubkey,
1008 created_time, updated_at, last_checked, payment_method,
1009 request_lookup_id_kind, extra_json
1010 )
1011 VALUES (
1012 :id, :amount, :unit, :request, :expiry, :request_lookup_id, :pubkey,
1013 :created_time, :updated_at, :last_checked, :payment_method,
1014 :request_lookup_id_kind, :extra_json
1015 )
1016 "#,
1017 )?
1018 .bind("id", quote.id.to_string())
1019 .bind("amount", quote.amount.clone().map(|a| a.to_i64()))
1020 .bind("unit", quote.unit.to_string())
1021 .bind("request", quote.request.clone())
1022 .bind("expiry", quote.expiry as i64)
1023 .bind("request_lookup_id", quote.request_lookup_id.to_string())
1024 .bind("pubkey", quote.pubkey.map(|p| p.to_string()))
1025 .bind("created_time", quote.created_time as i64)
1026 .bind("updated_at", quote.updated_at() as i64)
1027 .bind("last_checked", quote.last_checked() as i64)
1028 .bind("payment_method", quote.payment_method.to_string())
1029 .bind("request_lookup_id_kind", quote.request_lookup_id.kind())
1030 .bind(
1031 "extra_json",
1032 quote.extra_json.as_ref().map(|v| v.to_string()),
1033 )
1034 .execute(&self.inner)
1035 .await?;
1036
1037 Ok(quote.into())
1038 }
1039
1040 async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
1041 let fee_options_json = serde_json::to_string(quote.fee_options()).ok();
1047
1048 query(
1050 r#"
1051 INSERT INTO melt_quote
1052 (
1053 id, unit, amount, request, fee_reserve, state,
1054 expiry, payment_proof, estimated_blocks, fee_options, selected_fee_index,
1055 request_lookup_id, created_time, paid_time, options, request_lookup_id_kind,
1056 payment_method, extra_json
1057 )
1058 VALUES
1059 (
1060 :id, :unit, :amount, :request, :fee_reserve, :state,
1061 :expiry, :payment_proof, :estimated_blocks, :fee_options, :selected_fee_index,
1062 :request_lookup_id, :created_time, :paid_time, :options, :request_lookup_id_kind,
1063 :payment_method, :extra_json
1064 )
1065 "#,
1066 )?
1067 .bind("id", quote.id.to_string())
1068 .bind("unit", quote.unit.to_string())
1069 .bind("amount", quote.amount().to_i64())
1070 .bind("request", serde_json::to_string("e.request)?)
1071 .bind("fee_reserve", quote.fee_reserve().to_i64())
1072 .bind("state", quote.state.to_string())
1073 .bind("expiry", quote.expiry as i64)
1074 .bind("payment_proof", quote.payment_proof)
1075 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1076 .bind("fee_options", fee_options_json)
1077 .bind(
1078 "selected_fee_index",
1079 quote.selected_fee_index.map(i64::from),
1080 )
1081 .bind(
1082 "request_lookup_id",
1083 quote.request_lookup_id.as_ref().map(|id| id.to_string()),
1084 )
1085 .bind("created_time", quote.created_time as i64)
1086 .bind("paid_time", quote.paid_time.map(|t| t as i64))
1087 .bind(
1088 "options",
1089 quote.options.map(|o| serde_json::to_string(&o).ok()),
1090 )
1091 .bind(
1092 "request_lookup_id_kind",
1093 quote.request_lookup_id.map(|id| id.kind()),
1094 )
1095 .bind("payment_method", quote.payment_method.to_string())
1096 .bind(
1097 "extra_json",
1098 quote.extra_json.as_ref().map(|value| value.to_string()),
1099 )
1100 .execute(&self.inner)
1101 .await?;
1102
1103 Ok(())
1104 }
1105
1106 async fn update_melt_quote_request_lookup_id(
1107 &mut self,
1108 quote: &mut Acquired<mint::MeltQuote>,
1109 new_request_lookup_id: &PaymentIdentifier,
1110 ) -> Result<(), Self::Err> {
1111 query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)?
1112 .bind("new_req_id", new_request_lookup_id.to_string())
1113 .bind("new_kind", new_request_lookup_id.kind())
1114 .bind("id", quote.id.to_string())
1115 .execute(&self.inner)
1116 .await?;
1117 quote.request_lookup_id = Some(new_request_lookup_id.clone());
1118 Ok(())
1119 }
1120
1121 async fn update_melt_quote_state(
1122 &mut self,
1123 quote: &mut Acquired<mint::MeltQuote>,
1124 state: MeltQuoteState,
1125 payment_proof: Option<String>,
1126 ) -> Result<MeltQuoteState, Self::Err> {
1127 let old_state = quote.state;
1128
1129 check_melt_quote_state_transition(old_state, state)?;
1130
1131 let rec = if state == MeltQuoteState::Paid {
1138 let current_time = unix_time();
1139 quote.paid_time = Some(current_time);
1140 quote.payment_proof = payment_proof.clone();
1141 query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_proof = :payment_proof, fee_reserve = :fee_reserve, estimated_blocks = :estimated_blocks, selected_fee_index = :selected_fee_index WHERE id = :id"#)?
1142 .bind("state", state.to_string())
1143 .bind("paid_time", current_time as i64)
1144 .bind("payment_proof", payment_proof)
1145 .bind("fee_reserve", quote.fee_reserve().value() as i64)
1146 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1147 .bind("selected_fee_index", quote.selected_fee_index.map(i64::from))
1148 .bind("id", quote.id.to_string())
1149 .execute(&self.inner)
1150 .await
1151 } else {
1152 query(r#"UPDATE melt_quote SET state = :state, fee_reserve = :fee_reserve, estimated_blocks = :estimated_blocks, selected_fee_index = :selected_fee_index WHERE id = :id"#)?
1153 .bind("state", state.to_string())
1154 .bind("fee_reserve", quote.fee_reserve().value() as i64)
1155 .bind("estimated_blocks", quote.estimated_blocks.map(i64::from))
1156 .bind("selected_fee_index", quote.selected_fee_index.map(i64::from))
1157 .bind("id", quote.id.to_string())
1158 .execute(&self.inner)
1159 .await
1160 };
1161
1162 match rec {
1163 Ok(_) => {}
1164 Err(err) => {
1165 tracing::error!("SQLite Could not update melt quote");
1166 return Err(err);
1167 }
1168 };
1169
1170 quote.state = state;
1171
1172 if state == MeltQuoteState::Unpaid || state == MeltQuoteState::Failed {
1173 self.delete_melt_request("e.id).await?;
1174 }
1175
1176 Ok(old_state)
1177 }
1178
1179 async fn get_mint_quote(
1180 &mut self,
1181 quote_id: &QuoteId,
1182 ) -> Result<Option<Acquired<MintQuote>>, Self::Err> {
1183 get_mint_quote_inner(&self.inner, quote_id, true)
1184 .await
1185 .map(|quote| quote.map(|inner| inner.into()))
1186 }
1187
1188 async fn get_mint_quotes_by_ids(
1189 &mut self,
1190 quote_ids: &[QuoteId],
1191 ) -> Result<Vec<Option<Acquired<MintQuote>>>, Self::Err> {
1192 get_mint_quotes_inner(&self.inner, quote_ids, true)
1193 .await
1194 .map(|quotes| {
1195 quotes
1196 .into_iter()
1197 .map(|quote| quote.map(|inner| inner.into()))
1198 .collect()
1199 })
1200 }
1201
1202 async fn get_melt_quote(
1203 &mut self,
1204 quote_id: &QuoteId,
1205 ) -> Result<Option<Acquired<mint::MeltQuote>>, Self::Err> {
1206 get_melt_quote_inner(&self.inner, quote_id, true)
1207 .await
1208 .map(|quote| quote.map(|inner| inner.into()))
1209 }
1210
1211 async fn get_melt_quotes_by_request_lookup_id(
1212 &mut self,
1213 request_lookup_id: &PaymentIdentifier,
1214 ) -> Result<Vec<Acquired<mint::MeltQuote>>, Self::Err> {
1215 get_melt_quotes_by_request_lookup_id_inner(&self.inner, request_lookup_id, true)
1216 .await
1217 .map(|quote| quote.into_iter().map(|inner| inner.into()).collect())
1218 }
1219
1220 async fn lock_melt_quote_and_related(
1221 &mut self,
1222 quote_id: &QuoteId,
1223 ) -> Result<LockedMeltQuotes, Self::Err> {
1224 lock_melt_quote_and_related_inner(&self.inner, quote_id).await
1225 }
1226
1227 async fn get_mint_quote_by_request(
1228 &mut self,
1229 request: &str,
1230 ) -> Result<Option<Acquired<MintQuote>>, Self::Err> {
1231 get_mint_quote_by_request_inner(&self.inner, request, true)
1232 .await
1233 .map(|quote| quote.map(|inner| inner.into()))
1234 }
1235
1236 async fn get_mint_quote_by_request_lookup_id(
1237 &mut self,
1238 request_lookup_id: &PaymentIdentifier,
1239 ) -> Result<Option<Acquired<MintQuote>>, Self::Err> {
1240 get_mint_quote_by_request_lookup_id_inner(&self.inner, request_lookup_id, true)
1241 .await
1242 .map(|quote| quote.map(|inner| inner.into()))
1243 }
1244}
1245
1246#[async_trait]
1247impl<RM> MintQuotesDatabase for SQLMintDatabase<RM>
1248where
1249 RM: DatabasePool + 'static,
1250{
1251 type Err = Error;
1252
1253 async fn get_mint_quote(&self, quote_id: &QuoteId) -> Result<Option<MintQuote>, Self::Err> {
1254 #[cfg(feature = "prometheus")]
1255 let metrics = MintMetricGuard::new("get_mint_quote");
1256
1257 let result = async {
1258 let conn = self
1259 .pool
1260 .get()
1261 .await
1262 .map_err(|e| Error::Database(Box::new(e)))?;
1263 get_mint_quote_inner(&*conn, quote_id, false).await
1264 }
1265 .await;
1266
1267 #[cfg(feature = "prometheus")]
1268 {
1269 metrics.record(result.is_ok());
1270 }
1271
1272 result
1273 }
1274
1275 async fn try_update_mint_quote_last_checked(
1276 &self,
1277 quote_id: &QuoteId,
1278 last_checked: u64,
1279 min_interval: u64,
1280 ) -> Result<bool, Self::Err> {
1281 let conn = self
1282 .pool
1283 .get()
1284 .await
1285 .map_err(|e| Error::Database(Box::new(e)))?;
1286 let threshold = last_checked.saturating_sub(min_interval);
1287 let rows_affected = query(
1288 r#"
1289 UPDATE mint_quote
1290 SET last_checked = :last_checked
1291 WHERE id = :quote_id
1292 AND last_checked < :threshold
1293 "#,
1294 )?
1295 .bind("quote_id", quote_id.to_string())
1296 .bind("last_checked", last_checked as i64)
1297 .bind("threshold", threshold as i64)
1298 .execute(&*conn)
1299 .await?;
1300
1301 Ok(rows_affected > 0)
1302 }
1303
1304 async fn get_mint_quotes_by_ids(
1305 &self,
1306 quote_ids: &[QuoteId],
1307 ) -> Result<Vec<Option<MintQuote>>, Self::Err> {
1308 let conn = self
1309 .pool
1310 .get()
1311 .await
1312 .map_err(|e| Error::Database(Box::new(e)))?;
1313 get_mint_quotes_inner(&*conn, quote_ids, false).await
1314 }
1315
1316 async fn get_mint_quote_by_request(
1317 &self,
1318 request: &str,
1319 ) -> Result<Option<MintQuote>, Self::Err> {
1320 let conn = self
1321 .pool
1322 .get()
1323 .await
1324 .map_err(|e| Error::Database(Box::new(e)))?;
1325 get_mint_quote_by_request_inner(&*conn, request, false).await
1326 }
1327
1328 async fn get_mint_quote_by_request_lookup_id(
1329 &self,
1330 request_lookup_id: &PaymentIdentifier,
1331 ) -> Result<Option<MintQuote>, Self::Err> {
1332 let conn = self
1333 .pool
1334 .get()
1335 .await
1336 .map_err(|e| Error::Database(Box::new(e)))?;
1337 get_mint_quote_by_request_lookup_id_inner(&*conn, request_lookup_id, false).await
1338 }
1339
1340 async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
1341 let conn = self
1342 .pool
1343 .get()
1344 .await
1345 .map_err(|e| Error::Database(Box::new(e)))?;
1346 let mut mint_quotes = query(
1347 r#"
1348 SELECT
1349 id,
1350 amount,
1351 unit,
1352 request,
1353 expiry,
1354 request_lookup_id,
1355 pubkey,
1356 created_time,
1357 amount_paid,
1358 amount_issued,
1359 updated_at,
1360 last_checked,
1361 payment_method,
1362 request_lookup_id_kind,
1363 extra_json
1364 FROM
1365 mint_quote
1366 "#,
1367 )?
1368 .fetch_all(&*conn)
1369 .await?
1370 .into_iter()
1371 .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
1372 .collect::<Result<Vec<_>, _>>()?;
1373
1374 for quote in mint_quotes.as_mut_slice() {
1375 let payments = get_mint_quote_payments(&*conn, "e.id).await?;
1376 let issuance = get_mint_quote_issuance(&*conn, "e.id).await?;
1377 quote.issuance = issuance;
1378 quote.payments = payments;
1379 }
1380
1381 Ok(mint_quotes)
1382 }
1383
1384 async fn get_melt_quote(
1385 &self,
1386 quote_id: &QuoteId,
1387 ) -> Result<Option<mint::MeltQuote>, Self::Err> {
1388 #[cfg(feature = "prometheus")]
1389 let metrics = MintMetricGuard::new("get_melt_quote");
1390
1391 let result = async {
1392 let conn = self
1393 .pool
1394 .get()
1395 .await
1396 .map_err(|e| Error::Database(Box::new(e)))?;
1397 get_melt_quote_inner(&*conn, quote_id, false).await
1398 }
1399 .await;
1400
1401 #[cfg(feature = "prometheus")]
1402 {
1403 metrics.record(result.is_ok());
1404 }
1405
1406 result
1407 }
1408
1409 async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
1410 let conn = self
1411 .pool
1412 .get()
1413 .await
1414 .map_err(|e| Error::Database(Box::new(e)))?;
1415 Ok(query(
1416 r#"
1417 SELECT
1418 id,
1419 unit,
1420 amount,
1421 request,
1422 fee_reserve,
1423 expiry,
1424 state,
1425 payment_proof,
1426 estimated_blocks,
1427 request_lookup_id,
1428 created_time,
1429 paid_time,
1430 payment_method,
1431 options,
1432 request_lookup_id_kind,
1433 extra_json,
1434 fee_options,
1435 selected_fee_index
1436 FROM
1437 melt_quote
1438 "#,
1439 )?
1440 .fetch_all(&*conn)
1441 .await?
1442 .into_iter()
1443 .map(sql_row_to_melt_quote)
1444 .collect::<Result<Vec<_>, _>>()?)
1445 }
1446}