Repository navigation
fix: show insufficient funds when no hardware wallet covers a scan - #1790
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
J1 device run:
|
| Step | Input | Result |
|---|---|---|
| Over spendable | bitcoin:bcrt1q620mn9yydnua6mucnxw0wamcsuc6wlsrjqejqv?amount=0.19927976 (B − 30) |
"Insufficient Savings / More ₿ needed to pay this Bitcoin invoice." toast; no Amount (SendNumberField) or Confirm (ReviewAmount) screen. This is the new path's text; the earlier raw-balance check shows "₿ {amount} more needed" instead |
| Covered | same address, amount=0.19908006 (B − 20 000) |
Confirm opens with 19 908 006, From BITKIT TEST TREZOR, Normal fee 209 sats; closed without sending |
The 209-sat fee puts B − 30 between the Trezor's spendable amount and its balance, so the first scan exercises the fixed path.
Timing: the toast appeared about 14 s after the deep link. The hardware max-spendable compose for this many UTXOs runs first, and the home screen stays unchanged meanwhile. The same compose ran before this fix and then opened Amount, so the wait isn't new.
Recording of the over-spendable scan (the toast appears near the end):
j1-over-spendable.mp4
[Medium impact] The PR appears safe to merge; no new actionable issues were found. SummaryThis PR shows Insufficient Savings when neither Savings nor a hardware wallet can cover a scanned Bitcoin amount after fees.
Reviews (3) · Last reviewed commit: "test: attempt the exact hardware compose..." · Reviewed by Greptile |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. routeOnchainInvoiceAmount now shows Insufficient Savings when Savings cannot pay a fixed-amount on-chain scan and no hardware wallet's spendable after fees covers it either, instead of falling through to a locked Amount screen. Reviewed 3118c17, full tier. The review is static: on this track an auto review does not build the branch.
What I checked, and 4 candidates I ruled out
Read in full: routeOnchainInvoiceAmount, selectHardwareFundingSourceForAmount, validateAmount, hardwareMaxSpendable, maximumAvailableOnchainSats and the on-chain branch of the scan handler that calls the route
Call sites traced: routeOnchainInvoiceAmount (one caller), selectHardwareFundingSourceForAmount
CI: lint and detekt green. build-and-test and ui-tests still running
Ruled out
- Savings-only wallets now get a toast where they used to get Amount: with no hardware wallet,
maxSendOnchainismaxSendOnchainSats, and the earlieramount > maxSendOnchaincheck already toasted every amountvalidateAmountwould reject. The new branch only changes the raw-balance-versus-spendable gap it targets. - A wallet already selected:
hardwareWalletId != nullskips the new branch, so an active hardware wallet still routes as before. - State left behind by a failed selection:
selectHardwareFundingSourceForAmountwrites_sendUiStateonly on the wallet it returns true for, so the toast path leaves nothing half-selected. - Payment-request and contact paths: the route returns false at the top when a contact payment context is active, and the incoming-request branch above has its own insufficient-funds toast, so neither reaches this branch.
Merge confidence: 4/5. The new AppViewModelSendFlowTest case covers the branch, but build-and-test, which runs it, had not finished at review time.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the run metadata available to this reviewer does not report it)
coreyphillips
left a comment
There was a problem hiding this comment.
Two independent reviews, nothing blocking a merge.
nits
- New hardware wallet journey missing from the cross-platform table (journeys/README.md:153). The new journey has no iOS counterpart and no row in the cross-platform table. The table row at journeys/README.md:153 lists
hardware-wallet/receive-onchain.xmlandhardware-wallet/send-onchain.xmlas not ported, buthardware-wallet/scan-onchain-amount-over-spendable.xmlis not added there. CLAUDE.md asks for a row for each journey that is not ported, so the next person diffing against bitkit-ios does not have to work out the gap again. Adding the file to that row, with a short note on the iOS behavior from the PR's Gaps, would cover it.
Pull request was converted to draft
…le-hardware-amount
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le-hardware-amount
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
J1 rerun at 2ee82c2 (merged with master, including #1785)Pixel 9 emulator (API 37.1), dev flavor with
The wait before the toast or Amount screen is the same on both builds. Since #1785, wallet selection composes the exact payment first, then falls back to max-spendable, which is slower than the 14 s measured at 3118c17. Recording of the B − 30 scan on this PR (the toast appears near the 34 s mark): fix-2ee82c274-scan.mp4🤖 Generated with Claude Code |
|
Reply to coreyphillips' nit (#1790 (review)): fixed in 8e1618e.
Also this round:
🤖 Generated with Claude Code |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 22 new commits (fd6949c, 1900a79, 034bf57, and 19 more). Nothing blocking remains.
addressed
- New hardware wallet journey missing from the cross-platform table (journeys/README.md:153). Fixed: journeys/README.md:154 now lists
hardware-wallet/scan-onchain-amount-over-spendable.xmlas not ported, with the iOS behaviour and a note to port it with that fix (8e1618e). I read the row and confirmed the file name matches the journey. The file is also absent from the local bitkit-ios checkout, though that is a feature branch from 2026-10-21. The row states the iOS path as fact while the PR's Gaps calls two steps inferred; I did not trace those steps.
new since the last review
worth doing, does not block
- The revised scan test never attempts the exact compose (app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt:9541-9542)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (6f5ee70). Nothing blocking remains.
still open
- The revised scan test never attempts the exact compose (app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt:9541-9542). The added failure stub never runs because this test supplies no fee rates.
stubRepositories()initializes an emptyBlocktankState, sohardwareAvailableForPayment()skips the exact compose atAppViewModel.kt:4505and uses max-spendable directly. Supply nonzero fee rates and verify the estimate call to cover the merged failure path. The test at AppViewModelSendFlowTest.kt:9534-9552 is unchanged and still supplies no fee rates.stubRepositories()usesBlocktankState()(line 741), soresetSendStatestores null rates (AppViewModel.kt:7459).hardwareAvailableForPaymentthen skips the estimate at line 4505, and the failure stub at lines 9541-9542 never runs. To close this, supply nonzero rates as the test at lines 11482-11487 does and verify the estimate call. Read from the code, not run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (6d644d3). Nothing blocking remains.
addressed
- The revised scan test never attempts the exact compose (app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt:9541-9542). Fixed:
AppViewModelSendFlowTest.kt:9538-9542now supplies fee rates, whichresetSendStatestores (AppViewModel.kt:7459,7470), so the check at line 4505 passes and the failing stub at 4507 runs. Line 9554 verifies the 9,000 sat estimate. CI run 42192427693 on 6d644d3 shows this test passing, with 600 tests and 0 failures in the class. I traced the path by reading it. Local Gradle cannot resolvecom.synonym, so I did not rerun the fails-without-the-fix check.
ben-kaufman
left a comment
There was a problem hiding this comment.
No material findings in the reviewed change. Local runtime validation and independent security coverage remain incomplete.
Reviewer model: claude-opus-5-5 (independent inspection), gpt-6.1-sol (verification and delivery)
Reasoning effort: high for both





Closes #1793
Twin: synonymdev/bitkit-ios#1228
Found in review of #1787 (#1787 (review)). Introduced by 46cd209 (#1763).
A scanned on-chain request that Savings cannot cover, and that a hardware wallet's balance covers but not once the fee is taken, opens the Send Amount screen with the amount fixed and Continue disabled. The user cannot edit or continue.
Description
AppViewModel.routeOnchainInvoiceAmount: when Savings cannot cover the amount and no hardware wallet can either, show the Insufficient Savings toast and stop, instead of falling through to Amount. The earlier balance check compares against the raw hardware balance (maximumHardwareFundingBalanceSats); wallet selection compares against spendable after fees (hardwareMaxSpendable). The gap between them is what reached Amount.Design
N/A — no design available. The toast and copy are the existing Insufficient Savings ones shown for the same scan when no source covers the amount.
Preview
Insufficient Savings toast after scanning an amount the Trezor balance covers but not after fees (J1 run): #1790 (comment)
QA Notes
Setup
journeys/hardware-wallet/README.md:bitkit-dockerup.../bitkit-docker/scripts/trezor-emulator start.TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 ./gradlew installDevDebug.Journeys
scan-onchain-amount-over-spendable.xml— a scan between the Trezor's spendable and its balance shows Insufficient Savings and opens neither Amount nor Confirm; a smaller scan still opens Confirm on the TrezorManual Tests
N/A
Automated Checks
AppViewModelSendFlowTest.kt— a scan the hardware balance but not its spendable covers shows the Insufficient Savings toast and opens no Send sheetEvidence
AppViewModelSendFlowTest.kt: 598/598 pass.just compile,just test(4879 tests, 0 failures) andjust lintpass.master25ce099 opens the locked Amount screen for the same scan after 26–33 s.just compile,just test(4885 tests, 0 failures),just lintand theandroidTestcompile pass.Gaps
Models used
claude-opus-5-5(reasoning:medium)claude-opus-5-5(reasoning:medium)🤖 Generated with Claude Code