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