From 47a20b09d7d41d84217854f5d748a090fda5987c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 6 Feb 2024 16:00:02 +0100 Subject: [PATCH 1/4] Expose `LightningBalance`/`BalanceDetails` newtypes --- bindings/ldk_node.udl | 16 ++++ src/balance.rs | 197 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 32 +++++++ 3 files changed, 245 insertions(+) create mode 100644 src/balance.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 73a54d3b3..9d7672b2d 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -94,6 +94,7 @@ interface LDKNode { PaymentDetails? payment([ByRef]PaymentHash payment_hash); [Throws=NodeError] void remove_payment([ByRef]PaymentHash payment_hash); + BalanceDetails list_balances(); sequence list_payments(); sequence list_peers(); sequence list_channels(); @@ -266,6 +267,21 @@ dictionary PeerDetails { boolean is_connected; }; +[Enum] +interface LightningBalance { + ClaimableOnChannelClose ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis ); + ClaimableAwaitingConfirmations ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis, u32 confirmation_height ); + ContentiousClaimable ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis, u32 timeout_height, PaymentHash payment_hash, PaymentPreimage payment_preimage ); + MaybeTimeoutClaimableHTLC ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis, u32 claimable_height, PaymentHash payment_hash); + MaybePreimageClaimableHTLC ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis, u32 expiry_height, PaymentHash payment_hash); + CounterpartyRevokedOutputClaimable ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis ); +}; + +dictionary BalanceDetails { + u64 total_lightning_balance_sats; + sequence lightning_balances; +}; + interface ChannelConfig { constructor(); u32 forwarding_fee_proportional_millionths(); diff --git a/src/balance.rs b/src/balance.rs new file mode 100644 index 000000000..419bd110d --- /dev/null +++ b/src/balance.rs @@ -0,0 +1,197 @@ +use bitcoin::secp256k1::PublicKey; +use lightning::chain::channelmonitor::Balance as LdkBalance; +use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage}; + +/// Details of the known available balances returned by [`Node::list_balances`]. +/// +/// [`Node::list_balances`]: crate::Node::list_balances +#[derive(Debug, Clone)] +pub struct BalanceDetails { + /// The total balance that we would be able to claim across all our Lightning channels. + /// + /// Note this excludes balances that we are unsure if we are able to claim (e.g., as we are + /// waiting for a preimage or for a timeout to expire). These balances will however be included + /// as [`MaybePreimageClaimableHTLC`] and + /// [`MaybeTimeoutClaimableHTLC`] in [`lightning_balances`]. + /// + /// [`MaybePreimageClaimableHTLC`]: LightningBalance::MaybePreimageClaimableHTLC + /// [`MaybeTimeoutClaimableHTLC`]: LightningBalance::MaybeTimeoutClaimableHTLC + /// [`lightning_balances`]: Self::lightning_balances + pub total_lightning_balance_sats: u64, + /// A detailed list of all known Lightning balances that would be claimable on channel closure. + /// + /// Note that less than the listed amounts are spendable over lightning as further reserve + /// restrictions apply. Please refer to [`ChannelDetails::outbound_capacity_msat`] and + /// [`ChannelDetails::next_outbound_htlc_limit_msat`] as returned by [`Node::list_channels`] + /// for a better approximation of the spendable amounts. + /// + /// [`ChannelDetails::outbound_capacity_msat`]: crate::ChannelDetails::outbound_capacity_msat + /// [`ChannelDetails::next_outbound_htlc_limit_msat`]: crate::ChannelDetails::next_outbound_htlc_limit_msat + /// [`Node::list_channels`]: crate::Node::list_channels + pub lightning_balances: Vec, +} + +/// Details about the status of a known Lightning balance. +#[derive(Debug, Clone)] +pub enum LightningBalance { + /// The channel is not yet closed (or the commitment or closing transaction has not yet + /// appeared in a block). The given balance is claimable (less on-chain fees) if the channel is + /// force-closed now. + ClaimableOnChannelClose { + /// The identifier of the channel this balance belongs to. + channel_id: ChannelId, + /// The identifier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + amount_satoshis: u64, + }, + /// The channel has been closed, and the given balance is ours but awaiting confirmations until + /// we consider it spendable. + ClaimableAwaitingConfirmations { + /// The identifier of the channel this balance belongs to. + channel_id: ChannelId, + /// The identifier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount available to claim, in satoshis, possibly excluding the on-chain fees which + /// were spent in broadcasting the transaction. + amount_satoshis: u64, + /// The height at which an [`Event::SpendableOutputs`] event will be generated for this + /// amount. + /// + /// [`Event::SpendableOutputs`]: lightning::events::Event::SpendableOutputs + confirmation_height: u32, + }, + /// The channel has been closed, and the given balance should be ours but awaiting spending + /// transaction confirmation. If the spending transaction does not confirm in time, it is + /// possible our counterparty can take the funds by broadcasting an HTLC timeout on-chain. + /// + /// Once the spending transaction confirms, before it has reached enough confirmations to be + /// considered safe from chain reorganizations, the balance will instead be provided via + /// [`LightningBalance::ClaimableAwaitingConfirmations`]. + ContentiousClaimable { + /// The identifier of the channel this balance belongs to. + channel_id: ChannelId, + /// The identifier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount available to claim, in satoshis, excluding the on-chain fees which will be + /// required to do so. + amount_satoshis: u64, + /// The height at which the counterparty may be able to claim the balance if we have not + /// done so. + timeout_height: u32, + /// The payment hash that locks this HTLC. + payment_hash: PaymentHash, + /// The preimage that can be used to claim this HTLC. + payment_preimage: PaymentPreimage, + }, + /// HTLCs which we sent to our counterparty which are claimable after a timeout (less on-chain + /// fees) if the counterparty does not know the preimage for the HTLCs. These are somewhat + /// likely to be claimed by our counterparty before we do. + MaybeTimeoutClaimableHTLC { + /// The identifier of the channel this balance belongs to. + channel_id: ChannelId, + /// The identifier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount potentially available to claim, in satoshis, excluding the on-chain fees + /// which will be required to do so. + amount_satoshis: u64, + /// The height at which we will be able to claim the balance if our counterparty has not + /// done so. + claimable_height: u32, + /// The payment hash whose preimage our counterparty needs to claim this HTLC. + payment_hash: PaymentHash, + }, + /// HTLCs which we received from our counterparty which are claimable with a preimage which we + /// do not currently have. This will only be claimable if we receive the preimage from the node + /// to which we forwarded this HTLC before the timeout. + MaybePreimageClaimableHTLC { + /// The identifier of the channel this balance belongs to. + channel_id: ChannelId, + /// The identifier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount potentially available to claim, in satoshis, excluding the on-chain fees + /// which will be required to do so. + amount_satoshis: u64, + /// The height at which our counterparty will be able to claim the balance if we have not + /// yet received the preimage and claimed it ourselves. + expiry_height: u32, + /// The payment hash whose preimage we need to claim this HTLC. + payment_hash: PaymentHash, + }, + /// The channel has been closed, and our counterparty broadcasted a revoked commitment + /// transaction. + /// + /// Thus, we're able to claim all outputs in the commitment transaction, one of which has the + /// following amount. + CounterpartyRevokedOutputClaimable { + /// The identifier of the channel this balance belongs to. + channel_id: ChannelId, + /// The identifier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount, in satoshis, of the output which we can claim. + amount_satoshis: u64, + }, +} + +impl LightningBalance { + pub(crate) fn from_ldk_balance( + channel_id: ChannelId, counterparty_node_id: PublicKey, balance: LdkBalance, + ) -> Self { + match balance { + LdkBalance::ClaimableOnChannelClose { amount_satoshis } => { + Self::ClaimableOnChannelClose { channel_id, counterparty_node_id, amount_satoshis } + }, + LdkBalance::ClaimableAwaitingConfirmations { amount_satoshis, confirmation_height } => { + Self::ClaimableAwaitingConfirmations { + channel_id, + counterparty_node_id, + amount_satoshis, + confirmation_height, + } + }, + LdkBalance::ContentiousClaimable { + amount_satoshis, + timeout_height, + payment_hash, + payment_preimage, + } => Self::ContentiousClaimable { + channel_id, + counterparty_node_id, + amount_satoshis, + timeout_height, + payment_hash, + payment_preimage, + }, + LdkBalance::MaybeTimeoutClaimableHTLC { + amount_satoshis, + claimable_height, + payment_hash, + } => Self::MaybeTimeoutClaimableHTLC { + channel_id, + counterparty_node_id, + amount_satoshis, + claimable_height, + payment_hash, + }, + LdkBalance::MaybePreimageClaimableHTLC { + amount_satoshis, + expiry_height, + payment_hash, + } => Self::MaybePreimageClaimableHTLC { + channel_id, + counterparty_node_id, + amount_satoshis, + expiry_height, + payment_hash, + }, + LdkBalance::CounterpartyRevokedOutputClaimable { amount_satoshis } => { + Self::CounterpartyRevokedOutputClaimable { + channel_id, + counterparty_node_id, + amount_satoshis, + } + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8690cf923..b1a6434e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,7 @@ #![allow(ellipsis_inclusive_range_patterns)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +mod balance; mod builder; mod error; mod event; @@ -99,6 +100,7 @@ pub use bitcoin; pub use lightning; pub use lightning_invoice; +pub use balance::{BalanceDetails, LightningBalance}; pub use error::Error as NodeError; use error::Error; @@ -1744,6 +1746,36 @@ impl Node { self.payment_store.remove(&payment_hash) } + /// Retrieves an overview of all known balances. + pub fn list_balances(&self) -> BalanceDetails { + let mut total_lightning_balance_sats = 0; + let mut lightning_balances = Vec::new(); + for funding_txo in self.chain_monitor.list_monitors() { + match self.chain_monitor.get_monitor(funding_txo) { + Ok(monitor) => { + // TODO: Switch to `channel_id` with LDK 0.0.122: let channel_id = monitor.channel_id(); + let channel_id = funding_txo.to_channel_id(); + // unwrap safety: `get_counterparty_node_id` will always be `Some` after 0.0.110 and + // LDK Node 0.1 depended on 0.0.115 already. + let counterparty_node_id = monitor.get_counterparty_node_id().unwrap(); + for ldk_balance in monitor.get_claimable_balances() { + total_lightning_balance_sats += ldk_balance.claimable_amount_satoshis(); + lightning_balances.push(LightningBalance::from_ldk_balance( + channel_id, + counterparty_node_id, + ldk_balance, + )); + } + }, + Err(()) => { + continue; + }, + } + } + + BalanceDetails { total_lightning_balance_sats, lightning_balances } + } + /// Retrieves all payments that match the given predicate. /// /// For example, you could retrieve all stored outbound payments as follows: From 8e45c2a6ea9d6cc8363553061d833b00777ce5a7 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 15 Feb 2024 11:28:56 +0100 Subject: [PATCH 2/4] Expose onchain balances via `BalanceDetails` --- .../lightningdevkit/ldknode/LibraryTest.kt | 16 +++++------ bindings/ldk_node.udl | 6 ++-- bindings/python/src/ldk_node/test_ldk_node.py | 12 ++++---- src/balance.rs | 4 +++ src/lib.rs | 23 +++++++-------- tests/common.rs | 19 +++++++------ tests/integration_tests_rust.rs | 28 +++++++++---------- 7 files changed, 57 insertions(+), 51 deletions(-) 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 a65d4678d..39c19821d 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 @@ -162,10 +162,10 @@ class LibraryTest { node1.syncWallets() node2.syncWallets() - val spendableBalance1 = node1.spendableOnchainBalanceSats() - val spendableBalance2 = node2.spendableOnchainBalanceSats() - val totalBalance1 = node1.totalOnchainBalanceSats() - val totalBalance2 = node2.totalOnchainBalanceSats() + val spendableBalance1 = node1.listBalances().spendableOnchainBalanceSats + val spendableBalance2 = node2.listBalances().spendableOnchainBalanceSats + val totalBalance1 = node1.listBalances().totalOnchainBalanceSats + val totalBalance2 = node2.listBalances().totalOnchainBalanceSats println("Spendable balance 1: $spendableBalance1") println("Spendable balance 2: $spendableBalance1") println("Total balance 1: $totalBalance1") @@ -199,8 +199,8 @@ class LibraryTest { node1.syncWallets() node2.syncWallets() - val spendableBalance1AfterOpen = node1.spendableOnchainBalanceSats() - val spendableBalance2AfterOpen = node2.spendableOnchainBalanceSats() + val spendableBalance1AfterOpen = node1.listBalances().spendableOnchainBalanceSats + val spendableBalance2AfterOpen = node2.listBalances().spendableOnchainBalanceSats println("Spendable balance 1 after open: $spendableBalance1AfterOpen") println("Spendable balance 2 after open: $spendableBalance2AfterOpen") assert(spendableBalance1AfterOpen > 49000u) @@ -256,8 +256,8 @@ class LibraryTest { node1.syncWallets() node2.syncWallets() - val spendableBalance1AfterClose = node1.spendableOnchainBalanceSats() - val spendableBalance2AfterClose = node2.spendableOnchainBalanceSats() + val spendableBalance1AfterClose = node1.listBalances().spendableOnchainBalanceSats + val spendableBalance2AfterClose = node2.listBalances().spendableOnchainBalanceSats println("Spendable balance 1 after close: $spendableBalance1AfterClose") println("Spendable balance 2 after close: $spendableBalance2AfterClose") assert(spendableBalance1AfterClose > 95000u) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 9d7672b2d..2d8f1c26c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -56,10 +56,6 @@ interface LDKNode { [Throws=NodeError] Txid send_all_to_onchain_address([ByRef]Address address); [Throws=NodeError] - u64 spendable_onchain_balance_sats(); - [Throws=NodeError] - u64 total_onchain_balance_sats(); - [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] void disconnect(PublicKey node_id); @@ -278,6 +274,8 @@ interface LightningBalance { }; dictionary BalanceDetails { + u64 total_onchain_balance_sats; + u64 spendable_onchain_balance_sats; u64 total_lightning_balance_sats; sequence lightning_balances; }; diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 555ffbcc7..864ef7b43 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -138,10 +138,10 @@ def test_channel_full_cycle(self): node_1.sync_wallets() node_2.sync_wallets() - spendable_balance_1 = node_1.spendable_onchain_balance_sats() - spendable_balance_2 = node_2.spendable_onchain_balance_sats() - total_balance_1 = node_1.total_onchain_balance_sats() - total_balance_2 = node_2.total_onchain_balance_sats() + spendable_balance_1 = node_1.list_balances().spendable_onchain_balance_sats + spendable_balance_2 = node_2.list_balances().spendable_onchain_balance_sats + total_balance_1 = node_1.list_balances().total_onchain_balance_sats + total_balance_2 = node_2.list_balances().total_onchain_balance_sats print("SPENDABLE 1:", spendable_balance_1) self.assertEqual(spendable_balance_1, 100000) @@ -215,10 +215,10 @@ def test_channel_full_cycle(self): node_1.sync_wallets() node_2.sync_wallets() - spendable_balance_after_close_1 = node_1.spendable_onchain_balance_sats() + spendable_balance_after_close_1 = node_1.list_balances().spendable_onchain_balance_sats assert spendable_balance_after_close_1 > 95000 assert spendable_balance_after_close_1 < 100000 - spendable_balance_after_close_2 = node_2.spendable_onchain_balance_sats() + spendable_balance_after_close_2 = node_2.list_balances().spendable_onchain_balance_sats self.assertEqual(spendable_balance_after_close_2, 102500) # Stop nodes diff --git a/src/balance.rs b/src/balance.rs index 419bd110d..24894356d 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -7,6 +7,10 @@ use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage}; /// [`Node::list_balances`]: crate::Node::list_balances #[derive(Debug, Clone)] pub struct BalanceDetails { + /// The total balance of our on-chain wallet. + pub total_onchain_balance_sats: u64, + /// The currently spendable balance of our on-chain wallet. + pub spendable_onchain_balance_sats: u64, /// The total balance that we would be able to claim across all our Lightning channels. /// /// Note this excludes balances that we are unsure if we are able to claim (e.g., as we are diff --git a/src/lib.rs b/src/lib.rs index b1a6434e4..1ca45fdda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -882,16 +882,6 @@ impl Node { Ok(funding_address) } - /// Retrieve the currently spendable on-chain balance in satoshis. - pub fn spendable_onchain_balance_sats(&self) -> Result { - Ok(self.wallet.get_balance().map(|bal| bal.get_spendable())?) - } - - /// Retrieve the current total on-chain balance in satoshis. - pub fn total_onchain_balance_sats(&self) -> Result { - Ok(self.wallet.get_balance().map(|bal| bal.get_total())?) - } - /// Send an on-chain payment to the given address. pub fn send_to_onchain_address( &self, address: &bitcoin::Address, amount_sats: u64, @@ -1748,6 +1738,12 @@ impl Node { /// Retrieves an overview of all known balances. pub fn list_balances(&self) -> BalanceDetails { + let (total_onchain_balance_sats, spendable_onchain_balance_sats) = self + .wallet + .get_balance() + .map(|bal| (bal.get_total(), bal.get_spendable())) + .unwrap_or((0, 0)); + let mut total_lightning_balance_sats = 0; let mut lightning_balances = Vec::new(); for funding_txo in self.chain_monitor.list_monitors() { @@ -1773,7 +1769,12 @@ impl Node { } } - BalanceDetails { total_lightning_balance_sats, lightning_balances } + BalanceDetails { + total_onchain_balance_sats, + spendable_onchain_balance_sats, + total_lightning_balance_sats, + lightning_balances, + } } /// Retrieves all payments that match the given predicate. diff --git a/tests/common.rs b/tests/common.rs index 289b1d194..815056b82 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -333,8 +333,8 @@ pub(crate) fn do_channel_full_cycle( ); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.spendable_onchain_balance_sats().unwrap(), premine_amount_sat); - assert_eq!(node_b.spendable_onchain_balance_sats().unwrap(), premine_amount_sat); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); // Check we haven't got any events yet assert_eq!(node_a.next_event(), None); @@ -371,9 +371,9 @@ pub(crate) fn do_channel_full_cycle( let onchain_fee_buffer_sat = 1500; let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat; let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat; - assert!(node_a.spendable_onchain_balance_sats().unwrap() < node_a_upper_bound_sat); - assert!(node_a.spendable_onchain_balance_sats().unwrap() > node_a_lower_bound_sat); - assert_eq!(node_b.spendable_onchain_balance_sats().unwrap(), premine_amount_sat); + assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat); + assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); expect_channel_ready_event!(node_a, node_b.node_id()); @@ -539,10 +539,13 @@ pub(crate) fn do_channel_full_cycle( let node_a_upper_bound_sat = (premine_amount_sat - funding_amount_sat) + (funding_amount_sat - sum_of_all_payments_sat); let node_a_lower_bound_sat = node_a_upper_bound_sat - onchain_fee_buffer_sat; - assert!(node_a.spendable_onchain_balance_sats().unwrap() > node_a_lower_bound_sat); - assert!(node_a.spendable_onchain_balance_sats().unwrap() < node_a_upper_bound_sat); + assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat); + assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat); let expected_final_amount_node_b_sat = premine_amount_sat + sum_of_all_payments_sat; - assert_eq!(node_b.spendable_onchain_balance_sats().unwrap(), expected_final_amount_node_b_sat); + assert_eq!( + node_b.list_balances().spendable_onchain_balance_sats, + expected_final_amount_node_b_sat + ); // Check we handled all events assert_eq!(node_a.next_event(), None); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index a5b5102b0..55e3dc553 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -44,8 +44,8 @@ fn channel_open_fails_when_funds_insufficient() { ); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.spendable_onchain_balance_sats().unwrap(), premine_amount_sat); - assert_eq!(node_b.spendable_onchain_balance_sats().unwrap(), premine_amount_sat); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); println!("\nA -- connect_open_channel -> B"); assert_eq!( @@ -88,7 +88,7 @@ fn multi_hop_sending() { for n in &nodes { n.sync_wallets().unwrap(); - assert_eq!(n.spendable_onchain_balance_sats().unwrap(), premine_amount_sat); + assert_eq!(n.list_balances().spendable_onchain_balance_sats, premine_amount_sat); assert_eq!(n.next_event(), None); } @@ -168,7 +168,7 @@ fn start_stop_reinit() { let funding_address = node.new_onchain_address().unwrap(); - assert_eq!(node.total_onchain_balance_sats().unwrap(), 0); + assert_eq!(node.list_balances().total_onchain_balance_sats, 0); let expected_amount = Amount::from_sat(100000); premine_and_distribute_funds( @@ -179,7 +179,7 @@ fn start_stop_reinit() { ); node.sync_wallets().unwrap(); - assert_eq!(node.spendable_onchain_balance_sats().unwrap(), expected_amount.to_sat()); + assert_eq!(node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat()); let log_file_symlink = format!("{}/logs/ldk_node_latest.log", config.clone().storage_dir_path); assert!(std::path::Path::new(&log_file_symlink).is_symlink()); @@ -202,13 +202,13 @@ fn start_stop_reinit() { assert_eq!(reinitialized_node.node_id(), expected_node_id); assert_eq!( - reinitialized_node.spendable_onchain_balance_sats().unwrap(), + reinitialized_node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat() ); reinitialized_node.sync_wallets().unwrap(); assert_eq!( - reinitialized_node.spendable_onchain_balance_sats().unwrap(), + reinitialized_node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat() ); @@ -232,7 +232,7 @@ fn onchain_spend_receive() { node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_b.spendable_onchain_balance_sats().unwrap(), 100000); + 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)); @@ -243,9 +243,9 @@ fn onchain_spend_receive() { node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.spendable_onchain_balance_sats().unwrap(), 1000); - assert!(node_b.spendable_onchain_balance_sats().unwrap() > 98000); - assert!(node_b.spendable_onchain_balance_sats().unwrap() < 100000); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 1000); + 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(); @@ -255,9 +255,9 @@ fn onchain_spend_receive() { node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.total_onchain_balance_sats().unwrap(), 0); - assert!(node_b.spendable_onchain_balance_sats().unwrap() > 99000); - assert!(node_b.spendable_onchain_balance_sats().unwrap() < 100000); + assert_eq!(node_a.list_balances().total_onchain_balance_sats, 0); + assert!(node_b.list_balances().spendable_onchain_balance_sats > 99000); + assert!(node_b.list_balances().spendable_onchain_balance_sats < 100000); } #[test] From 0b901fc870906486bfeebefeb84b0b90ee8f6938 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 16 Feb 2024 14:56:37 +0100 Subject: [PATCH 3/4] Expose sweeper balances via `BalanceDetails` --- bindings/ldk_node.udl | 11 +++++ src/balance.rs | 104 +++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 10 +++- src/sweep.rs | 28 ++++++++---- src/uniffi_types.rs | 13 +++++- 5 files changed, 155 insertions(+), 11 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 2d8f1c26c..1a526a9f0 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -273,11 +273,19 @@ interface LightningBalance { CounterpartyRevokedOutputClaimable ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis ); }; +[Enum] +interface PendingSweepBalance { + PendingBroadcast ( ChannelId? channel_id, u64 amount_satoshis ); + BroadcastAwaitingConfirmation ( ChannelId? channel_id, u32 latest_broadcast_height, Txid latest_spending_txid, u64 amount_satoshis ); + AwaitingThresholdConfirmations ( ChannelId? channel_id, Txid latest_spending_txid, BlockHash confirmation_hash, u32 confirmation_height, u64 amount_satoshis); +}; + dictionary BalanceDetails { u64 total_onchain_balance_sats; u64 spendable_onchain_balance_sats; u64 total_lightning_balance_sats; sequence lightning_balances; + sequence pending_balances_from_channel_closures; }; interface ChannelConfig { @@ -308,6 +316,9 @@ enum LogLevel { [Custom] typedef string Txid; +[Custom] +typedef string BlockHash; + [Custom] typedef string SocketAddress; diff --git a/src/balance.rs b/src/balance.rs index 24894356d..f5a52073d 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -1,7 +1,11 @@ -use bitcoin::secp256k1::PublicKey; use lightning::chain::channelmonitor::Balance as LdkBalance; use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{BlockHash, Txid}; + +use crate::sweep::SpendableOutputInfo; + /// Details of the known available balances returned by [`Node::list_balances`]. /// /// [`Node::list_balances`]: crate::Node::list_balances @@ -33,6 +37,17 @@ pub struct BalanceDetails { /// [`ChannelDetails::next_outbound_htlc_limit_msat`]: crate::ChannelDetails::next_outbound_htlc_limit_msat /// [`Node::list_channels`]: crate::Node::list_channels pub lightning_balances: Vec, + /// A detailed list of balances currently being swept from the Lightning to the on-chain + /// wallet. + /// + /// These are balances resulting from channel closures that may have been encumbered by a + /// delay, but are now being claimed and useable once sufficiently confirmed on-chain. + /// + /// Note that, depending on the sync status of the wallets, swept balances listed here might or + /// might not already be accounted for in [`total_onchain_balance_sats`]. + /// + /// [`total_onchain_balance_sats`]: Self::total_onchain_balance_sats + pub pending_balances_from_channel_closures: Vec, } /// Details about the status of a known Lightning balance. @@ -199,3 +214,90 @@ impl LightningBalance { } } } + +/// Details about the status of a known balance currently being swept to our on-chain wallet. +#[derive(Debug, Clone)] +pub enum PendingSweepBalance { + /// The spendable output is about to be swept, but a spending transaction has yet to be generated and + /// broadcast. + PendingBroadcast { + /// The identifier of the channel this balance belongs to. + channel_id: Option, + /// The amount, in satoshis, of the output being swept. + amount_satoshis: u64, + }, + /// A spending transaction has been generated and broadcast and is awaiting confirmation + /// on-chain. + BroadcastAwaitingConfirmation { + /// The identifier of the channel this balance belongs to. + channel_id: Option, + /// The best height when we last broadcast a transaction spending the output being swept. + latest_broadcast_height: u32, + /// The identifier of the transaction spending the swept output we last broadcast. + latest_spending_txid: Txid, + /// The amount, in satoshis, of the output being swept. + amount_satoshis: u64, + }, + /// A spending transaction has been confirmed on-chain and is awaiting threshold confirmations. + /// + /// It will be considered irrevocably confirmed after reaching [`ANTI_REORG_DELAY`]. + /// + /// [`ANTI_REORG_DELAY`]: lightning::chain::channelmonitor::ANTI_REORG_DELAY + AwaitingThresholdConfirmations { + /// The identifier of the channel this balance belongs to. + channel_id: Option, + /// The identifier of the confirmed transaction spending the swept output. + latest_spending_txid: Txid, + /// The hash of the block in which the spending transaction was confirmed. + confirmation_hash: BlockHash, + /// The height at which the spending transaction was confirmed. + confirmation_height: u32, + /// The amount, in satoshis, of the output being swept. + amount_satoshis: u64, + }, +} + +impl PendingSweepBalance { + pub(crate) fn from_tracked_spendable_output(output_info: SpendableOutputInfo) -> Self { + if let Some(confirmation_hash) = output_info.confirmation_hash { + debug_assert!(output_info.confirmation_height.is_some()); + debug_assert!(output_info.latest_spending_tx.is_some()); + let channel_id = output_info.channel_id; + let confirmation_height = output_info + .confirmation_height + .expect("Height must be set if the output is confirmed"); + let latest_spending_txid = output_info + .latest_spending_tx + .as_ref() + .expect("Spending tx must be set if the output is confirmed") + .txid(); + let amount_satoshis = output_info.value_satoshis(); + Self::AwaitingThresholdConfirmations { + channel_id, + latest_spending_txid, + confirmation_hash, + confirmation_height, + amount_satoshis, + } + } else if let Some(latest_broadcast_height) = output_info.latest_broadcast_height { + debug_assert!(output_info.latest_spending_tx.is_some()); + let channel_id = output_info.channel_id; + let latest_spending_txid = output_info + .latest_spending_tx + .as_ref() + .expect("Spending tx must be set if the spend was broadcast") + .txid(); + let amount_satoshis = output_info.value_satoshis(); + Self::BroadcastAwaitingConfirmation { + channel_id, + latest_broadcast_height, + latest_spending_txid, + amount_satoshis, + } + } else { + let channel_id = output_info.channel_id; + let amount_satoshis = output_info.value_satoshis(); + Self::PendingBroadcast { channel_id, amount_satoshis } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1ca45fdda..32ca4147f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ pub use bitcoin; pub use lightning; pub use lightning_invoice; -pub use balance::{BalanceDetails, LightningBalance}; +pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; pub use error::Error as NodeError; use error::Error; @@ -1769,11 +1769,19 @@ impl Node { } } + let pending_balances_from_channel_closures = self + .output_sweeper + .tracked_spendable_outputs() + .into_iter() + .map(|o| PendingSweepBalance::from_tracked_spendable_output(o)) + .collect(); + BalanceDetails { total_onchain_balance_sats, spendable_onchain_balance_sats, total_lightning_balance_sats, lightning_balances, + pending_balances_from_channel_closures, } } diff --git a/src/sweep.rs b/src/sweep.rs index d12d9cb38..93dac19fa 100644 --- a/src/sweep.rs +++ b/src/sweep.rs @@ -29,14 +29,14 @@ const REGENERATE_SPEND_THRESHOLD: u32 = 144; #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct SpendableOutputInfo { - id: [u8; 32], - descriptor: SpendableOutputDescriptor, - channel_id: Option, - first_broadcast_hash: Option, - latest_broadcast_height: Option, - latest_spending_tx: Option, - confirmation_height: Option, - confirmation_hash: Option, + pub(crate) id: [u8; 32], + pub(crate) descriptor: SpendableOutputDescriptor, + pub(crate) channel_id: Option, + pub(crate) first_broadcast_hash: Option, + pub(crate) latest_broadcast_height: Option, + pub(crate) latest_spending_tx: Option, + pub(crate) confirmation_height: Option, + pub(crate) confirmation_hash: Option, } impl SpendableOutputInfo { @@ -77,6 +77,14 @@ impl SpendableOutputInfo { false } + + pub(crate) fn value_satoshis(&self) -> u64 { + match &self.descriptor { + SpendableOutputDescriptor::StaticOutput { output, .. } => output.value, + SpendableOutputDescriptor::DelayedPaymentOutput(output) => output.output.value, + SpendableOutputDescriptor::StaticPaymentOutput(output) => output.output.value, + } + } } impl_writeable_tlv_based!(SpendableOutputInfo, { @@ -184,6 +192,10 @@ where self.rebroadcast_if_necessary(); } + pub(crate) fn tracked_spendable_outputs(&self) -> Vec { + self.outputs.lock().unwrap().clone() + } + fn rebroadcast_if_necessary(&self) { let (cur_height, cur_hash) = { let best_block = self.best_block.lock().unwrap(); diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index baedd0aa6..81eeaaa7a 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -3,7 +3,7 @@ pub use lightning::ln::ChannelId; pub use lightning::ln::PaymentSecret; pub use lightning::util::string::UntrustedString; -pub use bitcoin::OutPoint; +pub use bitcoin::{BlockHash, OutPoint}; pub use bip39::Mnemonic; @@ -172,6 +172,17 @@ impl UniffiCustomTypeConverter for Txid { } } +impl UniffiCustomTypeConverter for BlockHash { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(BlockHash::from_str(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + impl UniffiCustomTypeConverter for Mnemonic { type Builtin = String; fn into_custom(val: Self::Builtin) -> uniffi::Result { From e809b2348665ede8307e4e65beb934d31dbee271 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 22 Feb 2024 11:54:58 +0100 Subject: [PATCH 4/4] Drop `balance_msat` from `ChannelDetails` This was discussed upstream but reverted for now. The field is very confusing and misleading, and we can finally drop it now that we have a better `list_balances` interface. --- bindings/ldk_node.udl | 1 - src/types.rs | 11 ++--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1a526a9f0..30b2d4a1b 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -232,7 +232,6 @@ dictionary ChannelDetails { u64? unspendable_punishment_reserve; UserChannelId user_channel_id; u32 feerate_sat_per_1000_weight; - u64 balance_msat; u64 outbound_capacity_msat; u64 inbound_capacity_msat; u32? confirmations_required; diff --git a/src/types.rs b/src/types.rs index 7a2485916..6269b3ddf 100644 --- a/src/types.rs +++ b/src/types.rs @@ -200,12 +200,6 @@ pub struct ChannelDetails { /// The currently negotiated fee rate denominated in satoshi per 1000 weight units, /// which is applied to commitment and HTLC transactions. pub feerate_sat_per_1000_weight: u32, - /// The total balance of the channel. This is the amount that will be returned to - /// the user if the channel is closed. - /// - /// The value is not exact, due to potential in-flight and fee-rate changes. Therefore, exactly - /// this amount is likely irrecoverable on close. - pub balance_msat: u64, /// The available outbound capacity for sending HTLCs to the remote peer. /// /// The amount does not include any pending HTLCs which are not yet resolved (and, thus, whose @@ -270,8 +264,8 @@ pub struct ChannelDetails { /// the current state and per-HTLC limit(s). This is intended for use when routing, allowing us /// to use a limit as close as possible to the HTLC limit we can currently send. /// - /// See also [`ChannelDetails::next_outbound_htlc_minimum_msat`], - /// [`ChannelDetails::balance_msat`], and [`ChannelDetails::outbound_capacity_msat`]. + /// See also [`ChannelDetails::next_outbound_htlc_minimum_msat`] and + /// [`ChannelDetails::outbound_capacity_msat`]. pub next_outbound_htlc_limit_msat: u64, /// The minimum value for sending a single HTLC to the remote peer. This is the equivalent of /// [`ChannelDetails::next_outbound_htlc_limit_msat`] but represents a lower-bound, rather than @@ -306,7 +300,6 @@ impl From for ChannelDetails { // unwrap safety: This value will be `None` for objects serialized with LDK versions // prior to 0.0.115. feerate_sat_per_1000_weight: value.feerate_sat_per_1000_weight.unwrap(), - balance_msat: value.balance_msat, outbound_capacity_msat: value.outbound_capacity_msat, inbound_capacity_msat: value.inbound_capacity_msat, confirmations_required: value.confirmations_required,