You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#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 snapshot — preTransferOnchainSats 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 covered — MANUAL_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 sync — syncTransferStates() 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().
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 selection — txTotalSats 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 window — maxSendOnchainSats / 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.
Review threads: Greptile and Codex comments on fix: keep pending transfer in total #1058 (concurrent deposit, stale snapshot, manual opens, settle-before-sync, HW path, fee estimate, send limits, migration NULLs)
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:
preTransferOnchainSats, re-applying the subtraction after LDK already reflected the send; total dips by up totxTotalSatsuntil settlement. (Greptile inline comment onDeriveBalanceStateUseCase.)preTransferOnchainSatsis captured beforesendOnChain(), butensureSyncedBeforeSend()runs inside it and can change the balance before broadcast; a too-high snapshot disables the subtraction entirely. (Codex comment onTransferViewModel.)MANUAL_SETUPtransfers havefundingTxIdbut nolspOrderId, so the current filter skips them; a txid-based check applies uniformly without needinglspOrderId. (Codex comment onBalanceState.)syncTransferStates()can settle a to-spending transfer while on-chain is still stale, dropping the correction one cycle early. (Codex comment onDeriveBalanceStateUseCase.) A txid check tied to activity rather than active-transfer status also closes this.The building blocks exist:
fundingTxIdis stored on transfers, andCoreService.activity.hasOnchainActivityForTxid()is already used byTransferRepo.syncTransferStates().2. Hardware-wallet funded transfers: HW-balance-aware correction
HW-funded Blocktank transfers intentionally store
NULLcorrection 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 intotalWithHardwareSats:balanceInTransferToSpendingaddsclientBalanceSatwhile 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 usingHwFundingBroadcastResult.totalSpent.3. Force-close to-savings total dip (iOS parity gap, pre-existing)
For
FORCE_CLOSEtransfers, once LDK's closing balance disappears but the sweep is not yet detected on-chain,getTransferToSavingsSatsreturns 0 andbalanceInTransferToSavingsdrops — now visible in the headline total after #1058. iOS covers this window by falling back totransfer.amountSatswhen there is no on-chain activity for the channel yet (getCloseTransferAmountsinBalanceManager). Android already hashasOnchainActivityForChannel; it is just not wired into balance derivation.4. Minor / optional
txTotalSatsis estimated without the UTXO set thatsendOnChain()actually selects; deviation is limited to the mining-fee delta. Becomes mostly irrelevant once item 1 lands. (Codex comment onTransferViewModel.)maxSendOnchainSats/channelFundableBalancestill 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 onDeriveBalanceStateUseCase.)NULLcorrection fields get pre-fix behavior for that single in-flight transfer; one-time upgrade transient, accepted, no action planned. (Codex comment onDeriveBalanceStateUseCase.)Cross-platform note
Items 1 and 2 apply equally to iOS (
BalanceManager.getOrderPaymentOnchainToSubtractuses the same heuristic and the samelspOrderIdfilter). Hardening should land on both platforms to keep balance behavior in parity; a sibling ticket inbitkit-iosis warranted for item 1.References