diff --git a/README.md b/README.md index 95dbe8ab..e04d41b7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The project is split up into several crates in the `crates/` directory: * [**cdk-axum**](./crates/cdk-axum/): Axum webserver for mint. * [**cdk-cln**](./crates/cdk-cln/): CLN Lightning backend for mint. * [**cdk-lnd**](./crates/cdk-lnd/): Lnd Lightning backend for mint. - * [**cdk-lnbits**](./crates/cdk-lnbits/): [LNbits](https://lnbits.com/) Lightning backend for mint. + * [**cdk-lnbits**](./crates/cdk-lnbits/): [LNbits](https://lnbits.com/) Lightning backend for mint. **Note: Only LNBits v1 API is supported.** * [**cdk-fake-wallet**](./crates/cdk-fake-wallet/): Fake Lightning backend for mint. To be used only for testing, quotes are automatically filled. * [**cdk-mint-rpc**](./crates/cdk-mint-rpc/): Mint management gRPC server and cli. * Binaries: diff --git a/crates/cdk-lnbits/Cargo.toml b/crates/cdk-lnbits/Cargo.toml index e99303cb..5429a037 100644 --- a/crates/cdk-lnbits/Cargo.toml +++ b/crates/cdk-lnbits/Cargo.toml @@ -13,7 +13,6 @@ readme = "README.md" [dependencies] async-trait.workspace = true anyhow.workspace = true -axum.workspace = true bitcoin.workspace = true cdk-common = { workspace = true, features = ["mint"] } futures.workspace = true @@ -21,6 +20,6 @@ tokio.workspace = true tokio-util.workspace = true tracing.workspace = true thiserror.workspace = true -lnbits-rs = "0.6.0" +lnbits-rs = "0.8.0" serde_json.workspace = true rustls.workspace = true diff --git a/crates/cdk-lnbits/README.md b/crates/cdk-lnbits/README.md index 2e74d8d7..46cffe01 100644 --- a/crates/cdk-lnbits/README.md +++ b/crates/cdk-lnbits/README.md @@ -8,6 +8,8 @@ LNBits backend implementation for the Cashu Development Kit (CDK). This provides integration with [LNBits](https://lnbits.com/) for Lightning Network functionality. +**Note: Only LNBits v1 API is supported.** This backend uses the websocket-based v1 API for real-time payment notifications. + ## Installation Add this to your `Cargo.toml`: diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index 6c346ce1..bda71b2a 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -11,7 +11,6 @@ use std::sync::Arc; use anyhow::anyhow; use async_trait::async_trait; -use axum::Router; use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT}; use cdk_common::common::FeeReserve; use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState}; @@ -36,7 +35,6 @@ pub mod error; pub struct LNbits { lnbits_api: LNBitsClient, fee_reserve: FeeReserve, - webhook_url: Option, wait_invoice_cancel_token: CancellationToken, wait_invoice_is_active: Arc, settings: Bolt11Settings, @@ -50,14 +48,12 @@ impl LNbits { invoice_api_key: String, api_url: String, fee_reserve: FeeReserve, - webhook_url: Option, ) -> Result { let lnbits_api = LNBitsClient::new("", &admin_api_key, &invoice_api_key, &api_url, None)?; Ok(Self { lnbits_api, fee_reserve, - webhook_url, wait_invoice_cancel_token: CancellationToken::new(), wait_invoice_is_active: Arc::new(AtomicBool::new(false)), settings: Bolt11Settings { @@ -83,6 +79,64 @@ impl LNbits { Error::Anyhow(err) }) } + + /// Process an incoming message from the websocket receiver + async fn process_message( + msg_option: Option, + api: &LNBitsClient, + _is_active: &Arc, + ) -> Option { + let msg = msg_option?; + + let payment = match api.get_payment_info(&msg).await { + Ok(payment) => payment, + Err(_) => return None, + }; + + if !payment.paid { + tracing::warn!( + "Received payment notification but payment not paid for {}", + msg + ); + return None; + } + + Self::create_payment_response(&msg, &payment).unwrap_or_else(|e| { + tracing::error!("Failed to create payment response: {}", e); + None + }) + } + + /// Create a payment response from payment info + fn create_payment_response( + msg: &str, + payment: &lnbits_rs::api::payment::Payment, + ) -> Result, Error> { + let amount = payment.details.amount; + + if amount == i64::MIN { + return Ok(None); + } + + let hash = Self::decode_payment_hash(msg)?; + + Ok(Some(WaitPaymentResponse { + payment_identifier: PaymentIdentifier::PaymentHash(hash), + payment_amount: Amount::from(amount.unsigned_abs()), + unit: CurrencyUnit::Msat, + payment_id: msg.to_string(), + })) + } + + /// Decode a hex payment hash string into a byte array + fn decode_payment_hash(hash_str: &str) -> Result<[u8; 32], Error> { + let decoded = hex::decode(hash_str) + .map_err(|e| Error::Anyhow(anyhow!("Failed to decode payment hash: {}", e)))?; + + decoded + .try_into() + .map_err(|_| Error::Anyhow(anyhow!("Invalid payment hash length"))) + } } #[async_trait] @@ -118,54 +172,14 @@ impl MintPayment for LNbits { tokio::select! { _ = cancel_token.cancelled() => { - // Stream is cancelled is_active.store(false, Ordering::SeqCst); tracing::info!("Waiting for lnbits invoice ending"); None } msg_option = receiver.recv() => { - match msg_option { - Some(msg) => { - let check = api.get_payment_info(&msg).await; - match check { - Ok(payment) => { - if payment.paid { - match hex::decode(msg.clone()) { - Ok(decoded) => { - match decoded.try_into() { - Ok(hash) => { - let response = WaitPaymentResponse { - payment_identifier: PaymentIdentifier::PaymentHash(hash), - payment_amount: Amount::from(payment.details.amount as u64), - unit: CurrencyUnit::Msat, - payment_id: msg.clone() - }; - Some((response, (api, cancel_token, is_active))) - }, - Err(e) => { - tracing::error!("Failed to convert payment hash bytes to array: {:?}", e); - None - } - } - }, - Err(e) => { - tracing::error!("Failed to decode payment hash hex string: {}", e); - None - } - } - } else { - tracing::warn!("Received payment notification but could not check payment for {}", msg); - None - } - }, - Err(_) => None - } - }, - None => { - is_active.store(false, Ordering::SeqCst); - None - } - } + Self::process_message(msg_option, &api, &is_active) + .await + .map(|response| (response, (api, cancel_token, is_active))) } } }, @@ -306,7 +320,6 @@ impl MintPayment for LNbits { memo: Some(description), unit: unit.to_string(), expiry, - webhook: self.webhook_url.clone(), internal: None, out: false, }; @@ -321,10 +334,8 @@ impl MintPayment for LNbits { Self::Err::Anyhow(anyhow!("Could not create invoice")) })?; - let request: Bolt11Invoice = create_invoice_response - .bolt11() - .ok_or_else(|| Self::Err::Anyhow(anyhow!("Missing bolt11 invoice")))? - .parse()?; + let request: Bolt11Invoice = create_invoice_response.bolt11().parse()?; + let expiry = request.expires_at().map(|t| t.as_secs()); Ok(CreateIncomingPaymentResponse { @@ -389,7 +400,7 @@ impl MintPayment for LNbits { let pay_response = MakePaymentResponse { payment_lookup_id: payment_identifier.clone(), payment_proof: payment.preimage, - status: lnbits_to_melt_status(&payment.details.status, payment.details.pending), + status: lnbits_to_melt_status(&payment.details.status), total_spent: Amount::from( payment.details.amount.unsigned_abs() + payment.details.fee.unsigned_abs(), ), @@ -400,11 +411,7 @@ impl MintPayment for LNbits { } } -fn lnbits_to_melt_status(status: &str, pending: Option) -> MeltQuoteState { - if pending.unwrap_or_default() { - return MeltQuoteState::Pending; - } - +fn lnbits_to_melt_status(status: &str) -> MeltQuoteState { match status { "success" => MeltQuoteState::Paid, "failed" => MeltQuoteState::Unpaid, @@ -412,15 +419,3 @@ fn lnbits_to_melt_status(status: &str, pending: Option) -> MeltQuoteState _ => MeltQuoteState::Unknown, } } - -impl LNbits { - /// Create invoice webhook - pub async fn create_invoice_webhook_router( - &self, - webhook_endpoint: &str, - ) -> anyhow::Result { - self.lnbits_api - .create_invoice_webhook_router(webhook_endpoint) - .await - } -} diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index f3389fa5..30983d1e 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -80,8 +80,7 @@ reserve_fee_min = 4 # admin_api_key = "" # invoice_api_key = "" # lnbits_api = "" -# To be set true to support pre v1 lnbits api -# retro_api=false +# Note: Only LNBits v1 API is supported (websocket-based) # [lnd] # address = "https://domain:port" diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 4c9af2a6..60290165 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -161,7 +161,6 @@ pub struct LNbits { pub lnbits_api: String, pub fee_percent: f32, pub reserve_fee_min: Amount, - pub retro_api: bool, } #[cfg(feature = "cln")] diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 05b6966c..e19da85f 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -103,7 +103,7 @@ pub fn setup_tracing( logging_config: &config::LoggingConfig, ) -> Result> { let default_filter = "debug"; - let hyper_filter = "hyper=warn"; + let hyper_filter = "hyper=warn,rustls=warn,reqwest=warn"; let h2_filter = "h2=warn"; let tower_http = "tower_http=warn"; diff --git a/crates/cdk-mintd/src/setup.rs b/crates/cdk-mintd/src/setup.rs index 0bbd950e..507de0e3 100644 --- a/crates/cdk-mintd/src/setup.rs +++ b/crates/cdk-mintd/src/setup.rs @@ -10,8 +10,6 @@ use axum::Router; #[cfg(feature = "fakewallet")] use bip39::rand::{thread_rng, Rng}; use cdk::cdk_payment::MintPayment; -#[cfg(feature = "lnbits")] -use cdk::mint_url::MintUrl; use cdk::nuts::CurrencyUnit; #[cfg(any( feature = "lnbits", @@ -67,58 +65,28 @@ impl LnBackendSetup for config::Cln { impl LnBackendSetup for config::LNbits { async fn setup( &self, - routers: &mut Vec, - settings: &Settings, + _routers: &mut Vec, + _settings: &Settings, _unit: CurrencyUnit, ) -> anyhow::Result { let admin_api_key = &self.admin_api_key; let invoice_api_key = &self.invoice_api_key; - // Channel used for lnbits web hook - let webhook_endpoint = "/webhook/lnbits/sat/invoice"; - let fee_reserve = FeeReserve { min_fee_reserve: self.reserve_fee_min, percent_fee_reserve: self.fee_percent, }; - let webhook_url = if settings - .lnbits - .as_ref() - .expect("Lnbits must be defined") - .retro_api - { - let mint_url: MintUrl = settings.info.url.parse()?; - let webhook_url = mint_url.join(webhook_endpoint)?; - - Some(webhook_url.to_string()) - } else { - None - }; - let lnbits = cdk_lnbits::LNbits::new( admin_api_key.clone(), invoice_api_key.clone(), self.lnbits_api.clone(), fee_reserve, - webhook_url, ) .await?; - if settings - .lnbits - .as_ref() - .expect("Lnbits must be defined") - .retro_api - { - let router = lnbits - .create_invoice_webhook_router(webhook_endpoint) - .await?; - - routers.push(router); - } else { - lnbits.subscribe_ws().await?; - }; + // Use v1 websocket API + lnbits.subscribe_ws().await?; Ok(lnbits) } diff --git a/crates/cdk/src/wallet/auth/auth_wallet.rs b/crates/cdk/src/wallet/auth/auth_wallet.rs index 5be0df2e..d61f2bb5 100644 --- a/crates/cdk/src/wallet/auth/auth_wallet.rs +++ b/crates/cdk/src/wallet/auth/auth_wallet.rs @@ -23,7 +23,7 @@ use crate::{Amount, Error, OidcClient}; /// JWT Claims structure for decoding tokens #[derive(Debug, Serialize, Deserialize)] -struct Claims { +struct _Claims { /// Subject sub: Option, /// Expiration time (as UTC timestamp)