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