diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ab2f483a1..69cf5dd03 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -422,7 +422,7 @@ interface ClosureReason { [Enum] interface PaymentKind { - Onchain(Txid txid, ConfirmationStatus status); + Onchain(Txid txid, ConfirmationStatus status, sequence conflicting_txids); Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits); Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity); diff --git a/src/builder.rs b/src/builder.rs index c0e39af7a..4e2670904 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -58,6 +58,7 @@ use crate::io::utils::{ use crate::io::vss_store::VssStore; use crate::io::{ self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE, REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::liquidity::{ LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, @@ -70,7 +71,7 @@ use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, - OnionMessenger, PaymentStore, PeerManager, Persister, + OnionMessenger, PaymentStore, PeerManager, Persister, ReplacedTransactionStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -1239,6 +1240,21 @@ fn build_with_store_internal( }, }; + let replaced_tx_store = + match io::utils::read_replaced_txs(Arc::clone(&kv_store), Arc::clone(&logger)) { + Ok(replaced_txs) => Arc::new(ReplacedTransactionStore::new( + replaced_txs, + REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::clone(&logger), + )), + Err(e) => { + log_error!(logger, "Failed to read replaced transaction data from store: {}", e); + return Err(BuildError::ReadFailed); + }, + }; + let wallet = Arc::new(Wallet::new( bdk_wallet, wallet_persister, @@ -1247,6 +1263,7 @@ fn build_with_store_internal( Arc::clone(&payment_store), Arc::clone(&config), Arc::clone(&logger), + Arc::clone(&replaced_tx_store), )); let chain_source = match chain_data_source_config { diff --git a/src/io/mod.rs b/src/io/mod.rs index 38fba5114..3763544ca 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -78,3 +78,7 @@ pub(crate) const BDK_WALLET_INDEXER_KEY: &str = "indexer"; /// /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices"; + +/// The replaced transaction information will be persisted under this prefix. +pub(crate) const REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE: &str = "replaced_txs"; +pub(crate) const REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/io/utils.rs b/src/io/utils.rs index 98993ff11..aa831b7ae 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -45,6 +45,7 @@ use crate::io::{ NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, }; use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::ReplacedOnchainTransactionDetails; use crate::peer_store::PeerStore; use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; @@ -617,6 +618,38 @@ pub(crate) fn read_bdk_wallet_change_set( Ok(Some(change_set)) } +/// Read previously persisted replaced transaction information from the store. +pub(crate) fn read_replaced_txs( + kv_store: Arc, logger: L, +) -> Result, std::io::Error> +where + L::Target: LdkLogger, +{ + let mut res = Vec::new(); + + for stored_key in KVStoreSync::list( + &*kv_store, + REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE, + REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE, + )? { + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, + REPLACED_TX_PERSISTENCE_PRIMARY_NAMESPACE, + REPLACED_TX_PERSISTENCE_SECONDARY_NAMESPACE, + &stored_key, + )?); + let payment = ReplacedOnchainTransactionDetails::read(&mut reader).map_err(|e| { + log_error!(logger, "Failed to deserialize ReplacedOnchainTransactionDetails: {}", e); + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to deserialize ReplacedOnchainTransactionDetails", + ) + })?; + res.push(payment); + } + Ok(res) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/payment/mod.rs b/src/payment/mod.rs index f629960e1..f60e35363 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod asynchronous; mod bolt11; mod bolt12; mod onchain; +mod replaced_transaction_store; mod spontaneous; pub(crate) mod store; mod unified_qr; @@ -18,6 +19,7 @@ mod unified_qr; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; +pub use replaced_transaction_store::ReplacedOnchainTransactionDetails; pub use spontaneous::SpontaneousPayment; pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, diff --git a/src/payment/replaced_transaction_store.rs b/src/payment/replaced_transaction_store.rs new file mode 100644 index 000000000..1651ec49f --- /dev/null +++ b/src/payment/replaced_transaction_store.rs @@ -0,0 +1,104 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use bitcoin::Txid; +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::DecodeError; +use lightning::util::ser::{Readable, Writeable}; +use lightning::{_init_and_read_len_prefixed_tlv_fields, write_tlv_fields}; + +use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; + +/// Details of an on-chain transaction that has replaced a previous transaction (e.g., via RBF). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReplacedOnchainTransactionDetails { + /// The new transaction ID. + pub new_txid: Txid, + /// The original transaction ID that was replaced. + pub original_txid: Txid, + /// The payment ID associated with the transaction. + pub payment_id: PaymentId, +} + +impl ReplacedOnchainTransactionDetails { + pub(crate) fn new(new_txid: Txid, original_txid: Txid, payment_id: PaymentId) -> Self { + Self { new_txid, original_txid, payment_id } + } +} + +impl Writeable for ReplacedOnchainTransactionDetails { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.new_txid, required), + (2, self.original_txid, required), + (4, self.payment_id, required), + }); + Ok(()) + } +} + +impl Readable for ReplacedOnchainTransactionDetails { + fn read( + reader: &mut R, + ) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, new_txid, required), + (2, original_txid, required), + (4, payment_id, required), + }); + + let new_txid: Txid = new_txid.0.ok_or(DecodeError::InvalidValue)?; + let original_txid: Txid = original_txid.0.ok_or(DecodeError::InvalidValue)?; + let payment_id: PaymentId = payment_id.0.ok_or(DecodeError::InvalidValue)?; + + Ok(ReplacedOnchainTransactionDetails { new_txid, original_txid, payment_id }) + } +} + +impl StorableObjectId for Txid { + fn encode_to_hex_str(&self) -> String { + self.to_string() + } +} +impl StorableObject for ReplacedOnchainTransactionDetails { + type Id = Txid; + type Update = ReplacedOnchainTransactionDetailsUpdate; + + fn id(&self) -> Self::Id { + self.new_txid + } + + fn update(&mut self, _update: &Self::Update) -> bool { + // We don't update, we delete on confirmation + false + } + + fn to_update(&self) -> Self::Update { + self.into() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ReplacedOnchainTransactionDetailsUpdate { + pub id: Txid, +} + +impl From<&ReplacedOnchainTransactionDetails> for ReplacedOnchainTransactionDetailsUpdate { + fn from(value: &ReplacedOnchainTransactionDetails) -> Self { + Self { id: value.new_txid } + } +} + +impl StorableObjectUpdate + for ReplacedOnchainTransactionDetailsUpdate +{ + fn id(&self) -> ::Id { + self.id + } +} diff --git a/src/payment/store.rs b/src/payment/store.rs index 184de2ea9..b735dbb98 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -291,6 +291,15 @@ impl StorableObject for PaymentDetails { } } + if let Some(conflicting_txids_opt) = &update.conflicting_txids { + match self.kind { + PaymentKind::Onchain { ref mut conflicting_txids, .. } => { + update_if_necessary!(*conflicting_txids, conflicting_txids_opt.to_vec()); + }, + _ => {}, + } + } + if updated { self.latest_update_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -351,6 +360,8 @@ pub enum PaymentKind { txid: Txid, /// The confirmation status of this payment. status: ConfirmationStatus, + /// Transaction IDs that have replaced or conflict with this payment. + conflicting_txids: Vec, }, /// A [BOLT 11] payment. /// @@ -448,6 +459,7 @@ pub enum PaymentKind { impl_writeable_tlv_based_enum!(PaymentKind, (0, Onchain) => { (0, txid, required), + (1, conflicting_txids, optional_vec), (2, status, required), }, (2, Bolt11) => { @@ -540,6 +552,7 @@ pub(crate) struct PaymentDetailsUpdate { pub direction: Option, pub status: Option, pub confirmation_status: Option, + pub conflicting_txids: Option>, } impl PaymentDetailsUpdate { @@ -555,6 +568,7 @@ impl PaymentDetailsUpdate { direction: None, status: None, confirmation_status: None, + conflicting_txids: None, } } } @@ -570,9 +584,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { _ => (None, None, None), }; - let confirmation_status = match value.kind { - PaymentKind::Onchain { status, .. } => Some(status), - _ => None, + let (confirmation_status, conflicting_txids) = match &value.kind { + PaymentKind::Onchain { status, conflicting_txids, .. } => { + (Some(*status), conflicting_txids.clone()) + }, + _ => (None, Vec::new()), }; let counterparty_skimmed_fee_msat = match value.kind { @@ -593,6 +609,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { direction: Some(value.direction), status: Some(value.status), confirmation_status, + conflicting_txids: Some(conflicting_txids), } } } diff --git a/src/types.rs b/src/types.rs index b8dc10b18..c48568688 100644 --- a/src/types.rs +++ b/src/types.rs @@ -34,7 +34,7 @@ use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::RuntimeSpawner; use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::PaymentDetails; +use crate::payment::{PaymentDetails, ReplacedOnchainTransactionDetails}; /// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the /// same time. @@ -443,3 +443,6 @@ impl From<&(u64, Vec)> for CustomTlvRecord { CustomTlvRecord { type_num: tlv.0, value: tlv.1.clone() } } } + +pub(crate) type ReplacedTransactionStore = + DataStore>; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 0f3797431..3ac435070 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -11,6 +11,7 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_wallet::event::WalletEvent; #[allow(deprecated)] use bdk_wallet::SignOptions; use bdk_wallet::{Balance, KeychainKind, PersistedWallet, Update}; @@ -47,8 +48,10 @@ use crate::config::Config; use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::store::ConfirmationStatus; -use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::types::{Broadcaster, PaymentStore}; +use crate::payment::{ + PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, ReplacedOnchainTransactionDetails, +}; +use crate::types::{Broadcaster, PaymentStore, ReplacedTransactionStore}; use crate::Error; pub(crate) enum OnchainSendAmount { @@ -69,6 +72,7 @@ pub(crate) struct Wallet { payment_store: Arc, config: Arc, logger: Arc, + replaced_tx_store: Arc, } impl Wallet { @@ -76,11 +80,20 @@ impl Wallet { wallet: bdk_wallet::PersistedWallet, wallet_persister: KVStoreWalletPersister, broadcaster: Arc, fee_estimator: Arc, payment_store: Arc, - config: Arc, logger: Arc, + config: Arc, logger: Arc, replaced_tx_store: Arc, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); - Self { inner, persister, broadcaster, fee_estimator, payment_store, config, logger } + Self { + inner, + persister, + broadcaster, + fee_estimator, + payment_store, + config, + logger, + replaced_tx_store, + } } pub(crate) fn get_full_scan_request(&self) -> FullScanRequest { @@ -112,15 +125,15 @@ impl Wallet { pub(crate) fn apply_update(&self, update: impl Into) -> Result<(), Error> { let mut locked_wallet = self.inner.lock().unwrap(); - match locked_wallet.apply_update(update) { - Ok(()) => { + match locked_wallet.apply_update_events(update) { + Ok(events) => { let mut locked_persister = self.persister.lock().unwrap(); locked_wallet.persist(&mut locked_persister).map_err(|e| { log_error!(self.logger, "Failed to persist wallet: {}", e); Error::PersistenceFailed })?; - self.update_payment_store(&mut *locked_wallet).map_err(|e| { + self.update_payment_store(&mut *locked_wallet, Some(&events)).map_err(|e| { log_error!(self.logger, "Failed to update payment store: {}", e); Error::PersistenceFailed })?; @@ -152,75 +165,194 @@ impl Wallet { fn update_payment_store<'a>( &self, locked_wallet: &'a mut PersistedWallet, + events: Option<&'a Vec>, ) -> Result<(), Error> { - 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 fee = locked_wallet.calculate_fee(&wtx.tx_node.tx).unwrap_or(Amount::ZERO); - 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(fee.to_sat()).saturating_sub(received.to_sat()) - * 1000, - ); - (direction, amount_msat) + if let Some(events) = events { + if events.is_empty() { + return Ok(()); + } + + let sorted_events: Vec<_> = if events.len() > 1 { + let mut events_vec: Vec<_> = events.iter().collect(); + events_vec.sort_by_key(|e| match e { + WalletEvent::TxReplaced { .. } => 0, + WalletEvent::TxUnconfirmed { .. } => 1, + WalletEvent::TxConfirmed { .. } => 2, + WalletEvent::ChainTipChanged { .. } => 3, + WalletEvent::TxDropped { .. } => 4, + _ => 5, + }); + events_vec } else { - let direction = PaymentDirection::Inbound; - let amount_msat = Some( - received.to_sat().saturating_sub(sent.to_sat().saturating_sub(fee.to_sat())) - * 1000, - ); - (direction, amount_msat) + events.iter().collect() }; - let fee_paid_msat = Some(fee.to_sat() * 1000); - - let payment = PaymentDetails::new( - id, - kind, - amount_msat, - fee_paid_msat, - direction, - payment_status, - ); + for event in sorted_events { + match event { + WalletEvent::TxConfirmed { txid, tx, block_time, .. } => { + let cur_height = locked_wallet.latest_checkpoint().height(); + let confirmation_height = block_time.block_id.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: block_time.block_id.hash, + height: confirmation_height, + timestamp: block_time.confirmation_time, + }; + + let payment_id = self + .find_payment_by_txid(*txid) + .unwrap_or_else(|| PaymentId(txid.to_byte_array())); + + let payment = self.create_payment_from_tx( + locked_wallet, + *txid, + payment_id, + tx, + payment_status, + confirmation_status, + None, + ); + self.payment_store.insert_or_update(payment)?; + + // Remove any replaced transactions associated with this payment + let replaced_txids = self + .replaced_tx_store + .list_filter(|r| r.payment_id == payment_id) + .iter() + .map(|p| p.new_txid) + .collect::>(); + for replaced_txid in replaced_txids { + self.replaced_tx_store.remove(&replaced_txid)?; + } + }, + WalletEvent::ChainTipChanged { new_tip, .. } => { + // Get all payments that are Pending with Confirmed status + let pending_payments: Vec = + self.payment_store.list_filter(|p| { + p.status == PaymentStatus::Pending + && matches!( + p.kind, + crate::payment::PaymentKind::Onchain { + status: ConfirmationStatus::Confirmed { .. }, + .. + } + ) + }); + + for mut payment in pending_payments { + if let crate::payment::PaymentKind::Onchain { + status: ConfirmationStatus::Confirmed { height, .. }, + .. + } = payment.kind + { + if new_tip.height >= height + ANTI_REORG_DELAY - 1 { + payment.status = PaymentStatus::Succeeded; + self.payment_store.insert_or_update(payment)?; + } + } + } + }, + WalletEvent::TxUnconfirmed { txid, tx, old_block_time: None } => { + let payment_id = self + .find_payment_by_txid(*txid) + .unwrap_or_else(|| PaymentId(txid.to_byte_array())); + + let payment = self.create_payment_from_tx( + locked_wallet, + *txid, + payment_id, + tx, + PaymentStatus::Pending, + ConfirmationStatus::Unconfirmed, + None, + ); + self.payment_store.insert_or_update(payment)?; + }, + WalletEvent::TxReplaced { txid, conflicts, .. } => { + let payment_id = self + .find_payment_by_txid(*txid) + .unwrap_or_else(|| PaymentId(txid.to_byte_array())); + + // Collect all conflict txids + let conflict_txids: Vec = + conflicts.iter().map(|(_, conflict_txid)| *conflict_txid).collect(); + + for conflict_txid in conflict_txids { + // Update the replaced transaction store + let replaced_tx_details = ReplacedOnchainTransactionDetails::new( + conflict_txid, + *txid, + payment_id, + ); - self.payment_store.insert_or_update(payment)?; + self.replaced_tx_store.insert_or_update(replaced_tx_details)?; + } + }, + WalletEvent::TxDropped { txid, tx } => { + let payment_id = self + .find_payment_by_txid(*txid) + .unwrap_or_else(|| PaymentId(txid.to_byte_array())); + let payment = self.create_payment_from_tx( + locked_wallet, + *txid, + payment_id, + tx, + PaymentStatus::Pending, + ConfirmationStatus::Unconfirmed, + None, + ); + self.payment_store.insert_or_update(payment)?; + }, + _ => { + continue; + }, + }; + } + } else { + for wtx in locked_wallet.transactions() { + 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) + }, + }; + let payment_id = self + .find_payment_by_txid(txid) + .unwrap_or_else(|| PaymentId(txid.to_byte_array())); + let payment = self.create_payment_from_tx( + locked_wallet, + txid, + payment_id, + &wtx.tx_node.tx, + payment_status, + confirmation_status, + Some(Vec::new()), + ); + self.payment_store.insert_or_update(payment)?; + } } - Ok(()) } @@ -693,6 +825,89 @@ impl Wallet { Ok(tx) } + + fn create_payment_from_tx( + &self, locked_wallet: &PersistedWallet, txid: Txid, + payment_id: PaymentId, tx: &Transaction, payment_status: PaymentStatus, + confirmation_status: ConfirmationStatus, conflicting_txids: Option>, + ) -> PaymentDetails { + // 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 existing_payment = self.payment_store.get(&payment_id); + let final_conflicting_txids = if let Some(provided_conflicts) = conflicting_txids { + provided_conflicts + } else if let Some(payment) = &existing_payment { + if let PaymentKind::Onchain { conflicting_txids: existing_conflicts, .. } = + &payment.kind + { + existing_conflicts.clone() + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let kind = crate::payment::PaymentKind::Onchain { + txid, + status: confirmation_status, + conflicting_txids: final_conflicting_txids, + }; + + let fee = locked_wallet.calculate_fee(tx).unwrap_or(Amount::ZERO); + let (sent, received) = locked_wallet.sent_and_received(tx); + let fee_sat = fee.to_sat(); + + let (direction, amount_msat) = if sent > received { + ( + PaymentDirection::Outbound, + Some( + (sent.to_sat().saturating_sub(fee_sat).saturating_sub(received.to_sat())) + * 1000, + ), + ) + } else { + ( + PaymentDirection::Inbound, + Some( + received.to_sat().saturating_sub(sent.to_sat().saturating_sub(fee_sat)) * 1000, + ), + ) + }; + + PaymentDetails::new( + payment_id, + kind, + amount_msat, + Some(fee_sat * 1000), + direction, + payment_status, + ) + } + + fn find_payment_by_txid(&self, target_txid: Txid) -> Option { + let direct_payment_id = PaymentId(target_txid.to_byte_array()); + if self.payment_store.get(&direct_payment_id).is_some() { + return Some(direct_payment_id); + } + + // Check if this txid is a replaced transaction + if let Some(replaced_details) = self.replaced_tx_store.get(&target_txid) { + return Some(replaced_details.payment_id); + } + + None + } } impl Listen for Wallet { @@ -723,7 +938,7 @@ impl Listen for Wallet { match locked_wallet.apply_block(block, height) { Ok(()) => { - if let Err(e) = self.update_payment_store(&mut *locked_wallet) { + if let Err(e) = self.update_payment_store(&mut *locked_wallet, None) { log_error!(self.logger, "Failed to update payment store: {}", e); return; } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index e2d4207cd..e6b5c61ed 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -452,7 +452,7 @@ async fn onchain_send_receive() { let payment_a = node_a.payment(&payment_id).unwrap(); match payment_a.kind { - PaymentKind::Onchain { txid: _txid, status } => { + PaymentKind::Onchain { txid: _txid, status, .. } => { assert_eq!(_txid, txid); assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); }, @@ -461,7 +461,7 @@ async fn onchain_send_receive() { let payment_b = node_a.payment(&payment_id).unwrap(); match payment_b.kind { - PaymentKind::Onchain { txid: _txid, status } => { + PaymentKind::Onchain { txid: _txid, status, .. } => { assert_eq!(_txid, txid); assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); },