Summary
Wallet::list_transactions() filters out all inbound payments whose status is not Completed. For Lightning that's the right call (it hides issued-but-unpaid invoices, as the code comment says). But the same filter also swallows inbound on-chain payments that are already observed on-chain — they stay invisible until ldk-node marks them Succeeded, which happens only after ANTI_REORG_DELAY (6) confirmations.
The result: a user's on-chain deposit is missing from the transaction list for ~3 minutes on Mutinynet (30s blocks) and ~1 hour on mainnet (10-min blocks), while get_balance() already reports it as pending. Balance and history disagree for the whole window, which reads as lost funds to users.
Where
At rev f393ed99e25cad4540dbe2c3fecb07f83b1cb91f, the no-metadata fallback arm of the lightning-payments loop in list_transactions (this is where ldk-node's PaymentKind::Onchain records land):
|
let status = payment.status.into(); |
|
if status != TxStatus::Completed { |
|
// We don't bother to surface pending inbound transactions (i.e. issued but |
|
// unpaid invoices) in our transaction list, in part because these may be |
|
// failed rebalances. |
|
continue; |
|
} |
(The equivalent filter in the trusted-payments loop at L857 is fine as-is — pending inbound there really are unpaid mint quotes.)
let status = payment.status.into();
if status != TxStatus::Completed {
// We don't bother to surface pending inbound transactions (i.e. issued but
// unpaid invoices) in our transaction list, in part because these may be
// failed rebalances.
continue;
}
An inbound PaymentKind::Onchain payment here is not an unpaid invoice: it only exists in the payment store because the transaction was actually seen in the mempool or in a block. Skipping it doesn't hide clutter; it hides real money in flight.
Timed reproduction (Mutinynet, real device)
50,000-sat on-chain send to a wallet running orange-sdk at the rev above:
| t |
Event |
| 04:07:00 |
tx broadcast, esplora indexes it |
| 04:07:38 |
next on-chain sync: BDK sees the tx, get_balance() shows the pending amount |
| 04:10:16 |
first sync after 6th confirmation: payment flips to Succeeded |
| ~04:10:25 |
tx finally appears in list_transactions() — 3m25s of balance/history disagreement |
On mainnet the same window is ~6 blocks ≈ 1 hour.
Proposed fix
Narrow the filter so it only hides what the comment intends — pending inbound invoice-kind payments — and let observed on-chain inbound payments through with their real (pending) status:
if status != TxStatus::Completed {
// Hide issued-but-unpaid invoices, but DO surface inbound on-chain
// payments: they only exist in the store once the tx has been observed
// in the mempool/chain, so they represent real funds in flight.
if !matches!(payment.kind, PaymentKind::Onchain { .. }) {
continue;
}
}
Transaction already carries status: TxStatus, so callers can render these as pending; anti-reorg conservatism for spending is untouched. If you'd prefer this behind an opt-in (a Tunables flag or a list_transactions_including_pending() variant) to avoid changing existing consumers' expectations, that works for us too — happy to send a PR for whichever shape you want.
Environment
- orange-sdk
f393ed99, features spark,cashu; ldk-node 0.8.0+git (109978de)
- Mutinynet signet, esplora chain source, Cashu trusted tier
Summary
Wallet::list_transactions()filters out all inbound payments whose status is notCompleted. For Lightning that's the right call (it hides issued-but-unpaid invoices, as the code comment says). But the same filter also swallows inbound on-chain payments that are already observed on-chain — they stay invisible until ldk-node marks themSucceeded, which happens only afterANTI_REORG_DELAY(6) confirmations.The result: a user's on-chain deposit is missing from the transaction list for ~3 minutes on Mutinynet (30s blocks) and ~1 hour on mainnet (10-min blocks), while
get_balance()already reports it as pending. Balance and history disagree for the whole window, which reads as lost funds to users.Where
At rev
f393ed99e25cad4540dbe2c3fecb07f83b1cb91f, the no-metadata fallback arm of the lightning-payments loop inlist_transactions(this is where ldk-node'sPaymentKind::Onchainrecords land):orange-sdk/orange-sdk/src/lib.rs
Lines 978 to 984 in f393ed9
(The equivalent filter in the trusted-payments loop at L857 is fine as-is — pending inbound there really are unpaid mint quotes.)
An inbound
PaymentKind::Onchainpayment here is not an unpaid invoice: it only exists in the payment store because the transaction was actually seen in the mempool or in a block. Skipping it doesn't hide clutter; it hides real money in flight.Timed reproduction (Mutinynet, real device)
50,000-sat on-chain send to a wallet running orange-sdk at the rev above:
get_balance()shows the pending amountSucceededlist_transactions()— 3m25s of balance/history disagreementOn mainnet the same window is ~6 blocks ≈ 1 hour.
Proposed fix
Narrow the filter so it only hides what the comment intends — pending inbound invoice-kind payments — and let observed on-chain inbound payments through with their real (pending) status:
Transactionalready carriesstatus: TxStatus, so callers can render these as pending; anti-reorg conservatism for spending is untouched. If you'd prefer this behind an opt-in (aTunablesflag or alist_transactions_including_pending()variant) to avoid changing existing consumers' expectations, that works for us too — happy to send a PR for whichever shape you want.Environment
f393ed99, featuresspark,cashu; ldk-node0.8.0+git(109978de)