From 1d3a93af97287edd9d2f45a0e57aeb9698adf207 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Tue, 10 Feb 2026 11:32:06 +0100 Subject: [PATCH 1/2] gl-plugin: Skip JIT channel negotiation when sufficient capacity exists Before creating an LSPS2 invoice that requires JIT channel negotiation, check if the node already has sufficient incoming capacity to receive the payment directly. When the receivable capacity (with a 5% buffer for fees) exceeds the invoice amount, create a regular invoice instead of negotiating with the LSP. This avoids unnecessary LSP fees and channel opening costs when the node can already receive the payment. The capacity check: - Sums receivable_msat across all CHANNELD_NORMAL channels with connected peers - Applies a 5% buffer to account for routing fees - Falls back to JIT negotiation if capacity is insufficient or for 'any amount' invoices --- libs/gl-plugin/src/node/mod.rs | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index 2fa13171..c865fb70 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -193,6 +193,71 @@ impl Node for PluginNodeServer { let mut rpc = rpc_arc.lock().await; + // Check if we have sufficient incoming capacity to skip JIT channel negotiation. + // We require capacity + 5% buffer to account for fees and routing. + // Only check for specific amounts (not "any" amount invoices). + if req.amount_msat > 0 { + let receivable = self + .get_receivable_capacity(&mut rpc) + .await + .unwrap_or(0); + + // Add 5% buffer: capacity >= amount * 1.05 + // Equivalent to: capacity * 100 >= amount * 105 + let has_sufficient_capacity = req.amount_msat + .saturating_mul(105) + .checked_div(100) + .map(|required| receivable >= required) + .unwrap_or(false); + + if has_sufficient_capacity { + log::info!( + "Sufficient incoming capacity ({} msat) for invoice amount ({} msat), creating regular invoice", + receivable, + req.amount_msat + ); + + // Create a regular invoice without JIT channel negotiation + let invreq = crate::requests::Invoice { + amount_msat: cln_rpc::primitives::AmountOrAny::Amount( + cln_rpc::primitives::Amount::from_msat(req.amount_msat), + ), + description: req.description.clone(), + label: req.label.clone(), + expiry: None, + fallbacks: None, + preimage: None, + cltv: Some(144), + deschashonly: None, + exposeprivatechannels: None, + dev_routes: None, + }; + + let res: crate::responses::Invoice = rpc + .call_raw("invoice", &invreq) + .await + .map_err(|e| Status::new(Code::Internal, e.to_string()))?; + + return Ok(Response::new(pb::LspInvoiceResponse { + bolt11: res.bolt11, + created_index: 0, // Not available in our Invoice response + expires_at: res.expiry_time, + payment_hash: hex::decode(&res.payment_hash) + .map_err(|e| Status::new(Code::Internal, format!("Invalid payment_hash: {}", e)))?, + payment_secret: res + .payment_secret + .map(|s| hex::decode(&s).unwrap_or_default()) + .unwrap_or_default(), + })); + } + + log::info!( + "Insufficient incoming capacity ({} msat) for invoice amount ({} msat), negotiating JIT channel", + receivable, + req.amount_msat + ); + } + // Get the CLN version to determine which RPC method to use let version = rpc .call_typed(&cln_rpc::model::requests::GetinfoRequest {}) @@ -783,6 +848,29 @@ impl PluginNodeServer { Ok(res) } + /// Get the total receivable capacity across all active channels. + /// + /// Returns the sum of `receivable_msat` for all channels in + /// `CHANNELD_NORMAL` state with a connected peer. + async fn get_receivable_capacity(&self, rpc: &mut cln_rpc::ClnRpc) -> Result { + use cln_rpc::primitives::ChannelState; + + let res = rpc + .call_typed(&cln_rpc::model::requests::ListpeerchannelsRequest { id: None }) + .await?; + + let total: u64 = res + .channels + .into_iter() + .filter(|c| c.peer_connected && c.state == ChannelState::CHANNELD_NORMAL) + .filter_map(|c| c.receivable_msat) + .map(|a| a.msat()) + .sum(); + + log::debug!("Total receivable capacity: {} msat", total); + Ok(total) + } + async fn get_reconnect_peers( &self, ) -> Result, Error> { From e9692a6260461c30544be4ecb76e25cd7935ae74 Mon Sep 17 00:00:00 2001 From: Christian Decker Date: Wed, 11 Feb 2026 13:27:05 +0100 Subject: [PATCH 2/2] gl-plugin: Use call_typed for regular invoice creation Replace call_raw with call_typed when creating a regular invoice in the LSP invoice handler. This provides better type safety by using the generated cln_rpc::model::requests::InvoiceRequest and cln_rpc::model::responses::InvoiceResponse types. Benefits: - Compile-time type checking for request/response fields - No manual hex decoding needed for payment_hash/payment_secret - Access to created_index field from the response - Cleaner code without custom request/response structs --- libs/gl-plugin/src/node/mod.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index c865fb70..f3bbd3cf 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -25,6 +25,7 @@ use tokio_stream::wrappers::ReceiverStream; use tonic::{transport::ServerTlsConfig, Code, Request, Response, Status}; mod wrapper; use gl_client::bitcoin; +use std::borrow::Borrow; use std::str::FromStr; pub use wrapper::WrappedNodeServer; @@ -218,7 +219,7 @@ impl Node for PluginNodeServer { ); // Create a regular invoice without JIT channel negotiation - let invreq = crate::requests::Invoice { + let invreq = cln_rpc::model::requests::InvoiceRequest { amount_msat: cln_rpc::primitives::AmountOrAny::Amount( cln_rpc::primitives::Amount::from_msat(req.amount_msat), ), @@ -230,24 +231,19 @@ impl Node for PluginNodeServer { cltv: Some(144), deschashonly: None, exposeprivatechannels: None, - dev_routes: None, }; - let res: crate::responses::Invoice = rpc - .call_raw("invoice", &invreq) + let res = rpc + .call_typed(&invreq) .await .map_err(|e| Status::new(Code::Internal, e.to_string()))?; return Ok(Response::new(pb::LspInvoiceResponse { bolt11: res.bolt11, - created_index: 0, // Not available in our Invoice response - expires_at: res.expiry_time, - payment_hash: hex::decode(&res.payment_hash) - .map_err(|e| Status::new(Code::Internal, format!("Invalid payment_hash: {}", e)))?, - payment_secret: res - .payment_secret - .map(|s| hex::decode(&s).unwrap_or_default()) - .unwrap_or_default(), + created_index: res.created_index.unwrap_or(0) as u32, + expires_at: res.expires_at as u32, + payment_hash: >::borrow(&res.payment_hash).to_vec(), + payment_secret: res.payment_secret.to_vec(), })); }