Explorar el Código

refactor(cdk-lnbits): migrate to LNbits v1 websocket API and remove w… (#987)

* refactor(cdk-lnbits): migrate to LNbits v1 websocket API and remove webhook support

- Remove webhook-based payment notifications in favor of v1 websocket API
- Add explicit documentation that only LNbits v1 API is supported
- Remove webhook_url parameter and related router setup code
- Simplify payment status handling by removing pending status logic
- Switch to local lnbits-rs dependency for development
- Remove unused axum dependency and clean up imports
- Update configuration documentation and examples

* refactor(cdk-lnbits): extract payment processing logic into helper methods

Improve code readability by separating message processing, payment response
creation, and payment hash decoding into dedicated methods. This reduces
complexity in the main payment waiting loop while maintaining identical
functionality

* chore: bump lnbits-rs
thesimplekid hace 2 meses
padre
commit
8dec41dd55

+ 1 - 1
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:

+ 1 - 2
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

+ 2 - 0
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`:

+ 65 - 70
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<String>,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
     settings: Bolt11Settings,
@@ -50,14 +48,12 @@ impl LNbits {
         invoice_api_key: String,
         api_url: String,
         fee_reserve: FeeReserve,
-        webhook_url: Option<String>,
     ) -> Result<Self, Error> {
         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<String>,
+        api: &LNBitsClient,
+        _is_active: &Arc<AtomicBool>,
+    ) -> Option<WaitPaymentResponse> {
+        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<Option<WaitPaymentResponse>, 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<bool>) -> 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<bool>) -> MeltQuoteState
         _ => MeltQuoteState::Unknown,
     }
 }
-
-impl LNbits {
-    /// Create invoice webhook
-    pub async fn create_invoice_webhook_router(
-        &self,
-        webhook_endpoint: &str,
-    ) -> anyhow::Result<Router> {
-        self.lnbits_api
-            .create_invoice_webhook_router(webhook_endpoint)
-            .await
-    }
-}

+ 1 - 2
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"

+ 0 - 1
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")]

+ 1 - 1
crates/cdk-mintd/src/lib.rs

@@ -103,7 +103,7 @@ pub fn setup_tracing(
     logging_config: &config::LoggingConfig,
 ) -> Result<Option<tracing_appender::non_blocking::WorkerGuard>> {
     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";
 

+ 4 - 36
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<Router>,
-        settings: &Settings,
+        _routers: &mut Vec<Router>,
+        _settings: &Settings,
         _unit: CurrencyUnit,
     ) -> anyhow::Result<cdk_lnbits::LNbits> {
         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)
     }

+ 1 - 1
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<String>,
     /// Expiration time (as UTC timestamp)