Skip to content

Harden pending-transfer balance corrections #1059

Description

@piotr-iohk

Context

#1058 (fixes #808) made the headline total include in-transfer buckets and added a temporary on-chain subtraction for Blocktank LSP funding payments, gated by a totals-comparison heuristic (currentOnchainSats >= preTransferOnchainSats). Review on #1058 (Greptile, Codex) surfaced several edge cases that were accepted as out of scope. All are transient display races that self-heal on wallet sync or transfer settlement, and all except item 4 mirror current iOS behavior. This ticket tracks the proper hardening.

Items

1. Replace the totals heuristic with a funding-txid presence check (core hardening)

Subtract the funding amount only while the funding txid is not yet present in the wallet's tx history / on-chain activity, instead of comparing balance totals. This is a one-way latch and eliminates the following windows in one change:

  • Concurrent deposit re-triggers subtraction — an incoming on-chain payment during a pending transfer can push the balance back above preTransferOnchainSats, re-applying the subtraction after LDK already reflected the send; total dips by up to txTotalSats until settlement. (Greptile inline comment on DeriveBalanceStateUseCase.)
  • Stale pre-transfer snapshotpreTransferOnchainSats is captured before sendOnChain(), but ensureSyncedBeforeSend() runs inside it and can change the balance before broadcast; a too-high snapshot disables the subtraction entirely. (Codex comment on TransferViewModel.)
  • Manual/external channel opens not coveredMANUAL_SETUP transfers have fundingTxId but no lspOrderId, so the current filter skips them; a txid-based check applies uniformly without needing lspOrderId. (Codex comment on BalanceState.)
  • Correction stops when the transfer settles before balance syncsyncTransferStates() can settle a to-spending transfer while on-chain is still stale, dropping the correction one cycle early. (Codex comment on DeriveBalanceStateUseCase.) A txid check tied to activity rather than active-transfer status also closes this.

The building blocks exist: fundingTxId is stored on transfers, and CoreService.activity.hasOnchainActivityForTxid() is already used by TransferRepo.syncTransferStates().

2. Hardware-wallet funded transfers: HW-balance-aware correction

HW-funded Blocktank transfers intentionally store NULL correction fields because the current mechanism subtracts from LDK's on-chain balance, while HW transfers spend from the Trezor's UTXOs. The real double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the HW watch-only balance still shows its pre-spend value, until the HW balance refresh sees the unconfirmed tx (typically seconds via blockbook). Manual testing showed no visible inflation, so low priority. If needed, correct against the hardware bucket using HwFundingBroadcastResult.totalSpent.

3. Force-close to-savings total dip (iOS parity gap, pre-existing)

For FORCE_CLOSE transfers, once LDK's closing balance disappears but the sweep is not yet detected on-chain, getTransferToSavingsSats returns 0 and balanceInTransferToSavings drops — now visible in the headline total after #1058. iOS covers this window by falling back to transfer.amountSats when there is no on-chain activity for the channel yet (getCloseTransferAmounts in BalanceManager). Android already has hasOnchainActivityForChannel; it is just not wired into balance derivation.

4. Minor / optional

  • Fee estimate vs actual UTXO selectiontxTotalSats is estimated without the UTXO set that sendOnChain() actually selects; deviation is limited to the mining-fee delta. Becomes mostly irrelevant once item 1 lands. (Codex comment on TransferViewModel.)
  • Send limits not corrected during the sync windowmaxSendOnchainSats / channelFundableBalance still derive from stale LDK balances while the funding spend is unreflected; pre-existing behavior, display-only fix in fix: keep pending transfer in total #1058 did not touch limits. Fold in only if observed in practice. (Codex comment on DeriveBalanceStateUseCase.)
  • Migrated in-flight orders — rows migrated with NULL correction fields get pre-fix behavior for that single in-flight transfer; one-time upgrade transient, accepted, no action planned. (Codex comment on DeriveBalanceStateUseCase.)

Cross-platform note

Items 1 and 2 apply equally to iOS (BalanceManager.getOrderPaymentOnchainToSubtract uses the same heuristic and the same lspOrderId filter). Hardening should land on both platforms to keep balance behavior in parity; a sibling ticket in bitkit-ios is warranted for item 1.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions