//! Quotes database implementation use std::str::FromStr; use async_trait::async_trait; use cdk_common::database::mint::{Acquired, LockedMeltQuotes}; use cdk_common::database::{ self, ConversionError, Error, MintQuotesDatabase, MintQuotesTransaction, }; use cdk_common::mint::{ self, IncomingPayment, Issuance, MeltPaymentRequest, MeltQuote, MintQuote, Operation, }; use cdk_common::payment::PaymentIdentifier; use cdk_common::quote_id::QuoteId; use cdk_common::state::check_melt_quote_state_transition; use cdk_common::util::unix_time; use cdk_common::{ Amount, BlindedMessage, CurrencyUnit, Id, MeltQuoteState, PaymentMethod, PublicKey, }; #[cfg(feature = "prometheus")] use cdk_prometheus::METRICS; use lightning_invoice::Bolt11Invoice; use tracing::instrument; use super::{SQLMintDatabase, SQLTransaction}; use crate::database::DatabaseExecutor; use crate::pool::DatabasePool; use crate::stmt::{query, Column}; use crate::{ column_as_nullable_number, column_as_nullable_string, column_as_number, column_as_string, unpack_into, }; async fn get_mint_quote_payments( conn: &C, quote_id: &QuoteId, ) -> Result, Error> where C: DatabaseExecutor + Send + Sync, { // Get payment IDs and timestamps from the mint_quote_payments table query( r#" SELECT p.payment_id, p.timestamp, p.amount, q.unit FROM mint_quote_payments p JOIN mint_quote q ON p.quote_id = q.id WHERE p.quote_id=:quote_id "#, )? .bind("quote_id", quote_id.to_string()) .fetch_all(conn) .await? .into_iter() .map(|row| { let amount: u64 = column_as_number!(row[2].clone()); let time: u64 = column_as_number!(row[1].clone()); let unit = column_as_string!(&row[3], CurrencyUnit::from_str); Ok(IncomingPayment::new( Amount::from(amount).with_unit(unit), column_as_string!(&row[0]), time, )) }) .collect() } async fn get_mint_quote_issuance(conn: &C, quote_id: &QuoteId) -> Result, Error> where C: DatabaseExecutor + Send + Sync, { // Get payment IDs and timestamps from the mint_quote_payments table query( r#" SELECT i.amount, i.timestamp, q.unit FROM mint_quote_issued i JOIN mint_quote q ON i.quote_id = q.id WHERE i.quote_id=:quote_id "#, )? .bind("quote_id", quote_id.to_string()) .fetch_all(conn) .await? .into_iter() .map(|row| { let time: u64 = column_as_number!(row[1].clone()); let unit = column_as_string!(&row[2], CurrencyUnit::from_str); Ok(Issuance::new( Amount::from_i64(column_as_number!(row[0].clone())) .expect("Is amount when put into db") .with_unit(unit), time, )) }) .collect() } // Inline helper functions that work with both connections and transactions pub(super) async fn get_mint_quote_inner( executor: &T, quote_id: &QuoteId, for_update: bool, ) -> Result, Error> where T: DatabaseExecutor, { let payments = get_mint_quote_payments(executor, quote_id).await?; let issuance = get_mint_quote_issuance(executor, quote_id).await?; let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; let query_str = format!( r#" SELECT id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method, request_lookup_id_kind FROM mint_quote WHERE id = :id {for_update_clause} "# ); query(&query_str)? .bind("id", quote_id.to_string()) .fetch_one(executor) .await? .map(|row| sql_row_to_mint_quote(row, payments, issuance)) .transpose() } pub(super) async fn get_mint_quote_by_request_inner( executor: &T, request: &str, for_update: bool, ) -> Result, Error> where T: DatabaseExecutor, { let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; let query_str = format!( r#" SELECT id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method, request_lookup_id_kind FROM mint_quote WHERE request = :request {for_update_clause} "# ); let mut mint_quote = query(&query_str)? .bind("request", request.to_string()) .fetch_one(executor) .await? .map(|row| sql_row_to_mint_quote(row, vec![], vec![])) .transpose()?; if let Some(quote) = mint_quote.as_mut() { let payments = get_mint_quote_payments(executor, "e.id).await?; let issuance = get_mint_quote_issuance(executor, "e.id).await?; quote.issuance = issuance; quote.payments = payments; } Ok(mint_quote) } pub(super) async fn get_mint_quote_by_request_lookup_id_inner( executor: &T, request_lookup_id: &PaymentIdentifier, for_update: bool, ) -> Result, Error> where T: DatabaseExecutor, { let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; let query_str = format!( r#" SELECT id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method, request_lookup_id_kind FROM mint_quote WHERE request_lookup_id = :request_lookup_id AND request_lookup_id_kind = :request_lookup_id_kind {for_update_clause} "# ); let mut mint_quote = query(&query_str)? .bind("request_lookup_id", request_lookup_id.to_string()) .bind("request_lookup_id_kind", request_lookup_id.kind()) .fetch_one(executor) .await? .map(|row| sql_row_to_mint_quote(row, vec![], vec![])) .transpose()?; if let Some(quote) = mint_quote.as_mut() { let payments = get_mint_quote_payments(executor, "e.id).await?; let issuance = get_mint_quote_issuance(executor, "e.id).await?; quote.issuance = issuance; quote.payments = payments; } Ok(mint_quote) } pub(super) async fn get_melt_quote_inner( executor: &T, quote_id: &QuoteId, for_update: bool, ) -> Result, Error> where T: DatabaseExecutor, { let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; let query_str = format!( r#" SELECT id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id, created_time, paid_time, payment_method, options, request_lookup_id_kind FROM melt_quote WHERE id=:id {for_update_clause} "# ); query(&query_str)? .bind("id", quote_id.to_string()) .fetch_one(executor) .await? .map(sql_row_to_melt_quote) .transpose() } pub(super) async fn get_melt_quotes_by_request_lookup_id_inner( executor: &T, request_lookup_id: &PaymentIdentifier, for_update: bool, ) -> Result, Error> where T: DatabaseExecutor, { let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; let query_str = format!( r#" SELECT id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id, created_time, paid_time, payment_method, options, request_lookup_id_kind FROM melt_quote WHERE request_lookup_id = :request_lookup_id AND request_lookup_id_kind = :request_lookup_id_kind {for_update_clause} "# ); query(&query_str)? .bind("request_lookup_id", request_lookup_id.to_string()) .bind("request_lookup_id_kind", request_lookup_id.kind()) .fetch_all(executor) .await? .into_iter() .map(sql_row_to_melt_quote) .collect::, _>>() } /// Locks a melt quote and all related quotes atomically to prevent deadlocks. /// /// This function acquires all locks in a single query with consistent ordering (by ID), /// preventing the circular wait condition that can occur when locks are acquired in /// separate queries. async fn lock_melt_quote_and_related_inner( executor: &T, quote_id: &QuoteId, ) -> Result where T: DatabaseExecutor, { // Use a single query with subquery to atomically lock: // 1. All quotes with the same request_lookup_id as the target quote, OR // 2. Just the target quote if it has no request_lookup_id // // The ORDER BY ensures consistent lock acquisition order across transactions, // preventing deadlocks. let query_str = r#" SELECT id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id, created_time, paid_time, payment_method, options, request_lookup_id_kind FROM melt_quote WHERE ( request_lookup_id IS NOT NULL AND request_lookup_id = (SELECT request_lookup_id FROM melt_quote WHERE id = :quote_id) AND request_lookup_id_kind = (SELECT request_lookup_id_kind FROM melt_quote WHERE id = :quote_id) ) OR ( id = :quote_id AND (SELECT request_lookup_id FROM melt_quote WHERE id = :quote_id) IS NULL ) ORDER BY id FOR UPDATE "#; let all_quotes: Vec = query(query_str)? .bind("quote_id", quote_id.to_string()) .fetch_all(executor) .await? .into_iter() .map(sql_row_to_melt_quote) .collect::, _>>()?; // Find the target quote from the locked set let target_quote = all_quotes.iter().find(|q| &q.id == quote_id).cloned(); Ok(LockedMeltQuotes { target: target_quote.map(|q| q.into()), all_related: all_quotes.into_iter().map(|q| q.into()).collect(), }) } #[instrument(skip_all)] fn sql_row_to_mint_quote( row: Vec, payments: Vec, issueances: Vec, ) -> Result { unpack_into!( let ( id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method, request_lookup_id_kind ) = row ); let request_str = column_as_string!(&request); let request_lookup_id = column_as_nullable_string!(&request_lookup_id).unwrap_or_else(|| { Bolt11Invoice::from_str(&request_str) .map(|invoice| invoice.payment_hash().to_string()) .unwrap_or_else(|_| request_str.clone()) }); let request_lookup_id_kind = column_as_string!(request_lookup_id_kind); let pubkey = column_as_nullable_string!(&pubkey) .map(|pk| PublicKey::from_hex(&pk)) .transpose()?; let id = column_as_string!(id); let amount: Option = column_as_nullable_number!(amount); let amount_paid: u64 = column_as_number!(amount_paid); let amount_issued: u64 = column_as_number!(amount_issued); let payment_method = column_as_string!(payment_method, PaymentMethod::from_str); let unit = column_as_string!(unit, CurrencyUnit::from_str); Ok(MintQuote::new( Some(QuoteId::from_str(&id)?), request_str, unit.clone(), amount.map(|a| Amount::from(a).with_unit(unit.clone())), column_as_number!(expiry), PaymentIdentifier::new(&request_lookup_id_kind, &request_lookup_id) .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?, pubkey, Amount::from(amount_paid).with_unit(unit.clone()), Amount::from(amount_issued).with_unit(unit), payment_method, column_as_number!(created_time), payments, issueances, None, )) } // FIXME: Replace unwrap with proper error handling fn sql_row_to_melt_quote(row: Vec) -> Result { unpack_into!( let ( id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id, created_time, paid_time, payment_method, options, request_lookup_id_kind ) = row ); let id = column_as_string!(id); let amount: u64 = column_as_number!(amount); let fee_reserve: u64 = column_as_number!(fee_reserve); let expiry = column_as_number!(expiry); let payment_preimage = column_as_nullable_string!(payment_preimage); let options = column_as_nullable_string!(options); let options = options.and_then(|o| serde_json::from_str(&o).ok()); let created_time: i64 = column_as_number!(created_time); let paid_time = column_as_nullable_number!(paid_time); let payment_method = PaymentMethod::from_str(&column_as_string!(payment_method))?; let state = MeltQuoteState::from_str(&column_as_string!(&state)).map_err(ConversionError::from)?; let unit = column_as_string!(unit); let request = column_as_string!(request); let request_lookup_id_kind = column_as_nullable_string!(request_lookup_id_kind); let request_lookup_id = column_as_nullable_string!(&request_lookup_id).or_else(|| { Bolt11Invoice::from_str(&request) .ok() .map(|invoice| invoice.payment_hash().to_string()) }); let request_lookup_id = if let (Some(id_kind), Some(request_lookup_id)) = (request_lookup_id_kind, request_lookup_id) { Some( PaymentIdentifier::new(&id_kind, &request_lookup_id) .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?, ) } else { None }; let request = match serde_json::from_str(&request) { Ok(req) => req, Err(err) => { tracing::debug!( "Melt quote from pre migrations defaulting to bolt11 {}.", err ); let bolt11 = Bolt11Invoice::from_str(&request) .map_err(|e| Error::Internal(format!("Could not parse invoice: {e}")))?; MeltPaymentRequest::Bolt11 { bolt11 } } }; let unit = CurrencyUnit::from_str(&unit)?; Ok(MeltQuote::from_db( QuoteId::from_str(&id)?, unit, request, amount, fee_reserve, state, expiry, payment_preimage, request_lookup_id, options, created_time as u64, paid_time, payment_method, )) } #[async_trait] impl MintQuotesTransaction for SQLTransaction where RM: DatabasePool + 'static, { type Err = Error; async fn add_melt_request( &mut self, quote_id: &QuoteId, inputs_amount: Amount, inputs_fee: Amount, ) -> Result<(), Self::Err> { // Insert melt_request query( r#" INSERT INTO melt_request (quote_id, inputs_amount, inputs_fee) VALUES (:quote_id, :inputs_amount, :inputs_fee) "#, )? .bind("quote_id", quote_id.to_string()) .bind("inputs_amount", inputs_amount.to_i64()) .bind("inputs_fee", inputs_fee.to_i64()) .execute(&self.inner) .await?; Ok(()) } async fn add_blinded_messages( &mut self, quote_id: Option<&QuoteId>, blinded_messages: &[BlindedMessage], operation: &Operation, ) -> Result<(), Self::Err> { let current_time = unix_time(); // Insert blinded_messages directly into blind_signature with c = NULL // Let the database constraint handle duplicate detection for message in blinded_messages { match query( r#" INSERT INTO blind_signature (blinded_message, amount, keyset_id, c, quote_id, created_time, operation_kind, operation_id) VALUES (:blinded_message, :amount, :keyset_id, NULL, :quote_id, :created_time, :operation_kind, :operation_id) "#, )? .bind( "blinded_message", message.blinded_secret.to_bytes().to_vec(), ) .bind("amount", message.amount.to_i64()) .bind("keyset_id", message.keyset_id.to_string()) .bind("quote_id", quote_id.map(|q| q.to_string())) .bind("created_time", current_time as i64) .bind("operation_kind", operation.kind().to_string()) .bind("operation_id", operation.id().to_string()) .execute(&self.inner) .await { Ok(_) => continue, Err(database::Error::Duplicate) => { // Primary key constraint violation - blinded message already exists // This could be either: // 1. Already signed (c IS NOT NULL) - definitely an error // 2. Already pending (c IS NULL) - also an error return Err(database::Error::Duplicate); } Err(err) => return Err(err), } } Ok(()) } async fn delete_blinded_messages( &mut self, blinded_secrets: &[PublicKey], ) -> Result<(), Self::Err> { if blinded_secrets.is_empty() { return Ok(()); } // Delete blinded messages from blind_signature table where c IS NULL // (only delete unsigned blinded messages) query( r#" DELETE FROM blind_signature WHERE blinded_message IN (:blinded_secrets) AND c IS NULL "#, )? .bind_vec( "blinded_secrets", blinded_secrets .iter() .map(|secret| secret.to_bytes().to_vec()) .collect(), ) .execute(&self.inner) .await?; Ok(()) } async fn get_melt_request_and_blinded_messages( &mut self, quote_id: &QuoteId, ) -> Result, Self::Err> { let melt_request_row = query( r#" SELECT mr.inputs_amount, mr.inputs_fee, mq.unit FROM melt_request mr JOIN melt_quote mq ON mr.quote_id = mq.id WHERE mr.quote_id = :quote_id FOR UPDATE "#, )? .bind("quote_id", quote_id.to_string()) .fetch_one(&self.inner) .await?; if let Some(row) = melt_request_row { let inputs_amount: u64 = column_as_number!(row[0].clone()); let inputs_fee: u64 = column_as_number!(row[1].clone()); let unit_str = column_as_string!(&row[2]); let unit = CurrencyUnit::from_str(&unit_str)?; // Get blinded messages from blind_signature table where c IS NULL let blinded_messages_rows = query( r#" SELECT blinded_message, keyset_id, amount FROM blind_signature WHERE quote_id = :quote_id AND c IS NULL "#, )? .bind("quote_id", quote_id.to_string()) .fetch_all(&self.inner) .await?; let blinded_messages: Result, Error> = blinded_messages_rows .into_iter() .map(|row| -> Result { let blinded_message_key = column_as_string!(&row[0], PublicKey::from_hex, PublicKey::from_slice); let keyset_id = column_as_string!(&row[1], Id::from_str, Id::from_bytes); let amount: u64 = column_as_number!(row[2].clone()); Ok(BlindedMessage { blinded_secret: blinded_message_key, keyset_id, amount: Amount::from(amount), witness: None, // Not storing witness in database currently }) }) .collect(); let blinded_messages = blinded_messages?; Ok(Some(database::mint::MeltRequestInfo { inputs_amount: Amount::from(inputs_amount).with_unit(unit.clone()), inputs_fee: Amount::from(inputs_fee).with_unit(unit), change_outputs: blinded_messages, })) } else { Ok(None) } } async fn delete_melt_request(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> { // Delete from melt_request table query( r#" DELETE FROM melt_request WHERE quote_id = :quote_id "#, )? .bind("quote_id", quote_id.to_string()) .execute(&self.inner) .await?; // Also delete blinded messages (where c IS NULL) from blind_signature table query( r#" DELETE FROM blind_signature WHERE quote_id = :quote_id AND c IS NULL "#, )? .bind("quote_id", quote_id.to_string()) .execute(&self.inner) .await?; Ok(()) } async fn update_mint_quote( &mut self, quote: &mut Acquired, ) -> Result<(), Self::Err> { let mut changes = if let Some(changes) = quote.take_changes() { changes } else { return Ok(()); }; if changes.issuances.is_none() && changes.payments.is_none() { return Ok(()); } for payment in changes.payments.take().unwrap_or_default() { query( r#" INSERT INTO mint_quote_payments (quote_id, payment_id, amount, timestamp) VALUES (:quote_id, :payment_id, :amount, :timestamp) "#, )? .bind("quote_id", quote.id.to_string()) .bind("payment_id", payment.payment_id) .bind("amount", payment.amount.to_i64()) .bind("timestamp", payment.time as i64) .execute(&self.inner) .await .map_err(|err| { tracing::error!("SQLite could not insert payment ID: {}", err); err })?; } let current_time = unix_time(); for amount_issued in changes.issuances.take().unwrap_or_default() { query( r#" INSERT INTO mint_quote_issued (quote_id, amount, timestamp) VALUES (:quote_id, :amount, :timestamp); "#, )? .bind("quote_id", quote.id.to_string()) .bind("amount", amount_issued.to_i64()) .bind("timestamp", current_time as i64) .execute(&self.inner) .await?; } query( r#" UPDATE mint_quote SET amount_issued = :amount_issued, amount_paid = :amount_paid WHERE id = :quote_id "#, )? .bind("quote_id", quote.id.to_string()) .bind("amount_issued", quote.amount_issued().to_i64()) .bind("amount_paid", quote.amount_paid().to_i64()) .execute(&self.inner) .await .inspect_err(|err| { tracing::error!("SQLite could not update mint quote amount_paid: {}", err); })?; Ok(()) } #[instrument(skip_all)] async fn add_mint_quote(&mut self, quote: MintQuote) -> Result, Self::Err> { query( r#" INSERT INTO mint_quote ( id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, payment_method, request_lookup_id_kind ) VALUES ( :id, :amount, :unit, :request, :expiry, :request_lookup_id, :pubkey, :created_time, :payment_method, :request_lookup_id_kind ) "#, )? .bind("id", quote.id.to_string()) .bind("amount", quote.amount.clone().map(|a| a.to_i64())) .bind("unit", quote.unit.to_string()) .bind("request", quote.request.clone()) .bind("expiry", quote.expiry as i64) .bind( "request_lookup_id", quote.request_lookup_id.to_string(), ) .bind("pubkey", quote.pubkey.map(|p| p.to_string())) .bind("created_time", quote.created_time as i64) .bind("payment_method", quote.payment_method.to_string()) .bind("request_lookup_id_kind", quote.request_lookup_id.kind()) .execute(&self.inner) .await?; Ok(quote.into()) } async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err> { // Now insert the new quote query( r#" INSERT INTO melt_quote ( id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id, created_time, paid_time, options, request_lookup_id_kind, payment_method ) VALUES ( :id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_preimage, :request_lookup_id, :created_time, :paid_time, :options, :request_lookup_id_kind, :payment_method ) "#, )? .bind("id", quote.id.to_string()) .bind("unit", quote.unit.to_string()) .bind("amount", quote.amount().to_i64()) .bind("request", serde_json::to_string("e.request)?) .bind("fee_reserve", quote.fee_reserve().to_i64()) .bind("state", quote.state.to_string()) .bind("expiry", quote.expiry as i64) .bind("payment_preimage", quote.payment_preimage) .bind( "request_lookup_id", quote.request_lookup_id.as_ref().map(|id| id.to_string()), ) .bind("created_time", quote.created_time as i64) .bind("paid_time", quote.paid_time.map(|t| t as i64)) .bind( "options", quote.options.map(|o| serde_json::to_string(&o).ok()), ) .bind( "request_lookup_id_kind", quote.request_lookup_id.map(|id| id.kind()), ) .bind("payment_method", quote.payment_method.to_string()) .execute(&self.inner) .await?; Ok(()) } async fn update_melt_quote_request_lookup_id( &mut self, quote: &mut Acquired, new_request_lookup_id: &PaymentIdentifier, ) -> Result<(), Self::Err> { query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)? .bind("new_req_id", new_request_lookup_id.to_string()) .bind("new_kind", new_request_lookup_id.kind()) .bind("id", quote.id.to_string()) .execute(&self.inner) .await?; quote.request_lookup_id = Some(new_request_lookup_id.clone()); Ok(()) } async fn update_melt_quote_state( &mut self, quote: &mut Acquired, state: MeltQuoteState, payment_proof: Option, ) -> Result { let old_state = quote.state; check_melt_quote_state_transition(old_state, state)?; let rec = if state == MeltQuoteState::Paid { let current_time = unix_time(); quote.paid_time = Some(current_time); quote.payment_preimage = payment_proof.clone(); query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)? .bind("state", state.to_string()) .bind("paid_time", current_time as i64) .bind("payment_preimage", payment_proof) .bind("id", quote.id.to_string()) .execute(&self.inner) .await } else { query(r#"UPDATE melt_quote SET state = :state WHERE id = :id"#)? .bind("state", state.to_string()) .bind("id", quote.id.to_string()) .execute(&self.inner) .await }; match rec { Ok(_) => {} Err(err) => { tracing::error!("SQLite Could not update melt quote"); return Err(err); } }; quote.state = state; if state == MeltQuoteState::Unpaid || state == MeltQuoteState::Failed { self.delete_melt_request("e.id).await?; } Ok(old_state) } async fn get_mint_quote( &mut self, quote_id: &QuoteId, ) -> Result>, Self::Err> { get_mint_quote_inner(&self.inner, quote_id, true) .await .map(|quote| quote.map(|inner| inner.into())) } async fn get_melt_quote( &mut self, quote_id: &QuoteId, ) -> Result>, Self::Err> { get_melt_quote_inner(&self.inner, quote_id, true) .await .map(|quote| quote.map(|inner| inner.into())) } async fn get_melt_quotes_by_request_lookup_id( &mut self, request_lookup_id: &PaymentIdentifier, ) -> Result>, Self::Err> { get_melt_quotes_by_request_lookup_id_inner(&self.inner, request_lookup_id, true) .await .map(|quote| quote.into_iter().map(|inner| inner.into()).collect()) } async fn lock_melt_quote_and_related( &mut self, quote_id: &QuoteId, ) -> Result { lock_melt_quote_and_related_inner(&self.inner, quote_id).await } async fn get_mint_quote_by_request( &mut self, request: &str, ) -> Result>, Self::Err> { get_mint_quote_by_request_inner(&self.inner, request, true) .await .map(|quote| quote.map(|inner| inner.into())) } async fn get_mint_quote_by_request_lookup_id( &mut self, request_lookup_id: &PaymentIdentifier, ) -> Result>, Self::Err> { get_mint_quote_by_request_lookup_id_inner(&self.inner, request_lookup_id, true) .await .map(|quote| quote.map(|inner| inner.into())) } } #[async_trait] impl MintQuotesDatabase for SQLMintDatabase where RM: DatabasePool + 'static, { type Err = Error; async fn get_mint_quote(&self, quote_id: &QuoteId) -> Result, Self::Err> { #[cfg(feature = "prometheus")] METRICS.inc_in_flight_requests("get_mint_quote"); #[cfg(feature = "prometheus")] let start_time = std::time::Instant::now(); let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; let result = get_mint_quote_inner(&*conn, quote_id, false).await; #[cfg(feature = "prometheus")] { let success = result.is_ok(); METRICS.record_mint_operation("get_mint_quote", success); METRICS.record_mint_operation_histogram( "get_mint_quote", success, start_time.elapsed().as_secs_f64(), ); METRICS.dec_in_flight_requests("get_mint_quote"); } result } async fn get_mint_quote_by_request( &self, request: &str, ) -> Result, Self::Err> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; get_mint_quote_by_request_inner(&*conn, request, false).await } async fn get_mint_quote_by_request_lookup_id( &self, request_lookup_id: &PaymentIdentifier, ) -> Result, Self::Err> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; get_mint_quote_by_request_lookup_id_inner(&*conn, request_lookup_id, false).await } async fn get_mint_quotes(&self) -> Result, Self::Err> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; let mut mint_quotes = query( r#" SELECT id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, amount_paid, amount_issued, payment_method, request_lookup_id_kind FROM mint_quote "#, )? .fetch_all(&*conn) .await? .into_iter() .map(|row| sql_row_to_mint_quote(row, vec![], vec![])) .collect::, _>>()?; for quote in mint_quotes.as_mut_slice() { let payments = get_mint_quote_payments(&*conn, "e.id).await?; let issuance = get_mint_quote_issuance(&*conn, "e.id).await?; quote.issuance = issuance; quote.payments = payments; } Ok(mint_quotes) } async fn get_melt_quote( &self, quote_id: &QuoteId, ) -> Result, Self::Err> { #[cfg(feature = "prometheus")] METRICS.inc_in_flight_requests("get_melt_quote"); #[cfg(feature = "prometheus")] let start_time = std::time::Instant::now(); let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; let result = get_melt_quote_inner(&*conn, quote_id, false).await; #[cfg(feature = "prometheus")] { let success = result.is_ok(); METRICS.record_mint_operation("get_melt_quote", success); METRICS.record_mint_operation_histogram( "get_melt_quote", success, start_time.elapsed().as_secs_f64(), ); METRICS.dec_in_flight_requests("get_melt_quote"); } result } async fn get_melt_quotes(&self) -> Result, Self::Err> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; Ok(query( r#" SELECT id, unit, amount, request, fee_reserve, expiry, state, payment_preimage, request_lookup_id, created_time, paid_time, payment_method, options, request_lookup_id_kind FROM melt_quote "#, )? .fetch_all(&*conn) .await? .into_iter() .map(sql_row_to_melt_quote) .collect::, _>>()?) } }