diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index a7db1ed3..e6a0f0b2 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -213,6 +213,12 @@ pub async fn post_melt_bolt11( 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 { @@ -320,7 +326,12 @@ pub async fn post_melt_bolt11( 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)); } }; diff --git a/crates/cdk-cln/src/error.rs b/crates/cdk-cln/src/error.rs index ad328ad9..c37b357f 100644 --- a/crates/cdk-cln/src/error.rs +++ b/crates/cdk-cln/src/error.rs @@ -2,15 +2,15 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { + /// Invoice amount not defined + #[error("Unknown invoice amount")] + UnknownInvoiceAmount, /// Wrong CLN response #[error("Wrong cln response")] WrongClnResponse, /// Unknown invoice #[error("Unknown invoice")] UnknownInvoice, - /// Invoice amount not defined - #[error("Unknown invoice amount")] - UnknownInvoiceAmount, /// Cln Error #[error(transparent)] Cln(#[from] cln_rpc::Error), diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index b9fcc9a8..158e85e2 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -15,9 +15,11 @@ use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteS use cdk::util::{hex, unix_time}; use cdk::{mint, Bolt11Invoice}; 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::primitives::{Amount as CLN_Amount, AmountOrAny}; use error::Error; @@ -131,6 +133,22 @@ impl MintLightning for Cln { max_fee_msats: Option, ) -> Result { 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 .call(Request::Pay(PayRequest { bolt11: melt_quote.request.to_string(), @@ -190,7 +208,7 @@ impl MintLightning for Cln { .call(cln_rpc::Request::Invoice(InvoiceRequest { amount_msat, description, - label: Uuid::new_v4().to_string(), + label: label.clone(), expiry: Some(unix_expiry - time_now), fallbacks: None, preimage: None, @@ -293,3 +311,38 @@ fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQ ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid, } } + +async fn check_pay_invoice_status( + cln_client: &mut cln_rpc::ClnRpc, + bolt11: String, +) -> Result { + 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) +} diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index bc41fe80..eda239c0 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -202,11 +202,17 @@ async fn main() -> anyhow::Result<()> { /// Update mint quote when called for a paid invoice async fn handle_paid_invoice(mint: Arc, request_lookup_id: &str) -> Result<()> { + tracing::debug!("Invoice with lookup id paid: {}", request_lookup_id); if let Ok(Some(mint_quote)) = mint .localstore .get_mint_quote_by_request_lookup_id(request_lookup_id) .await { + tracing::debug!( + "Quote {} paid by lookup id {}", + mint_quote.id, + request_lookup_id + ); mint.localstore .update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid) .await?; diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 918eec0b..86f823c6 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -14,6 +14,12 @@ use crate::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuot /// CDK Lightning Error #[derive(Debug, Error)] pub enum Error { + /// Invoice already paid + #[error("Invoice already paid")] + InvoiceAlreadyPaid, + /// Invoice pay pending + #[error("Invoice pay is pending")] + InvoicePaymentPending, /// Lightning Error #[error(transparent)] Lightning(Box), diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index 0752bbdc..335cacd3 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -30,6 +30,9 @@ pub enum Error { /// Melt Request is not valid #[error("Melt request is not valid")] MeltRequestInvalid, + /// Invoice already paid + #[error("Request already paid")] + RequestAlreadyPaid, /// Amount is not what expected #[error("Amount miss match")] Amount, diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 3b43c2a8..710e7eb1 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -124,7 +124,21 @@ impl Mint { expiry: u64, ln_lookup: String, ) -> Result { - 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?;