Skip to main content

cdk_sql_common/mint/
quotes.rs

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