fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: keep pending transfer in total - #1058

Merged
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance
Jul 6, 2026
Merged

fix: keep pending transfer in total#1058
jvsena42 merged 4 commits into
masterfrom
fix/808-pending-transfer-balance

Conversation

@piotr-iohk

@piotr-iohkpiotr-iohk commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 balanceInTransferToSpending but excluded from totalSats, 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:

  1. Includes balanceInTransferToSpending and balanceInTransferToSavings in totalSats.
  2. Stores txTotalSats and preTransferOnchainSats on Blocktank LSP order transfers and subtracts the funding amount from displayed on-chain balance until LDK reflects the send (iOS parity).
  3. Adds Room migration 6→7 (two nullable columns on transfers; existing rows stay NULL and keep prior behavior).

Out of scope / known follow-ups:

  • To-spending guard edge cases during concurrent on-chain deposits (same heuristic as iOS).
  • Force-close to-savings total dip when LDK closing balance disappears before sweep detection (iOS has an activity fallback we have not ported yet).
  • Manual / third-party LSP setup paths are unchanged beyond the shared totalSats fix; e2e covers those flows.

Preview

Screen.Recording.2026-07-02.at.14.59.23.mov

QA Notes

Manual Tests

Migration (Room 6→7)
  • 1. Install bitkit_dev_release_2.3.1-stag-universal.apk → create/restore wallet → fund on-chain → open spending channel via Blocktank → note headline total.
  • 2. Upgrade in place (no clear data) to bitkit_dev_release_2.3.1-808fix-stag-universal.apk → app opens without crash → balance, channels, and activity history match pre-upgrade.
  • 3.regression: After migration upgrade → pull-to-refresh on Home → balances and channels still correct.
  • 4. Optional schema check on emulator (adb root): transfers table has nullable txTotalSats and preTransferOnchainSats columns after upgrade.
Fix verification (#808)
  • 1. Savings → Spending → confirm Blocktank LSP transfer → during pending/setup: headline total stays near pre-transfer amount (no drop to zero, no brief inflation).
  • 2. Wait for channel to open → total settles correctly and in-transfer chip clears.
  • 3a.regression: Savings → Spending with send-all path (most/all of savings): total stable through pending.
    • 3b.regression: Partial-amount transfer: total stable through pending.

Automated Checks

  • Unit tests added: cover totalSats including in-transfer buckets in BalanceStateTest.kt.
  • Unit tests added: cover LSP funding on-chain subtraction while balance has not synced in DeriveBalanceStateUseCaseTest.kt.
  • Unit tests modified: update balance derivation assertions in DeriveBalanceStateUseCaseTest.kt.
  • Local: BalanceStateTest, DeriveBalanceStateUseCaseTest pass; just compile passes.
  • Changelog fragment: changelog.d/next/808.fixed.md (rename to 1058.fixed.md after PR number is known).
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

piotr-iohkand others added 2 commits July 2, 2026 13:38
@greptile-apps

greptile-appsBot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a balance display regression where the home screen total dropped to zero during a savings→spending Blocktank LSP transfer by including balanceInTransferToSpending and balanceInTransferToSavings in totalSats, and by temporarily subtracting the funding tx amount from displayed on-chain balance while LDK has not yet reflected the send.

  • BalanceState.totalSats extended to include both in-transfer buckets using saturating USat arithmetic; a Room migration 6→7 adds two nullable columns (txTotalSats, preTransferOnchainSats) to the transfers table with NULL defaults that preserve existing rows.
  • DeriveBalanceStateUseCase now computes orderPaymentsOnchainToSubtract and reduces totalOnchainSats accordingly, matching the iOS BalanceManager.getOrderPaymentOnchainToSubtract heuristic; the guard currentOnchainSats >= preTransferOnchainSats resets once LDK syncs the outgoing send.
  • TransferViewModel.onTransferToSpendingConfirm captures txTotalSats (spend-all or fee+mining) and preTransferOnchainSats (pre-broadcast on-chain total) before broadcasting and persists them through fundPaidOrder; the hardware-wallet path (onTransferToSpendingHwConfirm) is acknowledged in a previous review as a known follow-up.

Confidence Score: 5/5

Safe 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.

Important Files Changed

FilenameOverview
app/src/main/java/to/bitkit/models/BalanceState.kttotalSats 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.ktNew 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.ktonTransferToSpendingConfirm 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.ktMIGRATION_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.ktTwo 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.ktcreateTransfer signature extended with txTotalSats/preTransferOnchainSats optional params; passed through correctly to entity
app/src/test/java/to/bitkit/models/BalanceStateTest.ktNew 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.ktNew 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.ktHW 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.jsonSchema 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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Merge branch 'master' into fix/808-pendi..." | Re-trigger Greptile

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt Outdated
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
Comment threadapp/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@piotr-iohk
piotr-iohk marked this pull request as draft July 2, 2026 15:00
Co-authored-by: Cursor <cursoragent@cursor.com>
@piotr-iohk

Copy link
Copy Markdown
CollaboratorAuthor

Regarding:

P2 Hardware wallet path not getting the onchain-subtraction fix

onTransferToSpendingHwConfirm calls fundPaidOrder without txTotalSats or preTransferOnchainSats, so the transfer entity is stored with NULL for both fields. getOrderPaymentOnchainToSubtract skips transfers where either value is null, so the onchain balance is never reduced during the LDK sync window for HW-funded transfers. With balanceInTransferToSpending now always included in totalSats, this creates the brief inflation the PR is fixing — the total will include both the still-high totalOnchainSats (not yet synced) and the clientBalanceSat added by paidOrdersSats — until LDK syncs and drops the onchain balance on its own.

Re Greptile's outside-diff note on onTransferToSpendingHwConfirm (HW path not getting the onchain-subtraction fix):

The NULL fields for HW transfers are intentional, and the described inflation doesn't apply as stated:

getOrderPaymentOnchainToSubtract corrects LDK'stotalOnchainBalanceSats, but a HW-funded transfer spends from the Trezor's UTXOs — LDK's on-chain balance never contained those sats and doesn't need reducing. Storing txTotalSats/preTransferOnchainSats here would apply a wrong subtraction to LDK's balance.

The actual double-count risk is in totalWithHardwareSats: balanceInTransferToSpending adds clientBalanceSat while the hardware wallet's watch-only balance still shows its pre-spend value. That window lasts only until the HW balance refresh picks up the unconfirmed tx (blockbook indexes mempool txs within seconds) — not until LDK sync, as suggested. Manual testing on emulator shows no visible inflation during HW transfers.

@piotr-iohk
piotr-iohk marked this pull request as ready for review July 2, 2026 15:34

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadapp/src/main/java/to/bitkit/models/BalanceState.kt

@jvsena42jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tAck

@jvsena42
jvsena42 enabled auto-merge July 6, 2026 13:44
@jvsena42
jvsena42 merged commit 51a063d into masterJul 6, 2026
70 of 77 checks passed
@jvsena42
jvsena42 deleted the fix/808-pending-transfer-balance branch July 6, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: pending transfer not reflected in total balance

3 participants

@piotr-iohk@jvsena42@github-advanced-security