feat: check if invoice already paid on melt

feat: check if internal invoice already settled
This commit is contained in:
thesimplekid
2024-07-06 10:24:38 +01:00
parent 1b64e5d8fa
commit 46f8689b1f
7 changed files with 101 additions and 8 deletions

View File

@@ -213,6 +213,12 @@ pub async fn post_melt_bolt11(
let (preimage, amount_spent_quote_unit) = match mint_quote { let (preimage, amount_spent_quote_unit) = match mint_quote {
Some(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; let mut mint_quote = mint_quote;
if mint_quote.amount > inputs_amount_quote_unit { if mint_quote.amount > inputs_amount_quote_unit {
@@ -320,7 +326,12 @@ pub async fn post_melt_bolt11(
tracing::error!("Could not reset melt quote state: {}", err); tracing::error!("Could not reset melt quote state: {}", err);
} }
return Err(into_response(Error::PaymentFailed)); let err = match err {
cdk::cdk_lightning::Error::InvoiceAlreadyPaid => Error::RequestAlreadyPaid,
_ => Error::PaymentFailed,
};
return Err(into_response(err));
} }
}; };

View File

@@ -2,15 +2,15 @@ use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
/// Invoice amount not defined
#[error("Unknown invoice amount")]
UnknownInvoiceAmount,
/// Wrong CLN response /// Wrong CLN response
#[error("Wrong cln response")] #[error("Wrong cln response")]
WrongClnResponse, WrongClnResponse,
/// Unknown invoice /// Unknown invoice
#[error("Unknown invoice")] #[error("Unknown invoice")]
UnknownInvoice, UnknownInvoice,
/// Invoice amount not defined
#[error("Unknown invoice amount")]
UnknownInvoiceAmount,
/// Cln Error /// Cln Error
#[error(transparent)] #[error(transparent)]
Cln(#[from] cln_rpc::Error), Cln(#[from] cln_rpc::Error),

View File

@@ -15,9 +15,11 @@ use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteS
use cdk::util::{hex, unix_time}; use cdk::util::{hex, unix_time};
use cdk::{mint, Bolt11Invoice}; use cdk::{mint, Bolt11Invoice};
use cln_rpc::model::requests::{ use cln_rpc::model::requests::{
InvoiceRequest, ListinvoicesRequest, PayRequest, WaitanyinvoiceRequest, InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest,
};
use cln_rpc::model::responses::{
ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus, WaitanyinvoiceResponse,
}; };
use cln_rpc::model::responses::{ListinvoicesInvoicesStatus, PayStatus, WaitanyinvoiceResponse};
use cln_rpc::model::Request; use cln_rpc::model::Request;
use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny}; use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
use error::Error; use error::Error;
@@ -131,6 +133,22 @@ impl MintLightning for Cln {
max_fee_msats: Option<u64>, max_fee_msats: Option<u64>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<PayInvoiceResponse, Self::Err> {
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
let pay_state =
check_pay_invoice_status(&mut cln_client, melt_quote.request.to_string()).await?;
match pay_state {
MeltQuoteState::Paid => {
tracing::debug!("Melt attempted on invoice already paid");
return Err(Self::Err::InvoiceAlreadyPaid);
}
MeltQuoteState::Pending => {
tracing::debug!("Melt attempted on invoice already pending");
return Err(Self::Err::InvoicePaymentPending);
}
MeltQuoteState::Unpaid => (),
}
let cln_response = cln_client let cln_response = cln_client
.call(Request::Pay(PayRequest { .call(Request::Pay(PayRequest {
bolt11: melt_quote.request.to_string(), bolt11: melt_quote.request.to_string(),
@@ -190,7 +208,7 @@ impl MintLightning for Cln {
.call(cln_rpc::Request::Invoice(InvoiceRequest { .call(cln_rpc::Request::Invoice(InvoiceRequest {
amount_msat, amount_msat,
description, description,
label: Uuid::new_v4().to_string(), label: label.clone(),
expiry: Some(unix_expiry - time_now), expiry: Some(unix_expiry - time_now),
fallbacks: None, fallbacks: None,
preimage: None, preimage: None,
@@ -293,3 +311,38 @@ fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQ
ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid, ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid,
} }
} }
async fn check_pay_invoice_status(
cln_client: &mut cln_rpc::ClnRpc,
bolt11: String,
) -> Result<MeltQuoteState, cdk_lightning::Error> {
let cln_response = cln_client
.call(Request::ListPays(ListpaysRequest {
bolt11: Some(bolt11),
payment_hash: None,
status: None,
}))
.await
.map_err(Error::from)?;
let state = match cln_response {
cln_rpc::Response::ListPays(pay_response) => {
let pay = pay_response.pays.first();
match pay {
Some(pay) => match pay.status {
ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
ListpaysPaysStatus::PENDING => MeltQuoteState::Pending,
ListpaysPaysStatus::FAILED => MeltQuoteState::Unpaid,
},
None => MeltQuoteState::Unpaid,
}
}
_ => {
tracing::warn!("CLN returned wrong response kind. When checking pay status");
return Err(cdk_lightning::Error::from(Error::WrongClnResponse));
}
};
Ok(state)
}

View File

@@ -202,11 +202,17 @@ async fn main() -> anyhow::Result<()> {
/// Update mint quote when called for a paid invoice /// Update mint quote when called for a paid invoice
async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> { async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
tracing::debug!("Invoice with lookup id paid: {}", request_lookup_id);
if let Ok(Some(mint_quote)) = mint if let Ok(Some(mint_quote)) = mint
.localstore .localstore
.get_mint_quote_by_request_lookup_id(request_lookup_id) .get_mint_quote_by_request_lookup_id(request_lookup_id)
.await .await
{ {
tracing::debug!(
"Quote {} paid by lookup id {}",
mint_quote.id,
request_lookup_id
);
mint.localstore mint.localstore
.update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid) .update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
.await?; .await?;

View File

@@ -14,6 +14,12 @@ use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuot
/// CDK Lightning Error /// CDK Lightning Error
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
/// Invoice already paid
#[error("Invoice already paid")]
InvoiceAlreadyPaid,
/// Invoice pay pending
#[error("Invoice pay is pending")]
InvoicePaymentPending,
/// Lightning Error /// Lightning Error
#[error(transparent)] #[error(transparent)]
Lightning(Box<dyn std::error::Error + Send + Sync>), Lightning(Box<dyn std::error::Error + Send + Sync>),

View File

@@ -30,6 +30,9 @@ pub enum Error {
/// Melt Request is not valid /// Melt Request is not valid
#[error("Melt request is not valid")] #[error("Melt request is not valid")]
MeltRequestInvalid, MeltRequestInvalid,
/// Invoice already paid
#[error("Request already paid")]
RequestAlreadyPaid,
/// Amount is not what expected /// Amount is not what expected
#[error("Amount miss match")] #[error("Amount miss match")]
Amount, Amount,

View File

@@ -124,7 +124,21 @@ impl Mint {
expiry: u64, expiry: u64,
ln_lookup: String, ln_lookup: String,
) -> Result<MintQuote, Error> { ) -> Result<MintQuote, Error> {
let quote = MintQuote::new(mint_url, request, unit, amount, expiry, ln_lookup); let quote = MintQuote::new(
mint_url,
request,
unit.clone(),
amount,
expiry,
ln_lookup.clone(),
);
tracing::debug!(
"New mint quote {} for {} {} with request id {}",
quote.id,
amount,
unit,
&ln_lookup
);
self.localstore.add_mint_quote(quote.clone()).await?; self.localstore.add_mint_quote(quote.clone()).await?;