use std::collections::HashMap;
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,
{
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,
{
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()
}
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,
extra_json
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,
extra_json
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,
extra_json
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_mint_quotes_inner<T>(
executor: &T,
quote_ids: &[QuoteId],
for_update: bool,
) -> Result<Vec<Option<MintQuote>>, Error>
where
T: DatabaseExecutor,
{
if quote_ids.is_empty() {
return Ok(Vec::new());
}
let placeholders: Vec<String> = quote_ids
.iter()
.enumerate()
.map(|(i, _)| format!(":id{i}"))
.collect();
let in_clause = placeholders.join(", ");
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,
extra_json
FROM
mint_quote
WHERE id IN ({in_clause})
{for_update_clause}
"#
);
let mut stmt = query(&query_str)?;
for (i, id) in quote_ids.iter().enumerate() {
stmt = stmt.bind(format!("id{i}"), id.to_string());
}
let rows = stmt.fetch_all(executor).await?;
let mut quote_map: HashMap<String, MintQuote> = HashMap::with_capacity(rows.len());
for row in rows {
let quote = sql_row_to_mint_quote(row, vec![], vec![])?;
quote_map.insert(quote.id.to_string(), quote);
}
for quote in quote_map.values_mut() {
let payments = get_mint_quote_payments(executor, "e.id).await?;
let issuance = get_mint_quote_issuance(executor, "e.id).await?;
quote.payments = payments;
quote.issuance = issuance;
}
let result: Vec<Option<MintQuote>> = quote_ids
.iter()
.map(|id| quote_map.remove(&id.to_string()))
.collect();
Ok(result)
}
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<_>, _>>()
}
async fn lock_melt_quote_and_related_inner<T>(
executor: &T,
quote_id: &QuoteId,
) -> Result<LockedMeltQuotes, Error>
where
T: DatabaseExecutor,
{
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<_>, _>>()?;
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,
extra_json
) = 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);
let extra_json = column_as_nullable_string!(&extra_json)
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
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,
extra_json,
))
}
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> {
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();
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) => {
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(());
}
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)?;
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, })
})
.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> {
query(
r#"
DELETE FROM melt_request
WHERE quote_id = :quote_id
"#,
)?
.bind("quote_id", quote_id.to_string())
.execute(&self.inner)
.await?;
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, extra_json
)
VALUES (
:id, :amount, :unit, :request, :expiry, :request_lookup_id, :pubkey, :created_time, :payment_method, :request_lookup_id_kind, :extra_json
)
"#,
)?
.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())
.bind(
"extra_json",
quote.extra_json.as_ref().map(|v| v.to_string()),
)
.execute(&self.inner)
.await?;
Ok(quote.into())
}
async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
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_mint_quotes_by_ids(
&mut self,
quote_ids: &[QuoteId],
) -> Result<Vec<Option<Acquired<MintQuote>>>, Self::Err> {
get_mint_quotes_inner(&self.inner, quote_ids, true)
.await
.map(|quotes| {
quotes
.into_iter()
.map(|quote| quote.map(|inner| inner.into()))
.collect()
})
}
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_quotes_by_ids(
&self,
quote_ids: &[QuoteId],
) -> Result<Vec<Option<MintQuote>>, Self::Err> {
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
get_mint_quotes_inner(&*conn, quote_ids, false).await
}
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,
extra_json
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<_>, _>>()?)
}
}