From ffce1d018f7591e60158d0724ade9a2aeea2811d Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Wed, 15 Jul 2026 11:16:46 +0530 Subject: [PATCH 1/4] feat: add configurable route hints to BOLT11 receive APIs --- bindings/ldk_node.udl | 10 ++ src/ffi/types.rs | 2 +- src/lib.rs | 2 + src/payment/bolt11.rs | 289 ++++++++++++++++++++++++++++++++++---- src/payment/mod.rs | 2 +- src/payment/unified_qr.rs | 4 +- 6 files changed, 280 insertions(+), 29 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 6a37812adc..8bbc5a96bd 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -212,6 +212,12 @@ interface Bolt11InvoiceDescription { Direct(string description); }; +enum RouteHintsMode { + "None", + "Automatic", + "Custom", +}; + interface Bolt11Payment { [Throws=NodeError] PaymentId send([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters, u64? payment_timeout_secs); @@ -232,6 +238,10 @@ interface Bolt11Payment { [Throws=NodeError] Bolt11Invoice receive_variable_amount([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs); [Throws=NodeError] + Bolt11Invoice receive_with_route_hints(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, RouteHintsMode route_hints_mode, sequence? custom_route_hint_user_channel_ids); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount_with_route_hints([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, RouteHintsMode route_hints_mode, sequence? custom_route_hint_user_channel_ids); + [Throws=NodeError] Bolt11Invoice receive_variable_amount_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash); [Throws=NodeError] Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index e9ca21fe0e..ecb20e57ad 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -58,7 +58,7 @@ pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use crate::payment::QrPaymentResult; +pub use crate::payment::{QrPaymentResult, RouteHintsMode}; use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId}; impl UniffiCustomTypeConverter for PublicKey { diff --git a/src/lib.rs b/src/lib.rs index 5e3ebab618..931b1da088 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -845,6 +845,7 @@ impl Node { Bolt11Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), Arc::clone(&self.connection_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), @@ -863,6 +864,7 @@ impl Node { Arc::new(Bolt11Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), Arc::clone(&self.connection_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index fad0ffc388..f653b32f3a 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -12,14 +12,19 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; +use bitcoin::hashes::{sha256, Hash}; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; +use bitcoin::secp256k1::Secp256k1; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, + MIN_FINAL_CLTV_EXPIRY_DELTA, +}; +use lightning::routing::router::{ + PaymentParameters, RouteHint, RouteHintHop, RouteParameters, RouteParametersConfig, }; -use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning_invoice::{ Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, + InvoiceBuilder, RoutingFees, }; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -36,7 +41,21 @@ use crate::payment::store::{ }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::UserChannelId; + +const MAX_CUSTOM_ROUTE_HINTS: usize = 3; + +/// Controls which route hints are included when creating a BOLT11 invoice. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RouteHintsMode { + /// Do not include any route hints in the invoice. + None, + /// Automatically select route hints from eligible channels (default). + Automatic, + /// Include route hints only for the given [`UserChannelId`]s. + Custom, +} #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; @@ -66,6 +85,7 @@ fn invoice_description_str(invoice: &LdkBolt11Invoice) -> Option { pub struct Bolt11Payment { runtime: Arc, channel_manager: Arc, + keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, @@ -78,6 +98,7 @@ pub struct Bolt11Payment { impl Bolt11Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, + keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, @@ -86,6 +107,7 @@ impl Bolt11Payment { Self { runtime, channel_manager, + keys_manager, connection_manager, liquidity_source, payment_store, @@ -96,6 +118,158 @@ impl Bolt11Payment { } } + fn build_custom_route_hints( + &self, user_channel_ids: Vec, + ) -> Result, Error> { + if user_channel_ids.is_empty() { + return Err(Error::InvalidChannelId); + } + + let channels = self.channel_manager.list_channels(); + let mut hints = Vec::new(); + + for user_channel_id in user_channel_ids.into_iter().take(MAX_CUSTOM_ROUTE_HINTS) { + let channel = channels.iter().find(|chan| { + UserChannelId(chan.user_channel_id) == user_channel_id + }); + + let channel = match channel { + Some(channel) => channel, + None => { + log_info!( + self.logger, + "Skipping unknown user channel id {} for custom route hints", + user_channel_id + ); + continue; + }, + }; + + if !channel.is_channel_ready { + log_info!( + self.logger, + "Skipping channel {} for custom route hints as it is not ready", + channel.channel_id + ); + continue; + } + + let short_channel_id = match channel.inbound_scid_alias.or(channel.short_channel_id) { + Some(scid) => scid, + None => { + log_info!( + self.logger, + "Skipping channel {} for custom route hints as it has no inbound SCID", + channel.channel_id + ); + continue; + }, + }; + + let forwarding_info = match channel.counterparty.forwarding_info.as_ref() { + Some(info) => info, + None => { + log_info!( + self.logger, + "Skipping channel {} for custom route hints as it has no forwarding info", + channel.channel_id + ); + continue; + }, + }; + + hints.push(RouteHint(vec![RouteHintHop { + src_node_id: channel.counterparty.node_id, + short_channel_id, + fees: RoutingFees { + base_msat: forwarding_info.fee_base_msat, + proportional_millionths: forwarding_info.fee_proportional_millionths, + }, + cltv_expiry_delta: forwarding_info.cltv_expiry_delta, + htlc_minimum_msat: channel.inbound_htlc_minimum_msat, + htlc_maximum_msat: channel.inbound_htlc_maximum_msat, + }])); + } + + if hints.is_empty() { + return Err(Error::InvalidChannelId); + } + + Ok(hints) + } + + fn create_bolt11_invoice_with_route_hints( + &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, + expiry_secs: u32, manual_claim_payment_hash: Option, + route_hints_mode: RouteHintsMode, + custom_route_hint_user_channel_ids: Option>, + ) -> Result { + let (payment_hash, payment_secret) = match manual_claim_payment_hash { + Some(payment_hash) => { + let payment_secret = self + .channel_manager + .create_inbound_payment_for_hash( + payment_hash, + amount_msat, + expiry_secs, + None, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?; + (payment_hash, payment_secret) + }, + None => self + .channel_manager + .create_inbound_payment(amount_msat, expiry_secs, None) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?, + }; + + let payment_hash = sha256::Hash::from_slice(&payment_hash.0).map_err(|e| { + log_error!(self.logger, "Invalid payment hash: {:?}", e); + Error::InvoiceCreationFailed + })?; + + let currency = self.config.network.into(); + let mut invoice_builder = InvoiceBuilder::new(currency) + .invoice_description(invoice_description.clone()) + .payment_hash(payment_hash) + .payment_secret(payment_secret) + .current_timestamp() + .min_final_cltv_expiry_delta(MIN_FINAL_CLTV_EXPIRY_DELTA.into()) + .expiry_time(Duration::from_secs(expiry_secs.into())); + + if let Some(amount_msat) = amount_msat { + invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp(); + } + + if route_hints_mode == RouteHintsMode::Custom { + let hints = self.build_custom_route_hints( + custom_route_hint_user_channel_ids.unwrap_or_default(), + )?; + for hint in hints { + invoice_builder = invoice_builder.private_route(hint); + } + } + + let invoice = invoice_builder + .build_signed(|hash| { + Secp256k1::new() + .sign_ecdsa_recoverable(hash, &self.keys_manager.get_node_secret_key()) + }) + .map_err(|e| { + log_error!(self.logger, "Failed to build invoice: {:?}", e); + Error::InvoiceCreationFailed + })?; + + log_info!(self.logger, "Invoice created: {}", invoice); + Ok(invoice) + } + /// Send a payment given an invoice. /// /// If `route_parameters` are provided they will override the default as well as the @@ -430,7 +604,9 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner(Some(amount_msat), &description, expiry_secs, None)?; + let invoice = self.receive_inner( + Some(amount_msat), &description, expiry_secs, None, RouteHintsMode::Automatic, None, + )?; Ok(maybe_wrap(invoice)) } @@ -453,8 +629,31 @@ impl Bolt11Payment { payment_hash: PaymentHash, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = - self.receive_inner(Some(amount_msat), &description, expiry_secs, Some(payment_hash))?; + let invoice = self.receive_inner( + Some(amount_msat), &description, expiry_secs, Some(payment_hash), + RouteHintsMode::Automatic, None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request and receive a payment of the amount + /// given, with configurable route hints. + /// + /// The inbound payment will be automatically claimed upon arrival. + pub fn receive_with_route_hints( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + route_hints_mode: RouteHintsMode, + custom_route_hint_user_channel_ids: Option>, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner( + Some(amount_msat), + &description, + expiry_secs, + None, + route_hints_mode, + custom_route_hint_user_channel_ids, + )?; Ok(maybe_wrap(invoice)) } @@ -466,7 +665,9 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner(None, &description, expiry_secs, None)?; + let invoice = self.receive_inner( + None, &description, expiry_secs, None, RouteHintsMode::Automatic, None, + )?; Ok(maybe_wrap(invoice)) } @@ -488,33 +689,69 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner(None, &description, expiry_secs, Some(payment_hash))?; + let invoice = self.receive_inner( + None, &description, expiry_secs, Some(payment_hash), RouteHintsMode::Automatic, None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request and receive a payment for which the + /// amount is to be determined by the user, also known as a "zero-amount" invoice, with + /// configurable route hints. + /// + /// The inbound payment will be automatically claimed upon arrival. + pub fn receive_variable_amount_with_route_hints( + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, + route_hints_mode: RouteHintsMode, + custom_route_hint_user_channel_ids: Option>, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner( + None, + &description, + expiry_secs, + None, + route_hints_mode, + custom_route_hint_user_channel_ids, + )?; Ok(maybe_wrap(invoice)) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, + route_hints_mode: RouteHintsMode, + custom_route_hint_user_channel_ids: Option>, ) -> Result { - let invoice = { - let invoice_params = Bolt11InvoiceParameters { - amount_msats: amount_msat, - description: invoice_description.clone(), - invoice_expiry_delta_secs: Some(expiry_secs), - payment_hash: manual_claim_payment_hash, - ..Default::default() - }; + let invoice = match route_hints_mode { + RouteHintsMode::Automatic => { + let invoice_params = Bolt11InvoiceParameters { + amount_msats: amount_msat, + description: invoice_description.clone(), + invoice_expiry_delta_secs: Some(expiry_secs), + payment_hash: manual_claim_payment_hash, + ..Default::default() + }; - match self.channel_manager.create_bolt11_invoice(invoice_params) { - Ok(inv) => { - log_info!(self.logger, "Invoice created: {}", inv); - inv - }, - Err(e) => { - log_error!(self.logger, "Failed to create invoice: {}", e); - return Err(Error::InvoiceCreationFailed); - }, - } + match self.channel_manager.create_bolt11_invoice(invoice_params) { + Ok(inv) => { + log_info!(self.logger, "Invoice created: {}", inv); + inv + }, + Err(e) => { + log_error!(self.logger, "Failed to create invoice: {}", e); + return Err(Error::InvoiceCreationFailed); + }, + } + }, + RouteHintsMode::None | RouteHintsMode::Custom => self.create_bolt11_invoice_with_route_hints( + amount_msat, + invoice_description, + expiry_secs, + manual_claim_payment_hash, + route_hints_mode, + custom_route_hint_user_channel_ids, + )?, }; let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 6577645b4e..81646a1fc2 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -15,7 +15,7 @@ mod spontaneous; pub(crate) mod store; mod unified_qr; -pub use bolt11::Bolt11Payment; +pub use bolt11::{Bolt11Payment, RouteHintsMode}; pub use bolt12::Bolt12Payment; pub use onchain::{OnchainPayment, WalletUtxo}; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 0e21a7e892..a02b9af2d1 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -26,7 +26,7 @@ use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use crate::error::Error; use crate::ffi::maybe_wrap; use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, RouteHintsMode}; use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -112,6 +112,8 @@ impl UnifiedQrPayment { &invoice_description, expiry_sec, None, + RouteHintsMode::Automatic, + None, ) { Ok(invoice) => Some(invoice), Err(e) => { From e70a3c6cd2ee6295ecced95e4f740ed3d795042a Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Thu, 16 Jul 2026 14:10:37 +0530 Subject: [PATCH 2/4] fix(bolt11): fail fast on invalid custom route hints --- src/payment/bolt11.rs | 133 ++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 76 deletions(-) diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index f653b32f3a..eb7b629bf0 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -15,6 +15,7 @@ use std::time::Duration; use bitcoin::hashes::{sha256, Hash}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::secp256k1::Secp256k1; +use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, MIN_FINAL_CLTV_EXPIRY_DELTA, @@ -121,81 +122,10 @@ impl Bolt11Payment { fn build_custom_route_hints( &self, user_channel_ids: Vec, ) -> Result, Error> { - if user_channel_ids.is_empty() { - return Err(Error::InvalidChannelId); - } - - let channels = self.channel_manager.list_channels(); - let mut hints = Vec::new(); - - for user_channel_id in user_channel_ids.into_iter().take(MAX_CUSTOM_ROUTE_HINTS) { - let channel = channels.iter().find(|chan| { - UserChannelId(chan.user_channel_id) == user_channel_id - }); - - let channel = match channel { - Some(channel) => channel, - None => { - log_info!( - self.logger, - "Skipping unknown user channel id {} for custom route hints", - user_channel_id - ); - continue; - }, - }; - - if !channel.is_channel_ready { - log_info!( - self.logger, - "Skipping channel {} for custom route hints as it is not ready", - channel.channel_id - ); - continue; - } - - let short_channel_id = match channel.inbound_scid_alias.or(channel.short_channel_id) { - Some(scid) => scid, - None => { - log_info!( - self.logger, - "Skipping channel {} for custom route hints as it has no inbound SCID", - channel.channel_id - ); - continue; - }, - }; - - let forwarding_info = match channel.counterparty.forwarding_info.as_ref() { - Some(info) => info, - None => { - log_info!( - self.logger, - "Skipping channel {} for custom route hints as it has no forwarding info", - channel.channel_id - ); - continue; - }, - }; - - hints.push(RouteHint(vec![RouteHintHop { - src_node_id: channel.counterparty.node_id, - short_channel_id, - fees: RoutingFees { - base_msat: forwarding_info.fee_base_msat, - proportional_millionths: forwarding_info.fee_proportional_millionths, - }, - cltv_expiry_delta: forwarding_info.cltv_expiry_delta, - htlc_minimum_msat: channel.inbound_htlc_minimum_msat, - htlc_maximum_msat: channel.inbound_htlc_maximum_msat, - }])); - } - - if hints.is_empty() { - return Err(Error::InvalidChannelId); - } - - Ok(hints) + build_custom_route_hints_from_channels( + &self.channel_manager.list_channels(), + user_channel_ids, + ) } fn create_bolt11_invoice_with_route_hints( @@ -240,11 +170,12 @@ impl Bolt11Payment { .payment_hash(payment_hash) .payment_secret(payment_secret) .current_timestamp() + .basic_mpp() .min_final_cltv_expiry_delta(MIN_FINAL_CLTV_EXPIRY_DELTA.into()) .expiry_time(Duration::from_secs(expiry_secs.into())); if let Some(amount_msat) = amount_msat { - invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp(); + invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat); } if route_hints_mode == RouteHintsMode::Custom { @@ -1130,3 +1061,53 @@ impl Bolt11Payment { Ok(()) } } + +fn build_custom_route_hints_from_channels( + channels: &[LdkChannelDetails], user_channel_ids: Vec, +) -> Result, Error> { + if user_channel_ids.is_empty() { + return Err(Error::InvalidChannelId); + } + + if user_channel_ids.len() > MAX_CUSTOM_ROUTE_HINTS { + return Err(Error::InvalidChannelId); + } + + let mut hints = Vec::with_capacity(user_channel_ids.len()); + + for user_channel_id in user_channel_ids { + let channel = channels + .iter() + .find(|chan| UserChannelId(chan.user_channel_id) == user_channel_id) + .ok_or(Error::InvalidChannelId)?; + + if !channel.is_channel_ready { + return Err(Error::InvalidChannelId); + } + + let short_channel_id = channel + .inbound_scid_alias + .or(channel.short_channel_id) + .ok_or(Error::InvalidChannelId)?; + + let forwarding_info = channel + .counterparty + .forwarding_info + .as_ref() + .ok_or(Error::InvalidChannelId)?; + + hints.push(RouteHint(vec![RouteHintHop { + src_node_id: channel.counterparty.node_id, + short_channel_id, + fees: RoutingFees { + base_msat: forwarding_info.fee_base_msat, + proportional_millionths: forwarding_info.fee_proportional_millionths, + }, + cltv_expiry_delta: forwarding_info.cltv_expiry_delta, + htlc_minimum_msat: channel.inbound_htlc_minimum_msat, + htlc_maximum_msat: channel.inbound_htlc_maximum_msat, + }])); + } + + Ok(hints) +} From 2c520733c40e5596062489b443599921ac7d6a74 Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Tue, 28 Jul 2026 17:46:23 +0530 Subject: [PATCH 3/4] refactor(bolt11): use LDK route_hints_override and RouteHints enum --- Cargo.lock | 24 +- bindings/ldk_node.udl | 18 +- src/ffi/types.rs | 2 +- src/lib.rs | 2 - src/payment/bolt11.rs | 459 ++++++++++++++++++-------------------- src/payment/mod.rs | 2 +- src/payment/unified_qr.rs | 5 +- 7 files changed, 250 insertions(+), 262 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a62985004..adbb645c1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1977,7 +1977,7 @@ dependencies = [ [[package]] name = "lightning" version = "0.3.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bech32", "bitcoin", @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "lightning-background-processor" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "bitcoin-io", @@ -2035,7 +2035,7 @@ dependencies = [ [[package]] name = "lightning-block-sync" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "chunked_transfer", @@ -2059,7 +2059,7 @@ dependencies = [ [[package]] name = "lightning-invoice" version = "0.34.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bech32", "bitcoin", @@ -2085,7 +2085,7 @@ dependencies = [ [[package]] name = "lightning-liquidity" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "chrono", @@ -2111,7 +2111,7 @@ dependencies = [ [[package]] name = "lightning-macros" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "proc-macro2", "quote", @@ -2132,7 +2132,7 @@ dependencies = [ [[package]] name = "lightning-net-tokio" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "lightning 0.3.0+git", @@ -2153,7 +2153,7 @@ dependencies = [ [[package]] name = "lightning-persister" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "lightning 0.3.0+git", @@ -2176,7 +2176,7 @@ dependencies = [ [[package]] name = "lightning-rapid-gossip-sync" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "bitcoin-io", @@ -2201,7 +2201,7 @@ dependencies = [ [[package]] name = "lightning-transaction-sync" version = "0.2.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", "electrum-client 0.24.1", @@ -2223,7 +2223,7 @@ dependencies = [ [[package]] name = "lightning-types" version = "0.3.0+git" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "bitcoin", ] @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "possiblyrandom" version = "0.2.0" -source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#8601dac7f640e30cd7c7823430ca044322a40903" +source = "git+https://github.com/ZeusLN/rust-lightning?branch=lsps7-for-ldk-node-close-fix#a7fa715285f940e25f721516371eac59956ccf32" dependencies = [ "getrandom 0.2.17", ] diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 8bbc5a96bd..feb96cb627 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -212,10 +212,12 @@ interface Bolt11InvoiceDescription { Direct(string description); }; -enum RouteHintsMode { - "None", - "Automatic", - "Custom", +// Custom accepts at most 3 UserChannelId values; invalid or unusable IDs fail with InvalidChannelId. +[Enum] +interface RouteHints { + None(); + Automatic(); + Custom(sequence user_channel_ids); }; interface Bolt11Payment { @@ -238,9 +240,13 @@ interface Bolt11Payment { [Throws=NodeError] Bolt11Invoice receive_variable_amount([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs); [Throws=NodeError] - Bolt11Invoice receive_with_route_hints(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, RouteHintsMode route_hints_mode, sequence? custom_route_hint_user_channel_ids); + Bolt11Invoice receive_with_route_hints(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, RouteHints route_hints); + [Throws=NodeError] + Bolt11Invoice receive_for_hash_with_route_hints(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash, RouteHints route_hints); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount_with_route_hints([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, RouteHints route_hints); [Throws=NodeError] - Bolt11Invoice receive_variable_amount_with_route_hints([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, RouteHintsMode route_hints_mode, sequence? custom_route_hint_user_channel_ids); + Bolt11Invoice receive_variable_amount_for_hash_with_route_hints([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash, RouteHints route_hints); [Throws=NodeError] Bolt11Invoice receive_variable_amount_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash); [Throws=NodeError] diff --git a/src/ffi/types.rs b/src/ffi/types.rs index ecb20e57ad..2570264cd7 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -58,7 +58,7 @@ pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use crate::payment::{QrPaymentResult, RouteHintsMode}; +pub use crate::payment::{QrPaymentResult, RouteHints}; use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId}; impl UniffiCustomTypeConverter for PublicKey { diff --git a/src/lib.rs b/src/lib.rs index 931b1da088..5e3ebab618 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -845,7 +845,6 @@ impl Node { Bolt11Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), - Arc::clone(&self.keys_manager), Arc::clone(&self.connection_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), @@ -864,7 +863,6 @@ impl Node { Arc::new(Bolt11Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), - Arc::clone(&self.keys_manager), Arc::clone(&self.connection_manager), self.liquidity_source.clone(), Arc::clone(&self.payment_store), diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index eb7b629bf0..cb987e232f 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -12,20 +12,18 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; -use bitcoin::hashes::{sha256, Hash}; +use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::secp256k1::Secp256k1; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, - MIN_FINAL_CLTV_EXPIRY_DELTA, }; use lightning::routing::router::{ PaymentParameters, RouteHint, RouteHintHop, RouteParameters, RouteParametersConfig, }; use lightning_invoice::{ Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, - InvoiceBuilder, RoutingFees, + RoutingFees, }; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -42,20 +40,43 @@ use crate::payment::store::{ }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::types::{ChannelManager, PaymentStore}; use crate::UserChannelId; -const MAX_CUSTOM_ROUTE_HINTS: usize = 3; +/// Maximum number of channel IDs that may be specified in [`RouteHints::Custom`]. +pub const MAX_CUSTOM_ROUTE_HINTS: usize = 3; /// Controls which route hints are included when creating a BOLT11 invoice. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RouteHintsMode { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RouteHints { /// Do not include any route hints in the invoice. None, /// Automatically select route hints from eligible channels (default). Automatic, /// Include route hints only for the given [`UserChannelId`]s. - Custom, + /// + /// At most [`MAX_CUSTOM_ROUTE_HINTS`] channel IDs may be specified. Invalid or unusable + /// channel IDs cause invoice creation to fail immediately. Unlike [`RouteHints::Automatic`], + /// no capacity, connectivity, or announcement filters are applied — the given channels are + /// included as hints if they are ready and have a payment SCID and forwarding info. + Custom { + /// The channel IDs to include as route hints. + user_channel_ids: Vec, + }, +} + +impl RouteHints { + fn to_ldk_override( + self, logger: &Logger, channels: &[LdkChannelDetails], + ) -> Result>, Error> { + match self { + RouteHints::Automatic => Ok(None), + RouteHints::None => Ok(Some(vec![])), + RouteHints::Custom { user_channel_ids } => Ok(Some(build_custom_route_hints( + logger, channels, user_channel_ids, + )?)), + } + } } #[cfg(not(feature = "uniffi"))] @@ -86,7 +107,6 @@ fn invoice_description_str(invoice: &LdkBolt11Invoice) -> Option { pub struct Bolt11Payment { runtime: Arc, channel_manager: Arc, - keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, @@ -99,7 +119,6 @@ pub struct Bolt11Payment { impl Bolt11Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, - keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, @@ -108,7 +127,6 @@ impl Bolt11Payment { Self { runtime, channel_manager, - keys_manager, connection_manager, liquidity_source, payment_store, @@ -119,88 +137,6 @@ impl Bolt11Payment { } } - fn build_custom_route_hints( - &self, user_channel_ids: Vec, - ) -> Result, Error> { - build_custom_route_hints_from_channels( - &self.channel_manager.list_channels(), - user_channel_ids, - ) - } - - fn create_bolt11_invoice_with_route_hints( - &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, - expiry_secs: u32, manual_claim_payment_hash: Option, - route_hints_mode: RouteHintsMode, - custom_route_hint_user_channel_ids: Option>, - ) -> Result { - let (payment_hash, payment_secret) = match manual_claim_payment_hash { - Some(payment_hash) => { - let payment_secret = self - .channel_manager - .create_inbound_payment_for_hash( - payment_hash, - amount_msat, - expiry_secs, - None, - ) - .map_err(|e| { - log_error!(self.logger, "Failed to register inbound payment: {:?}", e); - Error::InvoiceCreationFailed - })?; - (payment_hash, payment_secret) - }, - None => self - .channel_manager - .create_inbound_payment(amount_msat, expiry_secs, None) - .map_err(|e| { - log_error!(self.logger, "Failed to register inbound payment: {:?}", e); - Error::InvoiceCreationFailed - })?, - }; - - let payment_hash = sha256::Hash::from_slice(&payment_hash.0).map_err(|e| { - log_error!(self.logger, "Invalid payment hash: {:?}", e); - Error::InvoiceCreationFailed - })?; - - let currency = self.config.network.into(); - let mut invoice_builder = InvoiceBuilder::new(currency) - .invoice_description(invoice_description.clone()) - .payment_hash(payment_hash) - .payment_secret(payment_secret) - .current_timestamp() - .basic_mpp() - .min_final_cltv_expiry_delta(MIN_FINAL_CLTV_EXPIRY_DELTA.into()) - .expiry_time(Duration::from_secs(expiry_secs.into())); - - if let Some(amount_msat) = amount_msat { - invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat); - } - - if route_hints_mode == RouteHintsMode::Custom { - let hints = self.build_custom_route_hints( - custom_route_hint_user_channel_ids.unwrap_or_default(), - )?; - for hint in hints { - invoice_builder = invoice_builder.private_route(hint); - } - } - - let invoice = invoice_builder - .build_signed(|hash| { - Secp256k1::new() - .sign_ecdsa_recoverable(hash, &self.keys_manager.get_node_secret_key()) - }) - .map_err(|e| { - log_error!(self.logger, "Failed to build invoice: {:?}", e); - Error::InvoiceCreationFailed - })?; - - log_info!(self.logger, "Invoice created: {}", invoice); - Ok(invoice) - } - /// Send a payment given an invoice. /// /// If `route_parameters` are provided they will override the default as well as the @@ -417,7 +353,8 @@ impl Bolt11Payment { } /// Allows to attempt manually claiming payments with the given preimage that have previously - /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. + /// been registered via [`receive_for_hash`], [`receive_variable_amount_for_hash`], + /// [`receive_for_hash_with_route_hints`], or [`receive_variable_amount_for_hash_with_route_hints`]. /// /// This should be called in reponse to a [`PaymentClaimable`] event as soon as the preimage is /// available. @@ -430,6 +367,8 @@ impl Bolt11Payment { /// /// [`receive_for_hash`]: Self::receive_for_hash /// [`receive_variable_amount_for_hash`]: Self::receive_variable_amount_for_hash + /// [`receive_for_hash_with_route_hints`]: Self::receive_for_hash_with_route_hints + /// [`receive_variable_amount_for_hash_with_route_hints`]: Self::receive_variable_amount_for_hash_with_route_hints /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`PaymentReceived`]: crate::Event::PaymentReceived pub fn claim_for_hash( @@ -482,7 +421,8 @@ impl Bolt11Payment { } /// Allows to manually fail payments with the given hash that have previously - /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. + /// been registered via [`receive_for_hash`], [`receive_variable_amount_for_hash`], + /// [`receive_for_hash_with_route_hints`], or [`receive_variable_amount_for_hash_with_route_hints`]. /// /// This should be called in reponse to a [`PaymentClaimable`] event if the payment needs to be /// failed back, e.g., if the correct preimage can't be retrieved in time before the claim @@ -493,6 +433,8 @@ impl Bolt11Payment { /// /// [`receive_for_hash`]: Self::receive_for_hash /// [`receive_variable_amount_for_hash`]: Self::receive_variable_amount_for_hash + /// [`receive_for_hash_with_route_hints`]: Self::receive_for_hash_with_route_hints + /// [`receive_variable_amount_for_hash_with_route_hints`]: Self::receive_variable_amount_for_hash_with_route_hints /// [`PaymentClaimable`]: crate::Event::PaymentClaimable pub fn fail_for_hash(&self, payment_hash: PaymentHash) -> Result<(), Error> { let payment_id = PaymentId(payment_hash.0); @@ -527,6 +469,16 @@ impl Bolt11Payment { Ok(()) } + fn receive_wrapped( + &self, amount_msat: Option, description: &Bolt11InvoiceDescription, + expiry_secs: u32, payment_hash: Option, route_hints: RouteHints, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + Ok(maybe_wrap(self.receive_inner( + amount_msat, &description, expiry_secs, payment_hash, route_hints, + )?)) + } + /// Returns a payable invoice that can be used to request and receive a payment of the amount /// given. /// @@ -534,11 +486,9 @@ impl Bolt11Payment { pub fn receive( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner( - Some(amount_msat), &description, expiry_secs, None, RouteHintsMode::Automatic, None, - )?; - Ok(maybe_wrap(invoice)) + self.receive_wrapped( + Some(amount_msat), description, expiry_secs, None, RouteHints::Automatic, + ) } /// Returns a payable invoice that can be used to request a payment of the amount @@ -559,12 +509,9 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner( - Some(amount_msat), &description, expiry_secs, Some(payment_hash), - RouteHintsMode::Automatic, None, - )?; - Ok(maybe_wrap(invoice)) + self.receive_wrapped( + Some(amount_msat), description, expiry_secs, Some(payment_hash), RouteHints::Automatic, + ) } /// Returns a payable invoice that can be used to request and receive a payment of the amount @@ -573,19 +520,32 @@ impl Bolt11Payment { /// The inbound payment will be automatically claimed upon arrival. pub fn receive_with_route_hints( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, - route_hints_mode: RouteHintsMode, - custom_route_hint_user_channel_ids: Option>, + route_hints: RouteHints, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner( - Some(amount_msat), - &description, - expiry_secs, - None, - route_hints_mode, - custom_route_hint_user_channel_ids, - )?; - Ok(maybe_wrap(invoice)) + self.receive_wrapped(Some(amount_msat), description, expiry_secs, None, route_hints) + } + + /// Returns a payable invoice that can be used to request a payment of the amount given for the + /// given payment hash, with configurable route hints. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once the + /// inbound payment arrives. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + pub fn receive_for_hash_with_route_hints( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: PaymentHash, route_hints: RouteHints, + ) -> Result { + self.receive_wrapped( + Some(amount_msat), description, expiry_secs, Some(payment_hash), route_hints, + ) } /// Returns a payable invoice that can be used to request and receive a payment for which the @@ -595,11 +555,7 @@ impl Bolt11Payment { pub fn receive_variable_amount( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner( - None, &description, expiry_secs, None, RouteHintsMode::Automatic, None, - )?; - Ok(maybe_wrap(invoice)) + self.receive_wrapped(None, description, expiry_secs, None, RouteHints::Automatic) } /// Returns a payable invoice that can be used to request a payment for the given payment hash @@ -619,11 +575,9 @@ impl Bolt11Payment { pub fn receive_variable_amount_for_hash( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner( - None, &description, expiry_secs, Some(payment_hash), RouteHintsMode::Automatic, None, - )?; - Ok(maybe_wrap(invoice)) + self.receive_wrapped( + None, description, expiry_secs, Some(payment_hash), RouteHints::Automatic, + ) } /// Returns a payable invoice that can be used to request and receive a payment for which the @@ -632,57 +586,65 @@ impl Bolt11Payment { /// /// The inbound payment will be automatically claimed upon arrival. pub fn receive_variable_amount_with_route_hints( - &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, - route_hints_mode: RouteHintsMode, - custom_route_hint_user_channel_ids: Option>, + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, route_hints: RouteHints, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner( - None, - &description, - expiry_secs, - None, - route_hints_mode, - custom_route_hint_user_channel_ids, - )?; - Ok(maybe_wrap(invoice)) + self.receive_wrapped(None, description, expiry_secs, None, route_hints) + } + + /// Returns a payable invoice that can be used to request a payment for the given payment hash + /// and the amount to be determined by the user, also known as a "zero-amount" invoice, with + /// configurable route hints. + /// + /// When using [`RouteHints::Custom`], at most [`MAX_CUSTOM_ROUTE_HINTS`] channel IDs may be + /// specified; invalid or unusable IDs fail immediately with [`Error::InvalidChannelId`]. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once the + /// inbound payment arrives. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + pub fn receive_variable_amount_for_hash_with_route_hints( + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, + route_hints: RouteHints, + ) -> Result { + self.receive_wrapped( + None, description, expiry_secs, Some(payment_hash), route_hints, + ) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, - expiry_secs: u32, manual_claim_payment_hash: Option, - route_hints_mode: RouteHintsMode, - custom_route_hint_user_channel_ids: Option>, + expiry_secs: u32, manual_claim_payment_hash: Option, route_hints: RouteHints, ) -> Result { - let invoice = match route_hints_mode { - RouteHintsMode::Automatic => { - let invoice_params = Bolt11InvoiceParameters { - amount_msats: amount_msat, - description: invoice_description.clone(), - invoice_expiry_delta_secs: Some(expiry_secs), - payment_hash: manual_claim_payment_hash, - ..Default::default() - }; + let route_hints_override = route_hints.to_ldk_override( + &self.logger, + &self.channel_manager.list_channels(), + )?; - match self.channel_manager.create_bolt11_invoice(invoice_params) { - Ok(inv) => { - log_info!(self.logger, "Invoice created: {}", inv); - inv - }, - Err(e) => { - log_error!(self.logger, "Failed to create invoice: {}", e); - return Err(Error::InvoiceCreationFailed); - }, - } + let invoice_params = Bolt11InvoiceParameters { + amount_msats: amount_msat, + description: invoice_description.clone(), + invoice_expiry_delta_secs: Some(expiry_secs), + payment_hash: manual_claim_payment_hash, + route_hints_override, + ..Default::default() + }; + + let invoice = match self.channel_manager.create_bolt11_invoice(invoice_params) { + Ok(inv) => { + log_info!(self.logger, "Invoice created: {}", inv); + inv + }, + Err(e) => { + log_error!(self.logger, "Failed to create invoice: {}", e); + return Err(Error::InvoiceCreationFailed); }, - RouteHintsMode::None | RouteHintsMode::Custom => self.create_bolt11_invoice_with_route_hints( - amount_msat, - invoice_description, - expiry_secs, - manual_claim_payment_hash, - route_hints_mode, - custom_route_hint_user_channel_ids, - )?, }; let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); @@ -720,6 +682,22 @@ impl Bolt11Payment { Ok(invoice) } + fn receive_via_jit_channel_wrapped( + &self, amount_msat: Option, description: &Bolt11InvoiceDescription, + expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + Ok(maybe_wrap(self.receive_via_jit_channel_inner( + amount_msat, + &description, + expiry_secs, + max_total_lsp_fee_limit_msat, + max_proportional_lsp_fee_limit_ppm_msat, + payment_hash, + )?)) + } + /// Returns a payable invoice that can be used to request a payment of the amount given and /// receive it via a newly created just-in-time (JIT) channel. /// @@ -734,16 +712,9 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( - Some(amount_msat), - &description, - expiry_secs, - max_total_lsp_fee_limit_msat, - None, - None, - )?; - Ok(maybe_wrap(invoice)) + self.receive_via_jit_channel_wrapped( + Some(amount_msat), description, expiry_secs, max_total_lsp_fee_limit_msat, None, None, + ) } /// Returns a payable invoice that can be used to request a payment of the amount given and @@ -773,16 +744,14 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + self.receive_via_jit_channel_wrapped( Some(amount_msat), - &description, + description, expiry_secs, max_total_lsp_fee_limit_msat, None, Some(payment_hash), - )?; - Ok(maybe_wrap(invoice)) + ) } /// Returns a payable invoice that can be used to request a variable amount payment (also known @@ -800,16 +769,9 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( - None, - &description, - expiry_secs, - None, - max_proportional_lsp_fee_limit_ppm_msat, - None, - )?; - Ok(maybe_wrap(invoice)) + self.receive_via_jit_channel_wrapped( + None, description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, None, + ) } /// Returns a payable invoice that can be used to request a variable amount payment (also known @@ -840,16 +802,14 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, ) -> Result { - let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + self.receive_via_jit_channel_wrapped( None, - &description, + description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, Some(payment_hash), - )?; - Ok(maybe_wrap(invoice)) + ) } fn receive_via_jit_channel_inner( @@ -1062,52 +1022,77 @@ impl Bolt11Payment { } } -fn build_custom_route_hints_from_channels( - channels: &[LdkChannelDetails], user_channel_ids: Vec, +fn build_custom_route_hints( + logger: &Logger, channels: &[LdkChannelDetails], user_channel_ids: Vec, ) -> Result, Error> { + let mut user_channel_ids = user_channel_ids; + user_channel_ids.sort_by_key(|id| id.0); + user_channel_ids.dedup(); + if user_channel_ids.is_empty() { + log_error!(logger, "Custom route hints require at least one channel ID"); return Err(Error::InvalidChannelId); } if user_channel_ids.len() > MAX_CUSTOM_ROUTE_HINTS { + log_error!( + logger, + "Custom route hints support at most {} channel IDs, got {}", + MAX_CUSTOM_ROUTE_HINTS, + user_channel_ids.len() + ); return Err(Error::InvalidChannelId); } - let mut hints = Vec::with_capacity(user_channel_ids.len()); - - for user_channel_id in user_channel_ids { - let channel = channels - .iter() - .find(|chan| UserChannelId(chan.user_channel_id) == user_channel_id) - .ok_or(Error::InvalidChannelId)?; + user_channel_ids + .into_iter() + .map(|user_channel_id| route_hint_from_channel(logger, channels, user_channel_id)) + .collect() +} - if !channel.is_channel_ready { - return Err(Error::InvalidChannelId); - } +fn route_hint_from_channel( + logger: &Logger, channels: &[LdkChannelDetails], user_channel_id: UserChannelId, +) -> Result { + let channel = channels + .iter() + .find(|chan| UserChannelId(chan.user_channel_id) == user_channel_id) + .ok_or_else(|| { + log_error!(logger, "Custom route hint channel ID {} is unknown", user_channel_id.0); + Error::InvalidChannelId + })?; - let short_channel_id = channel - .inbound_scid_alias - .or(channel.short_channel_id) - .ok_or(Error::InvalidChannelId)?; - - let forwarding_info = channel - .counterparty - .forwarding_info - .as_ref() - .ok_or(Error::InvalidChannelId)?; - - hints.push(RouteHint(vec![RouteHintHop { - src_node_id: channel.counterparty.node_id, - short_channel_id, - fees: RoutingFees { - base_msat: forwarding_info.fee_base_msat, - proportional_millionths: forwarding_info.fee_proportional_millionths, - }, - cltv_expiry_delta: forwarding_info.cltv_expiry_delta, - htlc_minimum_msat: channel.inbound_htlc_minimum_msat, - htlc_maximum_msat: channel.inbound_htlc_maximum_msat, - }])); + if !channel.is_channel_ready { + log_error!(logger, "Custom route hint channel ID {} is not ready", user_channel_id.0); + return Err(Error::InvalidChannelId); } - Ok(hints) + let short_channel_id = channel.get_inbound_payment_scid().ok_or_else(|| { + log_error!( + logger, + "Custom route hint channel ID {} has no short channel ID yet", + user_channel_id.0 + ); + Error::InvalidChannelId + })?; + + let forwarding_info = channel.counterparty.forwarding_info.as_ref().ok_or_else(|| { + log_error!( + logger, + "Custom route hint channel ID {} has no forwarding info yet; retry after the peer's first channel_update is processed", + user_channel_id.0 + ); + Error::InvalidChannelId + })?; + + Ok(RouteHint(vec![RouteHintHop { + src_node_id: channel.counterparty.node_id, + short_channel_id, + fees: RoutingFees { + base_msat: forwarding_info.fee_base_msat, + proportional_millionths: forwarding_info.fee_proportional_millionths, + }, + cltv_expiry_delta: forwarding_info.cltv_expiry_delta, + htlc_minimum_msat: channel.inbound_htlc_minimum_msat, + htlc_maximum_msat: channel.inbound_htlc_maximum_msat, + }])) } diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 81646a1fc2..ef06dcb045 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -15,7 +15,7 @@ mod spontaneous; pub(crate) mod store; mod unified_qr; -pub use bolt11::{Bolt11Payment, RouteHintsMode}; +pub use bolt11::{Bolt11Payment, RouteHints, MAX_CUSTOM_ROUTE_HINTS}; pub use bolt12::Bolt12Payment; pub use onchain::{OnchainPayment, WalletUtxo}; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index a02b9af2d1..ce42bd9e0a 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -26,7 +26,7 @@ use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use crate::error::Error; use crate::ffi::maybe_wrap; use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, RouteHintsMode}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, RouteHints}; use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -112,8 +112,7 @@ impl UnifiedQrPayment { &invoice_description, expiry_sec, None, - RouteHintsMode::Automatic, - None, + RouteHints::Automatic, ) { Ok(invoice) => Some(invoice), Err(e) => { From 206155eb93867150becdfca8853ca997650222cf Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Wed, 29 Jul 2026 09:13:04 +0530 Subject: [PATCH 4/4] fix(bolt11): rustfmt and document MAX_CUSTOM_ROUTE_HINTS on receive APIs --- src/payment/bolt11.rs | 79 +++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index cb987e232f..01be4dd889 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -12,8 +12,8 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; -use bitcoin::hashes::Hash; use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, @@ -72,9 +72,9 @@ impl RouteHints { match self { RouteHints::Automatic => Ok(None), RouteHints::None => Ok(Some(vec![])), - RouteHints::Custom { user_channel_ids } => Ok(Some(build_custom_route_hints( - logger, channels, user_channel_ids, - )?)), + RouteHints::Custom { user_channel_ids } => { + Ok(Some(build_custom_route_hints(logger, channels, user_channel_ids)?)) + }, } } } @@ -470,12 +470,16 @@ impl Bolt11Payment { } fn receive_wrapped( - &self, amount_msat: Option, description: &Bolt11InvoiceDescription, - expiry_secs: u32, payment_hash: Option, route_hints: RouteHints, + &self, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: Option, route_hints: RouteHints, ) -> Result { let description = maybe_try_convert_enum(description)?; Ok(maybe_wrap(self.receive_inner( - amount_msat, &description, expiry_secs, payment_hash, route_hints, + amount_msat, + &description, + expiry_secs, + payment_hash, + route_hints, )?)) } @@ -487,7 +491,11 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { self.receive_wrapped( - Some(amount_msat), description, expiry_secs, None, RouteHints::Automatic, + Some(amount_msat), + description, + expiry_secs, + None, + RouteHints::Automatic, ) } @@ -510,13 +518,20 @@ impl Bolt11Payment { payment_hash: PaymentHash, ) -> Result { self.receive_wrapped( - Some(amount_msat), description, expiry_secs, Some(payment_hash), RouteHints::Automatic, + Some(amount_msat), + description, + expiry_secs, + Some(payment_hash), + RouteHints::Automatic, ) } /// Returns a payable invoice that can be used to request and receive a payment of the amount /// given, with configurable route hints. /// + /// When using [`RouteHints::Custom`], at most [`MAX_CUSTOM_ROUTE_HINTS`] channel IDs may be + /// specified; invalid or unusable IDs fail immediately with [`Error::InvalidChannelId`]. + /// /// The inbound payment will be automatically claimed upon arrival. pub fn receive_with_route_hints( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, @@ -528,6 +543,9 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment of the amount given for the /// given payment hash, with configurable route hints. /// + /// When using [`RouteHints::Custom`], at most [`MAX_CUSTOM_ROUTE_HINTS`] channel IDs may be + /// specified; invalid or unusable IDs fail immediately with [`Error::InvalidChannelId`]. + /// /// We will register the given payment hash and emit a [`PaymentClaimable`] event once the /// inbound payment arrives. /// @@ -544,7 +562,11 @@ impl Bolt11Payment { payment_hash: PaymentHash, route_hints: RouteHints, ) -> Result { self.receive_wrapped( - Some(amount_msat), description, expiry_secs, Some(payment_hash), route_hints, + Some(amount_msat), + description, + expiry_secs, + Some(payment_hash), + route_hints, ) } @@ -576,7 +598,11 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { self.receive_wrapped( - None, description, expiry_secs, Some(payment_hash), RouteHints::Automatic, + None, + description, + expiry_secs, + Some(payment_hash), + RouteHints::Automatic, ) } @@ -584,6 +610,9 @@ impl Bolt11Payment { /// amount is to be determined by the user, also known as a "zero-amount" invoice, with /// configurable route hints. /// + /// When using [`RouteHints::Custom`], at most [`MAX_CUSTOM_ROUTE_HINTS`] channel IDs may be + /// specified; invalid or unusable IDs fail immediately with [`Error::InvalidChannelId`]. + /// /// The inbound payment will be automatically claimed upon arrival. pub fn receive_variable_amount_with_route_hints( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, route_hints: RouteHints, @@ -613,19 +642,15 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, route_hints: RouteHints, ) -> Result { - self.receive_wrapped( - None, description, expiry_secs, Some(payment_hash), route_hints, - ) + self.receive_wrapped(None, description, expiry_secs, Some(payment_hash), route_hints) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, route_hints: RouteHints, ) -> Result { - let route_hints_override = route_hints.to_ldk_override( - &self.logger, - &self.channel_manager.list_channels(), - )?; + let route_hints_override = + route_hints.to_ldk_override(&self.logger, &self.channel_manager.list_channels())?; let invoice_params = Bolt11InvoiceParameters { amount_msats: amount_msat, @@ -683,8 +708,8 @@ impl Bolt11Payment { } fn receive_via_jit_channel_wrapped( - &self, amount_msat: Option, description: &Bolt11InvoiceDescription, - expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, + &self, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result { let description = maybe_try_convert_enum(description)?; @@ -713,7 +738,12 @@ impl Bolt11Payment { max_total_lsp_fee_limit_msat: Option, ) -> Result { self.receive_via_jit_channel_wrapped( - Some(amount_msat), description, expiry_secs, max_total_lsp_fee_limit_msat, None, None, + Some(amount_msat), + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + None, ) } @@ -770,7 +800,12 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { self.receive_via_jit_channel_wrapped( - None, description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, None, + None, + description, + expiry_secs, + None, + max_proportional_lsp_fee_limit_ppm_msat, + None, ) }