diff --git a/Cargo.toml b/Cargo.toml index 51b0329c4..12cfb9a87 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ lightning-block-sync = { version = "0.2.0-rc1", features = ["rest-client", "rpc- lightning-transaction-sync = { version = "0.2.0-rc1", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } lightning-liquidity = { version = "0.2.0-rc1", features = ["std"] } lightning-macros = { version = "0.2.0-rc1" } +lightning-dns-resolver = { version = "0.3.0-rc1" } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } @@ -52,6 +53,7 @@ lightning-macros = { version = "0.2.0-rc1" } #lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } @@ -64,6 +66,7 @@ lightning-macros = { version = "0.2.0-rc1" } #lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -76,6 +79,7 @@ lightning-macros = { version = "0.2.0-rc1" } #lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } #lightning-macros = { path = "../rust-lightning/lightning-macros" } +#lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolvers" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -103,6 +107,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"] } @@ -147,4 +153,4 @@ check-cfg = [ "cfg(tokio_unstable)", "cfg(cln_test)", "cfg(lnd_test)", -] +] \ No newline at end of file diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ab2f483a1..5c101289f 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; RouteParametersConfig? route_parameters; + HumanReadableNamesConfig? hrn_config; }; dictionary AnchorChannelsConfig { @@ -130,7 +131,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); @@ -203,7 +204,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] @@ -252,11 +253,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); + [Throws=NodeError, Async] + UnifiedPaymentResult send([ByRef]string uri_str, u64? amount_msat); }; interface LSPS1Liquidity { @@ -322,6 +323,9 @@ enum NodeError { "LiquidityFeeTooHigh", "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", + "HrnParsingFailed", + "HrnResolverNotConfigured", + "TimeoutOccurred", }; dictionary NodeStatus { @@ -359,6 +363,7 @@ enum BuildError { "LoggerSetupFailed", "NetworkMismatch", "AsyncPaymentsConfigMismatch", + "DNSResolverSetupFailed", }; [Trait] @@ -431,7 +436,7 @@ interface PaymentKind { }; [Enum] -interface QrPaymentResult { +interface UnifiedPaymentResult { Onchain(Txid txid); Bolt11(PaymentId payment_id); Bolt12(PaymentId payment_id); @@ -476,6 +481,12 @@ dictionary RouteParametersConfig { u8 max_channel_saturation_power_of_half; }; +dictionary HumanReadableNamesConfig { + sequence default_dns_resolvers; + boolean is_hrn_resolver; + string dns_server_address; +}; + dictionary CustomTlvRecord { u64 type_num; sequence value; @@ -783,6 +794,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/builder.rs b/src/builder.rs index c0e39af7a..a56dffa63 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -19,12 +19,14 @@ 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}; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; use lightning::log_trace; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ @@ -38,6 +40,7 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +use lightning_dns_resolver::OMDomainResolver; use lightning_persister::fs_store::FilesystemStore; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; @@ -69,8 +72,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, Persister, + ChainMonitor, ChannelManager, DomainResolver, DynStore, GossipSync, Graph, HRNResolver, + KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, Persister, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -195,6 +198,8 @@ pub enum BuildError { NetworkMismatch, /// The role of the node in an asynchronous payments context is not compatible with the current configuration. AsyncPaymentsConfigMismatch, + /// An attempt to setup a DNS Resolver failed. + DNSResolverSetupFailed, } impl fmt::Display for BuildError { @@ -229,12 +234,20 @@ impl fmt::Display for BuildError { "The async payments role is not compatible with the current configuration." ) }, + 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. /// @@ -1536,6 +1549,32 @@ fn build_with_store_internal( })?; } + let resolver = if let Some(hrn_config) = &config.hrn_config { + if hrn_config.is_hrn_resolver { + let dns_addr = hrn_config.dns_server_address.as_str(); + + Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs( + dns_addr.parse().map_err(|_| BuildError::DNSResolverSetupFailed)?, + ))) + } else { + Resolver::HRN(Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone( + &network_graph, + )))) + } + } else { + // hrn_config is None, default to the HRN resolver. + 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 = if let Some(AsyncPaymentsRole::Server) = async_payments_role { @@ -1547,7 +1586,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )) } else { @@ -1559,7 +1598,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )) }; @@ -1691,6 +1730,18 @@ fn build_with_store_internal( Arc::clone(&keys_manager), )); + let peer_manager_clone = Arc::clone(&peer_manager); + + 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))); gossip_source.set_gossip_verifier( @@ -1797,6 +1848,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, + hrn_resolver: hrn_resolver.cloned(), }) } diff --git a/src/config.rs b/src/config.rs index ce361c45a..d8dac5d19 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,8 @@ pub(crate) const EXTERNAL_PATHFINDING_SCORES_SYNC_TIMEOUT_SECS: u64 = 5; /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | -/// | `route_parameters` | None | +/// | `route_parameters` | None | +/// | `hrn_config` | None | /// /// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. @@ -184,6 +185,10 @@ 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 route_parameters: Option, + /// Configuration options for Human-Readable Names ([BIP 353]). + /// + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub hrn_config: Option, } impl Default for Config { @@ -198,10 +203,28 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, + hrn_config: None, } } } +/// Configuration options for Human-Readable Names ([BIP 353]). +/// +/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki +#[derive(Debug, Clone)] +pub struct HumanReadableNamesConfig { + /// The Default DNS resolvers to be used for resolving Human-Readable Names. + /// + /// If not empty, the values set will be used as DNS resolvers when sending to HRNs. + /// + /// **Note:** If empty, DNS resolvers would be selected from the network graph. + pub default_dns_resolvers: Vec, + /// This allows us to use our node as a DNS resolver for 3rd party HRN resolutions. + pub is_hrn_resolver: bool, + /// The DNS Server which will be used for resolving HRNs. + pub dns_server_address: String, +} + /// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. /// diff --git a/src/error.rs b/src/error.rs index 7e9dbac20..9f02fc2ac 100644 --- a/src/error.rs +++ b/src/error.rs @@ -125,6 +125,12 @@ pub enum Error { InvalidBlindedPaths, /// Asynchronous payment services are disabled. AsyncPaymentServicesDisabled, + /// Parsing a Human-Readable Name has failed. + HrnParsingFailed, + /// A HRN resolver was not configured + HrnResolverNotConfigured, + /// A Timeout occurred during an operation. + TimeoutOccurred, } impl fmt::Display for Error { @@ -202,6 +208,15 @@ 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.") + }, + Self::HrnResolverNotConfigured => { + write!(f, "A HRN resolver was not configured.") + }, + Self::TimeoutOccurred => { + write!(f, "A Timeout occured during an operation.") + }, } } } diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 3c88a665f..cc68398da 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -45,7 +45,7 @@ pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; use crate::builder::sanitize_alias; pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, MaxDustHTLCExposure, + EsploraSyncConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, }; use crate::error::Error; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; @@ -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 { @@ -267,6 +270,59 @@ impl std::fmt::Display for Offer { } } +#[derive(Eq, Hash, PartialEq)] +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/lib.rs b/src/lib.rs index 6a26c6c5b..cd08f54e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,34 +131,34 @@ 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, - 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, }; +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, @@ -200,6 +200,7 @@ pub struct Node { node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, + hrn_resolver: Option>, } impl Node { @@ -927,34 +928,42 @@ 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(), Arc::clone(&self.config), Arc::clone(&self.logger), + Arc::new(self.hrn_resolver.clone()), ) } /// 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(), Arc::clone(&self.config), Arc::clone(&self.logger), + Arc::new(self.hrn_resolver.clone()), )) } @@ -1622,3 +1631,60 @@ pub(crate) fn total_anchor_channels_reserve_sats( * anchor_channels_config.per_channel_reserve_sats }) } + +/// Testing utils for DNSSEC proof resolution of offers associated with the given HRN. +pub mod dnssec_testing_utils { + use std::collections::HashMap; + #[cfg(feature = "uniffi")] + use std::sync::Arc; + use std::sync::{LazyLock, Mutex}; + + #[cfg(not(feature = "uniffi"))] + type Offer = lightning::offers::offer::Offer; + #[cfg(feature = "uniffi")] + type Offer = Arc; + + #[cfg(not(feature = "uniffi"))] + type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName; + #[cfg(feature = "uniffi")] + type HumanReadableName = Arc; + + static OFFER_OVERRIDE_MAP: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + + /// Sets a testing override for DNSSEC proof resolution of offers associated with the given HRN. + pub fn set_testing_dnssec_proof_offer_resolution_override(hrn: &str, offer: Offer) { + let hrn_key = { + #[cfg(not(feature = "uniffi"))] + { + lightning::onion_message::dns_resolution::HumanReadableName::from_encoded(hrn) + .unwrap() + } + + #[cfg(feature = "uniffi")] + { + Arc::new(crate::ffi::HumanReadableName::from_encoded(hrn).unwrap()) + } + }; + + OFFER_OVERRIDE_MAP.lock().unwrap().insert(hrn_key, offer); + } + + /// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given HRNs. + #[cfg(not(feature = "uniffi"))] + pub fn get_testing_offer_override(hrn: Option) -> Option { + OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned() + } + + /// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given HRNs. + #[cfg(feature = "uniffi")] + pub fn get_testing_offer_override(hrn: Option) -> Option { + let offer = OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned().unwrap(); + Some(offer) + } + + /// Clears all testing overrides for DNSSEC proof resolution of offers. + pub fn clear_testing_overrides() { + OFFER_OVERRIDE_MAP.lock().unwrap().clear(); + } +} diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 337eedf96..b1249b3a1 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,12 +188,27 @@ 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); } - let offer = maybe_deref(offer); + let mut current_offer = offer.clone(); + + if let Some(hrn_ref) = hrn.as_ref() { + current_offer = match crate::dnssec_testing_utils::get_testing_offer_override(Some( + hrn_ref.clone(), + )) { + Some(offer) => { + log_info!(self.logger, "Using test-specific Offer override."); + offer + }, + _ => offer.clone(), + }; + } + + let offer = maybe_deref(¤t_offer); let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); @@ -217,7 +237,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/mod.rs b/src/payment/mod.rs index f629960e1..c82f35c8f 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::{UnifiedPayment, UnifiedPaymentResult}; diff --git a/src/payment/unified_qr.rs b/src/payment/unified.rs similarity index 74% rename from src/payment/unified_qr.rs rename to src/payment/unified.rs index fc2eca150..dbe720691 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified.rs @@ -5,31 +5,46 @@ // 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::time::Duration; 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::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; 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; +use bitcoin::{Amount, Txid}; +use bitcoin_payment_instructions::{ + amount::Amount as BPIAmount, PaymentInstructions, PaymentMethod, +}; +use tokio::time::timeout; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; +#[cfg(not(feature = "uniffi"))] +type HumanReadableName = LdkHumanReadableName; +#[cfg(feature = "uniffi")] +type HumanReadableName = Arc; + #[derive(Debug, Clone)] struct Extras { bolt11_invoice: Option, @@ -39,26 +54,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 -pub struct UnifiedQrPayment { +/// [`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 UnifiedQrPayment { +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. @@ -128,54 +148,143 @@ impl UnifiedQrPayment { 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 uri_network_checked = - uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; - - 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 }), - 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), + /// [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 resolver = self.hrn_resolver.as_ref().clone().ok_or_else(|| { + log_error!(self.logger, "No HRN resolver configured. Cannot resolve HRNs."); + Error::HrnResolverNotConfigured + })?; + + const TIMEOUT_DURATION: Duration = Duration::from_secs(30); + + let target_network; + + if let Ok(hrn) = LdkHumanReadableName::from_encoded(uri_str) { + let hrn: HumanReadableName = maybe_wrap(hrn.clone()); + + target_network = + match crate::dnssec_testing_utils::get_testing_offer_override(Some(hrn.clone())) { + Some(_) => bitcoin::Network::Bitcoin, + _ => self.config.network, + }; + } else { + target_network = self.config.network; + }; + + let instructions = timeout( + TIMEOUT_DURATION, + PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false), + ) + .await + .map_err(|_| { + log_error!( + self.logger, + "Payment instruction parsing timed out after {:?}", + TIMEOUT_DURATION + ); + Error::TimeoutOccurred + })? + .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, resolver.as_ref()).await.map_err(|e| { + log_error!(self.logger, "Failed to set amount: {:?}", e); + Error::InvalidAmount + })? + }, + PaymentInstructions::FixedAmount(instr) => { + if let Some(user_amount) = amount_msat { + if instr.max_amount().map_or(false, |amt| user_amount < amt.milli_sats()) { + log_error!(self.logger, "Amount specified is less than the amount in the parsed URI. Aborting the payment."); + return Err(Error::InvalidAmount); + } + } + instr + }, + }; + + if let Some(PaymentMethod::LightningBolt12(offer)) = + resolved.methods().iter().find(|m| matches!(m, PaymentMethod::LightningBolt12(_))) + { + let offer = maybe_wrap(offer.clone()); + + let payment_result = if let Ok(hrn) = LdkHumanReadableName::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) } - } + .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(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 }), - 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 Ok(payment_id) = payment_result { + return Ok(UnifiedPaymentResult::Bolt12 { 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, - )?; + 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 }); + } + } - Ok(QrPaymentResult::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) } } @@ -188,7 +297,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,13 +411,10 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { + use super::{Amount, Bolt11Invoice, Extras, Offer}; + use bitcoin::{address::NetworkUnchecked, Address, Network}; use std::str::FromStr; - use bitcoin::{Address, Network}; - - use super::*; - use crate::payment::unified_qr::Extras; - #[test] fn parse_uri() { let uri_test1 = "BITCOIN:TB1QRSCD05XNY6QZ63TF9GJELGVK6D3UDJFEKK62VU?amount=1&message=Test%20message&lightning=LNTB1000M1PNXWM7MDQ523JHXAPQD4JHXUMPVAJSNP4QWP9QD2JFP8DUZ46JQG5LTKVDH04YG52G6UF2YAXP8H7YZPZM3DM5PP5KUP7YT429UP9Z4ACPA60R7WETSTL66549MG05P0JN0C4L2NCC40SSP5R0LH86DJCL0NK8HZHNZHX92VVUAAVNE48Z5RVKVY5DKTRQ0DMP7S9QYYSGQCQPCXQRRAQYR59FGN2VVC5R6DS0AZMETH493ZU56H0WSVMGYCW9LEPZ032PGQNZMQ6XKVEH90Z02C0NH3J5QGDAWCS2YC2ZNP22J0ZD0PPF78N4QQQEXTYS2"; diff --git a/src/types.rs b/src/types.rs index 800d9462d..e3d705872 100644 --- a/src/types.rs +++ b/src/types.rs @@ -15,6 +15,7 @@ use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::msgs::{RoutingMessageHandler, 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::CombinedScorer; @@ -24,6 +25,7 @@ use lightning::util::persist::{KVStore, KVStoreSync, MonitorUpdatingPersister}; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::{GossipVerifier, UtxoSource}; +use lightning_dns_resolver::OMDomainResolver; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; @@ -36,6 +38,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 +146,14 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - IgnoringMessageHandler, + Arc, IgnoringMessageHandler, >; +pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; + +pub(crate) type DomainResolver = OMDomainResolver; + pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 058a8df19..147d0fd5f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -28,7 +28,9 @@ use bitcoin::{ use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; use electrsd::{corepc_node, ElectrsD}; use electrum_client::ElectrumApi; -use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; +use ldk_node::config::{ + AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HumanReadableNamesConfig, +}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ @@ -281,7 +283,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); @@ -289,6 +291,13 @@ 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.hrn_config = Some(HumanReadableNamesConfig { + default_dns_resolvers: Vec::new(), + is_hrn_resolver: true, + dns_server_address: "8.8.8.8:53".to_string(), + }); + } 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 e2d4207cd..d9347e765 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -6,7 +6,6 @@ // accordance with one or both of these licenses. mod common; - use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; @@ -29,9 +28,9 @@ 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 ldk_node::{dnssec_testing_utils, Builder, DynStore, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; @@ -43,7 +42,7 @@ use log::LevelFilter; async 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) .await; } @@ -52,7 +51,7 @@ async fn channel_full_cycle() { async 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) .await; } @@ -61,7 +60,7 @@ async fn channel_full_cycle_electrum() { async 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) .await; } @@ -70,7 +69,7 @@ async fn channel_full_cycle_bitcoind_rpc_sync() { async 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) .await; } @@ -79,7 +78,7 @@ async fn channel_full_cycle_bitcoind_rest_sync() { async 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) .await; } @@ -88,7 +87,7 @@ async fn channel_full_cycle_force_close() { async 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) .await; } @@ -97,7 +96,7 @@ async fn channel_full_cycle_force_close_trusted_no_reserve() { async 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) .await; } @@ -106,7 +105,7 @@ async fn channel_full_cycle_0conf() { async 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) .await; } @@ -115,7 +114,7 @@ async fn channel_full_cycle_legacy_staticremotekey() { async 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(); @@ -319,7 +318,7 @@ async fn start_stop_reinit() { async 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(); @@ -520,7 +519,7 @@ async fn onchain_send_receive() { async 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(); @@ -835,7 +834,7 @@ async fn sign_verify_msg() { async fn connection_multi_listen() { 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_b = node_b.node_id(); @@ -855,7 +854,7 @@ async fn connection_restart_behavior() { async 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(); @@ -902,7 +901,7 @@ async fn do_connection_restart_behavior(persist: bool) { async 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); @@ -929,7 +928,7 @@ async fn concurrent_connections_succeed() { async 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; @@ -1023,7 +1022,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 +1031,7 @@ async fn simple_bolt12_send_receive() { expected_amount_msat, expected_quantity, expected_payer_note.clone(), + None, ) .unwrap(); @@ -1275,7 +1275,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; @@ -1382,7 +1382,7 @@ async 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; @@ -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,23 +1421,23 @@ 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)] -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); - 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; @@ -1471,18 +1471,18 @@ 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, None).await { + 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: _ }) => { - panic!("Expected Bolt12 payment but get On-chain transaction"); + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but got On-chain transaction"); }, Err(e) => { panic!("Expected Bolt12 payment but got error: {:?}", e); @@ -1494,15 +1494,15 @@ async fn unified_qr_send_receive() { // Cut off the BOLT12 part to fallback to BOLT11. let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); let invoice_payment_id: PaymentId = - match node_a.unified_qr_payment().send(uri_str_without_offer) { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + 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(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) => { @@ -1512,19 +1512,19 @@ async fn unified_qr_send_receive() { expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); let expect_onchain_amount_sats = 800_000; - let onchain_uqr_payment = - node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); + let onchain_uni_payment = + node_b.unified_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); // Cut off any lightning part to fallback to on-chain only. - let uri_str_without_lightning = onchain_uqr_payment.split("&lightning=").next().unwrap(); - let txid = match node_a.unified_qr_payment().send(&uri_str_without_lightning) { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap(); + let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None).await { + 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 }, @@ -1543,6 +1543,71 @@ async fn unified_qr_send_receive() { assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +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), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + 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)); + } + + let test_offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap(); + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); + + let hrn = "matt@mattcorallo.com"; + + dnssec_testing_utils::set_testing_dnssec_proof_offer_resolution_override( + hrn, + test_offer.clone(), + ); + + let offer_payment_id: PaymentId = match node_a.unified_payment().send(&hrn, Some(1000000)).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); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_service_integration() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -1787,7 +1852,7 @@ async fn facade_logging() { async 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;