Skip to content

Commit

Permalink
refactor: melt check
Browse files Browse the repository at this point in the history
  • Loading branch information
thesimplekid committed Sep 23, 2024
1 parent d0291c0 commit 8c06a05
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 135 deletions.
175 changes: 40 additions & 135 deletions crates/cdk-axum/src/router_handlers.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
use std::str::FromStr;

use anyhow::Result;
use axum::extract::{Json, Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use cdk::amount::Amount;
use cdk::cdk_lightning::to_unit;
use cdk::error::{Error, ErrorResponse};
use cdk::nuts::nut05::MeltBolt11Response;
use cdk::nuts::{
CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteState,
MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
MintQuoteBolt11Response, MintQuoteState, PaymentMethod, RestoreRequest, RestoreResponse,
SwapRequest, SwapResponse,
MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest,
SwapResponse,
};
use cdk::util::unix_time;
use cdk::Bolt11Invoice;

use crate::{LnKey, MintState};

Expand Down Expand Up @@ -212,138 +208,53 @@ pub async fn post_melt_bolt11(
}
};

// Check to see if there is a corresponding mint quote for a melt.
// In this case the mint can settle the payment internally and no ln payment is
// needed
let mint_quote = match state
.mint
.localstore
.get_mint_quote_by_request(&quote.request)
.await
{
Ok(mint_quote) => mint_quote,
Err(err) => {
tracing::debug!("Error attempting to get mint quote: {}", err);

if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(into_response(Error::Internal));
}
};

let inputs_amount_quote_unit = payload.proofs_amount().map_err(|_| {
tracing::error!("Proof inputs in melt quote overflowed");
into_response(Error::AmountOverflow)
})?;

let (preimage, amount_spent_quote_unit) = match mint_quote {
Some(mint_quote) => {
if mint_quote.state == MintQuoteState::Issued
|| mint_quote.state == MintQuoteState::Paid
{
return Err(into_response(Error::RequestAlreadyPaid));
}

let mut mint_quote = mint_quote;

if mint_quote.amount > inputs_amount_quote_unit {
tracing::debug!(
"Not enough inuts provided: {} needed {}",
inputs_amount_quote_unit,
mint_quote.amount
);
let settled_internally_amount =
match state.mint.handle_internal_melt_mint(&quote, &payload).await {
Ok(amount) => amount,
Err(err) => {
tracing::error!("Attempting to settle internally failed");
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(into_response(Error::InsufficientFunds));
}

mint_quote.state = MintQuoteState::Paid;

let amount = quote.amount;

if let Err(_err) = state.mint.update_mint_quote(mint_quote).await {
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
tracing::error!(
"Could not reset melt quote {} state: {}",
payload.quote,
err
);
}
return Err(into_response(Error::Internal));
return Err(into_response(err));
}
};

(None, amount)
}
let (preimage, amount_spent_quote_unit) = match settled_internally_amount {
Some(amount_spent) => (None, amount_spent),
None => {
let invoice = match Bolt11Invoice::from_str(&quote.request) {
Ok(bolt11) => bolt11,
Err(_) => {
tracing::error!("Melt quote has invalid payment request");
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(into_response(Error::InvalidPaymentRequest));
}
};

let mut partial_amount = None;

// If the quote unit is SAT or MSAT we can check that the expected fees are
// provided. We also check if the quote is less then the invoice
// amount in the case that it is a mmp However, if the quote is not
// of a bitcoin unit we cannot do these checks as the mint
// is unaware of a conversion rate. In this case it is assumed that the quote is
// correct and the mint should pay the full invoice amount if inputs
// > then quote.amount are included. This is checked in the
// verify_melt method.
if quote.unit == CurrencyUnit::Msat || quote.unit == CurrencyUnit::Sat {
let quote_msats = to_unit(quote.amount, &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(amount) => amount.into(),
None => {
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
// > `then quote.amount` are included. This is checked in the
// `verify_melt` method.
if quote.unit == CurrencyUnit::Msat || quote.unit == CurrencyUnit::Sat {}
let partial_amount = match quote.unit {
CurrencyUnit::Sat | CurrencyUnit::Msat => {
match state
.mint
.check_melt_expected_ln_fees(&quote, &payload)
.await
{
Ok(amount) => amount,
Err(err) => {
tracing::error!("Fee is not expected: {}", err);
if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
}
return Err(into_response(Error::Internal));
}
return Err(into_response(Error::InvoiceAmountUndefined));
}
};

partial_amount = match invoice_amount_msats > quote_msats {
true => {
let partial_msats = invoice_amount_msats - quote_msats;

Some(
to_unit(partial_msats, &CurrencyUnit::Msat, &quote.unit)
.map_err(|_| into_response(Error::UnitUnsupported))?,
)
}
false => None,
};

let amount_to_pay = match partial_amount {
Some(amount_to_pay) => amount_to_pay,
None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &quote.unit)
.map_err(|_| into_response(Error::UnitUnsupported))?,
};

if amount_to_pay + quote.fee_reserve > inputs_amount_quote_unit {
tracing::debug!(
"Not enough inuts provided: {} msats needed {} msats",
inputs_amount_quote_unit,
amount_to_pay
);

if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
tracing::error!("Could not reset melt quote state: {}", err);
}

return Err(into_response(Error::TransactionUnbalanced(
inputs_amount_quote_unit.into(),
amount_to_pay.into(),
quote.fee_reserve.into(),
)));
}
}
_ => None,
};

let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
Some(ln) => ln,
Expand All @@ -363,8 +274,6 @@ pub async fn post_melt_bolt11(
{
Ok(pay) => pay,
Err(err) => {
tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);

// If the error is that the invoice was already paid we do not want to hold
// hold the proofs as pending to we reset them and return an error.
if matches!(err, cdk::cdk_lightning::Error::InvoiceAlreadyPaid) {
Expand All @@ -375,6 +284,8 @@ pub async fn post_melt_bolt11(
return Err(into_response(Error::RequestAlreadyPaid));
}

tracing::error!("Error returned attempting to pay: {} {}", quote.id, err);

// If there error is something else we want to check the status of the payment ensure it is not pending or has been made.
match ln.check_outgoing_payment(&quote.request_lookup_id).await {
Ok(response) => response,
Expand Down Expand Up @@ -407,15 +318,9 @@ pub async fn post_melt_bolt11(
}

// Convert from unit of backend to quote unit
let amount_spent = to_unit(pre.total_spent, &pre.unit, &quote.unit).map_err(|_| {
tracing::error!(
"Could not convert from {} to {} in melt.",
pre.unit,
quote.unit
);

into_response(Error::UnitUnsupported)
})?;
// Note: this should never fail since these conversions happen earlier and would fail there.
// Since it will not fail and even if it does the ln payment has already been paid, proofs should still be burned
let amount_spent = to_unit(pre.total_spent, &pre.unit, &quote.unit).unwrap_or_default();

(pre.payment_preimage, amount_spent)
}
Expand Down
113 changes: 113 additions & 0 deletions crates/cdk/src/mint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ use std::sync::Arc;

use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
use bitcoin::secp256k1::{self, Secp256k1};
use lightning_invoice::Bolt11Invoice;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::instrument;

use self::nut05::QuoteState;
use self::nut11::EnforceSigFlag;
use crate::cdk_database::{self, MintDatabase};
use crate::cdk_lightning::to_unit;
use crate::dhke::{hash_to_curve, sign_message, verify_message};
use crate::error::Error;
use crate::fees::calculate_fee;
Expand Down Expand Up @@ -991,6 +993,117 @@ impl Mint {
Ok(())
}

/// Check melt has expected fees
#[instrument(skip_all)]
pub async fn check_melt_expected_ln_fees(
&self,
melt_quote: &MeltQuote,
melt_request: &MeltBolt11Request,
) -> 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 = invoice
.amount_milli_satoshis()
.ok_or(Error::InvoiceAmountUndefined)?
.into();

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::UnitUnsupported)?,
)
}
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::UnitUnsupported)?,
};

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)
}

/// Verify melt request is valid
/// Check to see if there is a corresponding mint quote for a melt.
/// In this case the mint can settle the payment internally and no ln payment is
/// needed
#[instrument(skip_all)]
pub async fn handle_internal_melt_mint(
&self,
melt_quote: &MeltQuote,
melt_request: &MeltBolt11Request,
) -> Result<Option<Amount>, Error> {
let mint_quote = match self
.localstore
.get_mint_quote_by_request(&melt_quote.request)
.await
{
Ok(Some(mint_quote)) => mint_quote,
// Not an internal melt -> mint
Ok(None) => return Ok(None),
Err(err) => {
tracing::debug!("Error attempting to get mint quote: {}", err);
return Err(Error::Internal);
}
};

// Mint quote has already been settled, proofs should not be burned or held.
if mint_quote.state == MintQuoteState::Issued || mint_quote.state == MintQuoteState::Paid {
return Err(Error::RequestAlreadyPaid);
}

let inputs_amount_quote_unit = melt_request.proofs_amount().map_err(|_| {
tracing::error!("Proof inputs in melt quote overflowed");
Error::AmountOverflow
})?;

let mut mint_quote = mint_quote;

if mint_quote.amount > inputs_amount_quote_unit {
tracing::debug!(
"Not enough inuts provided: {} needed {}",
inputs_amount_quote_unit,
mint_quote.amount
);
return Err(Error::InsufficientFunds);
}

mint_quote.state = MintQuoteState::Paid;

let amount = melt_quote.amount;

self.update_mint_quote(mint_quote).await?;

Ok(Some(amount))
}

/// Verify melt request is valid
#[instrument(skip_all)]
pub async fn verify_melt_request(
Expand Down

0 comments on commit 8c06a05

Please sign in to comment.