From 77a006f027cb71a16f0c6be3059b9576d4584fc2 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sat, 2 Aug 2025 17:08:31 +0100 Subject: [PATCH 1/8] Rename unified_qr module to unified Rename the source file for the Unified QR payment logic from unified_qr.rs to unified.rs. This change is made in preparation for expanding the module's scope beyond just QR code payment processing. --- src/payment/mod.rs | 4 ++-- src/payment/{unified_qr.rs => unified.rs} | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) rename src/payment/{unified_qr.rs => unified.rs} (99%) diff --git a/src/payment/mod.rs b/src/payment/mod.rs index f629960e1..8642ec1ee 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -13,7 +13,7 @@ mod bolt12; mod onchain; mod spontaneous; pub(crate) mod store; -mod unified_qr; +mod unified; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; @@ -22,4 +22,4 @@ pub use spontaneous::SpontaneousPayment; pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; +pub use unified::{QrPaymentResult, UnifiedQrPayment}; diff --git a/src/payment/unified_qr.rs b/src/payment/unified.rs similarity index 99% rename from src/payment/unified_qr.rs rename to src/payment/unified.rs index 6ebf25563..2017cef03 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified.rs @@ -308,12 +308,10 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { - use std::str::FromStr; - - use bitcoin::{Address, Network}; - use super::*; - use crate::payment::unified_qr::Extras; + use crate::payment::unified::Extras; + use bitcoin::{Address, Network}; + use std::str::FromStr; #[test] fn parse_uri() { From cf3f415e368f1b254eb57881f74524c4bba1130f Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sat, 2 Aug 2025 17:19:21 +0100 Subject: [PATCH 2/8] Rename UnifiedQRPayment to UnifiedPayment Rename the primary payment handler struct from UnifiedQRPayment to UnifiedPayment. Also rename QRPaymentResult to UnifiedPaymentResult. This change aligns with the prior module rename and reflects that the component handles a broader range of payment inputs. --- bindings/ldk_node.udl | 8 ++--- src/ffi/types.rs | 5 ++- src/lib.rs | 16 ++++++--- src/payment/mod.rs | 2 +- src/payment/unified.rs | 21 ++++++------ tests/integration_tests_rust.rs | 60 ++++++++++++++++----------------- 6 files changed, 61 insertions(+), 51 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c4ebf56a6..445ad24d2 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -149,7 +149,7 @@ interface Node { Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); - UnifiedQrPayment unified_qr_payment(); + UnifiedPayment unified_payment(); LSPS1Liquidity lsps1_liquidity(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); @@ -275,11 +275,11 @@ interface FeeRate { u64 to_sat_per_vb_ceil(); }; -interface UnifiedQrPayment { +interface UnifiedPayment { [Throws=NodeError] string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec); [Throws=NodeError] - QrPaymentResult send([ByRef]string uri_str, RouteParametersConfig? route_parameters); + UnifiedPaymentResult send([ByRef]string uri_str, RouteParametersConfig? route_parameters); }; interface LSPS1Liquidity { @@ -455,7 +455,7 @@ interface PaymentKind { }; [Enum] -interface QrPaymentResult { +interface UnifiedPaymentResult { Onchain(Txid txid); Bolt11(PaymentId payment_id); Bolt12(PaymentId payment_id); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index c69987c96..35aeb2530 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -55,7 +55,10 @@ 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::UnifiedPaymentResult; + +pub use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; + use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId}; impl UniffiCustomTypeConverter for PublicKey { diff --git a/src/lib.rs b/src/lib.rs index bbae8ac72..b96325818 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,7 +152,7 @@ use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, - UnifiedQrPayment, + UnifiedPayment, }; use peer_store::{PeerInfo, PeerStore}; use rand::Rng; @@ -947,12 +947,15 @@ impl Node { /// Returns a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], /// and [BOLT 12] payment options. /// + /// This handler allows you to send payments to these URIs as well as [BIP 353] HRNs. + /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki #[cfg(not(feature = "uniffi"))] - pub fn unified_qr_payment(&self) -> UnifiedQrPayment { - UnifiedQrPayment::new( + pub fn unified_payment(&self) -> UnifiedPayment { + UnifiedPayment::new( self.onchain_payment().into(), self.bolt11_payment().into(), self.bolt12_payment().into(), @@ -964,12 +967,15 @@ impl Node { /// Returns a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], /// and [BOLT 12] payment options. /// + /// This handler allows you to send payments to these URIs as well as [BIP 353] HRNs. + /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki #[cfg(feature = "uniffi")] - pub fn unified_qr_payment(&self) -> Arc { - Arc::new(UnifiedQrPayment::new( + pub fn unified_payment(&self) -> Arc { + Arc::new(UnifiedPayment::new( self.onchain_payment(), self.bolt11_payment(), self.bolt12_payment(), diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 8642ec1ee..c82f35c8f 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -22,4 +22,4 @@ pub use spontaneous::SpontaneousPayment; pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use unified::{QrPaymentResult, UnifiedQrPayment}; +pub use unified::{UnifiedPayment, UnifiedPaymentResult}; diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 2017cef03..d76572cbe 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -46,7 +46,7 @@ struct Extras { /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [`Node::unified_qr_payment`]: crate::Node::unified_qr_payment -pub struct UnifiedQrPayment { +pub struct UnifiedPayment { onchain_payment: Arc, bolt11_invoice: Arc, bolt12_payment: Arc, @@ -54,7 +54,7 @@ pub struct UnifiedQrPayment { logger: Arc, } -impl UnifiedQrPayment { +impl UnifiedPayment { pub(crate) fn new( onchain_payment: Arc, bolt11_invoice: Arc, bolt12_payment: Arc, config: Arc, logger: Arc, @@ -144,7 +144,7 @@ impl UnifiedQrPayment { /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki pub fn send( &self, uri_str: &str, route_parameters: Option, - ) -> Result { + ) -> Result { let uri: bip21::Uri = uri_str.parse().map_err(|_| Error::InvalidUri)?; @@ -153,16 +153,18 @@ impl UnifiedQrPayment { if let Some(offer) = uri_network_checked.extras.bolt12_offer { let offer = maybe_wrap(offer); + match self.bolt12_payment.send(&offer, None, None, route_parameters) { - Ok(payment_id) => return Ok(QrPaymentResult::Bolt12 { payment_id }), + Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt12 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e), } } if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { let invoice = maybe_wrap(invoice); + match self.bolt11_invoice.send(&invoice, route_parameters) { - Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }), + Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt11 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e), } } @@ -181,7 +183,7 @@ impl UnifiedQrPayment { None, )?; - Ok(QrPaymentResult::Onchain { txid }) + Ok(UnifiedPaymentResult::Onchain { txid }) } } @@ -194,7 +196,7 @@ impl UnifiedQrPayment { /// [`PaymentId`]: lightning::ln::channelmanager::PaymentId /// [`Txid`]: bitcoin::hash_types::Txid #[derive(Debug)] -pub enum QrPaymentResult { +pub enum UnifiedPaymentResult { /// An on-chain payment. Onchain { /// The transaction ID (txid) of the on-chain payment. @@ -308,9 +310,8 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { - use super::*; - use crate::payment::unified::Extras; - use bitcoin::{Address, Network}; + use super::{Amount, Bolt11Invoice, Extras, Offer}; + use bitcoin::{address::NetworkUnchecked, Address, Network}; use std::str::FromStr; #[test] diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 7c1ed8344..48561bf37 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -29,7 +29,7 @@ use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, - QrPaymentResult, + UnifiedPaymentResult, }; use ldk_node::{Builder, DynStore, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; @@ -1527,15 +1527,15 @@ async fn generate_bip21_uri() { // Test 1: Verify URI generation (on-chain + BOLT11) works // even before any channels are opened. This checks the graceful fallback behavior. - let initial_uqr_payment = node_b - .unified_qr_payment() + let initial_uni_payment = node_b + .unified_payment() .receive(expected_amount_sats, "asdf", expiry_sec) .expect("Failed to generate URI"); - println!("Initial URI (no channels): {}", initial_uqr_payment); + println!("Initial URI (no channels): {}", initial_uni_payment); - assert!(initial_uqr_payment.contains("bitcoin:")); - assert!(initial_uqr_payment.contains("lightning=")); - assert!(!initial_uqr_payment.contains("lno=")); // BOLT12 requires channels + assert!(initial_uni_payment.contains("bitcoin:")); + assert!(initial_uni_payment.contains("lightning=")); + assert!(!initial_uni_payment.contains("lno=")); // BOLT12 requires channels premine_and_distribute_funds( &bitcoind.client, @@ -1556,15 +1556,15 @@ async fn generate_bip21_uri() { expect_channel_ready_event!(node_b, node_a.node_id()); // Test 2: Verify URI generation (on-chain + BOLT11 + BOLT12) works after channels are established. - let uqr_payment = node_b - .unified_qr_payment() + let uni_payment = node_b + .unified_payment() .receive(expected_amount_sats, "asdf", expiry_sec) .expect("Failed to generate URI"); - println!("Generated URI: {}", uqr_payment); - assert!(uqr_payment.contains("bitcoin:")); - assert!(uqr_payment.contains("lightning=")); - assert!(uqr_payment.contains("lno=")); + println!("Generated URI: {}", uni_payment); + assert!(uni_payment.contains("bitcoin:")); + assert!(uni_payment.contains("lightning=")); + assert!(uni_payment.contains("lno=")); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1606,17 +1606,17 @@ async fn unified_qr_send_receive() { let expected_amount_sats = 100_000; let expiry_sec = 4_000; - let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec); - let uri_str = uqr_payment.clone().unwrap(); - let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_str, None) { - Ok(QrPaymentResult::Bolt12 { payment_id }) => { + let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec); + let uri_str = uni_payment.clone().unwrap(); + let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None) { + Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); payment_id }, - Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { panic!("Expected Bolt12 payment but got Bolt11"); }, - Ok(QrPaymentResult::Onchain { txid: _ }) => { + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { panic!("Expected Bolt12 payment but get On-chain transaction"); }, Err(e) => { @@ -1629,15 +1629,15 @@ async fn unified_qr_send_receive() { // Cut off the BOLT12 part to fallback to BOLT11. let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); let invoice_payment_id: PaymentId = - match node_a.unified_qr_payment().send(uri_str_without_offer, None) { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + match node_a.unified_payment().send(uri_str_without_offer, None) { + Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected Bolt11 payment but got Bolt12"); }, - Ok(QrPaymentResult::Bolt11 { payment_id }) => { + Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => { println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); payment_id }, - Ok(QrPaymentResult::Onchain { txid: _ }) => { + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { panic!("Expected Bolt11 payment but got on-chain transaction"); }, Err(e) => { @@ -1647,19 +1647,19 @@ async fn unified_qr_send_receive() { expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); let expect_onchain_amount_sats = 800_000; - let onchain_uqr_payment = - node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); + let onchain_uni_payment = + node_b.unified_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); // Cut off any lightning part to fallback to on-chain only. - let uri_str_without_lightning = onchain_uqr_payment.split("&lightning=").next().unwrap(); - let txid = match node_a.unified_qr_payment().send(&uri_str_without_lightning, None) { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap(); + let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None) { + Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected on-chain payment but got Bolt12") }, - Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { panic!("Expected on-chain payment but got Bolt11"); }, - Ok(QrPaymentResult::Onchain { txid }) => { + Ok(UnifiedPaymentResult::Onchain { txid }) => { println!("\nOn-chain transaction successful with Txid: {}", txid); txid }, From 5a3eb678b3a691b4f3d762038e8ceb4e9bba4a5e Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sat, 2 Aug 2025 17:42:22 +0100 Subject: [PATCH 3/8] Add bitcoin-payment-instructions to Cargo Introduce the bitcoin-payment-instructions library as a dependency in Cargo.toml. This library provides the necessary types and parsing logic to handle BIP 21 URIs and BIP 353 HRNs. --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e1459c77d..be62796e8 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,8 @@ log = { version = "0.4.22", default-features = false, features = ["std"]} vss-client = { package = "vss-client-ng", version = "0.4" } prost = { version = "0.11.6", default-features = false} +#bitcoin-payment-instructions = { version = "0.5" } +bitcoin-payment-instructions = { git = "https://github.com/chuksys/bitcoin-payment-instructions", branch = "bump-ldk-deps" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } From ee6e2681b60148afcd7c581729390ab87a7f1332 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 11 Aug 2025 16:47:39 +0100 Subject: [PATCH 4/8] Add HRN resolver to Node and UnifiedPayment Introduce the hrn_resolver dependency into the main Node struct and plumb it through to the UnifiedPayment component. This is necessary to allow the node to resolve offers presented via a Human-Readable Name (HRN). --- src/builder.rs | 14 ++++++++++++-- src/lib.rs | 7 +++++-- src/payment/unified.rs | 37 ++++++++++++++++++++++++------------- src/types.rs | 5 ++++- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 13a7567b7..7d0192b78 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,6 +18,7 @@ use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Network}; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::{chainmonitor, BestBlock, Watch}; use lightning::io::Cursor; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; @@ -1479,6 +1480,8 @@ fn build_with_store_internal( })?; } + let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); + // Initialize the PeerManager let onion_messenger: Arc = if let Some(AsyncPaymentsRole::Server) = async_payments_role { @@ -1490,7 +1493,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&hrn_resolver), IgnoringMessageHandler {}, )) } else { @@ -1502,7 +1505,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&hrn_resolver), IgnoringMessageHandler {}, )) }; @@ -1634,6 +1637,12 @@ fn build_with_store_internal( Arc::clone(&keys_manager), )); + let peer_manager_clone = Arc::clone(&peer_manager); + + hrn_resolver.register_post_queue_action(Box::new(move || { + peer_manager_clone.process_events(); + })); + liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::clone(&peer_manager))); gossip_source.set_gossip_verifier( @@ -1741,6 +1750,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, + hrn_resolver, }) } diff --git a/src/lib.rs b/src/lib.rs index b96325818..bcfd155cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,8 +158,8 @@ use peer_store::{PeerInfo, PeerStore}; use rand::Rng; use runtime::Runtime; use types::{ - Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, Graph, KeysManager, - OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, Graph, HRNResolver, + KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ ChannelDetails, CustomTlvRecord, DynStore, PeerDetails, SyncAndAsyncKVStore, UserChannelId, @@ -208,6 +208,7 @@ pub struct Node { node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, + hrn_resolver: Arc, } impl Node { @@ -961,6 +962,7 @@ impl Node { self.bolt12_payment().into(), Arc::clone(&self.config), Arc::clone(&self.logger), + Arc::clone(&self.hrn_resolver), ) } @@ -981,6 +983,7 @@ impl Node { self.bolt12_payment(), Arc::clone(&self.config), Arc::clone(&self.logger), + Arc::clone(&self.hrn_resolver), )) } diff --git a/src/payment/unified.rs b/src/payment/unified.rs index d76572cbe..e3d0cfc06 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -5,29 +5,33 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -//! Holds a payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], and [BOLT 12] payment +//! Holds a payment handler allowing to create [BIP 21] URIs with on-chain, [BOLT 11], and [BOLT 12] payment //! options. //! +//! Also allows to send payments using these URIs as well as [BIP 353] HRNs. +//! //! [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +//! [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md +use crate::error::Error; +use crate::ffi::maybe_wrap; +use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::types::HRNResolver; +use crate::Config; use std::sync::Arc; use std::vec::IntoIter; -use bip21::de::ParamKind; -use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; -use bitcoin::address::{NetworkChecked, NetworkUnchecked}; -use bitcoin::{Amount, Txid}; use lightning::ln::channelmanager::PaymentId; use lightning::offers::offer::Offer; use lightning::routing::router::RouteParametersConfig; 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::Config; +use bip21::de::ParamKind; +use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; +use bitcoin::address::{NetworkChecked, NetworkUnchecked}; +use bitcoin::{Amount, Txid}; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -40,26 +44,31 @@ struct Extras { /// A payment handler allowing to create [BIP 21] URIs with an on-chain, [BOLT 11], and [BOLT 12] payment /// option. /// -/// Should be retrieved by calling [`Node::unified_qr_payment`] +/// Should be retrieved by calling [`Node::unified_payment`] +/// +/// This handler allows you to send payments to these URIs as well as [BIP 353] HRNs. /// /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki +/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -/// [`Node::unified_qr_payment`]: crate::Node::unified_qr_payment +/// [`Node::unified_payment`]: crate::Node::unified_payment pub struct UnifiedPayment { onchain_payment: Arc, bolt11_invoice: Arc, bolt12_payment: Arc, config: Arc, logger: Arc, + hrn_resolver: Arc, } impl UnifiedPayment { pub(crate) fn new( onchain_payment: Arc, bolt11_invoice: Arc, bolt12_payment: Arc, config: Arc, logger: Arc, + hrn_resolver: Arc, ) -> Self { - Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger } + Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver } } /// Generates a URI with an on-chain address, [BOLT 11] invoice and [BOLT 12] offer. @@ -148,6 +157,8 @@ impl UnifiedPayment { let uri: bip21::Uri = uri_str.parse().map_err(|_| Error::InvalidUri)?; + let _resolver = &self.hrn_resolver; + let uri_network_checked = uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; diff --git a/src/types.rs b/src/types.rs index 38519eca7..8ad6e0f15 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,6 +10,7 @@ use std::sync::{Arc, Mutex}; use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; @@ -142,10 +143,12 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - IgnoringMessageHandler, + Arc, IgnoringMessageHandler, >; +pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; + pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, From 2605590934b6264360ecb8db2b2e2ffd87dcfa2b Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 11 Aug 2025 17:10:20 +0100 Subject: [PATCH 5/8] Refactor unified payment to support BIP 21 and HRNs Restructure the payment initiation logic in unified.rs to handle both BIP 21 URIs and BIP 353 Human-Readable Names (HRNs) as input sources. This provides a single, unified entry point for initiating payments. --- bindings/ldk_node.udl | 4 +- src/payment/unified.rs | 135 +++++++++++++++++++++++--------- tests/integration_tests_rust.rs | 35 +++++---- 3 files changed, 116 insertions(+), 58 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 445ad24d2..b36a5fcbd 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -278,8 +278,8 @@ interface FeeRate { interface UnifiedPayment { [Throws=NodeError] string receive(u64 amount_sats, [ByRef]string message, u32 expiry_sec); - [Throws=NodeError] - UnifiedPaymentResult send([ByRef]string uri_str, RouteParametersConfig? route_parameters); + [Throws=NodeError, Async] + UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat, RouteParametersConfig? route_parameters); }; interface LSPS1Liquidity { diff --git a/src/payment/unified.rs b/src/payment/unified.rs index e3d0cfc06..fa828485d 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -30,8 +30,11 @@ use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use bip21::de::ParamKind; use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; -use bitcoin::address::{NetworkChecked, NetworkUnchecked}; +use bitcoin::address::NetworkChecked; use bitcoin::{Amount, Txid}; +use bitcoin_payment_instructions::{ + amount::Amount as BPIAmount, PaymentInstructions, PaymentMethod, +}; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -138,63 +141,116 @@ impl UnifiedPayment { Ok(format_uri(uri)) } - /// Sends a payment given a [BIP 21] URI. + /// Sends a payment given a [BIP 21] URI or [BIP 353] HRN. /// /// This method parses the provided URI string and attempts to send the payment. If the URI /// has an offer and or invoice, it will try to pay the offer first followed by the invoice. /// If they both fail, the on-chain payment will be paid. /// - /// Returns a `QrPaymentResult` indicating the outcome of the payment. If an error + /// Returns a `UnifiedPaymentResult` indicating the outcome of the payment. If an error /// occurs, an `Error` is returned detailing the issue encountered. /// /// If `route_parameters` are provided they will override the default as well as the /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. /// /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki - pub fn send( - &self, uri_str: &str, route_parameters: Option, + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub async fn send( + &self, uri_str: &str, amount_msat: Option, + route_parameters: Option, ) -> Result { - let uri: bip21::Uri = - uri_str.parse().map_err(|_| Error::InvalidUri)?; - - let _resolver = &self.hrn_resolver; - - let uri_network_checked = - uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; + let instructions = PaymentInstructions::parse( + uri_str, + self.config.network, + self.hrn_resolver.as_ref(), + false, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Failed to parse payment instructions: {:?}", e); + Error::UriParameterParsingFailed + })?; + + let resolved = match instructions { + PaymentInstructions::ConfigurableAmount(instr) => { + let amount = amount_msat.ok_or_else(|| { + log_error!(self.logger, "No amount specified. Aborting the payment."); + Error::InvalidAmount + })?; + + let amt = BPIAmount::from_milli_sats(amount).map_err(|e| { + log_error!(self.logger, "Error while converting amount : {:?}", e); + Error::InvalidAmount + })?; + + instr.set_amount(amt, self.hrn_resolver.as_ref()).await.map_err(|e| { + log_error!(self.logger, "Failed to set amount: {:?}", e); + Error::InvalidAmount + })? + }, + PaymentInstructions::FixedAmount(instr) => { + if let Some(user_amount) = amount_msat { + if instr.max_amount().map_or(false, |amt| user_amount < amt.milli_sats()) { + log_error!(self.logger, "Amount specified is less than the amount in the parsed URI. Aborting the payment."); + return Err(Error::InvalidAmount); + } + } + instr + }, + }; - if let Some(offer) = uri_network_checked.extras.bolt12_offer { - let offer = maybe_wrap(offer); + if let Some(PaymentMethod::LightningBolt12(offer)) = + resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_))) + { + let offer = maybe_wrap(offer.clone()); + let payment_result = if let Some(amount_msat) = amount_msat { + self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) + } else { + self.bolt12_payment.send(&offer, None, None, route_parameters) + } + .map_err(|e| { + log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); + e + }); - match self.bolt12_payment.send(&offer, None, None, route_parameters) { - Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt12 { payment_id }), - Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e), + if let Ok(payment_id) = payment_result { + return Ok(UnifiedPaymentResult::Bolt12 { payment_id }); } } - if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { - let invoice = maybe_wrap(invoice); - - match self.bolt11_invoice.send(&invoice, route_parameters) { - Ok(payment_id) => return Ok(UnifiedPaymentResult::Bolt11 { payment_id }), - Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e), + if let Some(PaymentMethod::LightningBolt11(invoice)) = + resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt11(_))) + { + let invoice = maybe_wrap(invoice.clone()); + let payment_result = self.bolt11_invoice.send(&invoice, route_parameters) + .map_err(|e| { + log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e); + e + }); + + if let Ok(payment_id) = payment_result { + return Ok(UnifiedPaymentResult::Bolt11 { payment_id }); } } - let amount = match uri_network_checked.amount { - Some(amount) => amount, - None => { - log_error!(self.logger, "No amount specified in the URI. Aborting the payment."); - return Err(Error::InvalidAmount); - }, - }; - - let txid = self.onchain_payment.send_to_address( - &uri_network_checked.address, - amount.to_sat(), - None, - )?; - - Ok(UnifiedPaymentResult::Onchain { txid }) + if let Some(PaymentMethod::OnChain(address)) = + resolved.methods().iter().find(|m| matches!(m, PaymentMethod::OnChain(_))) + { + let amount = resolved.onchain_payment_amount().ok_or_else(|| { + log_error!(self.logger, "No amount specified. Aborting the payment."); + Error::InvalidAmount + })?; + + let amt_sats = amount.sats().map_err(|_| { + log_error!(self.logger, "Amount in sats returned an error. Aborting the payment."); + Error::InvalidAmount + })?; + + let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?; + return Ok(UnifiedPaymentResult::Onchain { txid }); + } + log_error!(self.logger, "Payable methods not found in URI"); + Err(Error::PaymentSendingFailed) } } @@ -321,7 +377,8 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { - use super::{Amount, Bolt11Invoice, Extras, Offer}; + use super::*; + use crate::payment::unified::Extras; use bitcoin::{address::NetworkUnchecked, Address, Network}; use std::str::FromStr; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 48561bf37..3a1ed91ac 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1608,28 +1608,29 @@ async fn unified_qr_send_receive() { let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec); let uri_str = uni_payment.clone().unwrap(); - let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None) { - Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { - println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id - }, - Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { - panic!("Expected Bolt12 payment but got Bolt11"); - }, - Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt12 payment but get On-chain transaction"); - }, - Err(e) => { - panic!("Expected Bolt12 payment but got error: {:?}", e); - }, - }; + let offer_payment_id: PaymentId = + match node_a.unified_payment().send(&uri_str, None, None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { + println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected Bolt12 payment but got Bolt11"); + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but get On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; expect_payment_successful_event!(node_a, Some(offer_payment_id), None); // Cut off the BOLT12 part to fallback to BOLT11. let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); let invoice_payment_id: PaymentId = - match node_a.unified_payment().send(uri_str_without_offer, None) { + match node_a.unified_payment().send(uri_str_without_offer, None, None).await { Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected Bolt11 payment but got Bolt12"); }, @@ -1652,7 +1653,7 @@ async fn unified_qr_send_receive() { // Cut off any lightning part to fallback to on-chain only. let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap(); - let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None) { + let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None, None).await { Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected on-chain payment but got Bolt12") }, From c9b7af5e5d2f8524083c580b9cc9215e78a3b9d6 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 11 Aug 2025 17:19:05 +0100 Subject: [PATCH 6/8] Fix typo and improve unified payment test name Correct a minor typo in the unified_payment send test. The test name is also updated to be more descriptive of the behavior being validated. --- tests/integration_tests_rust.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 3a1ed91ac..8e4f91d5d 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1568,7 +1568,7 @@ async fn generate_bip21_uri() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn unified_qr_send_receive() { +async fn unified_send_receive_qr_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); @@ -1618,7 +1618,7 @@ async fn unified_qr_send_receive() { panic!("Expected Bolt12 payment but got Bolt11"); }, Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt12 payment but get On-chain transaction"); + panic!("Expected Bolt12 payment but got On-chain transaction"); }, Err(e) => { panic!("Expected Bolt12 payment but got error: {:?}", e); From a9dad7d8e07005ef791cff37a0a30973573b85eb Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 21 Aug 2025 12:47:46 +0100 Subject: [PATCH 7/8] Use explicit imports in unified.rs tests Switch the module tests within unified.rs to use explicit use statements instead of glob imports (*). This improves code clarity by clearly indicating which items are being brought into scope. --- src/payment/unified.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/payment/unified.rs b/src/payment/unified.rs index fa828485d..6c4729ada 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -377,8 +377,7 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { - use super::*; - use crate::payment::unified::Extras; + use super::{Amount, Bolt11Invoice, Extras, Offer}; use bitcoin::{address::NetworkUnchecked, Address, Network}; use std::str::FromStr; From 4265a63bf61799e58f2df2e2a0585bb6e63daf85 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Tue, 23 Sep 2025 10:17:18 +0100 Subject: [PATCH 8/8] Adopt LDK's pay_for_offer_from_hrn method Adopt the upstream LDK change to use the dedicated pay_for_offer_from_hrn method when initiating a payment for an offer resolved via a Human-Readable Name (HRN). This ensures better alignment with LDK best practices. --- bindings/ldk_node.udl | 8 ++++ src/error.rs | 5 ++ src/ffi/types.rs | 68 +++++++++++++++++++++++++- src/payment/bolt12.rs | 44 ++++++++++++++++- src/payment/unified.rs | 105 +++++++++++++++++++++++------------------ 5 files changed, 180 insertions(+), 50 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b36a5fcbd..a9dc7fd19 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -346,6 +346,7 @@ enum NodeError { "LiquidityFeeTooHigh", "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", + "HrnParsingFailed", }; dictionary NodeStatus { @@ -807,6 +808,13 @@ interface Offer { PublicKey? issuer_signing_pubkey(); }; +interface HumanReadableName { + [Throws=NodeError, Name=from_encoded] + constructor([ByRef] string encoded); + string user(); + string domain(); +}; + [Traits=(Debug, Display, Eq)] interface Refund { [Throws=NodeError, Name=from_str] diff --git a/src/error.rs b/src/error.rs index 20b1cceab..c85a012e6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -127,6 +127,8 @@ pub enum Error { InvalidBlindedPaths, /// Asynchronous payment services are disabled. AsyncPaymentServicesDisabled, + /// Parsing a Human-Readable Name has failed. + HrnParsingFailed, } impl fmt::Display for Error { @@ -205,6 +207,9 @@ impl fmt::Display for Error { Self::AsyncPaymentServicesDisabled => { write!(f, "Asynchronous payment services are disabled.") }, + Self::HrnParsingFailed => { + write!(f, "Failed to parse a human-readable name.") + }, } } } diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 35aeb2530..e8c90ec17 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -57,7 +57,7 @@ pub use crate::payment::store::{ }; pub use crate::payment::UnifiedPaymentResult; -pub use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; +use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId}; @@ -271,6 +271,72 @@ impl std::fmt::Display for Offer { } } +/// A struct containing the two parts of a BIP 353 Human-Readable Name - the user and domain parts. +/// +/// The `user` and `domain` parts combined cannot exceed 231 bytes in length; +/// each DNS label within them must be non-empty and no longer than 63 bytes. +/// +/// If you intend to handle non-ASCII `user` or `domain` parts, you must handle [Homograph Attacks] +/// and do punycode en-/de-coding yourself. This struct will always handle only plain ASCII `user` +/// and `domain` parts. +/// +/// This struct can also be used for LN-Address recipients. +/// +/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack +pub struct HumanReadableName { + pub(crate) inner: LdkHumanReadableName, +} + +impl HumanReadableName { + /// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`. + /// + /// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by + /// BIP 353. + pub fn from_encoded(encoded: &str) -> Result { + let hrn = match LdkHumanReadableName::from_encoded(encoded) { + Ok(hrn) => Ok(hrn), + Err(_) => Err(Error::HrnParsingFailed), + }?; + + Ok(Self { inner: hrn }) + } + + /// Gets the `user` part of this Human-Readable Name + pub fn user(&self) -> String { + self.inner.user().to_string() + } + + /// Gets the `domain` part of this Human-Readable Name + pub fn domain(&self) -> String { + self.inner.domain().to_string() + } +} + +impl From for HumanReadableName { + fn from(ldk_hrn: LdkHumanReadableName) -> Self { + HumanReadableName { inner: ldk_hrn } + } +} + +impl From for LdkHumanReadableName { + fn from(wrapper: HumanReadableName) -> Self { + wrapper.inner + } +} + +impl Deref for HumanReadableName { + type Target = LdkHumanReadableName; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for HumanReadableName { + fn as_ref(&self) -> &LdkHumanReadableName { + self.deref() + } +} + /// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`]. /// /// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 0dd38edca..98f1d21ef 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -15,7 +15,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lightning::blinded_path::message::BlindedMessagePath; use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId, Retry}; -use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; +use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::routing::router::RouteParametersConfig; #[cfg(feature = "uniffi")] @@ -45,6 +45,11 @@ type Refund = lightning::offers::refund::Refund; #[cfg(feature = "uniffi")] type Refund = Arc; +#[cfg(not(feature = "uniffi"))] +type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName; +#[cfg(feature = "uniffi")] +type HumanReadableName = Arc; + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -193,6 +198,37 @@ impl Bolt12Payment { pub fn send_using_amount( &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, route_parameters: Option, + ) -> Result { + let payment_id = self.send_using_amount_inner( + offer, + amount_msat, + quantity, + payer_note, + route_parameters, + None, + )?; + Ok(payment_id) + } + + /// Internal helper to send a BOLT12 offer payment given an offer + /// and an amount in millisatoshi. + /// + /// This function contains the core payment logic and is called by + /// [`Self::send_using_amount`] and other internal logic that resolves + /// payment parameters (e.g. [`crate::UnifiedPayment::send`]). + /// + /// It wraps the core LDK `pay_for_offer` logic and handles necessary pre-checks, + /// payment ID generation, and payment details storage. + /// + /// The amount validation logic ensures the provided `amount_msat` is sufficient + /// based on the offer's required amount. + /// + /// If `hrn` is `Some`, the payment is initiated using [`ChannelManager::pay_for_offer_from_hrn`] + /// for offers resolved from a Human-Readable Name ([`HumanReadableName`]). + /// Otherwise, it falls back to the standard offer payment methods. + pub(crate) fn send_using_amount_inner( + &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, + route_parameters: Option, hrn: Option, ) -> Result { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); @@ -228,7 +264,11 @@ impl Bolt12Payment { retry_strategy, route_params_config: route_parameters, }; - let res = if let Some(quantity) = quantity { + let res = if let Some(hrn) = hrn { + let hrn = maybe_deref(&hrn); + let offer = OfferFromHrn { offer: offer.clone(), hrn: *hrn }; + self.channel_manager.pay_for_offer_from_hrn(&offer, amount_msat, payment_id, params) + } else if let Some(quantity) = quantity { self.channel_manager.pay_for_offer_with_quantity( &offer, Some(amount_msat), diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 6c4729ada..0e6ba738c 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -25,6 +25,7 @@ use std::vec::IntoIter; use lightning::ln::channelmanager::PaymentId; use lightning::offers::offer::Offer; +use lightning::onion_message::dns_resolution::HumanReadableName; use lightning::routing::router::RouteParametersConfig; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; @@ -199,56 +200,66 @@ impl UnifiedPayment { }, }; - if let Some(PaymentMethod::LightningBolt12(offer)) = - resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_))) - { - let offer = maybe_wrap(offer.clone()); - let payment_result = if let Some(amount_msat) = amount_msat { - self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) - } else { - self.bolt12_payment.send(&offer, None, None, route_parameters) - } - .map_err(|e| { - log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); - e - }); - - if let Ok(payment_id) = payment_result { - return Ok(UnifiedPaymentResult::Bolt12 { payment_id }); - } - } + let mut sorted_payment_methods = resolved.methods().to_vec(); + sorted_payment_methods.sort_by_key(|method| match method { + PaymentMethod::LightningBolt12(_) => 0, + PaymentMethod::LightningBolt11(_) => 1, + PaymentMethod::OnChain(_) => 2, + }); + + for method in sorted_payment_methods { + match method { + PaymentMethod::LightningBolt12(offer) => { + let offer = maybe_wrap(offer.clone()); + + let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) { + let hrn = maybe_wrap(hrn.clone()); + self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn)) + } else if let Some(amount_msat) = amount_msat { + self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) + } else { + self.bolt12_payment.send(&offer, None, None, route_parameters) + } + .map_err(|e| { + log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); + e + }); - if let Some(PaymentMethod::LightningBolt11(invoice)) = - resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt11(_))) - { - let invoice = maybe_wrap(invoice.clone()); - let payment_result = self.bolt11_invoice.send(&invoice, route_parameters) - .map_err(|e| { - log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e); - e - }); - - if let Ok(payment_id) = payment_result { - return Ok(UnifiedPaymentResult::Bolt11 { payment_id }); + if let Ok(payment_id) = payment_result { + return Ok(UnifiedPaymentResult::Bolt12 { payment_id }); + } + }, + PaymentMethod::LightningBolt11(invoice) => { + let invoice = maybe_wrap(invoice.clone()); + let payment_result = self.bolt11_invoice.send(&invoice, route_parameters) + .map_err(|e| { + log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e); + e + }); + + if let Ok(payment_id) = payment_result { + return Ok(UnifiedPaymentResult::Bolt11 { payment_id }); + } + }, + PaymentMethod::OnChain(address) => { + let amount = resolved.onchain_payment_amount().ok_or_else(|| { + log_error!(self.logger, "No amount specified. Aborting the payment."); + Error::InvalidAmount + })?; + + let amt_sats = amount.sats().map_err(|_| { + log_error!( + self.logger, + "Amount in sats returned an error. Aborting the payment." + ); + Error::InvalidAmount + })?; + + let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?; + return Ok(UnifiedPaymentResult::Onchain { txid }); + }, } } - - if let Some(PaymentMethod::OnChain(address)) = - resolved.methods().iter().find(|m| matches!(m, PaymentMethod::OnChain(_))) - { - let amount = resolved.onchain_payment_amount().ok_or_else(|| { - log_error!(self.logger, "No amount specified. Aborting the payment."); - Error::InvalidAmount - })?; - - let amt_sats = amount.sats().map_err(|_| { - log_error!(self.logger, "Amount in sats returned an error. Aborting the payment."); - Error::InvalidAmount - })?; - - let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?; - return Ok(UnifiedPaymentResult::Onchain { txid }); - } log_error!(self.logger, "Payable methods not found in URI"); Err(Error::PaymentSendingFailed) }