From 9ea92d5d462ffb73a23b33097ce995eb45943725 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sat, 2 Aug 2025 17:08:31 +0100 Subject: [PATCH 01/12] Rename unified_qr to unified This rename reflects that this module is a unified payment interface for both QR code payments and HRN payments passed in as a string without scanning a QR code --- src/payment/mod.rs | 4 ++-- src/payment/{unified_qr.rs => unified.rs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/payment/{unified_qr.rs => unified.rs} (99%) diff --git a/src/payment/mod.rs b/src/payment/mod.rs index b031e37fd..0a3d78b47 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -12,7 +12,7 @@ mod bolt12; mod onchain; mod spontaneous; pub(crate) mod store; -mod unified_qr; +mod unified; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; @@ -21,7 +21,7 @@ pub use spontaneous::SpontaneousPayment; pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; +pub use unified::{QrPaymentResult, UnifiedQrPayment}; /// Represents information used to send a payment. #[derive(Clone, Debug, PartialEq)] 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 af5ee1c7b..01454beb2 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified.rs @@ -304,7 +304,7 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { use super::*; - use crate::payment::unified_qr::Extras; + use crate::payment::unified::Extras; use bitcoin::{Address, Network}; use std::str::FromStr; From b11fead713854ccea72aa5fefe4cc25d884764f1 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sat, 2 Aug 2025 17:19:21 +0100 Subject: [PATCH 02/12] rename UnifiedQRPayment to UnifiedPayment, rename QRPaymentResult to UnifiedPaymentResult These renamings are necessary to reflect the expanded responsibilities for this module. --- bindings/ldk_node.udl | 8 ++--- src/ffi/types.rs | 2 +- src/lib.rs | 16 ++++++--- src/payment/mod.rs | 2 +- src/payment/unified.rs | 14 ++++---- tests/integration_tests_rust.rs | 60 ++++++++++++++++----------------- 6 files changed, 54 insertions(+), 48 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 076d7fc9b..e5e0d160b 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -127,7 +127,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); @@ -243,11 +243,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); + UnifiedPaymentResult send([ByRef]string uri_str); }; interface LSPS1Liquidity { @@ -418,7 +418,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 984e4da8f..91858d48f 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -20,7 +20,7 @@ pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; +pub use crate::payment::{MaxTotalRoutingFeeLimit, SendingParameters, UnifiedPaymentResult}; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; diff --git a/src/lib.rs b/src/lib.rs index da86fce73..39acec1d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,7 +139,7 @@ use io::utils::write_node_metrics; use liquidity::{LSPS1Liquidity, LiquiditySource}; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, - UnifiedQrPayment, + UnifiedPayment, }; use peer_store::{PeerInfo, PeerStore}; use runtime::Runtime; @@ -885,12 +885,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(), @@ -902,12 +905,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 0a3d78b47..0019120ce 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -21,7 +21,7 @@ pub use spontaneous::SpontaneousPayment; pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use unified::{QrPaymentResult, UnifiedQrPayment}; +pub use unified::{UnifiedPayment, UnifiedPaymentResult}; /// Represents information used to send a payment. #[derive(Clone, Debug, PartialEq)] diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 01454beb2..024373415 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, @@ -139,7 +139,7 @@ impl UnifiedQrPayment { /// occurs, an `Error` is returned detailing the issue encountered. /// /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki - pub fn send(&self, uri_str: &str) -> Result { + pub fn send(&self, uri_str: &str) -> Result { let uri: bip21::Uri = uri_str.parse().map_err(|_| Error::InvalidUri)?; @@ -149,7 +149,7 @@ 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) { - 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), } } @@ -157,7 +157,7 @@ impl UnifiedQrPayment { if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { let invoice = maybe_wrap(invoice); match self.bolt11_invoice.send(&invoice, None) { - 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), } } @@ -176,7 +176,7 @@ impl UnifiedQrPayment { None, )?; - Ok(QrPaymentResult::Onchain { txid }) + Ok(UnifiedPaymentResult::Onchain { txid }) } } @@ -189,7 +189,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. diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 0932116ef..eab91f09b 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -22,7 +22,7 @@ use ldk_node::config::EsploraSyncConfig; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, - QrPaymentResult, SendingParameters, + SendingParameters, UnifiedPaymentResult, }; use ldk_node::{Builder, Event, NodeError}; @@ -1235,15 +1235,15 @@ 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, @@ -1263,15 +1263,15 @@ 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=")); } #[test] @@ -1312,17 +1312,17 @@ 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) { - 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) { + 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) => { @@ -1335,15 +1335,15 @@ 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) { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + match node_a.unified_payment().send(uri_str_without_offer) { + 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) => { @@ -1353,19 +1353,19 @@ 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) { - 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) { + 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 998bdabf31f5f6cfe99cdbefc449faab6e76159b Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sat, 2 Aug 2025 17:42:22 +0100 Subject: [PATCH 03/12] add bitcoin-payment-instructions to cargo.toml --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 96a9eea53..dabe6888f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ log = { version = "0.4.22", default-features = false, features = ["std"]} vss-client = "0.3" prost = { version = "0.11.6", default-features = false} +bitcoin-payment-instructions = { version = "0.5" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } From 55522b9e5d02e869e1d517c677131a7f4862a19e Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 11 Aug 2025 16:47:39 +0100 Subject: [PATCH 04/12] Add hrn_resolver to Node and pass into UnifiedPayment This commit adds a HRN Resolver to the Node struct which will be useful for resolving HRNs when making BIP 353 payments. It also passes the HRN Resolver into UnifiedPayment. --- src/builder.rs | 13 ++++++++++++- src/lib.rs | 6 +++++- src/payment/unified.rs | 19 +++++++++++++++---- src/types.rs | 6 +++++- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index e160d1f6e..75ce76e37 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -80,6 +80,8 @@ use std::sync::{Arc, Mutex, Once, RwLock}; use std::time::SystemTime; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; + const VSS_HARDENED_CHILD_INDEX: u32 = 877; const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; const LSPS_HARDENED_CHILD_INDEX: u32 = 577; @@ -1454,6 +1456,8 @@ fn build_with_store_internal( })?; } + let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); + // Initialize the PeerManager let onion_messenger: Arc = Arc::new(OnionMessenger::new( Arc::clone(&keys_manager), @@ -1463,7 +1467,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), IgnoringMessageHandler {}, - IgnoringMessageHandler {}, + Arc::clone(&hrn_resolver), IgnoringMessageHandler {}, )); let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); @@ -1589,6 +1593,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( @@ -1696,6 +1706,7 @@ fn build_with_store_internal( is_running, is_listening, node_metrics, + hrn_resolver, }) } diff --git a/src/lib.rs b/src/lib.rs index 39acec1d4..43693b569 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,8 @@ use peer_store::{PeerInfo, PeerStore}; use runtime::Runtime; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, + HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, + Wallet, }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; @@ -205,6 +206,7 @@ pub struct Node { is_running: Arc>, is_listening: Arc, node_metrics: Arc>, + hrn_resolver: Arc, } impl Node { @@ -899,6 +901,7 @@ impl Node { self.bolt12_payment().into(), Arc::clone(&self.config), Arc::clone(&self.logger), + Arc::clone(&self.hrn_resolver), ) } @@ -919,6 +922,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 024373415..88ed6286f 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -5,16 +5,20 @@ // 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 lightning::ln::channelmanager::PaymentId; @@ -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. @@ -143,6 +152,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 3103ead3f..f75883a7b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -36,6 +36,8 @@ use lightning_net_tokio::SocketDescriptor; use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; + use std::sync::{Arc, Mutex}; pub(crate) type DynStore = dyn KVStore + Sync + Send; @@ -117,10 +119,12 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, IgnoringMessageHandler, - IgnoringMessageHandler, + Arc, IgnoringMessageHandler, >; +pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; + pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, From f1d1fa17c5b62b0dedce47bc92a015587f634fb6 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 11 Aug 2025 17:10:20 +0100 Subject: [PATCH 05/12] Refactor unified.rs to support sending to BIP 21 URIs as well as BIP 353 HRNs --- bindings/ldk_node.udl | 4 +- src/payment/unified.rs | 131 +++++++++++++++++++++++--------- tests/integration_tests_rust.rs | 10 +-- 3 files changed, 102 insertions(+), 43 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e5e0d160b..494e6a87b 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -246,8 +246,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); + [Throws=NodeError, Async] + UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat); }; interface LSPS1Liquidity { diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 88ed6286f..05814e53c 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -27,8 +27,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, +}; use std::sync::Arc; use std::vec::IntoIter; @@ -138,56 +141,112 @@ 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. /// /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki - pub fn send(&self, uri_str: &str) -> Result { - let uri: bip21::Uri = - uri_str.parse().map_err(|_| Error::InvalidUri)?; - - let _resolver = &self.hrn_resolver; + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub async fn send( + &self, uri_str: &str, amount_msat: Option, + ) -> Result { + 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 + }, + }; - let uri_network_checked = - uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; + 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) + } else { + self.bolt12_payment.send(&offer, None, None) + } + .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(offer) = uri_network_checked.extras.bolt12_offer { - let offer = maybe_wrap(offer); - match self.bolt12_payment.send(&offer, None, None) { - 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, None) { - 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, None) + .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) } } @@ -316,7 +375,7 @@ impl DeserializationError for Extras { mod tests { use super::*; use crate::payment::unified::Extras; - use bitcoin::{Address, Network}; + 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 eab91f09b..20090a68d 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1274,8 +1274,8 @@ fn generate_bip21_uri() { assert!(uni_payment.contains("lno=")); } -#[test] -fn unified_qr_send_receive() { +#[tokio::test(flavor = "multi_thread")] +async fn unified_qr_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); @@ -1314,7 +1314,7 @@ 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) { + let offer_payment_id: PaymentId = match node_a.unified_payment().send(&uri_str, None).await { Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); payment_id @@ -1335,7 +1335,7 @@ 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_payment().send(uri_str_without_offer) { + match node_a.unified_payment().send(uri_str_without_offer, None).await { Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected Bolt11 payment but got Bolt12"); }, @@ -1358,7 +1358,7 @@ 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) { + let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None).await { Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { panic!("Expected on-chain payment but got Bolt12") }, From 869839a6451bd0f465593fc6036de19a812be41c Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 11 Aug 2025 17:19:05 +0100 Subject: [PATCH 06/12] Fix typo in unified_payment send test and improve test name. --- 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 20090a68d..587ed446b 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1275,7 +1275,7 @@ fn generate_bip21_uri() { } #[tokio::test(flavor = "multi_thread")] -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); @@ -1323,7 +1323,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 38e17d1df11db46e94eda23882a71b1301d11f80 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 21 Aug 2025 12:47:46 +0100 Subject: [PATCH 07/12] Switch to explicit imports in unified.rs mod tests --- 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 05814e53c..6bf39a6ca 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -373,8 +373,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 da52af8cc252b76aed6aedc9c2f3571697767e49 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Wed, 3 Sep 2025 17:44:39 +0100 Subject: [PATCH 08/12] Add lightning-dns-resolver to Cargo.toml --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index dabe6888f..5c0b2a94d 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ log = { version = "0.4.22", default-features = false, features = ["std"]} vss-client = "0.3" prost = { version = "0.11.6", default-features = false} bitcoin-payment-instructions = { version = "0.5" } +lightning-dns-resolver = "0.2.0" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } From 7973b8c5aa04334cbf54e5fa3b996e1ec5e2e408 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Wed, 3 Sep 2025 17:51:28 +0100 Subject: [PATCH 09/12] Enable setting Node as HRN Resolver for 3rd parties in Config --- bindings/ldk_node.udl | 1 + src/config.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 494e6a87b..8cbf7b7f2 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,7 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; SendingParameters? sending_parameters; + boolean is_hrn_resolver; }; dictionary AnchorChannelsConfig { diff --git a/src/config.rs b/src/config.rs index 02df8bbc7..4378c98fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -115,6 +115,7 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64; /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | /// | `sending_parameters` | None | +/// | `is_hrn_resolver` | false | /// /// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their /// respective default values. @@ -179,6 +180,8 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub sending_parameters: Option, + /// This allows us to use our node as a DNS resolver for 3rd party HRN resolutions. + pub is_hrn_resolver: bool, } impl Default for Config { @@ -193,6 +196,7 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), sending_parameters: None, node_alias: None, + is_hrn_resolver: false, } } } From 6068c1c0a71fd7486af69a0617360a0eff57ac33 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:10:51 +0100 Subject: [PATCH 10/12] Pass HRNResolver or DomainResolver into OnionMessenger This commit allows us to enable our node to either be able to send HRN resolution requests or acts as a resolver for 3rd party nodes We also pass HRNResolver as an optional parameter to Node --- bindings/ldk_node.udl | 2 ++ src/builder.rs | 50 +++++++++++++++++++++++++++++++++++------- src/error.rs | 5 +++++ src/lib.rs | 6 ++--- src/payment/unified.rs | 32 ++++++++++++++++----------- src/types.rs | 6 ++++- 6 files changed, 76 insertions(+), 25 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 8cbf7b7f2..7fcf7a40e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -312,6 +312,7 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "HrnResolverNotConfigured", }; dictionary NodeStatus { @@ -348,6 +349,7 @@ enum BuildError { "WalletSetupFailed", "LoggerSetupFailed", "NetworkMismatch", + "DNSResolverSetupFailed", }; [Trait] diff --git a/src/builder.rs b/src/builder.rs index 75ce76e37..848ecefdc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -31,8 +31,8 @@ use crate::peer_store::PeerStore; use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, - OnionMessenger, PaymentStore, PeerManager, + ChainMonitor, ChannelManager, DomainResolver, DynStore, GossipSync, Graph, HRNResolver, + KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -43,6 +43,7 @@ use lightning::io::Cursor; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ @@ -59,6 +60,8 @@ use lightning::util::sweep::OutputSweeper; use lightning_persister::fs_store::FilesystemStore; +use lightning_dns_resolver::OMDomainResolver; + use bdk_wallet::template::Bip84; use bdk_wallet::KeychainKind; use bdk_wallet::Wallet as BdkWallet; @@ -193,6 +196,8 @@ pub enum BuildError { LoggerSetupFailed, /// The given network does not match the node's previously configured network. NetworkMismatch, + /// An attempt to setup a DNS Resolver failed. + DNSResolverSetupFailed, } impl fmt::Display for BuildError { @@ -221,12 +226,20 @@ impl fmt::Display for BuildError { Self::NetworkMismatch => { write!(f, "Given network does not match the node's previously configured network.") }, + Self::DNSResolverSetupFailed => { + write!(f, "An attempt to setup a DNS resolver has failed.") + }, } } } impl std::error::Error for BuildError {} +enum Resolver { + HRN(Arc), + DNS(Arc), +} + /// A builder for an [`Node`] instance, allowing to set some configuration and module choices from /// the getgo. /// @@ -1456,7 +1469,22 @@ fn build_with_store_internal( })?; } - let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); + let resolver = if config.is_hrn_resolver { + Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs( + "8.8.8.8:53".parse().map_err(|_| BuildError::DNSResolverSetupFailed)?, + ))) + } else { + Resolver::HRN(Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph)))) + }; + + let om_resolver = match resolver { + Resolver::DNS(ref dns_resolver) => { + Arc::clone(dns_resolver) as Arc + }, + Resolver::HRN(ref hrn_resolver) => { + Arc::clone(hrn_resolver) as Arc + }, + }; // Initialize the PeerManager let onion_messenger: Arc = Arc::new(OnionMessenger::new( @@ -1467,7 +1495,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), IgnoringMessageHandler {}, - Arc::clone(&hrn_resolver), + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )); let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); @@ -1595,9 +1623,15 @@ fn build_with_store_internal( let peer_manager_clone = Arc::clone(&peer_manager); - hrn_resolver.register_post_queue_action(Box::new(move || { - peer_manager_clone.process_events(); - })); + let hrn_resolver = match resolver { + Resolver::DNS(_) => None, + Resolver::HRN(ref hrn_resolver) => { + hrn_resolver.register_post_queue_action(Box::new(move || { + peer_manager_clone.process_events(); + })); + Some(hrn_resolver) + }, + }; liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::clone(&peer_manager))); @@ -1706,7 +1740,7 @@ fn build_with_store_internal( is_running, is_listening, node_metrics, - hrn_resolver, + hrn_resolver: hrn_resolver.cloned(), }) } diff --git a/src/error.rs b/src/error.rs index 2cb71186d..d0d1fa52c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -120,6 +120,8 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// A HRN resolver was not configured + HrnResolverNotConfigured, } impl fmt::Display for Error { @@ -193,6 +195,9 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::HrnResolverNotConfigured => { + write!(f, "A HRN resolver was not configured.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 43693b569..b130f3d57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -206,7 +206,7 @@ pub struct Node { is_running: Arc>, is_listening: Arc, node_metrics: Arc>, - hrn_resolver: Arc, + hrn_resolver: Option>, } impl Node { @@ -901,7 +901,7 @@ impl Node { self.bolt12_payment().into(), Arc::clone(&self.config), Arc::clone(&self.logger), - Arc::clone(&self.hrn_resolver), + Arc::new(self.hrn_resolver.clone()), ) } @@ -922,7 +922,7 @@ impl Node { self.bolt12_payment(), Arc::clone(&self.config), Arc::clone(&self.logger), - Arc::clone(&self.hrn_resolver), + Arc::new(self.hrn_resolver.clone()), )) } diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 6bf39a6ca..24e271140 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -62,14 +62,14 @@ pub struct UnifiedPayment { bolt12_payment: Arc, config: Arc, logger: Arc, - hrn_resolver: 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, + hrn_resolver: Arc>>, ) -> Self { Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver } } @@ -155,18 +155,24 @@ impl UnifiedPayment { pub async fn send( &self, uri_str: &str, amount_msat: Option, ) -> Result { - 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 resolver = self.hrn_resolver.as_ref().clone().ok_or_else(|| { + log_error!(self.logger, "No HRN resolver configured. Cannot resolve HRNs."); + Error::HrnResolverNotConfigured })?; + println!("Parsing instructions..."); + + let instructions = + PaymentInstructions::parse(uri_str, self.config.network, resolver.as_ref(), false) + .await + .map_err(|e| { + log_error!(self.logger, "Failed to parse payment instructions: {:?}", e); + println!("Failed to parse payment instructions: {:?}", e); + Error::UriParameterParsingFailed + })?; + + println!("Sending..."); + let resolved = match instructions { PaymentInstructions::ConfigurableAmount(instr) => { let amount = amount_msat.ok_or_else(|| { @@ -179,7 +185,7 @@ impl UnifiedPayment { Error::InvalidAmount })?; - instr.set_amount(amt, self.hrn_resolver.as_ref()).await.map_err(|e| { + instr.set_amount(amt, resolver.as_ref()).await.map_err(|e| { log_error!(self.logger, "Failed to set amount: {:?}", e); Error::InvalidAmount })? diff --git a/src/types.rs b/src/types.rs index f75883a7b..99c99a1e1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -21,6 +21,7 @@ use lightning::ln::msgs::RoutingMessageHandler; use lightning::ln::msgs::SocketAddress; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}; @@ -33,6 +34,8 @@ use lightning_block_sync::gossip::{GossipVerifier, UtxoSource}; use lightning_net_tokio::SocketDescriptor; +use lightning_dns_resolver::OMDomainResolver; + use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; @@ -119,11 +122,12 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, IgnoringMessageHandler, - Arc, + Arc, IgnoringMessageHandler, >; pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; +pub(crate) type DomainResolver = OMDomainResolver; pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, From 2149475cb8b9bbb8dd1ad3677ab734e19339f604 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:17:49 +0100 Subject: [PATCH 11/12] Testing Node Ann Broadcast when node is_hrn_resolver This commit will be dropped later --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b130f3d57..fa69277cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -468,6 +468,8 @@ impl Node { if let Some(node_alias) = node_alias.as_ref() { bcast_pm.broadcast_node_announcement([0; 3], node_alias.0, addresses); + println!("broadcasted announncement"); + let unix_time_secs_opt = SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); { From 0517dbc89505c80c21c8b1d8e756145107722030 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:22:27 +0100 Subject: [PATCH 12/12] Add end-to-end test for HRN resolution This commit adds an end-to-end test that asserts that HRNs are properly parsed and resolved, and sending to the corresponding offer works --- tests/common/mod.rs | 5 +- tests/integration_tests_rust.rs | 94 +++++++++++++++++++++++++++------ 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 780e9bbf4..87e140eab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -284,7 +284,7 @@ pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, + anchors_trusted_no_reserve: bool, second_node_is_hrn_resolver: bool, ) -> (TestNode, TestNode) { println!("== Node A =="); let config_a = random_config(anchor_channels); @@ -292,6 +292,9 @@ pub(crate) fn setup_two_nodes( println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); + if second_node_is_hrn_resolver { + config_b.node_config.is_hrn_resolver = true; + } if allow_0conf { config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 587ed446b..52fa43d79 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -47,7 +47,7 @@ use std::sync::Arc; fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } @@ -55,7 +55,7 @@ fn channel_full_cycle() { fn channel_full_cycle_electrum() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Electrum(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } @@ -63,7 +63,7 @@ fn channel_full_cycle_electrum() { fn channel_full_cycle_bitcoind_rpc_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } @@ -71,7 +71,7 @@ fn channel_full_cycle_bitcoind_rpc_sync() { fn channel_full_cycle_bitcoind_rest_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRestSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } @@ -79,7 +79,7 @@ fn channel_full_cycle_bitcoind_rest_sync() { fn channel_full_cycle_force_close() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true); } @@ -87,7 +87,7 @@ fn channel_full_cycle_force_close() { fn channel_full_cycle_force_close_trusted_no_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true); } @@ -95,7 +95,7 @@ fn channel_full_cycle_force_close_trusted_no_reserve() { fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) } @@ -103,7 +103,7 @@ fn channel_full_cycle_0conf() { fn channel_full_cycle_legacy_staticremotekey() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false); } @@ -111,7 +111,7 @@ fn channel_full_cycle_legacy_staticremotekey() { fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -312,7 +312,7 @@ fn start_stop_reinit() { fn onchain_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -512,7 +512,7 @@ fn onchain_send_receive() { fn onchain_send_all_retains_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); // Setup nodes let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -829,7 +829,7 @@ fn connection_restart_behavior() { fn do_connection_restart_behavior(persist: bool) { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -881,7 +881,7 @@ fn do_connection_restart_behavior(persist: bool) { fn concurrent_connections_succeed() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -912,7 +912,7 @@ fn concurrent_connections_succeed() { fn simple_bolt12_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; @@ -1225,7 +1225,7 @@ fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -1279,7 +1279,7 @@ async fn unified_send_receive_qr_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -1384,6 +1384,66 @@ async fn unified_send_receive_qr_uri() { assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } +#[tokio::test(flavor = "multi_thread")] +async fn unified_send_to_hrn() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, true); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premined_sats = 5_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premined_sats), + ); + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcast a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + println!("node_b has broadcasted node announcement!"); + + //let offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap(); + //println!("offer! {:?}", offer.to_string()); + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); + + let hrn = "chuks@peepswire.com"; + let offer_payment_id: PaymentId = match node_a.unified_payment().send(&hrn, 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 got On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; + + expect_payment_successful_event!(node_a, Some(offer_payment_id), None); +} + #[test] fn lsps2_client_service_integration() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -1627,7 +1687,7 @@ fn facade_logging() { fn spontaneous_send_with_custom_preimage() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_sat = 1_000_000;