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