| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145 |
- //! 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<C>(
- conn: &C,
- quote_id: &QuoteId,
- ) -> Result<Vec<IncomingPayment>, 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<C>(conn: &C, quote_id: &QuoteId) -> Result<Vec<Issuance>, 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<T>(
- executor: &T,
- quote_id: &QuoteId,
- for_update: bool,
- ) -> Result<Option<MintQuote>, 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<T>(
- executor: &T,
- request: &str,
- for_update: bool,
- ) -> Result<Option<MintQuote>, 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<T>(
- executor: &T,
- request_lookup_id: &PaymentIdentifier,
- for_update: bool,
- ) -> Result<Option<MintQuote>, 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<T>(
- executor: &T,
- quote_id: &QuoteId,
- for_update: bool,
- ) -> Result<Option<mint::MeltQuote>, 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<T>(
- executor: &T,
- request_lookup_id: &PaymentIdentifier,
- for_update: bool,
- ) -> Result<Vec<mint::MeltQuote>, 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::<Result<Vec<_>, _>>()
- }
- /// 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<T>(
- executor: &T,
- quote_id: &QuoteId,
- ) -> Result<LockedMeltQuotes, Error>
- 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<mint::MeltQuote> = query(query_str)?
- .bind("quote_id", quote_id.to_string())
- .fetch_all(executor)
- .await?
- .into_iter()
- .map(sql_row_to_melt_quote)
- .collect::<Result<Vec<_>, _>>()?;
- // 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<Column>,
- payments: Vec<IncomingPayment>,
- issueances: Vec<Issuance>,
- ) -> Result<MintQuote, Error> {
- 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<u64> = 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<Column>) -> Result<mint::MeltQuote, Error> {
- 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<RM> MintQuotesTransaction for SQLTransaction<RM>
- where
- RM: DatabasePool + 'static,
- {
- type Err = Error;
- async fn add_melt_request(
- &mut self,
- quote_id: &QuoteId,
- inputs_amount: Amount<CurrencyUnit>,
- inputs_fee: Amount<CurrencyUnit>,
- ) -> 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<Option<database::mint::MeltRequestInfo>, 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<Vec<BlindedMessage>, Error> = blinded_messages_rows
- .into_iter()
- .map(|row| -> Result<BlindedMessage, Error> {
- 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<mint::MintQuote>,
- ) -> 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<Acquired<MintQuote>, 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<mint::MeltQuote>,
- 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<mint::MeltQuote>,
- state: MeltQuoteState,
- payment_proof: Option<String>,
- ) -> Result<MeltQuoteState, Self::Err> {
- 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<Option<Acquired<MintQuote>>, 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<Option<Acquired<mint::MeltQuote>>, 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<Vec<Acquired<mint::MeltQuote>>, 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<LockedMeltQuotes, Self::Err> {
- lock_melt_quote_and_related_inner(&self.inner, quote_id).await
- }
- async fn get_mint_quote_by_request(
- &mut self,
- request: &str,
- ) -> Result<Option<Acquired<MintQuote>>, 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<Option<Acquired<MintQuote>>, 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<RM> MintQuotesDatabase for SQLMintDatabase<RM>
- where
- RM: DatabasePool + 'static,
- {
- type Err = Error;
- async fn get_mint_quote(&self, quote_id: &QuoteId) -> Result<Option<MintQuote>, 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<Option<MintQuote>, 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<Option<MintQuote>, 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<Vec<MintQuote>, 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::<Result<Vec<_>, _>>()?;
- 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<Option<mint::MeltQuote>, 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<Vec<mint::MeltQuote>, 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::<Result<Vec<_>, _>>()?)
- }
- }
|