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