fix: keep pending transfer in total#1058
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Greptile SummaryThis PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including
Confidence Score: 5/5Safe to merge; the core balance fix, database migration, and saturating arithmetic are all correct, and the two known follow-ups (concurrent-deposit heuristic, HW-wallet subtraction gap) are bounded, self-healing, and already tracked. The balance derivation logic is well-reasoned and matches the iOS reference implementation. The Room migration is additive (nullable columns, existing rows stay NULL), the USat saturating arithmetic prevents overflow in the new totalSats computation, and the guard condition correctly gates the on-chain subtraction on the LDK-sync window. Unit tests cover the new paths end-to-end. The two acknowledged edge cases (concurrent deposit, HW wallet) are correctly scoped as follow-ups with bounded, self-healing impact. No files require special attention beyond the acknowledged follow-ups already noted in previous review threads.
|
| Filename | Overview |
|---|---|
| app/src/main/java/to/bitkit/models/BalanceState.kt | totalSats now includes balanceInTransferToSavings and balanceInTransferToSpending with saturating USat arithmetic; final plus() is already saturating so the missing trailing .safe() is not a bug |
| app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt | New getOrderPaymentOnchainToSubtract correctly gates on currentOnchainSats >= preTransferOnchainSats; the known concurrent-deposit edge case (acknowledged in previous thread) causes a brief dip rather than inflation |
| app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt | onTransferToSpendingConfirm correctly captures txTotalSats and preTransferOnchainSats before broadcasting; HW wallet path still passes null for both (known follow-up per previous review) |
| app/src/main/java/to/bitkit/data/AppDb.kt | MIGRATION_6_7 adds two nullable INTEGER columns with DEFAULT NULL; correctly chained alongside MIGRATION_5_6; schema JSON matches the entity definition |
| app/src/main/java/to/bitkit/data/entities/TransferEntity.kt | Two nullable Long columns added with default null; schema JSON in 7.json matches; existing rows migrate safely |
| app/src/main/java/to/bitkit/repositories/TransferRepo.kt | createTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity |
| app/src/test/java/to/bitkit/models/BalanceStateTest.kt | New tests cover both-buckets inclusion and ULong.MAX_VALUE saturation; totalWithHardwareSats assertion correctly updated to 190 to reflect the new totalSats calculation |
| app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt | New test exercises the LSP funding subtraction path end-to-end; totalSats assertions added to several existing scenarios |
| app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt | HW confirm test updated to assert isNull() for both new params; no new test for the regular confirm path's non-null values |
| app/schemas/to.bitkit.data.AppDb/7.json | Schema snapshot v7 correctly records both new nullable INTEGER columns and matches the migration SQL |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User confirms Savings→Spending transfer] --> B[onTransferToSpendingConfirm]
B --> C[getBalancesAsync: capture preTransferOnchainSats]
C --> D{shouldUseSendAll?}
D -- Yes --> E[txTotalSats = spendableBalance]
D -- No --> F[txTotalSats = feeSat + miningFee]
E --> G[sendOnChain broadcast]
F --> G
G --> H[fundPaidOrder: persist txTotalSats + preTransferOnchainSats to DB]
H --> I[syncBalances triggered]
I --> J[DeriveBalanceStateUseCase.invoke]
J --> K[getOrderPaymentOnchainToSubtract]
K --> L{currentOnchainSats >= preTransferOnchainSats LDK not yet synced?}
L -- Yes --> M[subtract txTotalSats from totalOnchainSats]
L -- No --> N[no subtraction — LDK already reflected send]
M --> O[totalSats = totalOnchainSats + totalLightningSats + balanceInTransferToSavings + balanceInTransferToSpending]
N --> O
O --> P[Home screen shows stable total]
style P fill:#22c55e,color:#fff
style M fill:#f59e0b,color:#fff
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[User confirms Savings→Spending transfer] --> B[onTransferToSpendingConfirm]
B --> C[getBalancesAsync: capture preTransferOnchainSats]
C --> D{shouldUseSendAll?}
D -- Yes --> E[txTotalSats = spendableBalance]
D -- No --> F[txTotalSats = feeSat + miningFee]
E --> G[sendOnChain broadcast]
F --> G
G --> H[fundPaidOrder: persist txTotalSats + preTransferOnchainSats to DB]
H --> I[syncBalances triggered]
I --> J[DeriveBalanceStateUseCase.invoke]
J --> K[getOrderPaymentOnchainToSubtract]
K --> L{currentOnchainSats >= preTransferOnchainSats LDK not yet synced?}
L -- Yes --> M[subtract txTotalSats from totalOnchainSats]
L -- No --> N[no subtraction — LDK already reflected send]
M --> O[totalSats = totalOnchainSats + totalLightningSats + balanceInTransferToSavings + balanceInTransferToSpending]
N --> O
O --> P[Home screen shows stable total]
style P fill:#22c55e,color:#fff
style M fill:#f59e0b,color:#fff
Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a29b8c4b34
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Regarding:
Re Greptile's outside-diff note on The
The actual double-count risk is in |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5882321f27
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .safe() | ||
| .plus(balanceInTransferToSavings.safe()) | ||
| .safe() | ||
| .plus(balanceInTransferToSpending.safe()) |
There was a problem hiding this comment.
Apply the funding correction to manual opens
When users open an external/manual channel, ExternalNodeViewModel records a MANUAL_SETUP transfer with a funding tx but no lspOrderId, so the new on-chain correction in DeriveBalanceStateUseCase never applies to it. With this added balanceInTransferToSpending term, if LDK still reports the pre-open on-chain balance while the channel is pending, the home total becomes stale savings plus the pending channel amount until the wallet sync catches up.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid observation, intentionally out of scope: manual opens go through LDK's connectOpenChannel, which funds and broadcasts from LDK's own wallet in one step, so the stale-balance window is at most one sync cycle. iOS's equivalent correction also filters on lspOrderId and skips manual opens — this is parity. Tracked in #1059: the planned hardening (funding-txid presence check instead of the totals heuristic) applies uniformly to LSP and manual transfers and covers this case.
Fixes #808
This PR keeps pending transfer funds in the headline total balance during savings → spending moves, matching iOS behavior and avoiding a brief inflation window while the LSP funding transaction syncs.
Description
During an in-progress transfer to spending, funds were tracked in
balanceInTransferToSpendingbut excluded fromtotalSats, so the home total could drop to zero (especially on max-transfer flows). iOS already includes both in-transfer buckets in its total.This change:
balanceInTransferToSpendingandbalanceInTransferToSavingsintotalSats.txTotalSatsandpreTransferOnchainSatson Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).transfers; existing rows stayNULLand keep prior behavior).Out of scope / known follow-ups:
totalSatsfix; e2e covers those flows.Preview
Screen.Recording.2026-07-02.at.14.59.23.mov
QA Notes
Manual Tests
Migration (Room 6→7)
bitkit_dev_release_2.3.1-stag-universal.apk→ create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.bitkit_dev_release_2.3.1-808fix-stag-universal.apk→ app opens without crash → balance, channels, and activity history match pre-upgrade.regression:After migration upgrade → pull-to-refresh on Home → balances and channels still correct.adb root):transferstable has nullabletxTotalSatsandpreTransferOnchainSatscolumns after upgrade.Fix verification (#808)
regression:Savings → Spending with send-all path (most/all of savings): total stable through pending.regression:Partial-amount transfer: total stable through pending.Automated Checks
totalSatsincluding in-transfer buckets inBalanceStateTest.kt.DeriveBalanceStateUseCaseTest.kt.DeriveBalanceStateUseCaseTest.kt.BalanceStateTest,DeriveBalanceStateUseCaseTestpass;just compilepasses.changelog.d/next/808.fixed.md(rename to1058.fixed.mdafter PR number is known).