diff --git a/Cargo.toml b/Cargo.toml index 5ce10d6ad..c70355ccb 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.1.0", features = ["std"] } +lightning = { version = "0.1.0", features = ["std", "dnssec"] } lightning-types = { version = "0.2.0" } lightning-invoice = { version = "0.33.0", features = ["std"] } lightning-net-tokio = { version = "0.1.0" } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c2f0166c8..b7a5879ad 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,7 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; SendingParameters? sending_parameters; + sequence? dns_resolvers_node_ids; }; dictionary AnchorChannelsConfig { @@ -94,6 +95,8 @@ interface Builder { [Throws=BuildError] void set_node_alias(string node_alias); [Throws=BuildError] + void set_dns_resolvers(sequence dns_resolvers_node_ids); + [Throws=BuildError] Node build(); [Throws=BuildError] Node build_with_fs_store(); @@ -197,6 +200,8 @@ interface Bolt12Payment { [Throws=NodeError] PaymentId send_using_amount([ByRef]Offer offer, u64 amount_msat, u64? quantity, string? payer_note); [Throws=NodeError] + PaymentId send_to_human_readable_name([ByRef]string name, u64 amount_msat); + [Throws=NodeError] Offer receive(u64 amount_msat, [ByRef]string description, u32? expiry_secs, u64? quantity); [Throws=NodeError] Offer receive_variable_amount([ByRef]string description, u32? expiry_secs); @@ -302,6 +307,8 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "HrnParsingFailed", + "DnsResolversNotConfigured", }; dictionary NodeStatus { @@ -337,6 +344,7 @@ enum BuildError { "WalletSetupFailed", "LoggerSetupFailed", "NetworkMismatch", + "DnsResolversEmpty", }; [Trait] @@ -402,7 +410,7 @@ interface PaymentKind { Onchain(Txid txid, ConfirmationStatus status); Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits); - Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity); + Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId? offer_id, UntrustedString? payer_note, u64? quantity); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); }; @@ -797,4 +805,4 @@ typedef string NodeAlias; typedef string OrderId; [Custom] -typedef string DateTime; +typedef string DateTime; \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index 224cc9fa7..203ca3153 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -172,6 +172,8 @@ pub enum BuildError { LoggerSetupFailed, /// The given network does not match the node's previously configured network. NetworkMismatch, + /// The dns_resolvers list provided for HRN resolution is empty + DnsResolversEmpty, } impl fmt::Display for BuildError { @@ -199,6 +201,9 @@ impl fmt::Display for BuildError { Self::NetworkMismatch => { write!(f, "Given network does not match the node's previously configured network.") }, + Self::DnsResolversEmpty => { + write!(f, "The dns_resolvers list provided for HRN resolution is empty.") + }, } } } @@ -457,6 +462,17 @@ impl NodeBuilder { Ok(self) } + /// Sets the default dns_resolvers to be used when sending payments to HRNs. + pub fn set_dns_resolvers( + &mut self, dns_resolvers_node_ids: Vec, + ) -> Result<&mut Self, BuildError> { + if dns_resolvers_node_ids.is_empty() { + return Err(BuildError::DnsResolversEmpty); + } + self.config.dns_resolvers_node_ids = Some(dns_resolvers_node_ids); + Ok(self) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result { @@ -838,6 +854,13 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ()) } + /// Sets the default dns_resolvers to be used when sending payments to HRNs. + pub fn set_dns_resolvers( + &self, dns_resolvers_node_ids: Vec, + ) -> Result<(), BuildError> { + self.inner.write().unwrap().set_dns_resolvers(dns_resolvers_node_ids).map(|_| ()) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result, BuildError> { diff --git a/src/config.rs b/src/config.rs index 4a39c1b56..7f4da1457 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,6 +103,7 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64; /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | /// | `sending_parameters` | None | +/// | `dns_resolvers` | None | /// /// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their /// respective default values. @@ -167,6 +168,11 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub sending_parameters: Option, + /// The dns_resolvers node_ids to be used for resolving Human-readable Names. + /// + /// If set to `Some`, the values set will be used as dns_resolvers when sending to HRNs. + /// **Note:** If set to `None`, payments to HRNs will fail. + pub dns_resolvers_node_ids: Option>, } impl Default for Config { @@ -181,6 +187,7 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), sending_parameters: None, node_alias: None, + dns_resolvers_node_ids: None, } } } diff --git a/src/error.rs b/src/error.rs index 2cb71186d..bdbb3cf80 100644 --- a/src/error.rs +++ b/src/error.rs @@ -120,6 +120,10 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Parsing a Human-Readable Name has failed + HrnParsingFailed, + /// The given operation failed due to `dns-resolvers` not being configured in builder. + DnsResolversNotConfigured, } impl fmt::Display for Error { @@ -193,6 +197,12 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::HrnParsingFailed => { + write!(f, "Failed to parse a human-readable name.") + }, + Self::DnsResolversNotConfigured => { + write!(f, "The given operation failed due to `dns-resolvers` not being configured in builder.") + }, } } } diff --git a/src/event.rs b/src/event.rs index 00d8441e5..121517364 100644 --- a/src/event.rs +++ b/src/event.rs @@ -742,7 +742,7 @@ where hash: Some(payment_hash), preimage: payment_preimage, secret: Some(payment_secret), - offer_id, + offer_id: Some(offer_id), payer_note, quantity, }; diff --git a/src/lib.rs b/src/lib.rs index 7859a092e..fd35fe47c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -829,6 +829,11 @@ impl Node { self.config.node_alias } + /// Returns the list of dns_resolvers that will be used to resolve HRNs. + pub fn dns_resolvers(&self) -> Option> { + self.config.dns_resolvers_node_ids.clone() + } + /// Returns a payment handler allowing to create and pay [BOLT 11] invoices. /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md @@ -873,6 +878,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), Arc::clone(&self.logger), + Arc::clone(&self.config), ) } @@ -886,6 +892,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), Arc::clone(&self.logger), + Arc::clone(&self.config), )) } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index dbeee0ab8..8fcc1f101 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,7 +9,7 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ @@ -22,6 +22,8 @@ use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::{Amount, Offer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::offers::refund::Refund; +use lightning::onion_message::dns_resolution::HumanReadableName; +use lightning::onion_message::messenger::Destination; use lightning::util::string::UntrustedString; use rand::RngCore; @@ -41,15 +43,16 @@ pub struct Bolt12Payment { channel_manager: Arc, payment_store: Arc>>, logger: Arc, + config: Arc, } impl Bolt12Payment { pub(crate) fn new( runtime: Arc>>>, channel_manager: Arc, payment_store: Arc>>, - logger: Arc, + logger: Arc, config: Arc, ) -> Self { - Self { runtime, channel_manager, payment_store, logger } + Self { runtime, channel_manager, payment_store, logger, config } } /// Send a payment given an offer. @@ -105,7 +108,7 @@ impl Bolt12Payment { hash: None, preimage: None, secret: None, - offer_id: offer.id(), + offer_id: Some(offer.id()), payer_note: payer_note.map(UntrustedString), quantity, }; @@ -130,7 +133,7 @@ impl Bolt12Payment { hash: None, preimage: None, secret: None, - offer_id: offer.id(), + offer_id: Some(offer.id()), payer_note: payer_note.map(UntrustedString), quantity, }; @@ -211,7 +214,7 @@ impl Bolt12Payment { hash: None, preimage: None, secret: None, - offer_id: offer.id(), + offer_id: Some(offer.id()), payer_note: payer_note.map(UntrustedString), quantity, }; @@ -236,7 +239,7 @@ impl Bolt12Payment { hash: None, preimage: None, secret: None, - offer_id: offer.id(), + offer_id: Some(offer.id()), payer_note: payer_note.map(UntrustedString), quantity, }; @@ -256,6 +259,92 @@ impl Bolt12Payment { } } + /// Send a payment to an offer resolved from a human-readable name [BIP 353]. + /// + /// Paying to human-readable names makes it more intuitive to make payments for offers + /// as users can simply send payments to HRNs such as `user@example.com`. + /// + /// This can be used to pay so-called "zero-amount" offers, i.e., an offer that leaves the + /// amount paid to be determined by the user. + /// + /// If `dns_resolvers_node_ids` in Config is set to `None`, this operation will fail. + pub fn send_to_human_readable_name( + &self, name: &str, amount_msat: u64, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let hrn = HumanReadableName::from_encoded(&name).map_err(|_| Error::HrnParsingFailed)?; + + let mut random_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut random_bytes); + let payment_id = PaymentId(random_bytes); + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let max_total_routing_fee_msat = None; + + let dns_resolvers = match &self.config.dns_resolvers_node_ids { + Some(dns_resolvers) => Ok(dns_resolvers.clone()), + None => Err(Error::DnsResolversNotConfigured), + }?; + + let destinations: Vec = + dns_resolvers.into_iter().map(|public_key| Destination::Node(public_key)).collect(); + + match self.channel_manager.pay_for_offer_from_human_readable_name( + hrn.clone(), + amount_msat, + payment_id, + retry_strategy, + max_total_routing_fee_msat, + destinations, + ) { + Ok(()) => { + log_info!(self.logger, "Initiated sending {} msats to {}", amount_msat, name); + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: None, + payer_note: None, + quantity: None, + }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); + self.payment_store.insert(payment)?; + Ok(payment_id) + }, + Err(()) => { + log_error!(self.logger, "Failed to send payment to {}", name); + let kind = PaymentKind::Bolt12Offer { + hash: None, + preimage: None, + secret: None, + offer_id: None, + payer_note: None, + quantity: None, + }; + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Outbound, + PaymentStatus::Pending, + ); + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + } + /// Returns a payable offer that can be used to request and receive a payment of the amount /// given. pub fn receive( diff --git a/src/payment/store.rs b/src/payment/store.rs index 2a074031c..4aae1e63b 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -395,7 +395,7 @@ pub enum PaymentKind { /// The secret used by the payment. secret: Option, /// The ID of the offer this payment is for. - offer_id: OfferId, + offer_id: Option, /// The payer note for the payment. /// /// Truncated to [`PAYER_NOTE_LIMIT`] characters. diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f2dfa4b5e..4682859c0 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -721,7 +721,7 @@ fn simple_bolt12_send_receive() { } => { assert!(hash.is_some()); assert!(preimage.is_some()); - assert_eq!(offer_id, offer.id()); + assert_eq!(offer_id, Some(offer.id())); assert_eq!(&expected_quantity, qty); assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 @@ -742,7 +742,7 @@ fn simple_bolt12_send_receive() { assert!(hash.is_some()); assert!(preimage.is_some()); assert!(secret.is_some()); - assert_eq!(offer_id, offer.id()); + assert_eq!(offer_id, Some(offer.id())); }, _ => { panic!("Unexpected payment kind"); @@ -787,7 +787,7 @@ fn simple_bolt12_send_receive() { } => { assert!(hash.is_some()); assert!(preimage.is_some()); - assert_eq!(offer_id, offer.id()); + assert_eq!(offer_id, Some(offer.id())); assert_eq!(&expected_quantity, qty); assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 @@ -811,7 +811,7 @@ fn simple_bolt12_send_receive() { assert!(hash.is_some()); assert!(preimage.is_some()); assert!(secret.is_some()); - assert_eq!(offer_id, offer.id()); + assert_eq!(offer_id, Some(offer.id())); }, _ => { panic!("Unexpected payment kind");