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