diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c6c7f4f38..594443ac5 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -87,6 +87,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(); @@ -227,6 +228,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..60a64dc7a --- /dev/null +++ b/src/balance.rs @@ -0,0 +1,182 @@ +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. + pub total_lightning_balance_sats: u64, + /// A detailed list of all known balances. + pub lightning_balances: Vec, +} + +/// Details about the status of a known 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 identfiier 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 identfiier 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 identfiier 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 identfiier 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 identfiier 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 identfiier of our channel counterparty. + counterparty_node_id: PublicKey, + /// The amount, in satoshis, of the output which we can claim. + /// + /// Note that for outputs from HTLC balances this may be excluding some on-chain fees that + /// were already spent. + 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 0e64e9fe3..eb413c164 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; @@ -97,6 +98,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; @@ -1554,6 +1556,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: