Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose onchain transactions in store #432

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)

8 changes: 7 additions & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
@@ -358,7 +358,7 @@ interface ClosureReason {

[Enum]
interface PaymentKind {
Onchain();
Onchain(Txid txid, ConfirmationStatus status);
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits);
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
@@ -389,6 +389,12 @@ dictionary LSPFeeLimits {
u64? max_proportional_opening_fee_ppm_msat;
};

[Enum]
interface ConfirmationStatus {
Confirmed (BlockHash block_hash, u32 height, u64 timestamp);
Unconfirmed ();
};

dictionary PaymentDetails {
PaymentId id;
PaymentKind kind;
21 changes: 11 additions & 10 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -855,11 +855,22 @@ fn build_with_store_internal(

let tx_broadcaster = Arc::new(TransactionBroadcaster::new(Arc::clone(&logger)));
let fee_estimator = Arc::new(OnchainFeeEstimator::new());

let payment_store = match io::utils::read_payments(Arc::clone(&kv_store), Arc::clone(&logger)) {
Ok(payments) => {
Arc::new(PaymentStore::new(payments, Arc::clone(&kv_store), Arc::clone(&logger)))
},
Err(_) => {
return Err(BuildError::ReadFailed);
},
};

let wallet = Arc::new(Wallet::new(
bdk_wallet,
wallet_persister,
Arc::clone(&tx_broadcaster),
Arc::clone(&fee_estimator),
Arc::clone(&payment_store),
Arc::clone(&logger),
));

@@ -1249,16 +1260,6 @@ fn build_with_store_internal(
},
}

// Init payment info storage
let payment_store = match io::utils::read_payments(Arc::clone(&kv_store), Arc::clone(&logger)) {
Ok(payments) => {
Arc::new(PaymentStore::new(payments, Arc::clone(&kv_store), Arc::clone(&logger)))
},
Err(_) => {
return Err(BuildError::ReadFailed);
},
};

let event_queue = match io::utils::read_event_queue(Arc::clone(&kv_store), Arc::clone(&logger))
{
Ok(event_queue) => Arc::new(event_queue),
4 changes: 3 additions & 1 deletion src/payment/mod.rs
Original file line number Diff line number Diff line change
@@ -18,7 +18,9 @@ pub use bolt11::Bolt11Payment;
pub use bolt12::Bolt12Payment;
pub use onchain::OnchainPayment;
pub use spontaneous::SpontaneousPayment;
pub use store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
pub use store::{
ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
};
pub use unified_qr::{QrPaymentResult, UnifiedQrPayment};

/// Represents information used to send a payment.
270 changes: 214 additions & 56 deletions src/payment/store.rs
Original file line number Diff line number Diff line change
@@ -25,8 +25,10 @@ use lightning::{

use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};

use bitcoin::{BlockHash, Txid};

use std::collections::hash_map;
use std::collections::HashMap;
use std::iter::FromIterator;
use std::ops::Deref;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -59,6 +61,121 @@ impl PaymentDetails {
.as_secs();
Self { id, kind, amount_msat, direction, status, latest_update_timestamp }
}

pub(crate) fn update(&mut self, update: &PaymentDetailsUpdate) -> bool {
debug_assert_eq!(
self.id, update.id,
"We should only ever override payment data for the same payment id"
);

let mut updated = false;

macro_rules! update_if_necessary {
($val: expr, $update: expr) => {
if $val != $update {
$val = $update;
updated = true;
}
};
}

if let Some(hash_opt) = update.hash {
match self.kind {
PaymentKind::Bolt12Offer { ref mut hash, .. } => {
debug_assert_eq!(
self.direction,
PaymentDirection::Outbound,
"We should only ever override payment hash for outbound BOLT 12 payments"
);
debug_assert!(
hash.is_none() || *hash == hash_opt,
"We should never change a payment hash after being initially set"
);
update_if_necessary!(*hash, hash_opt);
},
PaymentKind::Bolt12Refund { ref mut hash, .. } => {
debug_assert_eq!(
self.direction,
PaymentDirection::Outbound,
"We should only ever override payment hash for outbound BOLT 12 payments"
);
debug_assert!(
hash.is_none() || *hash == hash_opt,
"We should never change a payment hash after being initially set"
);
update_if_necessary!(*hash, hash_opt);
},
_ => {
// We can omit updating the hash for BOLT11 payments as the payment hash
// will always be known from the beginning.
},
}
}
if let Some(preimage_opt) = update.preimage {
match self.kind {
PaymentKind::Bolt11 { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Bolt11Jit { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Bolt12Offer { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Bolt12Refund { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
PaymentKind::Spontaneous { ref mut preimage, .. } => {
update_if_necessary!(*preimage, preimage_opt)
},
_ => {},
}
}

if let Some(secret_opt) = update.secret {
match self.kind {
PaymentKind::Bolt11 { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
PaymentKind::Bolt11Jit { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
PaymentKind::Bolt12Offer { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
PaymentKind::Bolt12Refund { ref mut secret, .. } => {
update_if_necessary!(*secret, secret_opt)
},
_ => {},
}
}

if let Some(amount_opt) = update.amount_msat {
update_if_necessary!(self.amount_msat, amount_opt);
}

if let Some(status) = update.status {
update_if_necessary!(self.status, status);
}

if let Some(confirmation_status) = update.confirmation_status {
match self.kind {
PaymentKind::Onchain { ref mut status, .. } => {
update_if_necessary!(*status, confirmation_status);
},
_ => {},
}
}

if updated {
self.latest_update_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs();
}

updated
}
}

impl Writeable for PaymentDetails {
@@ -175,7 +292,17 @@ impl_writeable_tlv_based_enum!(PaymentStatus,
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaymentKind {
/// An on-chain payment.
Onchain,
///
/// 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,
/// The confirmation status of this payment.
status: ConfirmationStatus,
},
/// A [BOLT 11] payment.
///
/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
@@ -264,7 +391,10 @@ pub enum PaymentKind {
}

impl_writeable_tlv_based_enum!(PaymentKind,
(0, Onchain) => {},
(0, Onchain) => {
(0, txid, required),
(2, status, required),
},
(2, Bolt11) => {
(0, hash, required),
(2, preimage, option),
@@ -297,6 +427,31 @@ impl_writeable_tlv_based_enum!(PaymentKind,
}
);

/// Represents the confirmation status of a transaction.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ConfirmationStatus {
/// The transaction is confirmed in the best chain.
Confirmed {
/// The hash of the block in which the transaction was confirmed.
block_hash: BlockHash,
/// The height under which the block was confirmed.
height: u32,
/// The timestamp, in seconds since start of the UNIX epoch, when this entry was last updated.
timestamp: u64,
},
/// The transaction is unconfirmed.
Unconfirmed,
}

impl_writeable_tlv_based_enum!(ConfirmationStatus,
(0, Confirmed) => {
(0, block_hash, required),
(2, height, required),
(4, timestamp, required),
},
(2, Unconfirmed) => {},
);

/// Limits applying to how much fee we allow an LSP to deduct from the payment amount.
///
/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information.
@@ -326,6 +481,7 @@ pub(crate) struct PaymentDetailsUpdate {
pub amount_msat: Option<Option<u64>>,
pub direction: Option<PaymentDirection>,
pub status: Option<PaymentStatus>,
pub confirmation_status: Option<ConfirmationStatus>,
}

impl PaymentDetailsUpdate {
@@ -338,6 +494,36 @@ impl PaymentDetailsUpdate {
amount_msat: None,
direction: None,
status: None,
confirmation_status: None,
}
}
}

impl From<&PaymentDetails> for PaymentDetailsUpdate {
fn from(value: &PaymentDetails) -> Self {
let (hash, preimage, secret) = match value.kind {
PaymentKind::Bolt11 { hash, preimage, secret, .. } => (Some(hash), preimage, secret),
PaymentKind::Bolt11Jit { hash, preimage, secret, .. } => (Some(hash), preimage, secret),
PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => (hash, preimage, secret),
PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => (hash, preimage, secret),
PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None),
_ => (None, None, None),
};

let confirmation_status = match value.kind {
PaymentKind::Onchain { status, .. } => Some(status),
_ => None,
};

Self {
id: value.id,
hash: Some(hash),
preimage: Some(preimage),
secret: Some(secret),
amount_msat: Some(value.amount_msat),
direction: Some(value.direction),
status: Some(value.status),
confirmation_status,
}
}
}
@@ -370,6 +556,28 @@ where
Ok(updated)
}

pub(crate) fn insert_or_update(&self, payment: &PaymentDetails) -> Result<bool, Error> {
let mut locked_payments = self.payments.lock().unwrap();

let updated;
match locked_payments.entry(payment.id) {
hash_map::Entry::Occupied(mut e) => {
let update = payment.into();
updated = e.get_mut().update(&update);
if updated {
self.persist_info(&payment.id, e.get())?;
}
},
hash_map::Entry::Vacant(e) => {
e.insert(payment.clone());
self.persist_info(&payment.id, payment)?;
updated = true;
},
}

Ok(updated)
}

pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> {
let store_key = hex_utils::to_string(&id.0);
self.kv_store
@@ -401,60 +609,10 @@ where
let mut locked_payments = self.payments.lock().unwrap();

if let Some(payment) = locked_payments.get_mut(&update.id) {
if let Some(hash_opt) = update.hash {
match payment.kind {
PaymentKind::Bolt12Offer { ref mut hash, .. } => {
debug_assert_eq!(payment.direction, PaymentDirection::Outbound,
"We should only ever override payment hash for outbound BOLT 12 payments");
*hash = hash_opt
},
PaymentKind::Bolt12Refund { ref mut hash, .. } => {
debug_assert_eq!(payment.direction, PaymentDirection::Outbound,
"We should only ever override payment hash for outbound BOLT 12 payments");
*hash = hash_opt
},
_ => {
// We can omit updating the hash for BOLT11 payments as the payment hash
// will always be known from the beginning.
},
}
}
if let Some(preimage_opt) = update.preimage {
match payment.kind {
PaymentKind::Bolt11 { ref mut preimage, .. } => *preimage = preimage_opt,
PaymentKind::Bolt11Jit { ref mut preimage, .. } => *preimage = preimage_opt,
PaymentKind::Bolt12Offer { ref mut preimage, .. } => *preimage = preimage_opt,
PaymentKind::Bolt12Refund { ref mut preimage, .. } => *preimage = preimage_opt,
PaymentKind::Spontaneous { ref mut preimage, .. } => *preimage = preimage_opt,
_ => {},
}
updated = payment.update(update);
if updated {
self.persist_info(&update.id, payment)?;
}

if let Some(secret_opt) = update.secret {
match payment.kind {
PaymentKind::Bolt11 { ref mut secret, .. } => *secret = secret_opt,
PaymentKind::Bolt11Jit { ref mut secret, .. } => *secret = secret_opt,
PaymentKind::Bolt12Offer { ref mut secret, .. } => *secret = secret_opt,
PaymentKind::Bolt12Refund { ref mut secret, .. } => *secret = secret_opt,
_ => {},
}
}

if let Some(amount_opt) = update.amount_msat {
payment.amount_msat = amount_opt;
}

if let Some(status) = update.status {
payment.status = status;
}

payment.latest_update_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs();

self.persist_info(&update.id, payment)?;
updated = true;
}
Ok(updated)
}
4 changes: 3 additions & 1 deletion src/uniffi_types.rs
Original file line number Diff line number Diff line change
@@ -14,7 +14,9 @@ pub use crate::config::{
default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure,
};
pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo};
pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus};
pub use crate::payment::store::{
ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus,
};
pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters};

pub use lightning::chain::channelmonitor::BalanceSource;
95 changes: 91 additions & 4 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
@@ -7,15 +7,19 @@

use persist::KVStoreWalletPersister;

use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger};
use crate::logger::{log_debug, log_error, log_info, log_trace, Logger, LdkLogger};

use crate::fee_estimator::{ConfirmationTarget, FeeEstimator};
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 +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 },
@@ -66,6 +71,7 @@ where
persister: Mutex<KVStoreWalletPersister>,
broadcaster: B,
fee_estimator: E,
payment_store: Arc<PaymentStore<Arc<Logger>>>,
logger: L,
}

@@ -77,11 +83,12 @@ where
{
pub(crate) fn new(
wallet: bdk_wallet::PersistedWallet<KVStoreWalletPersister>,
wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E, logger: L,
wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E,
payment_store: Arc<PaymentStore<Arc<Logger>>>, logger: L,
) -> Self {
let inner = Mutex::new(wallet);
let persister = Mutex::new(wallet_persister);
Self { inner, persister, broadcaster, fee_estimator, logger }
Self { inner, persister, broadcaster, fee_estimator, payment_store, logger }
}

pub(crate) fn get_full_scan_request(&self) -> FullScanRequest<KeychainKind> {
@@ -107,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) => {
@@ -131,6 +143,76 @@ where
Ok(())
}

fn update_payment_store<'a>(
&self, locked_wallet: &'a mut PersistedWallet<KVStoreWalletPersister>,
) -> 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
};
Comment on lines +161 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say the transaction was re-orged out and was then RBF'ed. Is that possible? Would we have payment stuck in pending?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's a very valid concern. Tbh. I'm not entirely sure how to handle that: for one, the RBF'd transaction is technically pending as there could be another reorg that makes it part of the best chain again. So essentially any signed & broadcasted transaction remains 'pending' forever.

I think the only way I can come up with to handle this would be to never add Pending transactions but jump to Succeeded directly once they reach ANTI_REORG_DELAY. Or we just keep the current approach and add docs noting that transactions in pending might never succeed for one reason or another?

I think at least for BDK's own RBF's we could see if we find a better solution when we do #367.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we possibly detect that the UTXO has been spent and remove the pending payment?

Copy link
Collaborator Author

@tnull tnull Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, but that would require us to keep track and persist a list of all the UTXOs, write an adapter around lightning-transaction-sync's Filter implementation to also register spends for them, and then implement Confirm on Wallet, just to be able to remove any Pending entries that have been replaced.

For now that seems like a lot of overhead to just account for this rare edgecase (reorg + RBF)? But I wouldn't completely rule it out to do something like it in if we find we need to do additional RBF tracking anyways as part of #367.

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.
Comment on lines +178 to +188
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this gets more complicated with splicing in/out or more exotic transactions paying us and opening a channel, for instance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good point, now opened the more lightningdevkit/rust-lightning#3566 to supersede lightningdevkit/rust-lightning#3548

Would be great if an API allowing to query the type of a given transaction (Txid) could be added as part of the splicing work.

let kind = crate::payment::PaymentKind::Onchain { txid, status: confirmation_status };
let (sent, received) = locked_wallet.sent_and_received(&wtx.tx_node.tx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is change abstracted away from this API?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the docs state:

    /// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts
    /// that spend from previous txouts tracked by this wallet. Received is the summation
    /// of this tx's outputs that send to script pubkeys tracked by this wallet.

so IIUC received would include the change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the change be more than the amount sent, making it look like an inbound payment when it was really an outbound? Or am I misunderstanding?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the change be more than the amount sent, making it look like an inbound payment when it was really an outbound? Or am I misunderstanding?

IIUC, any change amount would be accounted for both in sent and received, so you can essentially ignore it in terms of comparisons (think: subtract on both sides).

Say you send a payment of 10 sats, and add a txin spending an wallet output with 100 sats. This would result in two txouts, one to the recipient with 10 sats, one change back to a wallet address with 85sats or so, leaving 5 for mining fees. IIUC, this would result in sent = 100, received = 85, resulting in sent > received => PaymentDirection::Outbound.

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)
};
Comment on lines +191 to +199
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider these two separate payments? I guess we could't use the txid as the payment id, though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhh, given that (IIUC) received includes the change, I'd rather not add each change output as an inbound payment?


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,
@@ -478,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,
99 changes: 88 additions & 11 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -484,6 +484,36 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
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<E: ElectrumApi>(
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<E: ElectrumApi>(
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<E: ElectrumApi>(
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<E: ElectrumApi>(
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);
78 changes: 70 additions & 8 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
@@ -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, .. } => {