diff --git a/Cargo.toml b/Cargo.toml index bf8bed08c..b9c14885c 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 36767b790..7da8b1913 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,11 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; SendingParameters? sending_parameters; + HumanReadableNamesConfig? hrn_config; +}; + +dictionary HumanReadableNamesConfig { + sequence dns_resolvers_node_ids; }; dictionary AnchorChannelsConfig { @@ -197,6 +202,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]HumanReadableName hrn, 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); @@ -248,6 +255,13 @@ interface LSPS1Liquidity { LSPS1OrderStatus check_order_status(OrderId order_id); }; +interface HumanReadableName { + [Throws=NodeError, Name=from_encoded] + constructor([ByRef] string encoded); + string user(); + string domain(); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -302,6 +316,8 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "HrnParsingFailed", + "DnsResolversUnavailable", }; dictionary NodeStatus { @@ -337,6 +353,7 @@ enum BuildError { "WalletSetupFailed", "LoggerSetupFailed", "NetworkMismatch", + "DnsResolversUnavailable", }; [Trait] @@ -402,7 +419,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); }; diff --git a/src/builder.rs b/src/builder.rs index 31a0fee45..8c73859d7 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -174,6 +174,10 @@ pub enum BuildError { LoggerSetupFailed, /// The given network does not match the node's previously configured network. NetworkMismatch, + /// The [`dns_resolvers_node_ids`] provided for HRN resolution is empty. + /// + /// [`dns_resolvers_node_ids`]: crate::config::HumanReadableNamesConfig::dns_resolvers_node_ids + DnsResolversUnavailable, } impl fmt::Display for BuildError { @@ -201,6 +205,9 @@ impl fmt::Display for BuildError { Self::NetworkMismatch => { write!(f, "Given network does not match the node's previously configured network.") }, + Self::DnsResolversUnavailable => { + write!(f, "The DNS resolvers provided for HRN resolution is empty.") + }, } } } @@ -1492,6 +1499,12 @@ fn build_with_store_internal( }, }; + if let Some(hrn_config) = &config.hrn_config { + if hrn_config.dns_resolvers_node_ids.is_empty() { + return Err(BuildError::DnsResolversUnavailable); + } + }; + let (stop_sender, _) = tokio::sync::watch::channel(()); let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); diff --git a/src/config.rs b/src/config.rs index 4a39c1b56..eaaeb8c7f 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 | +/// | `hrn_config` | None | /// /// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their /// respective default values. @@ -167,6 +168,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 sending_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 { @@ -181,10 +186,24 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), sending_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 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, payments to HRNs will fail. + pub dns_resolvers_node_ids: Vec, +} + /// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. /// @@ -306,6 +325,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; user_config.manually_accept_inbound_channels = true; + user_config.manually_handle_bolt12_invoices = true; user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); diff --git a/src/error.rs b/src/error.rs index 2cb71186d..3d186ddf1 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. + DnsResolversUnavailable, } 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::DnsResolversUnavailable => { + write!(f, "The given operation failed due to DNS resolvers not being configured.") + }, } } } diff --git a/src/event.rs b/src/event.rs index 22848bec1..767c608e9 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, }; @@ -1417,8 +1417,30 @@ where ); } }, - LdkEvent::InvoiceReceived { .. } => { - debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); + LdkEvent::InvoiceReceived { payment_id, invoice, context, responder: _ } => { + let update = PaymentDetailsUpdate { + hash: Some(Some(invoice.payment_hash())), + quantity: invoice.quantity(), + ..PaymentDetailsUpdate::new(payment_id) + }; + + match self.payment_store.update(&update) { + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Failed to access payment store: {}", e); + return Err(ReplayEvent()); + }, + }; + + match self + .channel_manager + .send_payment_for_bolt12_invoice(&invoice, context.as_ref()) + { + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Error while paying invoice: {:?}", e); + }, + }; }, LdkEvent::ConnectionNeeded { node_id, addresses } => { let runtime_lock = self.runtime.read().unwrap(); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 984e4da8f..83e5523b5 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -12,7 +12,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, MaxDustHTLCExposure, + EsploraSyncConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; @@ -36,6 +36,8 @@ pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; pub use lightning_liquidity::lsps1::msgs::{OrderId, OrderParameters, PaymentState}; +pub use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; + pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; pub use bip39::Mnemonic; @@ -1117,6 +1119,59 @@ impl UniffiCustomTypeConverter for DateTime { } } +pub struct HumanReadableName { + pub(crate) inner: LdkHumanReadableName, +} + +impl HumanReadableName { + /// Returns the underlying HumanReadableName [`LdkHumanReadableName`] + 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() + } +} + #[cfg(test)] mod tests { use std::{ diff --git a/src/lib.rs b/src/lib.rs index b09f9a9f7..39f81fb8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -874,6 +874,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), Arc::clone(&self.logger), + Arc::clone(&self.config), ) } @@ -887,6 +888,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 b9efa3241..5a8685c8b 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::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; @@ -19,6 +19,7 @@ use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; +use lightning::onion_message::messenger::Destination; use lightning::util::string::UntrustedString; use rand::RngCore; @@ -42,6 +43,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`]. @@ -53,15 +59,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. @@ -118,7 +125,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, }; @@ -143,7 +150,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, }; @@ -225,7 +232,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, }; @@ -250,7 +257,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, }; @@ -270,6 +277,105 @@ 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.hrn_config`] is empty, this operation will fail. + /// + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub fn send_to_human_readable_name( + &self, hrn: &HumanReadableName, amount_msat: u64, + ) -> Result { + let hrn = maybe_deref(hrn); + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + 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 destinations: Vec = match &self.config.hrn_config { + Some(hrn_config) => Ok(hrn_config + .dns_resolvers_node_ids + .iter() + .map(|node_id| Destination::Node(*node_id)) + .collect()), + None => Err(Error::DnsResolversUnavailable), + }?; + + 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, + hrn.user(), + hrn.domain() + ); + 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 ₿{}@{}", + hrn.user(), + hrn.domain() + ); + 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::Failed, + ); + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + } + pub(crate) fn receive_inner( &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, ) -> Result { diff --git a/src/payment/store.rs b/src/payment/store.rs index 75b2b1b2a..7620a6f9d 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -404,7 +404,11 @@ pub enum PaymentKind { /// The secret used by the payment. secret: Option, /// The ID of the offer this payment is for. - offer_id: OfferId, + /// + /// This will be set to `None` when sending payments to Human-Readable Names ([BIP 353]). + /// + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + offer_id: Option, /// The payer note for the payment. /// /// Truncated to [`PAYER_NOTE_LIMIT`] characters. @@ -470,7 +474,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (2, preimage, option), (3, quantity, option), (4, secret, option), - (6, offer_id, required), + (6, offer_id, option), }, (8, Spontaneous) => { (0, hash, required), @@ -542,6 +546,7 @@ pub(crate) struct PaymentDetailsUpdate { pub direction: Option, pub status: Option, pub confirmation_status: Option, + pub quantity: Option, } impl PaymentDetailsUpdate { @@ -557,6 +562,7 @@ impl PaymentDetailsUpdate { direction: None, status: None, confirmation_status: None, + quantity: None, } } } @@ -584,6 +590,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { _ => None, }; + let quantity = match value.kind { + PaymentKind::Bolt12Offer { quantity, .. } => quantity, + _ => None, + }; + Self { id: value.id, hash: Some(hash), @@ -595,6 +606,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { direction: Some(value.direction), status: Some(value.status), confirmation_status, + quantity, } } } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index db48eca23..a556648e4 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -820,7 +820,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 @@ -841,7 +841,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"); @@ -886,7 +886,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 @@ -910,7 +910,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");