diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e39357001..60b1d59a6 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -234,11 +234,19 @@ interface LightningBalance { CounterpartyRevokedOutputClaimable ( ChannelId channel_id, PublicKey counterparty_node_id, u64 amount_satoshis ); }; +[Enum] +interface SweeperBalance { + 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 sweeper_balances; }; interface ChannelConfig { @@ -269,6 +277,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 474ef3e64..bfe80eaff 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 @@ -15,6 +19,15 @@ pub struct BalanceDetails { pub total_lightning_balance_sats: u64, /// A detailed list of all known Lightning balances. 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 [`Self::total_onchain_balance_sats`]. + pub sweeper_balances: Vec, } /// Details about the status of a known Lightning balance. @@ -184,3 +197,90 @@ impl LightningBalance { } } } + +/// Details about the status of a known balance currently being swept to our on-chain wallet. +#[derive(Debug, Clone)] +pub enum SweeperBalance { + /// 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 SweeperBalance { + 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 f74867877..10935dd12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,7 +98,7 @@ pub use bitcoin; pub use lightning; pub use lightning_invoice; -pub use balance::{BalanceDetails, LightningBalance}; +pub use balance::{BalanceDetails, LightningBalance, SweeperBalance}; pub use error::Error as NodeError; use error::Error; @@ -108,7 +108,10 @@ pub use types::ChannelConfig; pub use io::utils::generate_entropy_mnemonic; #[cfg(feature = "uniffi")] -use {bip39::Mnemonic, bitcoin::OutPoint, lightning::ln::PaymentSecret, uniffi_types::*}; +use { + bip39::Mnemonic, bitcoin::BlockHash, bitcoin::OutPoint, lightning::ln::PaymentSecret, + uniffi_types::*, +}; #[cfg(feature = "uniffi")] pub use builder::ArcedNodeBuilder as Builder; @@ -1579,11 +1582,19 @@ impl Node { } } + let sweeper_balances = self + .output_sweeper + .tracked_spendable_outputs() + .into_iter() + .map(|o| SweeperBalance::from_tracked_spendable_output(o)) + .collect(); + BalanceDetails { total_onchain_balance_sats, spendable_onchain_balance_sats, total_lightning_balance_sats, lightning_balances, + sweeper_balances, } } diff --git a/src/sweep.rs b/src/sweep.rs index d23afb029..dbb159c1a 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 964571c7f..b51b8bf50 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -8,7 +8,7 @@ use crate::{Node, SocketAddress, UserChannelId}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Address, Txid}; +use bitcoin::{Address, BlockHash, Txid}; use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_invoice::{Bolt11Invoice, SignedRawBolt11Invoice}; @@ -165,6 +165,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 {