Skip to main content

cdk_sql_common/mint/
quotes.rs

1//! Quotes database implementation
2
3use 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    // Get payment IDs and timestamps from the mint_quote_payments table
44    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    // Get payment IDs and timestamps from the mint_quote_payments table
80    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
105// Inline helper functions that work with both connections and transactions
106pub(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    // Read payments and issuance while the row lock is held (when for_update=true).
147    // Any concurrent writer must wait for our transaction before it can acquire its
148    // own lock, so these reads reflect the true committed state.
149    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, &quote.id).await?;
201        let issuance = get_mint_quote_issuance(executor, &quote.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, &quote.id).await?;
253        let issuance = get_mint_quote_issuance(executor, &quote.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    // Build a map from quote ID to MintQuote (without payments/issuance yet)
349    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    // Now fetch payments and issuance for each found quote
357    for quote in quote_map.values_mut() {
358        let payments = get_mint_quote_payments(executor, &quote.id).await?;
359        let issuance = get_mint_quote_issuance(executor, &quote.id).await?;
360        quote.payments = payments;
361        quote.issuance = issuance;
362    }
363
364    // Reconstruct in the same order as input IDs
365    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
422/// Locks a melt quote and all related quotes atomically to prevent deadlocks.
423///
424/// This function acquires all locks in a single query with consistent ordering (by ID),
425/// preventing the circular wait condition that can occur when locks are acquired in
426/// separate queries.
427async 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    // Use a single query with subquery to atomically lock:
435    // 1. All quotes with the same request_lookup_id as the target quote, OR
436    // 2. Just the target quote if it has no request_lookup_id
437    //
438    // The ORDER BY ensures consistent lock acquisition order across transactions,
439    // preventing deadlocks.
440    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    // Find the target quote from the locked set
486    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
553// FIXME: Replace unwrap with proper error handling
554fn 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        // Insert melt_request
672        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        // Insert blinded_messages directly into blind_signature with c = NULL
698        // Let the database constraint handle duplicate detection
699        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                    // Primary key constraint violation - blinded message already exists
725                    // This could be either:
726                    // 1. Already signed (c IS NOT NULL) - definitely an error
727                    // 2. Already pending (c IS NULL) - also an error
728                    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        // Delete blinded messages from blind_signature table where c IS NULL
742        // (only delete unsigned blinded messages)
743        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, // Not storing witness in database currently
811                    })
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        // Delete from melt_request table
828        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        // Also delete blinded messages (where c IS NULL) from blind_signature table
839        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        // `fee_options` is captured by value here so the subsequent chained
965        // binds can move fields out of `quote` without a borrow conflict.
966        // It is also never rewritten after insert (see the UPDATE queries
967        // below) — the NUT rule "fixed for the lifetime of the quote" is
968        // enforced by never touching this column on update.
969        let fee_options_json = serde_json::to_string(quote.fee_options()).ok();
970
971        // Now insert the new quote
972        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(&quote.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        // NOTE: `fee_options` is intentionally omitted from both UPDATE
1055        // queries below. Per the NUT spec the returned `fee_options` are
1056        // fixed for the lifetime of the quote, so we never rewrite them
1057        // after insert. Only state/paid_time/payment_proof/fee_reserve/
1058        // estimated_blocks/selected_fee_index may change over the
1059        // quote's lifetime.
1060        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(&quote.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, &quote.id).await?;
1298            let issuance = get_mint_quote_issuance(&*conn, &quote.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}