Skip to content

list_transactions() hides pending inbound on-chain payments until 6 confirmations (~1 hour invisible on mainnet) #84

Description

@hash-money

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions