use std::str::FromStr;
use anyhow::bail;
use cdk_common::nut00::ProofsMethods;
use cdk_common::MeltOptions;
use lightning_invoice::Bolt11Invoice;
use tracing::instrument;
use uuid::Uuid;
use super::{
CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
Mint, PaymentMethod, PublicKey, State,
};
use crate::amount::to_unit;
use crate::cdk_payment::{MakePaymentResponse, MintPayment};
use crate::mint::verification::Verification;
use crate::mint::SigFlag;
use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag};
use crate::nuts::MeltQuoteState;
use crate::types::PaymentProcessorKey;
use crate::util::unix_time;
use crate::{cdk_payment, ensure_cdk, Amount, Error};
impl Mint {
#[instrument(skip_all)]
async fn check_melt_request_acceptable(
&self,
amount: Amount,
unit: CurrencyUnit,
method: PaymentMethod,
request: String,
options: Option<MeltOptions>,
) -> Result<(), Error> {
let mint_info = self.localstore.get_mint_info().await?;
let nut05 = mint_info.nuts.nut05;
let nut15 = mint_info.nuts.nut15;
ensure_cdk!(!nut05.disabled, Error::MeltingDisabled);
let settings = nut05
.get_settings(&unit, &method)
.ok_or(Error::UnsupportedUnit)?;
let amount = match options {
Some(MeltOptions::Mpp { mpp: _ }) => {
if (self.localstore.get_mint_quote_by_request(&request).await?).is_some() {
return Err(Error::InternalMultiPartMeltQuote);
}
if !nut15
.methods
.into_iter()
.any(|m| m.method == method && m.unit == unit)
{
return Err(Error::MppUnitMethodNotSupported(unit, method));
}
amount
}
None => amount,
};
let is_above_max = matches!(settings.max_amount, Some(max) if amount > max);
let is_below_min = matches!(settings.min_amount, Some(min) if amount < min);
match is_above_max || is_below_min {
true => {
tracing::error!(
"Melt amount out of range: {} is not within {} and {}",
amount,
settings.min_amount.unwrap_or_default(),
settings.max_amount.unwrap_or_default(),
);
Err(Error::AmountOutofLimitRange(
settings.min_amount.unwrap_or_default(),
settings.max_amount.unwrap_or_default(),
amount,
))
}
false => Ok(()),
}
}
#[instrument(skip_all)]
pub async fn get_melt_bolt11_quote(
&self,
melt_request: &MeltQuoteBolt11Request,
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
let MeltQuoteBolt11Request {
request,
unit,
options,
..
} = melt_request;
let amount_msats = melt_request.amount_msat()?;
let amount_quote_unit = to_unit(amount_msats, &CurrencyUnit::Msat, unit)?;
self.check_melt_request_acceptable(
amount_quote_unit,
unit.clone(),
PaymentMethod::Bolt11,
request.to_string(),
*options,
)
.await?;
let ln = self
.ln
.get(&PaymentProcessorKey::new(
unit.clone(),
PaymentMethod::Bolt11,
))
.ok_or_else(|| {
tracing::info!("Could not get ln backend for {}, bolt11 ", unit);
Error::UnsupportedUnit
})?;
let payment_quote = ln
.get_payment_quote(
&melt_request.request.to_string(),
&melt_request.unit,
melt_request.options,
)
.await
.map_err(|err| {
tracing::error!(
"Could not get payment quote for mint quote, {} bolt11, {}",
unit,
err
);
Error::UnsupportedUnit
})?;
let msats_to_pay = options.map(|opt| opt.amount_msat());
let melt_ttl = self.localstore.get_quote_ttl().await?.melt_ttl;
let quote = MeltQuote::new(
request.to_string(),
unit.clone(),
payment_quote.amount,
payment_quote.fee,
unix_time() + melt_ttl,
payment_quote.request_lookup_id.clone(),
msats_to_pay,
);
tracing::debug!(
"New melt quote {} for {} {} with request id {}",
quote.id,
amount_quote_unit,
unit,
payment_quote.request_lookup_id
);
self.localstore.add_melt_quote(quote.clone()).await?;
Ok(quote.into())
}
#[instrument(skip(self))]
pub async fn check_melt_quote(
&self,
quote_id: &Uuid,
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
let quote = self
.localstore
.get_melt_quote(quote_id)
.await?
.ok_or(Error::UnknownQuote)?;
let blind_signatures = self
.localstore
.get_blind_signatures_for_quote(quote_id)
.await?;
let change = (!blind_signatures.is_empty()).then_some(blind_signatures);
Ok(MeltQuoteBolt11Response {
quote: quote.id,
paid: Some(quote.state == MeltQuoteState::Paid),
state: quote.state,
expiry: quote.expiry,
amount: quote.amount,
fee_reserve: quote.fee_reserve,
payment_preimage: quote.payment_preimage,
change,
request: Some(quote.request.clone()),
unit: Some(quote.unit.clone()),
})
}
#[instrument(skip_all)]
pub async fn update_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
self.localstore.add_melt_quote(quote).await?;
Ok(())
}
#[instrument(skip_all)]
pub async fn melt_quotes(&self) -> Result<Vec<MeltQuote>, Error> {
let quotes = self.localstore.get_melt_quotes().await?;
Ok(quotes)
}
#[instrument(skip(self))]
pub async fn remove_melt_quote(&self, quote_id: &Uuid) -> Result<(), Error> {
self.localstore.remove_melt_quote(quote_id).await?;
Ok(())
}
#[instrument(skip_all)]
pub async fn check_melt_expected_ln_fees(
&self,
melt_quote: &MeltQuote,
melt_request: &MeltBolt11Request<Uuid>,
) -> Result<Option<Amount>, Error> {
let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
.expect("Quote unit is checked above that it can convert to msat");
let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
Some(amt) => amt.into(),
None => melt_quote
.msat_to_pay
.ok_or(Error::InvoiceAmountUndefined)?,
};
let partial_amount = match invoice_amount_msats > quote_msats {
true => {
let partial_msats = invoice_amount_msats - quote_msats;
Some(
to_unit(partial_msats, &CurrencyUnit::Msat, &melt_quote.unit)
.map_err(|_| Error::UnsupportedUnit)?,
)
}
false => None,
};
let amount_to_pay = match partial_amount {
Some(amount_to_pay) => amount_to_pay,
None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &melt_quote.unit)
.map_err(|_| Error::UnsupportedUnit)?,
};
let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
tracing::error!("Proof inputs in melt quote overflowed");
Error::AmountOverflow
})?;
if amount_to_pay + melt_quote.fee_reserve > inputs_amount_quote_unit {
tracing::debug!(
"Not enough inputs provided: {} msats needed {} msats",
inputs_amount_quote_unit,
amount_to_pay
);
return Err(Error::TransactionUnbalanced(
inputs_amount_quote_unit.into(),
amount_to_pay.into(),
melt_quote.fee_reserve.into(),
));
}
Ok(partial_amount)
}
#[instrument(skip_all)]
pub async fn verify_melt_request(
&self,
melt_request: &MeltBolt11Request<Uuid>,
) -> Result<MeltQuote, Error> {
let state = self
.localstore
.update_melt_quote_state(&melt_request.quote, MeltQuoteState::Pending)
.await?;
match state {
MeltQuoteState::Unpaid | MeltQuoteState::Failed => Ok(()),
MeltQuoteState::Pending => Err(Error::PendingQuote),
MeltQuoteState::Paid => Err(Error::PaidQuote),
MeltQuoteState::Unknown => Err(Error::UnknownPaymentState),
}?;
let quote = self
.localstore
.get_melt_quote(&melt_request.quote)
.await?
.ok_or(Error::UnknownQuote)?;
let Verification {
amount: input_amount,
unit: input_unit,
} = self.verify_inputs(&melt_request.inputs).await?;
ensure_cdk!(input_unit.is_some(), Error::UnsupportedUnit);
let input_ys = melt_request.inputs.ys()?;
let fee = self.get_proofs_fee(&melt_request.inputs).await?;
let required_total = quote.amount + quote.fee_reserve + fee;
if input_amount < required_total {
tracing::info!(
"Swap request unbalanced: {}, outputs {}, fee {}",
input_amount,
quote.amount,
fee
);
return Err(Error::TransactionUnbalanced(
input_amount.into(),
quote.amount.into(),
(fee + quote.fee_reserve).into(),
));
}
self.localstore
.add_proofs(melt_request.inputs.clone(), None)
.await?;
self.check_ys_spendable(&input_ys, State::Pending).await?;
let EnforceSigFlag { sig_flag, .. } = enforce_sig_flag(melt_request.inputs.clone());
ensure_cdk!(sig_flag.ne(&SigFlag::SigAll), Error::SigAllUsedInMelt);
if let Some(outputs) = &melt_request.outputs {
if !outputs.is_empty() {
let Verification {
amount: _,
unit: output_unit,
} = self.verify_outputs(outputs).await?;
ensure_cdk!(input_unit == output_unit, Error::UnsupportedUnit);
}
}
tracing::debug!("Verified melt quote: {}", melt_request.quote);
Ok(quote)
}
#[instrument(skip_all)]
pub async fn process_unpaid_melt(
&self,
melt_request: &MeltBolt11Request<Uuid>,
) -> Result<(), Error> {
let input_ys = melt_request.inputs.ys()?;
self.localstore
.remove_proofs(&input_ys, Some(melt_request.quote))
.await?;
self.localstore
.update_melt_quote_state(&melt_request.quote, MeltQuoteState::Unpaid)
.await?;
if let Ok(Some(quote)) = self.localstore.get_melt_quote(&melt_request.quote).await {
self.pubsub_manager
.melt_quote_status(quote, None, None, MeltQuoteState::Unpaid);
}
for public_key in input_ys {
self.pubsub_manager
.proof_state((public_key, State::Unspent));
}
Ok(())
}
#[instrument(skip_all)]
pub async fn melt_bolt11(
&self,
melt_request: &MeltBolt11Request<Uuid>,
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
use std::sync::Arc;
async fn check_payment_state(
ln: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
melt_quote: &MeltQuote,
) -> anyhow::Result<MakePaymentResponse> {
match ln
.check_outgoing_payment(&melt_quote.request_lookup_id)
.await
{
Ok(response) => Ok(response),
Err(check_err) => {
tracing::error!(
"Could not check the status of payment for {},. Proofs stuck as pending",
melt_quote.id
);
tracing::error!("Checking payment error: {}", check_err);
bail!("Could not check payment status")
}
}
}
let quote = match self.verify_melt_request(melt_request).await {
Ok(quote) => quote,
Err(err) => {
tracing::debug!("Error attempting to verify melt quote: {}", err);
if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!(
"Could not reset melt quote {} state: {}",
melt_request.quote,
err
);
}
return Err(err);
}
};
let settled_internally_amount =
match self.handle_internal_melt_mint("e, melt_request).await {
Ok(amount) => amount,
Err(err) => {
tracing::error!("Attempting to settle internally failed");
if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!(
"Could not reset melt quote {} state: {}",
melt_request.quote,
err
);
}
return Err(err);
}
};
let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
Some(amount_spent) => (None, amount_spent),
None => {
let partial_amount = match quote.unit {
CurrencyUnit::Sat | CurrencyUnit::Msat => {
match self.check_melt_expected_ln_fees("e, melt_request).await {
Ok(amount) => amount,
Err(err) => {
tracing::error!("Fee is not expected: {}", err);
if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(Error::Internal);
}
}
}
_ => None,
};
tracing::debug!("partial_amount: {:?}", partial_amount);
let ln = match self.ln.get(&PaymentProcessorKey::new(
quote.unit.clone(),
PaymentMethod::Bolt11,
)) {
Some(ln) => ln,
None => {
tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(Error::UnsupportedUnit);
}
};
let pre = match ln
.make_payment(quote.clone(), partial_amount, Some(quote.fee_reserve))
.await
{
Ok(pay)
if pay.status == MeltQuoteState::Unknown
|| pay.status == MeltQuoteState::Failed =>
{
let check_response = check_payment_state(Arc::clone(ln), "e)
.await
.map_err(|_| Error::Internal)?;
if check_response.status == MeltQuoteState::Paid {
tracing::warn!("Pay invoice returned {} but check returned {}. Proofs stuck as pending", pay.status.to_string(), check_response.status.to_string());
return Err(Error::Internal);
}
check_response
}
Ok(pay) => pay,
Err(err) => {
if matches!(err, cdk_payment::Error::InvoiceAlreadyPaid) {
tracing::debug!("Invoice already paid, resetting melt quote");
if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(Error::RequestAlreadyPaid);
}
tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);
let check_response = check_payment_state(Arc::clone(ln), "e)
.await
.map_err(|_| Error::Internal)?;
if check_response.status == MeltQuoteState::Paid {
tracing::warn!("Pay invoice returned an error but check returned {}. Proofs stuck as pending", check_response.status.to_string());
return Err(Error::Internal);
}
check_response
}
};
match pre.status {
MeltQuoteState::Paid => (),
MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
tracing::info!(
"Lightning payment for quote {} failed.",
melt_request.quote
);
if let Err(err) = self.process_unpaid_melt(melt_request).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(Error::PaymentFailed);
}
MeltQuoteState::Pending => {
tracing::warn!(
"LN payment pending, proofs are stuck as pending for quote: {}",
melt_request.quote
);
return Err(Error::PendingQuote);
}
}
let amount_spent =
to_unit(pre.total_spent, &pre.unit, "e.unit).unwrap_or_default();
let payment_lookup_id = pre.payment_lookup_id;
if payment_lookup_id != quote.request_lookup_id {
tracing::info!(
"Payment lookup id changed post payment from {} to {}",
quote.request_lookup_id,
payment_lookup_id
);
let mut melt_quote = quote;
melt_quote.request_lookup_id = payment_lookup_id;
if let Err(err) = self.localstore.add_melt_quote(melt_quote).await {
tracing::warn!("Could not update payment lookup id: {}", err);
}
}
(pre.payment_proof, amount_spent)
}
};
let res = self
.process_melt_request(melt_request, preimage, amount_spent_quote_unit)
.await
.map_err(|err| {
tracing::error!("Could not process melt request: {}", err);
err
})?;
Ok(res)
}
#[instrument(skip_all)]
pub async fn process_melt_request(
&self,
melt_request: &MeltBolt11Request<Uuid>,
payment_preimage: Option<String>,
total_spent: Amount,
) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
tracing::debug!("Processing melt quote: {}", melt_request.quote);
let quote = self
.localstore
.get_melt_quote(&melt_request.quote)
.await?
.ok_or(Error::UnknownQuote)?;
let input_ys = melt_request.inputs.ys()?;
self.localstore
.update_proofs_states(&input_ys, State::Spent)
.await?;
self.localstore
.update_melt_quote_state(&melt_request.quote, MeltQuoteState::Paid)
.await?;
self.pubsub_manager.melt_quote_status(
"e,
payment_preimage.clone(),
None,
MeltQuoteState::Paid,
);
for public_key in input_ys {
self.pubsub_manager.proof_state((public_key, State::Spent));
}
let mut change = None;
if melt_request.proofs_amount()? > total_spent {
if let Some(outputs) = melt_request.outputs.clone() {
let blinded_messages: Vec<PublicKey> =
outputs.iter().map(|b| b.blinded_secret).collect();
if self
.localstore
.get_blind_signatures(&blinded_messages)
.await?
.iter()
.flatten()
.next()
.is_some()
{
tracing::info!("Output has already been signed");
return Err(Error::BlindedMessageAlreadySigned);
}
let change_target = melt_request.proofs_amount()? - total_spent;
let mut amounts = change_target.split();
let mut change_sigs = Vec::with_capacity(amounts.len());
if outputs.len().lt(&amounts.len()) {
tracing::debug!(
"Providing change requires {} blinded messages, but only {} provided",
amounts.len(),
outputs.len()
);
amounts.sort_by(|a, b| b.cmp(a));
}
let mut outputs = outputs;
for (amount, blinded_message) in amounts.iter().zip(&mut outputs) {
blinded_message.amount = *amount;
let blinded_signature = self.blind_sign(blinded_message).await?;
change_sigs.push(blinded_signature)
}
self.localstore
.add_blind_signatures(
&outputs[0..change_sigs.len()]
.iter()
.map(|o| o.blinded_secret)
.collect::<Vec<PublicKey>>(),
&change_sigs,
Some(quote.id),
)
.await?;
change = Some(change_sigs);
}
}
Ok(MeltQuoteBolt11Response {
amount: quote.amount,
paid: Some(true),
payment_preimage,
change,
quote: quote.id,
fee_reserve: quote.fee_reserve,
state: MeltQuoteState::Paid,
expiry: quote.expiry,
request: Some(quote.request.clone()),
unit: Some(quote.unit.clone()),
})
}
}