From b784f0bdbb3e8f8aa09b1bd454974956575b0284 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 5 Mar 2024 11:58:42 +0100 Subject: [PATCH 1/7] Move BOLT11 payments API to `Bolt11PaymentHandler` --- README.md | 4 +- .../lightningdevkit/ldknode/LibraryTest.kt | 4 +- bindings/ldk_node.udl | 36 +- bindings/python/src/ldk_node/test_ldk_node.py | 4 +- src/lib.rs | 503 ++--------------- src/payment/bolt11.rs | 517 ++++++++++++++++++ src/payment/mod.rs | 5 + src/uniffi_types.rs | 4 +- tests/common/mod.rs | 41 +- tests/integration_tests_cln.rs | 4 +- tests/integration_tests_rust.rs | 4 +- 11 files changed, 621 insertions(+), 505 deletions(-) create mode 100644 src/payment/bolt11.rs create mode 100644 src/payment/mod.rs diff --git a/README.md b/README.md index 270bf25a7..eccb85c40 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A ready-to-go Lightning node library built using [LDK][ldk] and [BDK][bdk]. LDK Node is a self-custodial Lightning node in library form. Its central goal is to provide a small, simple, and straightforward interface that enables users to easily set up and run a Lightning node with an integrated on-chain wallet. While minimalism is at its core, LDK Node aims to be sufficiently modular and configurable to be useful for a variety of use cases. ## Getting Started -The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `connect_open_channel`, `send_payment`, etc. +The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `connect_open_channel`, `send`, etc. ```rust use ldk_node::Builder; @@ -44,7 +44,7 @@ fn main() { node.event_handled(); let invoice = Bolt11Invoice::from_str("INVOICE_STR").unwrap(); - node.send_payment(&invoice).unwrap(); + node.bolt11_payment().send(&invoice).unwrap(); node.stop().unwrap(); } diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt index 39c19821d..cacd33bd6 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt @@ -222,9 +222,9 @@ class LibraryTest { else -> return } - val invoice = node2.receivePayment(2500000u, "asdf", 9217u) + val invoice = node2.bolt11Payment().receive(2500000u, "asdf", 9217u) - node1.sendPayment(invoice) + node1.bolt11Payment().send(invoice) val paymentSuccessfulEvent = node1.waitNextEvent() println("Got event: $paymentSuccessfulEvent") diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c39d841cb..d896b2518 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -53,6 +53,7 @@ interface Node { void event_handled(); PublicKey node_id(); sequence? listening_addresses(); + Bolt11Payment bolt11_payment(); [Throws=NodeError] Address new_onchain_address(); [Throws=NodeError] @@ -72,25 +73,9 @@ interface Node { [Throws=NodeError] void sync_wallets(); [Throws=NodeError] - PaymentHash send_payment([ByRef]Bolt11Invoice invoice); - [Throws=NodeError] - PaymentHash send_payment_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); - [Throws=NodeError] PaymentHash send_spontaneous_payment(u64 amount_msat, PublicKey node_id); [Throws=NodeError] - void send_payment_probes([ByRef]Bolt11Invoice invoice); - [Throws=NodeError] void send_spontaneous_payment_probes(u64 amount_msat, PublicKey node_id); - [Throws=NodeError] - void send_payment_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); - [Throws=NodeError] - Bolt11Invoice receive_payment(u64 amount_msat, [ByRef]string description, u32 expiry_secs); - [Throws=NodeError] - Bolt11Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs); - [Throws=NodeError] - Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); - [Throws=NodeError] - Bolt11Invoice receive_variable_amount_payment_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); PaymentDetails? payment([ByRef]PaymentHash payment_hash); [Throws=NodeError] void remove_payment([ByRef]PaymentHash payment_hash); @@ -103,6 +88,25 @@ interface Node { boolean verify_signature([ByRef]sequence msg, [ByRef]string sig, [ByRef]PublicKey pkey); }; +interface Bolt11Payment { + [Throws=NodeError] + PaymentHash send([ByRef]Bolt11Invoice invoice); + [Throws=NodeError] + PaymentHash send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + [Throws=NodeError] + void send_probes([ByRef]Bolt11Invoice invoice); + [Throws=NodeError] + void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + [Throws=NodeError] + Bolt11Invoice receive(u64 amount_msat, [ByRef]string description, u32 expiry_secs); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount([ByRef]string description, u32 expiry_secs); + [Throws=NodeError] + Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); +}; + [Error] enum NodeError { "AlreadyRunning", diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 864ef7b43..468800efe 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -185,8 +185,8 @@ def test_channel_full_cycle(self): print("EVENT:", channel_ready_event_2) node_2.event_handled() - invoice = node_2.receive_payment(2500000, "asdf", 9217) - node_1.send_payment(invoice) + invoice = node_2.bolt11_payment().receive(2500000, "asdf", 9217) + node_1.bolt11_payment().send(invoice) payment_successful_event_1 = node_1.wait_next_event() assert isinstance(payment_successful_event_1, Event.PAYMENT_SUCCESSFUL) diff --git a/src/lib.rs b/src/lib.rs index a803730c1..03188806e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ //! The primary abstraction of the library is the [`Node`], which can be retrieved by setting up //! and configuring a [`Builder`] to your liking and calling [`build`]. `Node` can then be //! controlled via commands such as [`start`], [`stop`], [`connect_open_channel`], -//! [`send_payment`], etc.: +//! [`send`], etc.: //! //! ```no_run //! use ldk_node::Builder; @@ -56,7 +56,7 @@ //! node.event_handled(); //! //! let invoice = Bolt11Invoice::from_str("INVOICE_STR").unwrap(); -//! node.send_payment(&invoice).unwrap(); +//! node.bolt11_payment().send(&invoice).unwrap(); //! //! node.stop().unwrap(); //! } @@ -66,7 +66,7 @@ //! [`start`]: Node::start //! [`stop`]: Node::stop //! [`connect_open_channel`]: Node::connect_open_channel -//! [`send_payment`]: Node::send_payment +//! [`send`]: Bolt11Payment::send //! #![cfg_attr(not(feature = "uniffi"), deny(missing_docs))] #![deny(rustdoc::broken_intra_doc_links)] @@ -88,6 +88,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; +pub mod payment; mod payment_store; mod peer_store; mod sweep; @@ -129,6 +130,7 @@ use connection::ConnectionManager; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; +use payment::Bolt11Payment; use payment_store::PaymentStore; pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; use peer_store::{PeerInfo, PeerStore}; @@ -155,11 +157,8 @@ use lightning_background_processor::process_events_async; use lightning_transaction_sync::EsploraSyncClient; use lightning::routing::router::{PaymentParameters, RouteParameters}; -use lightning_invoice::{payment, Bolt11Invoice, Currency}; -use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; - use bitcoin::{Address, Txid}; use rand::Rng; @@ -818,6 +817,42 @@ impl Node { self.config.listening_addresses.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 + #[cfg(not(feature = "uniffi"))] + pub fn bolt11_payment(&self) -> Bolt11Payment { + Bolt11Payment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + Arc::clone(&self.keys_manager), + self.liquidity_source.clone(), + Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) + } + + /// 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 + #[cfg(feature = "uniffi")] + pub fn bolt11_payment(&self) -> Arc { + Arc::new(Bolt11Payment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + Arc::clone(&self.keys_manager), + self.liquidity_source.clone(), + Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + )) + } + /// Retrieve a new on-chain/funding address. pub fn new_onchain_address(&self) -> Result { let funding_address = self.wallet.get_new_address()?; @@ -1104,190 +1139,6 @@ impl Node { } } - /// Send a payment given an invoice. - pub fn send_payment(&self, invoice: &Bolt11Invoice) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let (payment_hash, recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_payment_using_amount instead."); - Error::InvalidInvoice - })?; - - if let Some(payment) = self.payment_store.get(&payment_hash) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: an invoice must not be paid twice."); - return Err(Error::DuplicatePayment); - } - } - - let payment_secret = Some(*invoice.payment_secret()); - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); - let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - - match self.channel_manager.send_payment( - payment_hash, - recipient_onion, - payment_id, - route_params, - retry_strategy, - ) { - Ok(()) => { - let payee_pubkey = invoice.recover_payee_pub_key(); - let amt_msat = invoice.amount_milli_satoshis().unwrap(); - log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); - - let payment = PaymentDetails { - preimage: None, - hash: payment_hash, - secret: payment_secret, - amount_msat: invoice.amount_milli_satoshis(), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - }; - self.payment_store.insert(payment)?; - - Ok(payment_hash) - }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - match e { - channelmanager::RetryableSendFailure::DuplicatePayment => { - Err(Error::DuplicatePayment) - }, - _ => { - let payment = PaymentDetails { - preimage: None, - hash: payment_hash, - secret: payment_secret, - amount_msat: invoice.amount_milli_satoshis(), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, - lsp_fee_limits: None, - }; - - self.payment_store.insert(payment)?; - Err(Error::PaymentSendingFailed) - }, - } - }, - } - } - - /// Send a payment given an invoice and an amount in millisatoshi. - /// - /// This will fail if the amount given is less than the value required by the given invoice. - /// - /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the - /// amount paid to be determined by the user. - pub fn send_payment_using_amount( - &self, invoice: &Bolt11Invoice, amount_msat: u64, - ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { - if amount_msat < invoice_amount_msat { - log_error!( - self.logger, - "Failed to pay as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); - return Err(Error::InvalidAmount); - } - } - - let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - if let Some(payment) = self.payment_store.get(&payment_hash) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: an invoice must not be paid twice."); - return Err(Error::DuplicatePayment); - } - } - - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); - let payment_secret = invoice.payment_secret(); - let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); - let mut payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), - invoice.min_final_cltv_expiry_delta() as u32, - ) - .with_expiry_time(expiry_time.as_secs()) - .with_route_hints(invoice.route_hints()) - .map_err(|_| Error::InvalidInvoice)?; - if let Some(features) = invoice.features() { - payment_params = payment_params - .with_bolt11_features(features.clone()) - .map_err(|_| Error::InvalidInvoice)?; - } - let route_params = - RouteParameters::from_payment_params_and_value(payment_params, amount_msat); - - let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); - - match self.channel_manager.send_payment( - payment_hash, - recipient_fields, - payment_id, - route_params, - retry_strategy, - ) { - Ok(_payment_id) => { - let payee_pubkey = invoice.recover_payee_pub_key(); - log_info!( - self.logger, - "Initiated sending {} msat to {}", - amount_msat, - payee_pubkey - ); - - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - }; - self.payment_store.insert(payment)?; - - Ok(payment_hash) - }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - - match e { - channelmanager::RetryableSendFailure::DuplicatePayment => { - Err(Error::DuplicatePayment) - }, - _ => { - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, - lsp_fee_limits: None, - }; - self.payment_store.insert(payment)?; - - Err(Error::PaymentSendingFailed) - }, - } - }, - } - } - /// Send a spontaneous, aka. "keysend", payment pub fn send_spontaneous_payment( &self, amount_msat: u64, node_id: PublicKey, @@ -1364,46 +1215,10 @@ impl Node { } } - /// Sends payment probes over all paths of a route that would be used to pay the given invoice. - /// - /// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting - /// the actual payment. Note this is only useful if there likely is sufficient time for the - /// probe to settle before sending out the actual payment, e.g., when waiting for user - /// confirmation in a wallet UI. - /// - /// Otherwise, there is a chance the probe could take up some liquidity needed to complete the - /// actual payment. Users should therefore be cautious and might avoid sending probes if - /// liquidity is scarce and/or they don't expect the probe to return before they send the - /// payment. To mitigate this issue, channels with available liquidity less than the required - /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send - /// pre-flight probes. - pub fn send_payment_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let (_payment_hash, _recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_payment_probes_using_amount instead."); - Error::InvalidInvoice - })?; - - let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); - - self.channel_manager - .send_preflight_probes(route_params, liquidity_limit_multiplier) - .map_err(|e| { - log_error!(self.logger, "Failed to send payment probes: {:?}", e); - Error::ProbeSendingFailed - })?; - - Ok(()) - } - /// Sends payment probes over all paths of a route that would be used to pay the given /// amount to the given `node_id`. /// - /// See [`Self::send_payment_probes`] for more information. + /// See [`Bolt11Payment::send_probes`] for more information. pub fn send_spontaneous_payment_probes( &self, amount_msat: u64, node_id: PublicKey, ) -> Result<(), Error> { @@ -1430,240 +1245,6 @@ impl Node { Ok(()) } - /// Sends payment probes over all paths of a route that would be used to pay the given - /// zero-value invoice using the given amount. - /// - /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an - /// invoice that leaves the amount paid to be determined by the user. - /// - /// See [`Self::send_payment_probes`] for more information. - pub fn send_payment_probes_using_amount( - &self, invoice: &Bolt11Invoice, amount_msat: u64, - ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = - invoice.amount_milli_satoshis() - { - if amount_msat < invoice_amount_msat { - log_error!( - self.logger, - "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); - return Err(Error::InvalidAmount); - } - - payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); - Error::InvalidInvoice - })? - } else { - payment::payment_parameters_from_zero_amount_invoice(&invoice, amount_msat).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); - Error::InvalidInvoice - })? - }; - - let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); - - self.channel_manager - .send_preflight_probes(route_params, liquidity_limit_multiplier) - .map_err(|e| { - log_error!(self.logger, "Failed to send payment probes: {:?}", e); - Error::ProbeSendingFailed - })?; - - Ok(()) - } - - /// Returns a payable invoice that can be used to request and receive a payment of the amount - /// given. - pub fn receive_payment( - &self, amount_msat: u64, description: &str, expiry_secs: u32, - ) -> Result { - self.receive_payment_inner(Some(amount_msat), description, expiry_secs) - } - - /// Returns a payable invoice that can be used to request and receive a payment for which the - /// amount is to be determined by the user, also known as a "zero-amount" invoice. - pub fn receive_variable_amount_payment( - &self, description: &str, expiry_secs: u32, - ) -> Result { - self.receive_payment_inner(None, description, expiry_secs) - } - - fn receive_payment_inner( - &self, amount_msat: Option, description: &str, expiry_secs: u32, - ) -> Result { - let currency = Currency::from(self.config.network); - let keys_manager = Arc::clone(&self.keys_manager); - let invoice = match lightning_invoice::utils::create_invoice_from_channelmanager( - &self.channel_manager, - keys_manager, - Arc::clone(&self.logger), - currency, - amount_msat, - description.to_string(), - expiry_secs, - None, - ) { - Ok(inv) => { - log_info!(self.logger, "Invoice created: {}", inv); - inv - }, - Err(e) => { - log_error!(self.logger, "Failed to create invoice: {}", e); - return Err(Error::InvoiceCreationFailed); - }, - }; - - let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), - amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - }; - - self.payment_store.insert(payment)?; - - Ok(invoice) - } - - /// Returns a payable invoice that can be used to request a payment of the amount given and - /// receive it via a newly created just-in-time (JIT) channel. - /// - /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel - /// to us, supplying just-in-time inbound liquidity. - /// - /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the - /// channel to us. We'll use its cheapest offer otherwise. - /// - /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - pub fn receive_payment_via_jit_channel( - &self, amount_msat: u64, description: &str, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, - ) -> Result { - self.receive_payment_via_jit_channel_inner( - Some(amount_msat), - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - None, - ) - } - - /// Returns a payable invoice that can be used to request a variable amount payment (also known - /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. - /// - /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel - /// to us, supplying just-in-time inbound liquidity. - /// - /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in - /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. - /// We'll use its cheapest offer otherwise. - /// - /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - pub fn receive_variable_amount_payment_via_jit_channel( - &self, description: &str, expiry_secs: u32, - max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { - self.receive_payment_via_jit_channel_inner( - None, - description, - expiry_secs, - None, - max_proportional_lsp_fee_limit_ppm_msat, - ) - } - - fn receive_payment_via_jit_channel_inner( - &self, amount_msat: Option, description: &str, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, - max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { - let liquidity_source = - self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - - let (node_id, address) = liquidity_source - .get_liquidity_source_details() - .ok_or(Error::LiquiditySourceUnavailable)?; - - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); - - let peer_info = PeerInfo { node_id, address }; - - let con_node_id = peer_info.node_id; - let con_addr = peer_info.address.clone(); - let con_cm = Arc::clone(&self.connection_manager); - - // We need to use our main runtime here as a local runtime might not be around to poll - // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) - })?; - - log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); - - let liquidity_source = Arc::clone(&liquidity_source); - let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = - tokio::task::block_in_place(move || { - runtime.block_on(async move { - if let Some(amount_msat) = amount_msat { - liquidity_source - .lsps2_receive_to_jit_channel( - amount_msat, - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - ) - .await - .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) - } else { - liquidity_source - .lsps2_receive_variable_amount_to_jit_channel( - description, - expiry_secs, - max_proportional_lsp_fee_limit_ppm_msat, - ) - .await - .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) - } - }) - })?; - - // Register payment in payment store. - let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - let lsp_fee_limits = Some(LSPFeeLimits { - max_total_opening_fee_msat: lsp_total_opening_fee, - max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, - }); - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), - amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - lsp_fee_limits, - }; - - self.payment_store.insert(payment)?; - - // Persist LSP peer to make sure we reconnect on restart. - self.peer_store.add_peer(peer_info)?; - - Ok(invoice) - } - /// Retrieve the details of a specific payment with the given hash. /// /// Returns `Some` if the payment was known and `None` otherwise. diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs new file mode 100644 index 000000000..8ac15fe0a --- /dev/null +++ b/src/payment/bolt11.rs @@ -0,0 +1,517 @@ +//! Holds a payment handler allowing to create and pay [BOLT 11] invoices. +//! +//! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::connection::ConnectionManager; +use crate::error::Error; +use crate::liquidity::LiquiditySource; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::payment_store::{ + LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore, +}; +use crate::peer_store::{PeerInfo, PeerStore}; +use crate::types::{ChannelManager, KeysManager}; + +use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; +use lightning::ln::PaymentHash; +use lightning::routing::router::{PaymentParameters, RouteParameters}; + +use lightning_invoice::{payment, Bolt11Invoice, Currency}; + +use bitcoin::hashes::Hash; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to create and pay [BOLT 11] invoices. +/// +/// Should be retrieved by calling [`Node::bolt11_payment`]. +/// +/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +/// [`Node::bolt11_payment`]: crate::Node::bolt11_payment +pub struct Bolt11Payment { + runtime: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, + keys_manager: Arc, + liquidity_source: Option>>>, + payment_store: Arc>>, + peer_store: Arc>>, + config: Arc, + logger: Arc, +} + +impl Bolt11Payment { + pub(crate) fn new( + runtime: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, + keys_manager: Arc, + liquidity_source: Option>>>, + payment_store: Arc>>, + peer_store: Arc>>, config: Arc, + logger: Arc, + ) -> Self { + Self { + runtime, + channel_manager, + connection_manager, + keys_manager, + liquidity_source, + payment_store, + peer_store, + config, + logger, + } + } + + /// Send a payment given an invoice. + pub fn send(&self, invoice: &Bolt11Invoice) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let (payment_hash, recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."); + Error::InvalidInvoice + })?; + + if let Some(payment) = self.payment_store.get(&payment_hash) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } + + let payment_secret = Some(*invoice.payment_secret()); + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + + match self.channel_manager.send_payment( + payment_hash, + recipient_onion, + payment_id, + route_params, + retry_strategy, + ) { + Ok(()) => { + let payee_pubkey = invoice.recover_payee_pub_key(); + let amt_msat = invoice.amount_milli_satoshis().unwrap(); + log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); + + let payment = PaymentDetails { + preimage: None, + hash: payment_hash, + secret: payment_secret, + amount_msat: invoice.amount_milli_satoshis(), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + self.payment_store.insert(payment)?; + + Ok(payment_hash) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + match e { + RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + _ => { + let payment = PaymentDetails { + preimage: None, + hash: payment_hash, + secret: payment_secret, + amount_msat: invoice.amount_milli_satoshis(), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Failed, + lsp_fee_limits: None, + }; + + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Send a payment given an invoice and an amount in millisatoshi. + /// + /// This will fail if the amount given is less than the value required by the given invoice. + /// + /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the + /// amount paid to be determined by the user. + pub fn send_using_amount( + &self, invoice: &Bolt11Invoice, amount_msat: u64, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { + if amount_msat < invoice_amount_msat { + log_error!( + self.logger, + "Failed to pay as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + return Err(Error::InvalidAmount); + } + } + + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + if let Some(payment) = self.payment_store.get(&payment_hash) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } + + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + let payment_secret = invoice.payment_secret(); + let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); + let mut payment_params = PaymentParameters::from_node_id( + invoice.recover_payee_pub_key(), + invoice.min_final_cltv_expiry_delta() as u32, + ) + .with_expiry_time(expiry_time.as_secs()) + .with_route_hints(invoice.route_hints()) + .map_err(|_| Error::InvalidInvoice)?; + if let Some(features) = invoice.features() { + payment_params = payment_params + .with_bolt11_features(features.clone()) + .map_err(|_| Error::InvalidInvoice)?; + } + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); + + match self.channel_manager.send_payment( + payment_hash, + recipient_fields, + payment_id, + route_params, + retry_strategy, + ) { + Ok(_payment_id) => { + let payee_pubkey = invoice.recover_payee_pub_key(); + log_info!( + self.logger, + "Initiated sending {} msat to {}", + amount_msat, + payee_pubkey + ); + + let payment = PaymentDetails { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + self.payment_store.insert(payment)?; + + Ok(payment_hash) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + + match e { + RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + _ => { + let payment = PaymentDetails { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Failed, + lsp_fee_limits: None, + }; + self.payment_store.insert(payment)?; + + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Returns a payable invoice that can be used to request and receive a payment of the amount + /// given. + pub fn receive( + &self, amount_msat: u64, description: &str, expiry_secs: u32, + ) -> Result { + self.receive_inner(Some(amount_msat), description, expiry_secs) + } + + /// Returns a payable invoice that can be used to request and receive a payment for which the + /// amount is to be determined by the user, also known as a "zero-amount" invoice. + pub fn receive_variable_amount( + &self, description: &str, expiry_secs: u32, + ) -> Result { + self.receive_inner(None, description, expiry_secs) + } + + fn receive_inner( + &self, amount_msat: Option, description: &str, expiry_secs: u32, + ) -> Result { + let currency = Currency::from(self.config.network); + let keys_manager = Arc::clone(&self.keys_manager); + let invoice = match lightning_invoice::utils::create_invoice_from_channelmanager( + &self.channel_manager, + keys_manager, + Arc::clone(&self.logger), + currency, + amount_msat, + description.to_string(), + expiry_secs, + None, + ) { + Ok(inv) => { + log_info!(self.logger, "Invoice created: {}", inv); + inv + }, + Err(e) => { + log_error!(self.logger, "Failed to create invoice: {}", e); + return Err(Error::InvoiceCreationFailed); + }, + }; + + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment = PaymentDetails { + hash: payment_hash, + preimage: None, + secret: Some(invoice.payment_secret().clone()), + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + + self.payment_store.insert(payment)?; + + Ok(invoice) + } + + /// Returns a payable invoice that can be used to request a payment of the amount given and + /// receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the + /// channel to us. We'll use its cheapest offer otherwise. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn receive_via_jit_channel( + &self, amount_msat: u64, description: &str, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, + ) -> Result { + self.receive_via_jit_channel_inner( + Some(amount_msat), + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + ) + } + + /// Returns a payable invoice that can be used to request a variable amount payment (also known + /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in + /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. + /// We'll use its cheapest offer otherwise. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn receive_variable_amount_via_jit_channel( + &self, description: &str, expiry_secs: u32, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + self.receive_via_jit_channel_inner( + None, + description, + expiry_secs, + None, + max_proportional_lsp_fee_limit_ppm_msat, + ) + } + + fn receive_via_jit_channel_inner( + &self, amount_msat: Option, description: &str, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (node_id, address) = liquidity_source + .get_liquidity_source_details() + .ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); + + let peer_info = PeerInfo { node_id, address }; + + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); + + let liquidity_source = Arc::clone(&liquidity_source); + let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = + tokio::task::block_in_place(move || { + runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_receive_to_jit_channel( + amount_msat, + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + ) + .await + .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) + } + }) + })?; + + // Register payment in payment store. + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let lsp_fee_limits = Some(LSPFeeLimits { + max_total_opening_fee_msat: lsp_total_opening_fee, + max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, + }); + let payment = PaymentDetails { + hash: payment_hash, + preimage: None, + secret: Some(invoice.payment_secret().clone()), + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits, + }; + + self.payment_store.insert(payment)?; + + // Persist LSP peer to make sure we reconnect on restart. + self.peer_store.add_peer(peer_info)?; + + Ok(invoice) + } + + /// Sends payment probes over all paths of a route that would be used to pay the given invoice. + /// + /// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting + /// the actual payment. Note this is only useful if there likely is sufficient time for the + /// probe to settle before sending out the actual payment, e.g., when waiting for user + /// confirmation in a wallet UI. + /// + /// Otherwise, there is a chance the probe could take up some liquidity needed to complete the + /// actual payment. Users should therefore be cautious and might avoid sending probes if + /// liquidity is scarce and/or they don't expect the probe to return before they send the + /// payment. To mitigate this issue, channels with available liquidity less than the required + /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send + /// pre-flight probes. + pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let (_payment_hash, _recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_probes_using_amount instead."); + Error::InvalidInvoice + })?; + + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); + + self.channel_manager + .send_preflight_probes(route_params, liquidity_limit_multiplier) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment probes: {:?}", e); + Error::ProbeSendingFailed + })?; + + Ok(()) + } + + /// Sends payment probes over all paths of a route that would be used to pay the given + /// zero-value invoice using the given amount. + /// + /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an + /// invoice that leaves the amount paid to be determined by the user. + /// + /// See [`Self::send_probes`] for more information. + pub fn send_probes_using_amount( + &self, invoice: &Bolt11Invoice, amount_msat: u64, + ) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = + invoice.amount_milli_satoshis() + { + if amount_msat < invoice_amount_msat { + log_error!( + self.logger, + "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + return Err(Error::InvalidAmount); + } + + payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); + Error::InvalidInvoice + })? + } else { + payment::payment_parameters_from_zero_amount_invoice(&invoice, amount_msat).map_err(|_| { + log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); + Error::InvalidInvoice + })? + }; + + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); + + self.channel_manager + .send_preflight_probes(route_params, liquidity_limit_multiplier) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment probes: {:?}", e); + Error::ProbeSendingFailed + })?; + + Ok(()) + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs new file mode 100644 index 000000000..adbe4df7b --- /dev/null +++ b/src/payment/mod.rs @@ -0,0 +1,5 @@ +//! Handlers for different types of payments. + +mod bolt11; + +pub use bolt11::Bolt11Payment; diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 0cef5d682..959c6e9ed 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -3,6 +3,8 @@ pub use lightning::ln::ChannelId; pub use lightning::ln::PaymentSecret; pub use lightning::util::string::UntrustedString; +pub use lightning_invoice::Bolt11Invoice; + pub use bitcoin::{BlockHash, Network, OutPoint}; pub use bip39::Mnemonic; @@ -18,7 +20,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Txid}; use lightning::ln::{PaymentHash, PaymentPreimage}; -use lightning_invoice::{Bolt11Invoice, SignedRawBolt11Invoice}; +use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; use std::str::FromStr; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b72ecdc75..c57ed4dbf 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -415,13 +415,13 @@ pub(crate) fn do_channel_full_cycle( let user_channel_id = expect_channel_ready_event!(node_b, node_a.node_id()); - println!("\nB receive_payment"); + println!("\nB receive"); let invoice_amount_1_msat = 2500_000; - let invoice = node_b.receive_payment(invoice_amount_1_msat, &"asdf", 9217).unwrap(); + let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, &"asdf", 9217).unwrap(); - println!("\nA send_payment"); - let payment_hash = node_a.send_payment(&invoice).unwrap(); - assert_eq!(node_a.send_payment(&invoice), Err(NodeError::DuplicatePayment)); + println!("\nA send"); + let payment_hash = node_a.bolt11_payment().send(&invoice).unwrap(); + assert_eq!(node_a.bolt11_payment().send(&invoice), Err(NodeError::DuplicatePayment)); assert_eq!(node_a.list_payments().first().unwrap().hash, payment_hash); @@ -451,7 +451,7 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); // Assert we fail duplicate outbound payments and check the status hasn't changed. - assert_eq!(Err(NodeError::DuplicatePayment), node_a.send_payment(&invoice)); + assert_eq!(Err(NodeError::DuplicatePayment), node_a.bolt11_payment().send(&invoice)); assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); @@ -461,20 +461,21 @@ pub(crate) fn do_channel_full_cycle( // Test under-/overpayment let invoice_amount_2_msat = 2500_000; - let invoice = node_b.receive_payment(invoice_amount_2_msat, &"asdf", 9217).unwrap(); + let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &"asdf", 9217).unwrap(); let underpaid_amount = invoice_amount_2_msat - 1; assert_eq!( Err(NodeError::InvalidAmount), - node_a.send_payment_using_amount(&invoice, underpaid_amount) + node_a.bolt11_payment().send_using_amount(&invoice, underpaid_amount) ); - println!("\nB overpaid receive_payment"); - let invoice = node_b.receive_payment(invoice_amount_2_msat, &"asdf", 9217).unwrap(); + println!("\nB overpaid receive"); + let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &"asdf", 9217).unwrap(); let overpaid_amount_msat = invoice_amount_2_msat + 100; - println!("\nA overpaid send_payment"); - let payment_hash = node_a.send_payment_using_amount(&invoice, overpaid_amount_msat).unwrap(); + println!("\nA overpaid send"); + let payment_hash = + node_a.bolt11_payment().send_using_amount(&invoice, overpaid_amount_msat).unwrap(); expect_event!(node_a, PaymentSuccessful); let received_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { @@ -496,12 +497,18 @@ pub(crate) fn do_channel_full_cycle( // Test "zero-amount" invoice payment println!("\nB receive_variable_amount_payment"); - let variable_amount_invoice = node_b.receive_variable_amount_payment(&"asdf", 9217).unwrap(); + let variable_amount_invoice = + node_b.bolt11_payment().receive_variable_amount(&"asdf", 9217).unwrap(); let determined_amount_msat = 2345_678; - assert_eq!(Err(NodeError::InvalidInvoice), node_a.send_payment(&variable_amount_invoice)); - println!("\nA send_payment_using_amount"); - let payment_hash = - node_a.send_payment_using_amount(&variable_amount_invoice, determined_amount_msat).unwrap(); + assert_eq!( + Err(NodeError::InvalidInvoice), + node_a.bolt11_payment().send(&variable_amount_invoice) + ); + println!("\nA send_using_amount"); + let payment_hash = node_a + .bolt11_payment() + .send_using_amount(&variable_amount_invoice, determined_amount_msat) + .unwrap(); expect_event!(node_a, PaymentSuccessful); let received_amount = match node_b.wait_next_event() { diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index dd4fefb4f..8dce54b28 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -97,7 +97,7 @@ fn test_cln() { cln_client.invoice(Some(10_000_000), &rand_label, &rand_label, None, None, None).unwrap(); let parsed_invoice = Bolt11Invoice::from_str(&cln_invoice.bolt11).unwrap(); - node.send_payment(&parsed_invoice).unwrap(); + node.bolt11_payment().send(&parsed_invoice).unwrap(); common::expect_event!(node, PaymentSuccessful); let cln_listed_invoices = cln_client.listinvoices(Some(&rand_label), None, None, None).unwrap().invoices; @@ -106,7 +106,7 @@ fn test_cln() { // Send a payment to LDK let rand_label: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let ldk_invoice = node.receive_payment(10_000_000, &rand_label, 3600).unwrap(); + let ldk_invoice = node.bolt11_payment().receive(10_000_000, &rand_label, 3600).unwrap(); cln_client.pay(&ldk_invoice.to_string(), Default::default()).unwrap(); common::expect_event!(node, PaymentReceived); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f0e222fd3..fad071c13 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -133,8 +133,8 @@ fn multi_hop_sending() { // Sleep a bit for gossip to propagate. std::thread::sleep(std::time::Duration::from_secs(1)); - let invoice = nodes[4].receive_payment(2_500_000, &"asdf", 9217).unwrap(); - nodes[0].send_payment(&invoice).unwrap(); + let invoice = nodes[4].bolt11_payment().receive(2_500_000, &"asdf", 9217).unwrap(); + nodes[0].bolt11_payment().send(&invoice).unwrap(); let payment_hash = expect_payment_received_event!(&nodes[4], 2_500_000); let fee_paid_msat = Some(2000); From 38eb21ec650e536e4f94391b806709085545409b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 5 Mar 2024 11:59:24 +0100 Subject: [PATCH 2/7] Move spontaneous payments API to `SpontaneousPaymentHandler` --- bindings/ldk_node.udl | 12 ++- src/lib.rs | 145 ++++++++----------------------------- src/payment/mod.rs | 2 + src/payment/spontaneous.rs | 143 ++++++++++++++++++++++++++++++++++++ src/uniffi_types.rs | 4 +- tests/common/mod.rs | 2 +- 6 files changed, 185 insertions(+), 123 deletions(-) create mode 100644 src/payment/spontaneous.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d896b2518..d4f80d25b 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -54,6 +54,7 @@ interface Node { PublicKey node_id(); sequence? listening_addresses(); Bolt11Payment bolt11_payment(); + SpontaneousPayment spontaneous_payment(); [Throws=NodeError] Address new_onchain_address(); [Throws=NodeError] @@ -72,10 +73,6 @@ interface Node { void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config); [Throws=NodeError] void sync_wallets(); - [Throws=NodeError] - PaymentHash send_spontaneous_payment(u64 amount_msat, PublicKey node_id); - [Throws=NodeError] - void send_spontaneous_payment_probes(u64 amount_msat, PublicKey node_id); PaymentDetails? payment([ByRef]PaymentHash payment_hash); [Throws=NodeError] void remove_payment([ByRef]PaymentHash payment_hash); @@ -107,6 +104,13 @@ interface Bolt11Payment { Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); }; +interface SpontaneousPayment { + [Throws=NodeError] + PaymentHash send(u64 amount_msat, PublicKey node_id); + [Throws=NodeError] + void send_probes(u64 amount_msat, PublicKey node_id); +}; + [Error] enum NodeError { "AlreadyRunning", diff --git a/src/lib.rs b/src/lib.rs index 03188806e..b622cf0d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,14 +123,14 @@ pub use builder::BuildError; pub use builder::NodeBuilder as Builder; use config::{ - LDK_PAYMENT_RETRY_TIMEOUT, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, - RGS_SYNC_INTERVAL, WALLET_SYNC_INTERVAL_MINIMUM_SECS, + NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use connection::ConnectionManager; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; -use payment::Bolt11Payment; +use payment::{Bolt11Payment, SpontaneousPayment}; use payment_store::PaymentStore; pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; use peer_store::{PeerInfo, PeerStore}; @@ -143,11 +143,8 @@ pub use types::{ChannelDetails, PeerDetails, UserChannelId}; use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; use lightning::chain::{BestBlock, Confirm}; -use lightning::ln::channelmanager::{self, PaymentId, RecipientOnionFields, Retry}; use lightning::ln::msgs::SocketAddress; -use lightning::ln::{PaymentHash, PaymentPreimage}; - -use lightning::sign::EntropySource; +use lightning::ln::PaymentHash; use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; pub use lightning::util::logger::Level as LogLevel; @@ -156,8 +153,6 @@ use lightning_background_processor::process_events_async; use lightning_transaction_sync::EsploraSyncClient; -use lightning::routing::router::{PaymentParameters, RouteParameters}; - use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Txid}; @@ -853,6 +848,32 @@ impl Node { )) } + /// Returns a payment handler allowing to send spontaneous ("keysend") payments. + #[cfg(not(feature = "uniffi"))] + pub fn spontaneous_payment(&self) -> SpontaneousPayment { + SpontaneousPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), + Arc::clone(&self.payment_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) + } + + /// Returns a payment handler allowing to send spontaneous ("keysend") payments. + #[cfg(feature = "uniffi")] + pub fn spontaneous_payment(&self) -> Arc { + Arc::new(SpontaneousPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), + Arc::clone(&self.payment_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + )) + } + /// Retrieve a new on-chain/funding address. pub fn new_onchain_address(&self) -> Result { let funding_address = self.wallet.get_new_address()?; @@ -1139,112 +1160,6 @@ impl Node { } } - /// Send a spontaneous, aka. "keysend", payment - pub fn send_spontaneous_payment( - &self, amount_msat: u64, node_id: PublicKey, - ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); - let payment_hash = PaymentHash::from(payment_preimage); - - if let Some(payment) = self.payment_store.get(&payment_hash) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: must not send duplicate payments."); - return Err(Error::DuplicatePayment); - } - } - - let route_params = RouteParameters::from_payment_params_and_value( - PaymentParameters::from_node_id(node_id, self.config.default_cltv_expiry_delta), - amount_msat, - ); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); - - match self.channel_manager.send_spontaneous_payment_with_retry( - Some(payment_preimage), - recipient_fields, - PaymentId(payment_hash.0), - route_params, - Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT), - ) { - Ok(_payment_id) => { - log_info!(self.logger, "Initiated sending {}msat to {}.", amount_msat, node_id); - - let payment = PaymentDetails { - hash: payment_hash, - preimage: Some(payment_preimage), - secret: None, - status: PaymentStatus::Pending, - direction: PaymentDirection::Outbound, - amount_msat: Some(amount_msat), - lsp_fee_limits: None, - }; - self.payment_store.insert(payment)?; - - Ok(payment_hash) - }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - - match e { - channelmanager::RetryableSendFailure::DuplicatePayment => { - Err(Error::DuplicatePayment) - }, - _ => { - let payment = PaymentDetails { - hash: payment_hash, - preimage: Some(payment_preimage), - secret: None, - status: PaymentStatus::Failed, - direction: PaymentDirection::Outbound, - amount_msat: Some(amount_msat), - lsp_fee_limits: None, - }; - - self.payment_store.insert(payment)?; - Err(Error::PaymentSendingFailed) - }, - } - }, - } - } - - /// Sends payment probes over all paths of a route that would be used to pay the given - /// amount to the given `node_id`. - /// - /// See [`Bolt11Payment::send_probes`] for more information. - pub fn send_spontaneous_payment_probes( - &self, amount_msat: u64, node_id: PublicKey, - ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); - let cltv_expiry_delta = self.config.default_cltv_expiry_delta; - - self.channel_manager - .send_spontaneous_preflight_probes( - node_id, - amount_msat, - cltv_expiry_delta, - liquidity_limit_multiplier, - ) - .map_err(|e| { - log_error!(self.logger, "Failed to send payment probes: {:?}", e); - Error::ProbeSendingFailed - })?; - - Ok(()) - } - /// Retrieve the details of a specific payment with the given hash. /// /// Returns `Some` if the payment was known and `None` otherwise. diff --git a/src/payment/mod.rs b/src/payment/mod.rs index adbe4df7b..5774f95a5 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -1,5 +1,7 @@ //! Handlers for different types of payments. mod bolt11; +mod spontaneous; pub use bolt11::Bolt11Payment; +pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs new file mode 100644 index 000000000..775fb2ff4 --- /dev/null +++ b/src/payment/spontaneous.rs @@ -0,0 +1,143 @@ +//! Holds a payment handler allowing to send spontaneous ("keysend") payments. + +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::error::Error; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::payment_store::{PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore}; +use crate::types::{ChannelManager, KeysManager}; + +use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; +use lightning::ln::{PaymentHash, PaymentPreimage}; +use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::sign::EntropySource; + +use bitcoin::secp256k1::PublicKey; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to send spontaneous ("keysend") payments. +/// +/// Should be retrieved by calling [`Node::spontaneous_payment`]. +/// +/// [`Node::spontaneous_payment`]: crate::Node::spontaneous_payment +pub struct SpontaneousPayment { + runtime: Arc>>, + channel_manager: Arc, + keys_manager: Arc, + payment_store: Arc>>, + config: Arc, + logger: Arc, +} + +impl SpontaneousPayment { + pub(crate) fn new( + runtime: Arc>>, + channel_manager: Arc, keys_manager: Arc, + payment_store: Arc>>, config: Arc, + logger: Arc, + ) -> Self { + Self { runtime, channel_manager, keys_manager, payment_store, config, logger } + } + + /// Send a spontaneous, aka. "keysend", payment + pub fn send(&self, amount_msat: u64, node_id: PublicKey) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); + let payment_hash = PaymentHash::from(payment_preimage); + + if let Some(payment) = self.payment_store.get(&payment_hash) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: must not send duplicate payments."); + return Err(Error::DuplicatePayment); + } + } + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(node_id, self.config.default_cltv_expiry_delta), + amount_msat, + ); + let recipient_fields = RecipientOnionFields::spontaneous_empty(); + + match self.channel_manager.send_spontaneous_payment_with_retry( + Some(payment_preimage), + recipient_fields, + PaymentId(payment_hash.0), + route_params, + Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT), + ) { + Ok(_payment_id) => { + log_info!(self.logger, "Initiated sending {}msat to {}.", amount_msat, node_id); + + let payment = PaymentDetails { + hash: payment_hash, + preimage: Some(payment_preimage), + secret: None, + status: PaymentStatus::Pending, + direction: PaymentDirection::Outbound, + amount_msat: Some(amount_msat), + lsp_fee_limits: None, + }; + self.payment_store.insert(payment)?; + + Ok(payment_hash) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + + match e { + RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + _ => { + let payment = PaymentDetails { + hash: payment_hash, + preimage: Some(payment_preimage), + secret: None, + status: PaymentStatus::Failed, + direction: PaymentDirection::Outbound, + amount_msat: Some(amount_msat), + lsp_fee_limits: None, + }; + + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Sends payment probes over all paths of a route that would be used to pay the given + /// amount to the given `node_id`. + /// + /// See [`Bolt11Payment::send_probes`] for more information. + /// + /// [`Bolt11Payment::send_probes`]: crate::payment::Bolt11Payment + pub fn send_probes(&self, amount_msat: u64, node_id: PublicKey) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); + let cltv_expiry_delta = self.config.default_cltv_expiry_delta; + + self.channel_manager + .send_spontaneous_preflight_probes( + node_id, + amount_msat, + cltv_expiry_delta, + liquidity_limit_multiplier, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment probes: {:?}", e); + Error::ProbeSendingFailed + })?; + + Ok(()) + } +} diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 959c6e9ed..0cef466a1 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,6 +1,5 @@ pub use lightning::events::{ClosureReason, PaymentFailureReason}; -pub use lightning::ln::ChannelId; -pub use lightning::ln::PaymentSecret; +pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; @@ -19,7 +18,6 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Txid}; -use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c57ed4dbf..05cceb268 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -533,7 +533,7 @@ pub(crate) fn do_channel_full_cycle( println!("\nA send_spontaneous_payment"); let keysend_amount_msat = 2500_000; let keysend_payment_hash = - node_a.send_spontaneous_payment(keysend_amount_msat, node_b.node_id()).unwrap(); + node_a.spontaneous_payment().send(keysend_amount_msat, node_b.node_id()).unwrap(); expect_event!(node_a, PaymentSuccessful); let received_keysend_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { From 5e502e6dcdf902fec676a5dc17c44f8d7825e122 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 5 Mar 2024 12:18:52 +0100 Subject: [PATCH 3/7] Move onchain payments API to `OnchainPaymentHandler` --- README.md | 2 +- .../lightningdevkit/ldknode/AndroidLibTest.kt | 4 +- .../lightningdevkit/ldknode/LibraryTest.kt | 4 +- bindings/ldk_node.udl | 16 +++-- bindings/python/src/ldk_node/test_ldk_node.py | 4 +- src/lib.rs | 51 ++++++--------- src/payment/mod.rs | 2 + src/payment/onchain.rs | 63 +++++++++++++++++++ src/uniffi_types.rs | 3 +- tests/common/mod.rs | 4 +- tests/integration_tests_cln.rs | 2 +- tests/integration_tests_rust.rs | 23 ++++--- 12 files changed, 117 insertions(+), 61 deletions(-) create mode 100644 src/payment/onchain.rs diff --git a/README.md b/README.md index eccb85c40..4078ce67b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ fn main() { node.start().unwrap(); - let funding_address = node.new_onchain_address(); + let funding_address = node.onchain_payment().new_address(); // .. fund address .. diff --git a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt index a5ca6eac0..763862a33 100644 --- a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt +++ b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt @@ -51,10 +51,10 @@ class AndroidLibTest { val nodeId2 = node2.nodeId() println("Node Id 2: $nodeId2") - val address1 = node1.newOnchainAddress() + val address1 = node1.onchain_payment().newOnchainAddress() println("Funding address 1: $address1") - val address2 = node2.newOnchainAddress() + val address2 = node2.onchain_payment().newOnchainAddress() println("Funding address 2: $address2") node1.stop() diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt index cacd33bd6..b6b86da9c 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt @@ -146,10 +146,10 @@ class LibraryTest { val nodeId2 = node2.nodeId() println("Node Id 2: $nodeId2") - val address1 = node1.newOnchainAddress() + val address1 = node1.onchainPayment().newAddress() println("Funding address 1: $address1") - val address2 = node2.newOnchainAddress() + val address2 = node2.onchainPayment().newAddress() println("Funding address 2: $address2") val txid1 = sendToAddress(address1, 100000u) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d4f80d25b..91425b75e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -55,12 +55,7 @@ interface Node { sequence? listening_addresses(); Bolt11Payment bolt11_payment(); SpontaneousPayment spontaneous_payment(); - [Throws=NodeError] - Address new_onchain_address(); - [Throws=NodeError] - Txid send_to_onchain_address([ByRef]Address address, u64 amount_msat); - [Throws=NodeError] - Txid send_all_to_onchain_address([ByRef]Address address); + OnchainPayment onchain_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -111,6 +106,15 @@ interface SpontaneousPayment { void send_probes(u64 amount_msat, PublicKey node_id); }; +interface OnchainPayment { + [Throws=NodeError] + Address new_address(); + [Throws=NodeError] + Txid send_to_address([ByRef]Address address, u64 amount_msat); + [Throws=NodeError] + Txid send_all_to_address([ByRef]Address address); +}; + [Error] enum NodeError { "AlreadyRunning", diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 468800efe..a593078c1 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -125,9 +125,9 @@ def test_channel_full_cycle(self): node_id_2 = node_2.node_id() print("Node ID 2:", node_id_2) - address_1 = node_1.new_onchain_address() + address_1 = node_1.onchain_payment().new_address() txid_1 = send_to_address(address_1, 100000) - address_2 = node_2.new_onchain_address() + address_2 = node_2.onchain_payment().new_address() txid_2 = send_to_address(address_2, 100000) wait_for_tx(esplora_endpoint, txid_1) diff --git a/src/lib.rs b/src/lib.rs index b622cf0d1..1d718a994 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ //! //! node.start().unwrap(); //! -//! let funding_address = node.new_onchain_address(); +//! let funding_address = node.onchain_payment().new_address(); //! //! // .. fund address .. //! @@ -130,7 +130,7 @@ use connection::ConnectionManager; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; -use payment::{Bolt11Payment, SpontaneousPayment}; +use payment::{Bolt11Payment, OnchainPayment, SpontaneousPayment}; use payment_store::PaymentStore; pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; use peer_store::{PeerInfo, PeerStore}; @@ -154,7 +154,6 @@ use lightning_background_processor::process_events_async; use lightning_transaction_sync::EsploraSyncClient; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Address, Txid}; use rand::Rng; @@ -874,38 +873,24 @@ impl Node { )) } - /// Retrieve a new on-chain/funding address. - pub fn new_onchain_address(&self) -> Result { - let funding_address = self.wallet.get_new_address()?; - log_info!(self.logger, "Generated new funding address: {}", funding_address); - Ok(funding_address) - } - - /// Send an on-chain payment to the given address. - pub fn send_to_onchain_address( - &self, address: &bitcoin::Address, amount_sats: u64, - ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let cur_balance = self.wallet.get_balance()?; - if cur_balance.get_spendable() < amount_sats { - log_error!(self.logger, "Unable to send payment due to insufficient funds."); - return Err(Error::InsufficientFunds); - } - self.wallet.send_to_address(address, Some(amount_sats)) + /// Returns a payment handler allowing to send and receive on-chain payments. + #[cfg(not(feature = "uniffi"))] + pub fn onchain_payment(&self) -> OnchainPayment { + OnchainPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.logger), + ) } - /// Send an on-chain payment to the given address, draining all the available funds. - pub fn send_all_to_onchain_address(&self, address: &bitcoin::Address) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - self.wallet.send_to_address(address, None) + /// Returns a payment handler allowing to send and receive on-chain payments. + #[cfg(feature = "uniffi")] + pub fn onchain_payment(&self) -> Arc { + Arc::new(OnchainPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.logger), + )) } /// Retrieve a list of known channels. diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 5774f95a5..1e0a09a84 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -1,7 +1,9 @@ //! Handlers for different types of payments. mod bolt11; +mod onchain; mod spontaneous; pub use bolt11::Bolt11Payment; +pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs new file mode 100644 index 000000000..c178e95a0 --- /dev/null +++ b/src/payment/onchain.rs @@ -0,0 +1,63 @@ +//! Holds a payment handler allowing to send and receive on-chain payments. + +use crate::error::Error; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::types::Wallet; + +use bitcoin::{Address, Txid}; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to send and receive on-chain payments. +/// +/// Should be retrieved by calling [`Node::onchain_payment`]. +/// +/// [`Node::onchain_payment`]: crate::Node::onchain_payment +pub struct OnchainPayment { + runtime: Arc>>, + wallet: Arc, + logger: Arc, +} + +impl OnchainPayment { + pub(crate) fn new( + runtime: Arc>>, wallet: Arc, + logger: Arc, + ) -> Self { + Self { runtime, wallet, logger } + } + + /// Retrieve a new on-chain/funding address. + pub fn new_address(&self) -> Result { + let funding_address = self.wallet.get_new_address()?; + log_info!(self.logger, "Generated new funding address: {}", funding_address); + Ok(funding_address) + } + + /// Send an on-chain payment to the given address. + pub fn send_to_address( + &self, address: &bitcoin::Address, amount_sats: u64, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let cur_balance = self.wallet.get_balance()?; + if cur_balance.get_spendable() < amount_sats { + log_error!(self.logger, "Unable to send payment due to insufficient funds."); + return Err(Error::InsufficientFunds); + } + self.wallet.send_to_address(address, Some(amount_sats)) + } + + /// Send an on-chain payment to the given address, draining all the available funds. + pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + self.wallet.send_to_address(address, None) + } +} diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 0cef466a1..7204f67f1 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -4,7 +4,7 @@ pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; -pub use bitcoin::{BlockHash, Network, OutPoint}; +pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; pub use bip39::Mnemonic; @@ -17,7 +17,6 @@ use crate::{SocketAddress, UserChannelId}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Address, Txid}; use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 05cceb268..a942e66f2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -355,8 +355,8 @@ pub fn open_channel( pub(crate) fn do_channel_full_cycle( node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool, ) { - let addr_a = node_a.new_onchain_address().unwrap(); - let addr_b = node_b.new_onchain_address().unwrap(); + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); let premine_amount_sat = 100_000; diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index 8dce54b28..ec752f474 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -44,7 +44,7 @@ fn test_cln() { node.start().unwrap(); // Premine some funds and distribute - let address = node.new_onchain_address().unwrap(); + let address = node.onchain_payment().new_address().unwrap(); let premine_amount = Amount::from_sat(5_000_000); common::premine_and_distribute_funds( &bitcoind_client, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fad071c13..fa8986c71 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -34,8 +34,8 @@ fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let (node_a, node_b) = setup_two_nodes(&electrsd, false); - let addr_a = node_a.new_onchain_address().unwrap(); - let addr_b = node_b.new_onchain_address().unwrap(); + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); let premine_amount_sat = 100_000; @@ -80,7 +80,7 @@ fn multi_hop_sending() { nodes.push(node); } - let addresses = nodes.iter().map(|n| n.new_onchain_address().unwrap()).collect(); + let addresses = nodes.iter().map(|n| n.onchain_payment().new_address().unwrap()).collect(); let premine_amount_sat = 5_000_000; premine_and_distribute_funds( &bitcoind.client, @@ -171,7 +171,7 @@ fn start_stop_reinit() { let expected_node_id = node.node_id(); assert_eq!(node.start(), Err(NodeError::AlreadyRunning)); - let funding_address = node.new_onchain_address().unwrap(); + let funding_address = node.onchain_payment().new_address().unwrap(); assert_eq!(node.list_balances().total_onchain_balance_sats, 0); @@ -225,8 +225,8 @@ fn onchain_spend_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let (node_a, node_b) = setup_two_nodes(&electrsd, false); - let addr_a = node_a.new_onchain_address().unwrap(); - let addr_b = node_b.new_onchain_address().unwrap(); + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); premine_and_distribute_funds( &bitcoind.client, @@ -239,9 +239,12 @@ fn onchain_spend_receive() { node_b.sync_wallets().unwrap(); assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 100000); - assert_eq!(Err(NodeError::InsufficientFunds), node_a.send_to_onchain_address(&addr_b, 1000)); + assert_eq!( + Err(NodeError::InsufficientFunds), + node_a.onchain_payment().send_to_address(&addr_b, 1000) + ); - let txid = node_b.send_to_onchain_address(&addr_a, 1000).unwrap(); + let txid = node_b.onchain_payment().send_to_address(&addr_a, 1000).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); @@ -252,8 +255,8 @@ fn onchain_spend_receive() { assert!(node_b.list_balances().spendable_onchain_balance_sats > 98000); assert!(node_b.list_balances().spendable_onchain_balance_sats < 100000); - let addr_b = node_b.new_onchain_address().unwrap(); - let txid = node_a.send_all_to_onchain_address(&addr_b).unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let txid = node_a.onchain_payment().send_all_to_address(&addr_b).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); From a78ed43b7a9e8c21540c2098c59b17405827f7a5 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 5 Mar 2024 13:48:19 +0100 Subject: [PATCH 4/7] Move `payment_store` to `payment` submodule .. which declutters our top-level docs a bit. --- src/builder.rs | 2 +- src/event.rs | 2 +- src/lib.rs | 9 ++++----- src/payment/bolt11.rs | 2 +- src/payment/mod.rs | 4 +++- src/payment/spontaneous.rs | 2 +- src/{payment_store.rs => payment/store.rs} | 0 src/uniffi_types.rs | 2 ++ tests/common/mod.rs | 5 ++--- 9 files changed, 15 insertions(+), 13 deletions(-) rename src/{payment_store.rs => payment/store.rs} (100%) diff --git a/src/builder.rs b/src/builder.rs index 204088c6a..6d3db420f 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,7 +11,7 @@ use crate::io::sqlite_store::SqliteStore; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment_store::PaymentStore; +use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ diff --git a/src/event.rs b/src/event.rs index 6e9ab587c..05fead0ae 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,7 +3,7 @@ use crate::{ hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, }; -use crate::payment_store::{ +use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentStatus, PaymentStore, }; diff --git a/src/lib.rs b/src/lib.rs index 1d718a994..53df151f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,6 @@ mod liquidity; mod logger; mod message_handler; pub mod payment; -mod payment_store; mod peer_store; mod sweep; mod tx_broadcaster; @@ -130,9 +129,8 @@ use connection::ConnectionManager; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; -use payment::{Bolt11Payment, OnchainPayment, SpontaneousPayment}; -use payment_store::PaymentStore; -pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; +use payment::store::PaymentStore; +use payment::{Bolt11Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, ChainMonitor, ChannelManager, DynStore, FeeEstimator, KeysManager, NetworkGraph, @@ -1208,7 +1206,8 @@ impl Node { /// /// For example, you could retrieve all stored outbound payments as follows: /// ``` - /// # use ldk_node::{Builder, Config, PaymentDirection}; + /// # use ldk_node::{Builder, Config}; + /// # use ldk_node::payment::PaymentDirection; /// # use ldk_node::bitcoin::Network; /// # let mut config = Config::default(); /// # config.network = Network::Regtest; diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 8ac15fe0a..10bbd5fbd 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -7,7 +7,7 @@ use crate::connection::ConnectionManager; use crate::error::Error; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; -use crate::payment_store::{ +use crate::payment::store::{ LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore, }; use crate::peer_store::{PeerInfo, PeerStore}; diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 1e0a09a84..0666bd69f 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -1,9 +1,11 @@ -//! Handlers for different types of payments. +//! Objects for different types of payments. mod bolt11; mod onchain; mod spontaneous; +pub(crate) mod store; pub use bolt11::Bolt11Payment; pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; +pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 775fb2ff4..f81c23a4b 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -3,7 +3,7 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; -use crate::payment_store::{PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore}; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore}; use crate::types::{ChannelManager, KeysManager}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; diff --git a/src/payment_store.rs b/src/payment/store.rs similarity index 100% rename from src/payment_store.rs rename to src/payment/store.rs diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 7204f67f1..2bddfee13 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,3 +1,5 @@ +pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentStatus}; + pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning::util::string::UntrustedString; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a942e66f2..a9acb63a0 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,9 +2,8 @@ #![allow(dead_code)] use ldk_node::io::sqlite_store::SqliteStore; -use ldk_node::{ - Builder, Config, Event, LogLevel, Node, NodeError, PaymentDirection, PaymentStatus, -}; +use ldk_node::payment::{PaymentDirection, PaymentStatus}; +use ldk_node::{Builder, Config, Event, LogLevel, Node, NodeError}; use lightning::ln::msgs::SocketAddress; use lightning::util::persist::KVStore; From 7cd8a7261d56d041d2518217e75fad4b4f688b44 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 8 Mar 2024 11:17:29 +0100 Subject: [PATCH 5/7] Introduce `PaymentKind` to payment store, track by `PaymentId` --- bindings/ldk_node.udl | 28 +++-- src/error.rs | 3 + src/event.rs | 91 +++++++++----- src/lib.rs | 14 +-- src/payment/bolt11.rs | 89 +++++++------ src/payment/mod.rs | 2 +- src/payment/spontaneous.rs | 32 +++-- src/payment/store.rs | 250 +++++++++++++++++++++++++++---------- src/uniffi_types.rs | 21 +++- tests/common/mod.rs | 79 ++++++------ 10 files changed, 400 insertions(+), 209 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 91425b75e..1d776782b 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -68,9 +68,9 @@ interface Node { void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config); [Throws=NodeError] void sync_wallets(); - PaymentDetails? payment([ByRef]PaymentHash payment_hash); + PaymentDetails? payment([ByRef]PaymentId payment_id); [Throws=NodeError] - void remove_payment([ByRef]PaymentHash payment_hash); + void remove_payment([ByRef]PaymentId payment_id); BalanceDetails list_balances(); sequence list_payments(); sequence list_peers(); @@ -82,9 +82,9 @@ interface Node { interface Bolt11Payment { [Throws=NodeError] - PaymentHash send([ByRef]Bolt11Invoice invoice); + PaymentId send([ByRef]Bolt11Invoice invoice); [Throws=NodeError] - PaymentHash send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); [Throws=NodeError] void send_probes([ByRef]Bolt11Invoice invoice); [Throws=NodeError] @@ -101,7 +101,7 @@ interface Bolt11Payment { interface SpontaneousPayment { [Throws=NodeError] - PaymentHash send(u64 amount_msat, PublicKey node_id); + PaymentId send(u64 amount_msat, PublicKey node_id); [Throws=NodeError] void send_probes(u64 amount_msat, PublicKey node_id); }; @@ -139,6 +139,7 @@ enum NodeError { "InvalidSocketAddress", "InvalidPublicKey", "InvalidSecretKey", + "InvalidPaymentId", "InvalidPaymentHash", "InvalidPaymentPreimage", "InvalidPaymentSecret", @@ -219,6 +220,14 @@ interface ClosureReason { HTLCsTimedOut(); }; +[Enum] +interface PaymentKind { + Onchain(); + Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); + Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits); + Spontaneous(PaymentHash hash, PaymentPreimage? preimage); +}; + enum PaymentDirection { "Inbound", "Outbound", @@ -236,13 +245,11 @@ dictionary LSPFeeLimits { }; dictionary PaymentDetails { - PaymentHash hash; - PaymentPreimage? preimage; - PaymentSecret? secret; + PaymentId id; + PaymentKind kind; u64? amount_msat; PaymentDirection direction; PaymentStatus status; - LSPFeeLimits? lsp_fee_limits; }; [NonExhaustive] @@ -364,6 +371,9 @@ typedef string Address; [Custom] typedef string Bolt11Invoice; +[Custom] +typedef string PaymentId; + [Custom] typedef string PaymentHash; diff --git a/src/error.rs b/src/error.rs index c5234a6d4..5acc75af8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -47,6 +47,8 @@ pub enum Error { InvalidPublicKey, /// The given secret key is invalid. InvalidSecretKey, + /// The given payment id is invalid. + InvalidPaymentId, /// The given payment hash is invalid. InvalidPaymentHash, /// The given payment pre-image is invalid. @@ -100,6 +102,7 @@ impl fmt::Display for Error { Self::InvalidSocketAddress => write!(f, "The given network address is invalid."), Self::InvalidPublicKey => write!(f, "The given public key is invalid."), Self::InvalidSecretKey => write!(f, "The given secret key is invalid."), + Self::InvalidPaymentId => write!(f, "The given payment id is invalid."), Self::InvalidPaymentHash => write!(f, "The given payment hash is invalid."), Self::InvalidPaymentPreimage => write!(f, "The given payment preimage is invalid."), Self::InvalidPaymentSecret => write!(f, "The given payment secret is invalid."), diff --git a/src/event.rs b/src/event.rs index 05fead0ae..d4e3d57bc 100644 --- a/src/event.rs +++ b/src/event.rs @@ -4,7 +4,8 @@ use crate::{ }; use crate::payment::store::{ - PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentStatus, PaymentStore, + PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, + PaymentStore, }; use crate::io::{ @@ -17,6 +18,7 @@ use lightning::chain::chaininterface::ConfirmationTarget; use lightning::events::{ClosureReason, PaymentPurpose}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; use lightning::impl_writeable_tlv_based_enum; +use lightning::ln::channelmanager::PaymentId; use lightning::ln::{ChannelId, PaymentHash}; use lightning::routing::gossip::NodeId; use lightning::util::errors::APIError; @@ -410,7 +412,8 @@ where onion_fields: _, counterparty_skimmed_fee_msat, } => { - if let Some(info) = self.payment_store.get(&payment_hash) { + let payment_id = PaymentId(payment_hash.0); + if let Some(info) = self.payment_store.get(&payment_id) { if info.status == PaymentStatus::Succeeded { log_info!( self.logger, @@ -422,7 +425,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); @@ -431,17 +434,22 @@ where return; } - let max_total_opening_fee_msat = info - .lsp_fee_limits - .and_then(|l| { - l.max_total_opening_fee_msat.or_else(|| { - l.max_proportional_opening_fee_ppm_msat.and_then(|max_prop_fee| { - // If it's a variable amount payment, compute the actual fee. - compute_opening_fee(amount_msat, 0, max_prop_fee) + let max_total_opening_fee_msat = match info.kind { + PaymentKind::Bolt11Jit { lsp_fee_limits, .. } => { + lsp_fee_limits + .max_total_opening_fee_msat + .or_else(|| { + lsp_fee_limits.max_proportional_opening_fee_ppm_msat.and_then( + |max_prop_fee| { + // If it's a variable amount payment, compute the actual fee. + compute_opening_fee(amount_msat, 0, max_prop_fee) + }, + ) }) - }) - }) - .unwrap_or(0); + .unwrap_or(0) + }, + _ => 0, + }; if counterparty_skimmed_fee_msat > max_total_opening_fee_msat { log_info!( @@ -455,7 +463,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); @@ -516,7 +524,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); @@ -538,6 +546,7 @@ where hex_utils::to_string(&payment_hash.0), amount_msat, ); + let payment_id = PaymentId(payment_hash.0); match purpose { PaymentPurpose::Bolt11InvoicePayment { payment_preimage, @@ -549,7 +558,7 @@ where secret: Some(Some(payment_secret)), amount_msat: Some(Some(amount_msat)), status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; match self.payment_store.update(&update) { Ok(true) => (), @@ -592,13 +601,14 @@ where }, PaymentPurpose::SpontaneousPayment(preimage) => { let payment = PaymentDetails { - preimage: Some(preimage), - hash: payment_hash, - secret: None, + id: payment_id, + kind: PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(preimage), + }, amount_msat: Some(amount_msat), direction: PaymentDirection::Inbound, status: PaymentStatus::Succeeded, - lsp_fee_limits: None, }; match self.payment_store.insert(payment) { @@ -631,14 +641,32 @@ where panic!("Failed to push to event queue"); }); }, - LdkEvent::PaymentSent { payment_preimage, payment_hash, fee_paid_msat, .. } => { - if let Some(mut payment) = self.payment_store.get(&payment_hash) { - payment.preimage = Some(payment_preimage); - payment.status = PaymentStatus::Succeeded; - self.payment_store.insert(payment.clone()).unwrap_or_else(|e| { - log_error!(self.logger, "Failed to access payment store: {}", e); - panic!("Failed to access payment store"); - }); + LdkEvent::PaymentSent { + payment_id, + payment_preimage, + payment_hash, + fee_paid_msat, + .. + } => { + let payment_id = if let Some(id) = payment_id { + id + } else { + debug_assert!(false, "payment_id should always be set."); + return; + }; + + let update = PaymentDetailsUpdate { + preimage: Some(Some(payment_preimage)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }; + + self.payment_store.update(&update).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to access payment store: {}", e); + panic!("Failed to access payment store"); + }); + + self.payment_store.get(&payment_id).map(|payment| { log_info!( self.logger, "Successfully sent payment of {}msat{} from \ @@ -652,7 +680,8 @@ where hex_utils::to_string(&payment_hash.0), hex_utils::to_string(&payment_preimage.0) ); - } + }); + self.event_queue .add_event(Event::PaymentSuccessful { payment_hash, fee_paid_msat }) .unwrap_or_else(|e| { @@ -660,7 +689,7 @@ where panic!("Failed to push to event queue"); }); }, - LdkEvent::PaymentFailed { payment_hash, reason, .. } => { + LdkEvent::PaymentFailed { payment_id, payment_hash, reason, .. } => { log_info!( self.logger, "Failed to send payment to payment hash {:?} due to {:?}.", @@ -670,7 +699,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); diff --git a/src/lib.rs b/src/lib.rs index 53df151f1..3d619cebb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,8 +141,8 @@ pub use types::{ChannelDetails, PeerDetails, UserChannelId}; use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; use lightning::chain::{BestBlock, Confirm}; +use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; -use lightning::ln::PaymentHash; use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; pub use lightning::util::logger::Level as LogLevel; @@ -1143,16 +1143,16 @@ impl Node { } } - /// Retrieve the details of a specific payment with the given hash. + /// Retrieve the details of a specific payment with the given id. /// /// Returns `Some` if the payment was known and `None` otherwise. - pub fn payment(&self, payment_hash: &PaymentHash) -> Option { - self.payment_store.get(payment_hash) + pub fn payment(&self, payment_id: &PaymentId) -> Option { + self.payment_store.get(payment_id) } - /// Remove the payment with the given hash from the store. - pub fn remove_payment(&self, payment_hash: &PaymentHash) -> Result<(), Error> { - self.payment_store.remove(&payment_hash) + /// Remove the payment with the given id from the store. + pub fn remove_payment(&self, payment_id: &PaymentId) -> Result<(), Error> { + self.payment_store.remove(&payment_id) } /// Retrieves an overview of all known balances. diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 10bbd5fbd..c23f7b670 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -8,7 +8,7 @@ use crate::error::Error; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; use crate::payment::store::{ - LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore, + LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::types::{ChannelManager, KeysManager}; @@ -66,7 +66,7 @@ impl Bolt11Payment { } /// Send a payment given an invoice. - pub fn send(&self, invoice: &Bolt11Invoice) -> Result { + pub fn send(&self, invoice: &Bolt11Invoice) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -77,7 +77,8 @@ impl Bolt11Payment { Error::InvalidInvoice })?; - if let Some(payment) = self.payment_store.get(&payment_hash) { + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + if let Some(payment) = self.payment_store.get(&payment_id) { if payment.status == PaymentStatus::Pending || payment.status == PaymentStatus::Succeeded { @@ -87,7 +88,6 @@ impl Bolt11Payment { } let payment_secret = Some(*invoice.payment_secret()); - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); match self.channel_manager.send_payment( @@ -103,17 +103,20 @@ impl Bolt11Payment { log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); let payment = PaymentDetails { - preimage: None, - hash: payment_hash, - secret: payment_secret, + id: payment_id, + kind: PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: payment_secret, + }, amount_msat: invoice.amount_milli_satoshis(), direction: PaymentDirection::Outbound, status: PaymentStatus::Pending, - lsp_fee_limits: None, }; + self.payment_store.insert(payment)?; - Ok(payment_hash) + Ok(payment_id) }, Err(e) => { log_error!(self.logger, "Failed to send payment: {:?}", e); @@ -121,13 +124,15 @@ impl Bolt11Payment { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { let payment = PaymentDetails { - preimage: None, - hash: payment_hash, - secret: payment_secret, + id: payment_id, + kind: PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: payment_secret, + }, amount_msat: invoice.amount_milli_satoshis(), direction: PaymentDirection::Outbound, status: PaymentStatus::Failed, - lsp_fee_limits: None, }; self.payment_store.insert(payment)?; @@ -146,7 +151,7 @@ impl Bolt11Payment { /// amount paid to be determined by the user. pub fn send_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, - ) -> Result { + ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -162,7 +167,8 @@ impl Bolt11Payment { } let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - if let Some(payment) = self.payment_store.get(&payment_hash) { + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + if let Some(payment) = self.payment_store.get(&payment_id) { if payment.status == PaymentStatus::Pending || payment.status == PaymentStatus::Succeeded { @@ -171,7 +177,6 @@ impl Bolt11Payment { } } - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); let payment_secret = invoice.payment_secret(); let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); let mut payment_params = PaymentParameters::from_node_id( @@ -199,7 +204,7 @@ impl Bolt11Payment { route_params, retry_strategy, ) { - Ok(_payment_id) => { + Ok(()) => { let payee_pubkey = invoice.recover_payee_pub_key(); log_info!( self.logger, @@ -209,17 +214,19 @@ impl Bolt11Payment { ); let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), + id: payment_id, + kind: PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), + }, amount_msat: Some(amount_msat), direction: PaymentDirection::Outbound, status: PaymentStatus::Pending, - lsp_fee_limits: None, }; self.payment_store.insert(payment)?; - Ok(payment_hash) + Ok(payment_id) }, Err(e) => { log_error!(self.logger, "Failed to send payment: {:?}", e); @@ -228,13 +235,15 @@ impl Bolt11Payment { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), + id: payment_id, + kind: PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), + }, amount_msat: Some(amount_msat), direction: PaymentDirection::Outbound, status: PaymentStatus::Failed, - lsp_fee_limits: None, }; self.payment_store.insert(payment)?; @@ -287,14 +296,18 @@ impl Bolt11Payment { }; let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let id = PaymentId(payment_hash.0); let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), + id, + kind: PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(invoice.payment_secret().clone()), + }, + amount_msat, direction: PaymentDirection::Inbound, status: PaymentStatus::Pending, - lsp_fee_limits: None, }; self.payment_store.insert(payment)?; @@ -409,18 +422,22 @@ impl Bolt11Payment { // Register payment in payment store. let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - let lsp_fee_limits = Some(LSPFeeLimits { + let lsp_fee_limits = LSPFeeLimits { max_total_opening_fee_msat: lsp_total_opening_fee, max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, - }); + }; + let id = PaymentId(payment_hash.0); let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), + id, + kind: PaymentKind::Bolt11Jit { + hash: payment_hash, + preimage: None, + secret: Some(invoice.payment_secret().clone()), + lsp_fee_limits, + }, amount_msat, direction: PaymentDirection::Inbound, status: PaymentStatus::Pending, - lsp_fee_limits, }; self.payment_store.insert(payment)?; diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 0666bd69f..3649f1fcc 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -8,4 +8,4 @@ pub(crate) mod store; pub use bolt11::Bolt11Payment; pub use onchain::OnchainPayment; pub use spontaneous::SpontaneousPayment; -pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; +pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index f81c23a4b..fcca8065a 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -3,7 +3,9 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; -use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentStatus, PaymentStore}; +use crate::payment::store::{ + PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, +}; use crate::types::{ChannelManager, KeysManager}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; @@ -40,7 +42,7 @@ impl SpontaneousPayment { } /// Send a spontaneous, aka. "keysend", payment - pub fn send(&self, amount_msat: u64, node_id: PublicKey) -> Result { + pub fn send(&self, amount_msat: u64, node_id: PublicKey) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -48,8 +50,9 @@ impl SpontaneousPayment { let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); let payment_hash = PaymentHash::from(payment_preimage); + let payment_id = PaymentId(payment_hash.0); - if let Some(payment) = self.payment_store.get(&payment_hash) { + if let Some(payment) = self.payment_store.get(&payment_id) { if payment.status == PaymentStatus::Pending || payment.status == PaymentStatus::Succeeded { @@ -71,21 +74,22 @@ impl SpontaneousPayment { route_params, Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT), ) { - Ok(_payment_id) => { + Ok(_hash) => { log_info!(self.logger, "Initiated sending {}msat to {}.", amount_msat, node_id); let payment = PaymentDetails { - hash: payment_hash, - preimage: Some(payment_preimage), - secret: None, + id: payment_id, + kind: PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(payment_preimage), + }, status: PaymentStatus::Pending, direction: PaymentDirection::Outbound, amount_msat: Some(amount_msat), - lsp_fee_limits: None, }; self.payment_store.insert(payment)?; - Ok(payment_hash) + Ok(payment_id) }, Err(e) => { log_error!(self.logger, "Failed to send payment: {:?}", e); @@ -94,13 +98,15 @@ impl SpontaneousPayment { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { let payment = PaymentDetails { - hash: payment_hash, - preimage: Some(payment_preimage), - secret: None, + id: payment_id, + kind: PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(payment_preimage), + }, + status: PaymentStatus::Failed, direction: PaymentDirection::Outbound, amount_msat: Some(amount_msat), - lsp_fee_limits: None, }; self.payment_store.insert(payment)?; diff --git a/src/payment/store.rs b/src/payment/store.rs index 524984e9e..1a85152e2 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -6,9 +6,14 @@ use crate::logger::{log_error, Logger}; use crate::types::DynStore; use crate::Error; +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::DecodeError; use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; -use lightning::util::ser::Writeable; -use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; +use lightning::util::ser::{Readable, Writeable}; +use lightning::{ + _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, + impl_writeable_tlv_based_enum, write_tlv_fields, +}; use std::collections::HashMap; use std::iter::FromIterator; @@ -18,38 +23,88 @@ use std::sync::{Arc, Mutex}; /// Represents a payment. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PaymentDetails { - /// The payment hash, i.e., the hash of the `preimage`. - pub hash: PaymentHash, - /// The pre-image used by the payment. - pub preimage: Option, - /// The secret used by the payment. - pub secret: Option, + /// The identifier of this payment. + pub id: PaymentId, + /// The kind of the payment. + pub kind: PaymentKind, /// The amount transferred. pub amount_msat: Option, /// The direction of the payment. pub direction: PaymentDirection, /// The status of the payment. pub status: PaymentStatus, - /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. - /// - /// This is only `Some` for payments received via a JIT-channel, in which case the first - /// inbound payment will pay for the LSP's channel opening fees. - /// - /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. - /// - /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs - pub lsp_fee_limits: Option, } -impl_writeable_tlv_based!(PaymentDetails, { - (0, hash, required), - (1, lsp_fee_limits, option), - (2, preimage, required), - (4, secret, required), - (6, amount_msat, required), - (8, direction, required), - (10, status, required) -}); +impl Writeable for PaymentDetails { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.id, required), // Used to be `hash` for v0.2.1 and prior + // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. + // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 + (2, None::>, required), + (3, self.kind, required), + // 4 used to be `secret` before it was moved to `kind` in v0.3.0 + (4, None::>, required), + (6, self.amount_msat, required), + (8, self.direction, required), + (10, self.status, required) + }); + Ok(()) + } +} + +impl Readable for PaymentDetails { + fn read(reader: &mut R) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, id, required), // Used to be `hash` + (1, lsp_fee_limits, option), + (2, preimage, required), + (3, kind_opt, option), + (4, secret, required), + (6, amount_msat, required), + (8, direction, required), + (10, status, required) + }); + + let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; + let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; + let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; + let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; + let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; + let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; + + let kind = if let Some(kind) = kind_opt { + // If we serialized the payment kind, use it. + // This will always be the case for any version after v0.2.1. + kind + } else { + // Otherwise we persisted with v0.2.1 or before, and puzzle together the kind from the + // provided fields. + + // We used to track everything by hash, but switched to track everything by id + // post-v0.2.1. As both are serialized identically, we just switched the `0`-type field above + // from `PaymentHash` to `PaymentId` and serialize a separate `PaymentHash` in + // `PaymentKind` when needed. Here, for backwards compat, we can just re-create the + // `PaymentHash` from the id, as 'back then' `payment_hash == payment_id` was always + // true. + let hash = PaymentHash(id.0); + + if secret.is_some() { + if let Some(lsp_fee_limits) = lsp_fee_limits { + PaymentKind::Bolt11Jit { hash, preimage, secret, lsp_fee_limits } + } else { + PaymentKind::Bolt11 { hash, preimage, secret } + } + } else { + PaymentKind::Spontaneous { hash, preimage } + } + }; + + Ok(PaymentDetails { id, kind, amount_msat, direction, status }) + } +} /// Represents the direction of a payment. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -82,6 +137,73 @@ impl_writeable_tlv_based_enum!(PaymentStatus, (4, Failed) => {}; ); +/// Represents the kind of a payment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PaymentKind { + /// An on-chain payment. + Onchain, + /// A [BOLT 11] payment. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + // TODO: Bolt11 { invoice: Option }, + Bolt11 { + /// The payment hash, i.e., the hash of the `preimage`. + hash: PaymentHash, + /// The pre-image used by the payment. + preimage: Option, + /// The secret used by the payment. + secret: Option, + }, + /// A [BOLT 11] payment intended to open an [LSPS 2] just-in-time channel. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + /// [LSPS 2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + // TODO: Bolt11Jit { invoice: Option }, + Bolt11Jit { + /// The payment hash, i.e., the hash of the `preimage`. + hash: PaymentHash, + /// The pre-image used by the payment. + preimage: Option, + /// The secret used by the payment. + secret: Option, + /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. + /// + /// Allowing them to deduct this fee from the first inbound payment will pay for the LSP's + /// channel opening fees. + /// + /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. + /// + /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs + lsp_fee_limits: LSPFeeLimits, + }, + /// A spontaneous ("keysend") payment. + Spontaneous { + /// The payment hash, i.e., the hash of the `preimage`. + hash: PaymentHash, + /// The pre-image used by the payment. + preimage: Option, + }, +} + +impl_writeable_tlv_based_enum!(PaymentKind, + (0, Onchain) => {}, + (2, Bolt11) => { + (0, hash, required), + (2, preimage, option), + (4, secret, option), + }, + (4, Bolt11Jit) => { + (0, hash, required), + (2, preimage, option), + (4, secret, option), + (6, lsp_fee_limits, required), + }, + (8, Spontaneous) => { + (0, hash, required), + (2, preimage, option), + }; +); + /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. /// /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. @@ -104,26 +226,17 @@ impl_writeable_tlv_based!(LSPFeeLimits, { #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct PaymentDetailsUpdate { - pub hash: PaymentHash, + pub id: PaymentId, pub preimage: Option>, pub secret: Option>, pub amount_msat: Option>, pub direction: Option, pub status: Option, - pub lsp_fee_limits: Option>, } impl PaymentDetailsUpdate { - pub fn new(hash: PaymentHash) -> Self { - Self { - hash, - preimage: None, - secret: None, - amount_msat: None, - direction: None, - status: None, - lsp_fee_limits: None, - } + pub fn new(id: PaymentId) -> Self { + Self { id, preimage: None, secret: None, amount_msat: None, direction: None, status: None } } } @@ -131,7 +244,7 @@ pub(crate) struct PaymentStore where L::Target: Logger, { - payments: Mutex>, + payments: Mutex>, kv_store: Arc, logger: L, } @@ -142,7 +255,7 @@ where { pub(crate) fn new(payments: Vec, kv_store: Arc, logger: L) -> Self { let payments = Mutex::new(HashMap::from_iter( - payments.into_iter().map(|payment| (payment.hash, payment)), + payments.into_iter().map(|payment| (payment.id, payment)), )); Self { payments, kv_store, logger } } @@ -150,14 +263,13 @@ where pub(crate) fn insert(&self, payment: PaymentDetails) -> Result { let mut locked_payments = self.payments.lock().unwrap(); - let hash = payment.hash.clone(); - let updated = locked_payments.insert(hash.clone(), payment.clone()).is_some(); - self.persist_info(&hash, &payment)?; + let updated = locked_payments.insert(payment.id, payment.clone()).is_some(); + self.persist_info(&payment.id, &payment)?; Ok(updated) } - pub(crate) fn remove(&self, hash: &PaymentHash) -> Result<(), Error> { - let store_key = hex_utils::to_string(&hash.0); + pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> { + let store_key = hex_utils::to_string(&id.0); self.kv_store .remove( PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, @@ -178,21 +290,30 @@ where }) } - pub(crate) fn get(&self, hash: &PaymentHash) -> Option { - self.payments.lock().unwrap().get(hash).cloned() + pub(crate) fn get(&self, id: &PaymentId) -> Option { + self.payments.lock().unwrap().get(id).cloned() } pub(crate) fn update(&self, update: &PaymentDetailsUpdate) -> Result { let mut updated = false; let mut locked_payments = self.payments.lock().unwrap(); - if let Some(payment) = locked_payments.get_mut(&update.hash) { + if let Some(payment) = locked_payments.get_mut(&update.id) { if let Some(preimage_opt) = update.preimage { - payment.preimage = preimage_opt; + match payment.kind { + PaymentKind::Bolt11 { ref mut preimage, .. } => *preimage = preimage_opt, + PaymentKind::Bolt11Jit { ref mut preimage, .. } => *preimage = preimage_opt, + PaymentKind::Spontaneous { ref mut preimage, .. } => *preimage = preimage_opt, + _ => {}, + } } if let Some(secret_opt) = update.secret { - payment.secret = secret_opt; + match payment.kind { + PaymentKind::Bolt11 { ref mut secret, .. } => *secret = secret_opt, + PaymentKind::Bolt11Jit { ref mut secret, .. } => *secret = secret_opt, + _ => {}, + } } if let Some(amount_opt) = update.amount_msat { @@ -203,11 +324,7 @@ where payment.status = status; } - if let Some(lsp_fee_limits) = update.lsp_fee_limits { - payment.lsp_fee_limits = lsp_fee_limits - } - - self.persist_info(&update.hash, payment)?; + self.persist_info(&update.id, payment)?; updated = true; } @@ -227,8 +344,8 @@ where .collect::>() } - fn persist_info(&self, hash: &PaymentHash, payment: &PaymentDetails) -> Result<(), Error> { - let store_key = hex_utils::to_string(&hash.0); + fn persist_info(&self, id: &PaymentId, payment: &PaymentDetails) -> Result<(), Error> { + let store_key = hex_utils::to_string(&id.0); let data = payment.encode(); self.kv_store .write( @@ -265,7 +382,8 @@ mod tests { let payment_store = PaymentStore::new(Vec::new(), Arc::clone(&store), logger); let hash = PaymentHash([42u8; 32]); - assert!(payment_store.get(&hash).is_none()); + let id = PaymentId([42u8; 32]); + assert!(payment_store.get(&id).is_none()); let store_key = hex_utils::to_string(&hash.0); assert!(store @@ -277,17 +395,15 @@ mod tests { .is_err()); let payment = PaymentDetails { - hash, - preimage: None, - secret: None, + id, + kind: PaymentKind::Bolt11 { hash, preimage: None, secret: None }, amount_msat: None, direction: PaymentDirection::Inbound, status: PaymentStatus::Pending, - lsp_fee_limits: None, }; assert_eq!(Ok(false), payment_store.insert(payment.clone())); - assert!(payment_store.get(&hash).is_some()); + assert!(payment_store.get(&id).is_some()); assert!(store .read( PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, @@ -297,13 +413,13 @@ mod tests { .is_ok()); assert_eq!(Ok(true), payment_store.insert(payment)); - assert!(payment_store.get(&hash).is_some()); + assert!(payment_store.get(&id).is_some()); - let mut update = PaymentDetailsUpdate::new(hash); + let mut update = PaymentDetailsUpdate::new(id); update.status = Some(PaymentStatus::Succeeded); assert_eq!(Ok(true), payment_store.update(&update)); - assert!(payment_store.get(&hash).is_some()); + assert!(payment_store.get(&id).is_some()); - assert_eq!(PaymentStatus::Succeeded, payment_store.get(&hash).unwrap().status); + assert_eq!(PaymentStatus::Succeeded, payment_store.get(&id).unwrap().status); } } diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 2bddfee13..e979b5ce9 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,4 +1,4 @@ -pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentStatus}; +pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; @@ -19,6 +19,7 @@ use crate::{SocketAddress, UserChannelId}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; +use lightning::ln::channelmanager::PaymentId; use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; @@ -74,6 +75,24 @@ impl UniffiCustomTypeConverter for Bolt11Invoice { } } +impl UniffiCustomTypeConverter for PaymentId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentId(bytes)); + } + } + Err(Error::InvalidPaymentId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + impl UniffiCustomTypeConverter for PaymentHash { type Builtin = String; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a9acb63a0..f8022639a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -419,10 +419,10 @@ pub(crate) fn do_channel_full_cycle( let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, &"asdf", 9217).unwrap(); println!("\nA send"); - let payment_hash = node_a.bolt11_payment().send(&invoice).unwrap(); + let payment_id = node_a.bolt11_payment().send(&invoice).unwrap(); assert_eq!(node_a.bolt11_payment().send(&invoice), Err(NodeError::DuplicatePayment)); - assert_eq!(node_a.list_payments().first().unwrap().hash, payment_hash); + assert_eq!(node_a.list_payments().first().unwrap().id, payment_id); let outbound_payments_a = node_a.list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound); @@ -442,21 +442,21 @@ pub(crate) fn do_channel_full_cycle( expect_event!(node_a, PaymentSuccessful); expect_event!(node_b, PaymentReceived); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); // Assert we fail duplicate outbound payments and check the status hasn't changed. assert_eq!(Err(NodeError::DuplicatePayment), node_a.bolt11_payment().send(&invoice)); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); // Test under-/overpayment let invoice_amount_2_msat = 2500_000; @@ -473,7 +473,7 @@ pub(crate) fn do_channel_full_cycle( let overpaid_amount_msat = invoice_amount_2_msat + 100; println!("\nA overpaid send"); - let payment_hash = + let payment_id = node_a.bolt11_payment().send_using_amount(&invoice, overpaid_amount_msat).unwrap(); expect_event!(node_a, PaymentSuccessful); let received_amount = match node_b.wait_next_event() { @@ -487,12 +487,12 @@ pub(crate) fn do_channel_full_cycle( }, }; assert_eq!(received_amount, overpaid_amount_msat); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(overpaid_amount_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(overpaid_amount_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); // Test "zero-amount" invoice payment println!("\nB receive_variable_amount_payment"); @@ -504,7 +504,7 @@ pub(crate) fn do_channel_full_cycle( node_a.bolt11_payment().send(&variable_amount_invoice) ); println!("\nA send_using_amount"); - let payment_hash = node_a + let payment_id = node_a .bolt11_payment() .send_using_amount(&variable_amount_invoice, determined_amount_msat) .unwrap(); @@ -521,17 +521,17 @@ pub(crate) fn do_channel_full_cycle( }, }; assert_eq!(received_amount, determined_amount_msat); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(determined_amount_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(determined_amount_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); // Test spontaneous/keysend payments println!("\nA send_spontaneous_payment"); let keysend_amount_msat = 2500_000; - let keysend_payment_hash = + let keysend_payment_id = node_a.spontaneous_payment().send(keysend_amount_msat, node_b.node_id()).unwrap(); expect_event!(node_a, PaymentSuccessful); let received_keysend_amount = match node_b.wait_next_event() { @@ -545,21 +545,12 @@ pub(crate) fn do_channel_full_cycle( }, }; assert_eq!(received_keysend_amount, keysend_amount_msat); - assert_eq!(node_a.payment(&keysend_payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!( - node_a.payment(&keysend_payment_hash).unwrap().direction, - PaymentDirection::Outbound - ); - assert_eq!( - node_a.payment(&keysend_payment_hash).unwrap().amount_msat, - Some(keysend_amount_msat) - ); - assert_eq!(node_b.payment(&keysend_payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&keysend_payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!( - node_b.payment(&keysend_payment_hash).unwrap().amount_msat, - Some(keysend_amount_msat) - ); + assert_eq!(node_a.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); + assert_eq!(node_b.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); println!("\nB close_channel"); node_b.close_channel(&user_channel_id, node_a.node_id()).unwrap(); From 51e25803573f04aebd711155657b38b629b754c9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 11 Mar 2024 10:34:49 +0100 Subject: [PATCH 6/7] Add test ensuring de/ser compatibility with old format .. as we changed quite a bit and moved required fields, we add a test here that adds the old `PaymentDetails` as a test struct and ensures we're able to parse them just fine --- src/payment/store.rs | 158 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/src/payment/store.rs b/src/payment/store.rs index 1a85152e2..8bcbf41e6 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -372,9 +372,36 @@ where #[cfg(test)] mod tests { use super::*; - use lightning::util::test_utils::{TestLogger, TestStore}; + use lightning::util::{ + ser::Readable, + test_utils::{TestLogger, TestStore}, + }; + use std::io::Cursor; use std::sync::Arc; + /// We refactored `PaymentDetails` to hold a payment id and moved some required fields into + /// `PaymentKind`. Here, we keep the old layout available in order test de/ser compatibility. + #[derive(Clone, Debug, PartialEq, Eq)] + struct OldPaymentDetails { + pub hash: PaymentHash, + pub preimage: Option, + pub secret: Option, + pub amount_msat: Option, + pub direction: PaymentDirection, + pub status: PaymentStatus, + pub lsp_fee_limits: Option, + } + + impl_writeable_tlv_based!(OldPaymentDetails, { + (0, hash, required), + (1, lsp_fee_limits, option), + (2, preimage, required), + (4, secret, required), + (6, amount_msat, required), + (8, direction, required), + (10, status, required) + }); + #[test] fn payment_info_is_persisted() { let store: Arc = Arc::new(TestStore::new(false)); @@ -422,4 +449,133 @@ mod tests { assert_eq!(PaymentStatus::Succeeded, payment_store.get(&id).unwrap().status); } + + #[test] + fn old_payment_details_deser_compat() { + // We refactored `PaymentDetails` to hold a payment id and moved some required fields into + // `PaymentKind`. Here, we test compatibility with the old layout. + let hash = PaymentHash([42u8; 32]); + let preimage = Some(PaymentPreimage([43u8; 32])); + let secret = Some(PaymentSecret([44u8; 32])); + let amount_msat = Some(45_000_000); + + // Test `Bolt11` de/ser + { + let old_bolt11_payment = OldPaymentDetails { + hash, + preimage, + secret, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + + let old_bolt11_encoded = old_bolt11_payment.encode(); + assert_eq!( + old_bolt11_payment, + OldPaymentDetails::read(&mut Cursor::new(old_bolt11_encoded.clone())).unwrap() + ); + + let bolt11_decoded = + PaymentDetails::read(&mut Cursor::new(old_bolt11_encoded)).unwrap(); + let bolt11_reencoded = bolt11_decoded.encode(); + assert_eq!( + bolt11_decoded, + PaymentDetails::read(&mut Cursor::new(bolt11_reencoded)).unwrap() + ); + + match bolt11_decoded.kind { + PaymentKind::Bolt11 { hash: h, preimage: p, secret: s } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + assert_eq!(secret, s); + }, + _ => { + panic!("Unexpected kind!"); + }, + } + } + + // Test `Bolt11Jit` de/ser + { + let lsp_fee_limits = Some(LSPFeeLimits { + max_total_opening_fee_msat: Some(46_000), + max_proportional_opening_fee_ppm_msat: Some(47_000), + }); + + let old_bolt11_jit_payment = OldPaymentDetails { + hash, + preimage, + secret, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits, + }; + + let old_bolt11_jit_encoded = old_bolt11_jit_payment.encode(); + assert_eq!( + old_bolt11_jit_payment, + OldPaymentDetails::read(&mut Cursor::new(old_bolt11_jit_encoded.clone())).unwrap() + ); + + let bolt11_jit_decoded = + PaymentDetails::read(&mut Cursor::new(old_bolt11_jit_encoded)).unwrap(); + let bolt11_jit_reencoded = bolt11_jit_decoded.encode(); + assert_eq!( + bolt11_jit_decoded, + PaymentDetails::read(&mut Cursor::new(bolt11_jit_reencoded)).unwrap() + ); + + match bolt11_jit_decoded.kind { + PaymentKind::Bolt11Jit { hash: h, preimage: p, secret: s, lsp_fee_limits: l } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + assert_eq!(secret, s); + assert_eq!(lsp_fee_limits, Some(l)); + }, + _ => { + panic!("Unexpected kind!"); + }, + } + } + + // Test `Spontaneous` de/ser + { + let old_spontaneous_payment = OldPaymentDetails { + hash, + preimage, + secret: None, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + + let old_spontaneous_encoded = old_spontaneous_payment.encode(); + assert_eq!( + old_spontaneous_payment, + OldPaymentDetails::read(&mut Cursor::new(old_spontaneous_encoded.clone())).unwrap() + ); + + let spontaneous_decoded = + PaymentDetails::read(&mut Cursor::new(old_spontaneous_encoded)).unwrap(); + let spontaneous_reencoded = spontaneous_decoded.encode(); + assert_eq!( + spontaneous_decoded, + PaymentDetails::read(&mut Cursor::new(spontaneous_reencoded)).unwrap() + ); + + match spontaneous_decoded.kind { + PaymentKind::Spontaneous { hash: h, preimage: p } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + }, + _ => { + panic!("Unexpected kind!"); + }, + } + } + } } From 1fab656f15f9dafa043aa0aed17ac56a625a6443 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 11 Mar 2024 16:11:48 +0100 Subject: [PATCH 7/7] Add `payment_id` to `Payment{Successful,Failed,Received}` events --- bindings/ldk_node.udl | 6 +++--- src/event.rs | 33 ++++++++++++++++++++++++++++++--- tests/common/mod.rs | 10 +++++----- tests/integration_tests_rust.rs | 4 ++-- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1d776782b..58fab0d52 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -186,9 +186,9 @@ enum BuildError { [Enum] interface Event { - PaymentSuccessful(PaymentHash payment_hash, u64? fee_paid_msat); - PaymentFailed(PaymentHash payment_hash, PaymentFailureReason? reason); - PaymentReceived(PaymentHash payment_hash, u64 amount_msat); + PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, u64? fee_paid_msat); + PaymentFailed(PaymentId? payment_id, PaymentHash payment_hash, PaymentFailureReason? reason); + PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat); ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); diff --git a/src/event.rs b/src/event.rs index d4e3d57bc..78188452f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -46,6 +46,10 @@ use std::time::Duration; pub enum Event { /// A sent payment was successful. PaymentSuccessful { + /// A local identifier used to track the payment. + /// + /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. + payment_id: Option, /// The hash of the payment. payment_hash: PaymentHash, /// The total fee which was spent at intermediate hops in this payment. @@ -53,6 +57,10 @@ pub enum Event { }, /// A sent payment has failed. PaymentFailed { + /// A local identifier used to track the payment. + /// + /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. + payment_id: Option, /// The hash of the payment. payment_hash: PaymentHash, /// The reason why the payment failed. @@ -62,6 +70,10 @@ pub enum Event { }, /// A payment has been received. PaymentReceived { + /// A local identifier used to track the payment. + /// + /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. + payment_id: Option, /// The hash of the payment. payment_hash: PaymentHash, /// The value, in thousandths of a satoshi, that has been received. @@ -110,13 +122,16 @@ impl_writeable_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), (1, fee_paid_msat, option), + (3, payment_id, option), }, (1, PaymentFailed) => { (0, payment_hash, required), (1, reason, option), + (3, payment_id, option), }, (2, PaymentReceived) => { (0, payment_hash, required), + (1, payment_id, option), (2, amount_msat, required), }, (3, ChannelReady) => { @@ -635,7 +650,11 @@ where }; self.event_queue - .add_event(Event::PaymentReceived { payment_hash, amount_msat }) + .add_event(Event::PaymentReceived { + payment_id: Some(payment_id), + payment_hash, + amount_msat, + }) .unwrap_or_else(|e| { log_error!(self.logger, "Failed to push to event queue: {}", e); panic!("Failed to push to event queue"); @@ -683,7 +702,11 @@ where }); self.event_queue - .add_event(Event::PaymentSuccessful { payment_hash, fee_paid_msat }) + .add_event(Event::PaymentSuccessful { + payment_id: Some(payment_id), + payment_hash, + fee_paid_msat, + }) .unwrap_or_else(|e| { log_error!(self.logger, "Failed to push to event queue: {}", e); panic!("Failed to push to event queue"); @@ -706,7 +729,11 @@ where panic!("Failed to access payment store"); }); self.event_queue - .add_event(Event::PaymentFailed { payment_hash, reason }) + .add_event(Event::PaymentFailed { + payment_id: Some(payment_id), + payment_hash, + reason, + }) .unwrap_or_else(|e| { log_error!(self.logger, "Failed to push to event queue: {}", e); panic!("Failed to push to event queue"); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f8022639a..bcb47accb 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -82,11 +82,11 @@ pub(crate) use expect_channel_ready_event; macro_rules! expect_payment_received_event { ($node: expr, $amount_msat: expr) => {{ match $node.wait_next_event() { - ref e @ Event::PaymentReceived { payment_hash, amount_msat } => { + ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(amount_msat, $amount_msat); $node.event_handled(); - payment_hash + payment_id }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); @@ -98,12 +98,12 @@ macro_rules! expect_payment_received_event { pub(crate) use expect_payment_received_event; macro_rules! expect_payment_successful_event { - ($node: expr, $payment_hash: expr, $fee_paid_msat: expr) => {{ + ($node: expr, $payment_id: expr, $fee_paid_msat: expr) => {{ match $node.wait_next_event() { - ref e @ Event::PaymentSuccessful { payment_hash, fee_paid_msat } => { + ref e @ Event::PaymentSuccessful { payment_id, fee_paid_msat, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(fee_paid_msat, $fee_paid_msat); - assert_eq!(payment_hash, $payment_hash); + assert_eq!(payment_id, $payment_id); $node.event_handled(); }, ref e => { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fa8986c71..1820ef76a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -136,9 +136,9 @@ fn multi_hop_sending() { let invoice = nodes[4].bolt11_payment().receive(2_500_000, &"asdf", 9217).unwrap(); nodes[0].bolt11_payment().send(&invoice).unwrap(); - let payment_hash = expect_payment_received_event!(&nodes[4], 2_500_000); + let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); let fee_paid_msat = Some(2000); - expect_payment_successful_event!(nodes[0], payment_hash, fee_paid_msat); + expect_payment_successful_event!(nodes[0], payment_id, fee_paid_msat); } #[test]