diff --git a/src/payment/store.rs b/src/payment/store.rs index bd26f8f33..3be966ee7 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -284,6 +284,11 @@ impl_writeable_tlv_based_enum!(PaymentStatus, #[derive(Clone, Debug, PartialEq, Eq)] pub enum PaymentKind { /// An on-chain payment. + /// + /// Payments of this kind will be considered pending until the respective transaction has + /// reached [`ANTI_REORG_DELAY`] confirmations on-chain. + /// + /// [`ANTI_REORG_DELAY`]: lightning::chain::channelmonitor::ANTI_REORG_DELAY Onchain { /// The transaction identifier of this payment. txid: Txid, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f2ae0a789..25c7f89df 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -10,13 +10,16 @@ use persist::KVStoreWalletPersister; use crate::logger::{log_debug, log_error, log_info, log_trace, FilesystemLogger, Logger}; use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; -use crate::payment::store::PaymentStore; +use crate::payment::store::{ConfirmationStatus, PaymentStore}; +use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; use crate::Error; use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::chain::{BestBlock, Listen}; use lightning::events::bump_transaction::{Utxo, WalletSource}; +use lightning::ln::channelmanager::PaymentId; use lightning::ln::inbound_payment::ExpandedKey; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; use lightning::ln::script::ShutdownScript; @@ -45,6 +48,7 @@ use bitcoin::{ use std::ops::Deref; use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub(crate) enum OnchainSendAmount { ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 }, @@ -109,6 +113,11 @@ where Error::PersistenceFailed })?; + self.update_payment_store(&mut *locked_wallet).map_err(|e| { + log_error!(self.logger, "Failed to update payment store: {}", e); + Error::PersistenceFailed + })?; + Ok(()) }, Err(e) => { @@ -133,6 +142,80 @@ where Ok(()) } + fn update_payment_store<'a>( + &self, locked_wallet: &'a mut PersistedWallet, + ) -> Result<(), Error> { + let latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + + let payments = locked_wallet + .transactions() + .map(|wtx| { + let id = PaymentId(wtx.tx_node.txid.to_byte_array()); + let txid = wtx.tx_node.txid; + let (payment_status, confirmation_status) = match wtx.chain_position { + bdk_chain::ChainPosition::Confirmed { anchor, .. } => { + let confirmation_height = anchor.block_id.height; + let cur_height = locked_wallet.latest_checkpoint().height(); + let payment_status = + if cur_height >= confirmation_height + ANTI_REORG_DELAY - 1 { + PaymentStatus::Succeeded + } else { + PaymentStatus::Pending + }; + let confirmation_status = ConfirmationStatus::Confirmed { + block_hash: anchor.block_id.hash, + height: confirmation_height, + timestamp: anchor.confirmation_time, + }; + (payment_status, confirmation_status) + }, + bdk_chain::ChainPosition::Unconfirmed { .. } => { + (PaymentStatus::Pending, ConfirmationStatus::Unconfirmed) + }, + }; + // TODO: It would be great to introduce additional variants for + // `ChannelFunding` and `ChannelClosing`. For the former, we could just + // take a reference to `ChannelManager` here and check against + // `list_channels`. But for the latter the best approach is much less + // clear: for force-closes/HTLC spends we should be good querying + // `OutputSweeper::tracked_spendable_outputs`, but regular channel closes + // (i.e., `SpendableOutputDescriptor::StaticOutput` variants) are directly + // spent to a wallet address. The only solution I can come up with is to + // create and persist a list of 'static pending outputs' that we could use + // here to determine the `PaymentKind`, but that's not really satisfactory, so + // we're punting on it until we can come up with a better solution. + let kind = + crate::payment::PaymentKind::Onchain { txid, status: confirmation_status }; + let (sent, received) = locked_wallet.sent_and_received(&wtx.tx_node.tx); + let (direction, amount_msat) = if sent > received { + let direction = PaymentDirection::Outbound; + let amount_msat = Some(sent.to_sat().saturating_sub(received.to_sat()) * 1000); + (direction, amount_msat) + } else { + let direction = PaymentDirection::Inbound; + let amount_msat = Some(received.to_sat().saturating_sub(sent.to_sat()) * 1000); + (direction, amount_msat) + }; + + PaymentDetails { + id, + kind, + amount_msat, + direction, + status: payment_status, + latest_update_timestamp, + } + }) + .collect(); + + self.payment_store.batch_update(payments)?; + + Ok(()) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, locktime: LockTime, @@ -477,7 +560,12 @@ where } match locked_wallet.apply_block(block, height) { - Ok(()) => (), + Ok(()) => { + if let Err(e) = self.update_payment_store(&mut *locked_wallet) { + log_error!(self.logger, "Failed to update payment store: {}", e); + return; + } + }, Err(e) => { log_error!( self.logger, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f3cff00dc..deaf3b80b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -482,6 +482,36 @@ pub(crate) fn do_channel_full_cycle( 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 saw the node funding transactions. + assert_eq!( + node_a + .list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 1 + ); + assert_eq!( + node_a + .list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 0 + ); + assert_eq!( + node_b + .list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 1 + ); + assert_eq!( + node_b + .list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 0 + ); + // Check we haven't got any events yet assert_eq!(node_a.next_event(), None); assert_eq!(node_b.next_event(), None); @@ -514,6 +544,15 @@ pub(crate) fn do_channel_full_cycle( node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + // Check we now see the channel funding transaction as outbound. + assert_eq!( + node_a + .list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 1 + ); + let onchain_fee_buffer_sat = 5000; let node_a_anchor_reserve_sat = if expect_anchor_channel { 25_000 } else { 0 }; let node_a_upper_bound_sat = @@ -558,22 +597,26 @@ pub(crate) fn do_channel_full_cycle( let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); assert_eq!(node_a.bolt11_payment().send(&invoice, None), Err(NodeError::DuplicatePayment)); - assert_eq!(node_a.list_payments().first().unwrap().id, payment_id); + assert!(!node_a.list_payments_with_filter(|p| p.id == payment_id).is_empty()); - let outbound_payments_a = - node_a.list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound); + let outbound_payments_a = node_a.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Outbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); assert_eq!(outbound_payments_a.len(), 1); - let inbound_payments_a = - node_a.list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound); + let inbound_payments_a = node_a.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); assert_eq!(inbound_payments_a.len(), 0); - let outbound_payments_b = - node_b.list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound); + let outbound_payments_b = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Outbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); assert_eq!(outbound_payments_b.len(), 0); - let inbound_payments_b = - node_b.list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound); + let inbound_payments_b = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); assert_eq!(inbound_payments_b.len(), 1); expect_event!(node_a, PaymentSuccessful); @@ -789,8 +832,26 @@ pub(crate) fn do_channel_full_cycle( node_b.payment(&keysend_payment_id).unwrap().kind, PaymentKind::Spontaneous { .. } )); - assert_eq!(node_a.list_payments().len(), 6); - assert_eq!(node_b.list_payments().len(), 7); + assert_eq!( + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(), + 5 + ); + assert_eq!( + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(), + 6 + ); + assert_eq!( + node_a + .list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Spontaneous { .. })) + .len(), + 1 + ); + assert_eq!( + node_b + .list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Spontaneous { .. })) + .len(), + 1 + ); println!("\nB close_channel (force: {})", force_close); if force_close { @@ -911,6 +972,22 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.list_balances().total_anchor_channels_reserve_sats, 0); assert_eq!(node_b.list_balances().total_anchor_channels_reserve_sats, 0); + // Now we should have seen the channel closing transaction on-chain. + assert_eq!( + node_a + .list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 2 + ); + assert_eq!( + node_b + .list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 2 + ); + // Check we handled all events assert_eq!(node_a.next_event(), None); assert_eq!(node_b.next_event(), None);