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 c82a5d92e..c8c43c49c 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 @@ -296,8 +296,8 @@ class LibraryTest { assert(paymentReceivedEvent is Event.PaymentReceived) node2.eventHandled() - assert(node1.listPayments().size == 1) - assert(node2.listPayments().size == 1) + assert(node1.listPayments().size == 3) + assert(node2.listPayments().size == 2) node2.closeChannel(userChannelId, nodeId1) diff --git a/src/payment/store.rs b/src/payment/store.rs index 2c968dc96..edae27329 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -292,6 +292,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 4cb2f88b2..6f8b7b185 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, Logger, LdkLogger}; 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; @@ -46,6 +49,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 }, @@ -110,6 +114,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) => { @@ -134,6 +143,76 @@ 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(); + + for wtx in locked_wallet.transactions() { + 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) + }; + + let payment = PaymentDetails { + id, + kind, + amount_msat, + direction, + status: payment_status, + latest_update_timestamp, + }; + + self.payment_store.insert_or_update(&payment)?; + } + + Ok(()) + } + pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, locktime: LockTime, @@ -481,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 cb58a28cd..31a08eb07 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -484,6 +484,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); @@ -516,6 +546,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 = @@ -565,22 +604,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); @@ -814,8 +857,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 { @@ -936,6 +997,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); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 8dd39133b..2dc74cea9 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -15,7 +15,10 @@ use common::{ }; use ldk_node::config::EsploraSyncConfig; -use ldk_node::payment::{PaymentKind, QrPaymentResult, SendingParameters}; +use ldk_node::payment::{ + ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, + SendingParameters, +}; use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; @@ -299,6 +302,24 @@ fn onchain_spend_receive() { 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); + let node_a_payments = node_a.list_payments(); + let node_b_payments = node_b.list_payments(); + for payments in [&node_a_payments, &node_b_payments] { + assert_eq!(payments.len(), 1) + } + for p in [node_a_payments.first().unwrap(), node_b_payments.first().unwrap()] { + assert_eq!(p.amount_msat, Some(premine_amount_sat * 1000)); + assert_eq!(p.direction, PaymentDirection::Inbound); + // We got only 1-conf here, so we're only pending for now. + assert_eq!(p.status, PaymentStatus::Pending); + match p.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + } + let channel_amount_sat = 1_000_000; let reserve_amount_sat = 25_000; open_channel(&node_b, &node_a, channel_amount_sat, true, &electrsd); @@ -309,6 +330,13 @@ fn onchain_spend_receive() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 1); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 2); + let onchain_fee_buffer_sat = 1000; let expected_node_a_balance = premine_amount_sat - reserve_amount_sat; let expected_node_b_balance_lower = @@ -340,6 +368,13 @@ fn onchain_spend_receive() { assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 2); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 3); + let addr_b = node_b.onchain_payment().new_address().unwrap(); let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); @@ -356,6 +391,13 @@ fn onchain_spend_receive() { assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 3); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 4); + let addr_b = node_b.onchain_payment().new_address().unwrap(); let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false, None).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); @@ -372,6 +414,13 @@ fn onchain_spend_receive() { assert_eq!(node_a.list_balances().total_onchain_balance_sats, expected_node_a_balance); assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); + + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 4); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 5); } #[test] @@ -613,7 +662,8 @@ fn simple_bolt12_send_receive() { .unwrap(); expect_payment_successful_event!(node_a, Some(payment_id), None); - let node_a_payments = node_a.list_payments(); + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); assert_eq!(node_a_payments.len(), 1); match node_a_payments.first().unwrap().kind { PaymentKind::Bolt12Offer { @@ -639,7 +689,8 @@ fn simple_bolt12_send_receive() { assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); expect_payment_received_event!(node_b, expected_amount_msat); - let node_b_payments = node_b.list_payments(); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); assert_eq!(node_b_payments.len(), 1); match node_b_payments.first().unwrap().kind { PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { @@ -676,7 +727,9 @@ fn simple_bolt12_send_receive() { .unwrap(); expect_payment_successful_event!(node_a, Some(payment_id), None); - let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id); + let node_a_payments = node_a.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == payment_id + }); assert_eq!(node_a_payments.len(), 1); let payment_hash = match node_a_payments.first().unwrap().kind { PaymentKind::Bolt12Offer { @@ -704,7 +757,9 @@ fn simple_bolt12_send_receive() { expect_payment_received_event!(node_b, expected_amount_msat); let node_b_payment_id = PaymentId(payment_hash.0); - let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); + let node_b_payments = node_b.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == node_b_payment_id + }); assert_eq!(node_b_payments.len(), 1); match node_b_payments.first().unwrap().kind { PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { @@ -731,13 +786,18 @@ fn simple_bolt12_send_receive() { expect_payment_received_event!(node_a, overpaid_amount); let node_b_payment_id = node_b - .list_payments_with_filter(|p| p.amount_msat == Some(overpaid_amount)) + .list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Refund { .. }) + && p.amount_msat == Some(overpaid_amount) + }) .first() .unwrap() .id; expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); - let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); + let node_b_payments = node_b.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_b_payment_id + }); assert_eq!(node_b_payments.len(), 1); match node_b_payments.first().unwrap().kind { PaymentKind::Bolt12Refund { @@ -761,7 +821,9 @@ fn simple_bolt12_send_receive() { assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); let node_a_payment_id = PaymentId(invoice.payment_hash().0); - let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id); + let node_a_payments = node_a.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_a_payment_id + }); assert_eq!(node_a_payments.len(), 1); match node_a_payments.first().unwrap().kind { PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => {