From dd725bc5866c1d9399a829455aa7a409ca239646 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 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} | 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 fc2eca150..b7e82d2b8 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified.rs @@ -302,12 +302,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 02ed4791aaec92125971c72d8831822f21d8c251 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 QRPaymentResult to UnifiedPaymentResult These renamings are necessary to reflect the expanded responsibilities for this module. --- bindings/ldk_node.udl | 8 ++-- src/ffi/types.rs | 5 +- src/lib.rs | 40 +++++++++------- src/payment/mod.rs | 2 +- src/payment/unified.rs | 19 ++++---- tests/integration_tests_rust.rs | 85 ++++++++++++++++----------------- 6 files changed, 83 insertions(+), 76 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ab2f483a1..89c69eec9 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -130,7 +130,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); @@ -252,11 +252,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 { @@ -431,7 +431,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 3c88a665f..7a6a39fad 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -54,7 +54,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 6a26c6c5b..1b10cab6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,25 +131,13 @@ use gossip::GossipSource; use graph::NetworkGraph; pub use io::utils::generate_entropy_mnemonic; use io::utils::write_node_metrics; -use lightning::chain::BestBlock; -use lightning::events::bump_transaction::Wallet as LdkWallet; -use lightning::impl_writeable_tlv_based; -use lightning::ln::channel_state::ChannelShutdownState; -use lightning::ln::channelmanager::PaymentId; -use lightning::ln::msgs::SocketAddress; -use lightning::routing::gossip::NodeAlias; -use lightning::util::persist::KVStoreSync; -use lightning_background_processor::process_events_async; use liquidity::{LSPS1Liquidity, LiquiditySource}; -use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; -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; use runtime::Runtime; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, Graph, KeysManager, @@ -159,6 +147,18 @@ pub use types::{ ChannelDetails, CustomTlvRecord, DynStore, PeerDetails, SyncAndAsyncKVStore, UserChannelId, }; +use lightning::chain::BestBlock; +use lightning::events::bump_transaction::Wallet as LdkWallet; +use lightning::impl_writeable_tlv_based; +use lightning::ln::channel_state::ChannelShutdownState; +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::SocketAddress; +use lightning::routing::gossip::NodeAlias; +use lightning::util::persist::KVStoreSync; +use lightning_background_processor::process_events_async; +use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use payment::asynchronous::om_mailbox::OnionMessageMailbox; +use rand::Rng; pub use { bip39, bitcoin, lightning, lightning_invoice, lightning_liquidity, lightning_types, tokio, vss_client, @@ -927,12 +927,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(), @@ -944,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(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 b7e82d2b8..1c5c21d5d 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -45,7 +45,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, @@ -53,7 +53,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, @@ -138,7 +138,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)?; @@ -148,7 +148,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), } } @@ -156,7 +156,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), } } @@ -175,7 +175,7 @@ impl UnifiedQrPayment { None, )?; - Ok(QrPaymentResult::Onchain { txid }) + Ok(UnifiedPaymentResult::Onchain { txid }) } } @@ -188,7 +188,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. @@ -302,9 +302,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 e2d4207cd..02c2575ec 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -6,7 +6,7 @@ // accordance with one or both of these licenses. mod common; - +use lightning::util::persist::KVStoreSync; use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; @@ -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; @@ -1392,15 +1392,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, @@ -1421,15 +1421,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)] @@ -1471,17 +1471,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) { - 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) => { @@ -1493,38 +1493,37 @@ 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) { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { - panic!("Expected Bolt11 payment but got Bolt12"); - }, - Ok(QrPaymentResult::Bolt11 { payment_id }) => { - println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id - }, - Ok(QrPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt11 payment but got on-chain transaction"); - }, - Err(e) => { - panic!("Expected Bolt11 payment but got error: {:?}", e); - }, - }; + let invoice_payment_id: PaymentId = match node_a.unified_payment().send(uri_str_without_offer) { + Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { + panic!("Expected Bolt11 payment but got Bolt12"); + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => { + println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt11 payment but got on-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt11 payment but got error: {:?}", e); + }, + }; 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 7e6e7e77ff79c92caa735eed31f696cfa0aa1800 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.toml --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 51b0329c4..5a9b7bd92 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,8 @@ 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" } +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 6d20a9e2606edf96dae2adbf71db1db2691b1500 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 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. --- Cargo.toml | 2 +- src/builder.rs | 14 ++++++++++++-- src/lib.rs | 7 +++++-- src/payment/unified.rs | 37 ++++++++++++++++++++++++------------- src/types.rs | 6 +++++- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5a9b7bd92..691933964 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,4 +149,4 @@ check-cfg = [ "cfg(tokio_unstable)", "cfg(cln_test)", "cfg(lnd_test)", -] +] \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index c0e39af7a..95cfb28fd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -19,6 +19,7 @@ use bip39::Mnemonic; 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}; @@ -1536,6 +1537,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 { @@ -1547,7 +1550,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&hrn_resolver), IgnoringMessageHandler {}, )) } else { @@ -1559,7 +1562,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&hrn_resolver), IgnoringMessageHandler {}, )) }; @@ -1691,6 +1694,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( @@ -1797,6 +1806,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 1b10cab6b..72308c2d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,8 +140,8 @@ use payment::{ use peer_store::{PeerInfo, PeerStore}; 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, @@ -200,6 +200,7 @@ pub struct Node { node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, + hrn_resolver: Arc, } impl Node { @@ -941,6 +942,7 @@ impl Node { self.bolt12_payment().into(), Arc::clone(&self.config), Arc::clone(&self.logger), + Arc::clone(&self.hrn_resolver), ) } @@ -961,6 +963,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 1c5c21d5d..dbc32d421 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -5,28 +5,32 @@ // 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_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>; @@ -39,26 +43,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. @@ -142,6 +151,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 800d9462d..edfc194e8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -36,6 +36,8 @@ use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::PaymentDetails; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; + /// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the /// same time. pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {} @@ -142,10 +144,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 2c809b12bdccf5e96b066029396a5545dfa9f475 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.rs to support sending to BIP 21 URIs as well as BIP 353 HRNs --- bindings/ldk_node.udl | 4 +- src/payment/unified.rs | 132 +++++++++++++++++++++++--------- tests/integration_tests_rust.rs | 35 +++++---- 3 files changed, 116 insertions(+), 55 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 89c69eec9..9d5da68d4 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -255,8 +255,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 dbc32d421..f0127b2d5 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -29,8 +29,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>; @@ -137,56 +140,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) } } @@ -313,7 +372,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 02c2575ec..ebe963b04 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1473,7 +1473,7 @@ 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) { + 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 @@ -1493,21 +1493,22 @@ 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_payment().send(uri_str_without_offer) { - Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { - panic!("Expected Bolt11 payment but got Bolt12"); - }, - Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => { - println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id - }, - Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt11 payment but got on-chain transaction"); - }, - Err(e) => { - panic!("Expected Bolt11 payment but got error: {:?}", e); - }, - }; + let invoice_payment_id: PaymentId = + match node_a.unified_payment().send(uri_str_without_offer, None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { + panic!("Expected Bolt11 payment but got Bolt12"); + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => { + println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt11 payment but got on-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt11 payment but got error: {:?}", e); + }, + }; expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); let expect_onchain_amount_sats = 800_000; @@ -1516,7 +1517,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) { + 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 869626940b7ab535cfb4fd9bbd78766b5f03f1e5 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 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 ebe963b04..2b505362d 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1433,7 +1433,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); @@ -1482,7 +1482,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 9b7584bd86070dc24b184396681e6b885ecec1a1 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 21 Aug 2025 12:47:46 +0100 Subject: [PATCH 7/8] 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 f0127b2d5..df26cbe12 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -372,8 +372,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 3e303472cf526abf91cfb36f757f3197587153a7 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Tue, 23 Sep 2025 10:17:18 +0100 Subject: [PATCH 8/8] Use the pay_for_offer_from_hrn method from LDK upstream This commit ensures that when using the unified API to send to a HRN, we use pay_for_offer_from_hrn --- bindings/ldk_node.udl | 10 ++++++- src/error.rs | 5 ++++ src/ffi/types.rs | 52 +++++++++++++++++++++++++++++++++ src/payment/bolt12.rs | 14 +++++++-- src/payment/unified.rs | 9 ++++-- tests/integration_tests_rust.rs | 5 ++-- 6 files changed, 88 insertions(+), 7 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 9d5da68d4..3ab695067 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -203,7 +203,7 @@ interface Bolt12Payment { [Throws=NodeError] PaymentId send([ByRef]Offer offer, u64? quantity, string? payer_note); [Throws=NodeError] - PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note); + PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note, HumanReadableName? hrn); [Throws=NodeError] Offer receive(u64 amount_msat, [ByRef]string description, u32? expiry_secs, u64? quantity); [Throws=NodeError] @@ -322,6 +322,7 @@ enum NodeError { "LiquidityFeeTooHigh", "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", + "HrnParsingFailed", }; dictionary NodeStatus { @@ -783,6 +784,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 7e9dbac20..475996052 100644 --- a/src/error.rs +++ b/src/error.rs @@ -125,6 +125,8 @@ pub enum Error { InvalidBlindedPaths, /// Asynchronous payment services are disabled. AsyncPaymentServicesDisabled, + /// Parsing a Human-Readable Name has failed. + HrnParsingFailed, } impl fmt::Display for Error { @@ -202,6 +204,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 7a6a39fad..87c725a9f 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -270,6 +270,58 @@ impl std::fmt::Display for Offer { } } +pub struct HumanReadableName { + pub(crate) inner: LdkHumanReadableName, +} + +impl HumanReadableName { + pub fn into_inner(&self) -> LdkHumanReadableName { + self.inner.clone() + } + + 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 }) + } + + pub fn user(&self) -> String { + self.inner.user().to_string() + } + + 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.into_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 337eedf96..338fd21a1 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`]. @@ -183,6 +188,7 @@ impl Bolt12Payment { /// response. pub fn send_using_amount( &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, + hrn: Option, ) -> Result { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); @@ -217,7 +223,11 @@ impl Bolt12Payment { retry_strategy, route_params_config, }; - 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 df26cbe12..027e441d6 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_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use bip21::de::ParamKind; @@ -198,8 +199,12 @@ impl UnifiedPayment { 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) + + let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) { + let hrn = maybe_wrap(hrn.clone()); + self.bolt12_payment.send_using_amount(&offer, amount_msat.unwrap_or(0), None, None, Some(hrn)) + } else if let Some(amount_msat) = amount_msat { + self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, None) } else { self.bolt12_payment.send(&offer, None, None) } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2b505362d..43e5afafd 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1023,7 +1023,7 @@ async fn simple_bolt12_send_receive() { let expected_payer_note = Some("Test".to_string()); assert!(node_a .bolt12_payment() - .send_using_amount(&offer, less_than_offer_amount, None, None) + .send_using_amount(&offer, less_than_offer_amount, None, None, None) .is_err()); let payment_id = node_a .bolt12_payment() @@ -1032,6 +1032,7 @@ async fn simple_bolt12_send_receive() { expected_amount_msat, expected_quantity, expected_payer_note.clone(), + None, ) .unwrap(); @@ -1275,7 +1276,7 @@ async fn async_payment() { node_receiver.stop().unwrap(); let payment_id = - node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None).unwrap(); + node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None, None).unwrap(); // Sleep to allow the payment reach a state where the htlc is held and waiting for the receiver to come online. tokio::time::sleep(std::time::Duration::from_millis(3000)).await;