Skip to content

fix: show insufficient funds when no hardware wallet covers a scan - #1790

Merged
jvsena42 merged 9 commits into
masterfrom
fix/scan-unaffordable-hardware-amount
Nov 6, 2026
Merged

jvsena42 merged 9 commits into
masterfrom
fix/scan-unaffordable-hardware-amount

Conversation

@jvsena42

@jvsena42 jvsena42 commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

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

  • Dev flavor with the Bridge Trezor emulator, per journeys/hardware-wallet/README.md:
    • bitkit-docker up.
    • ../bitkit-docker/scripts/trezor-emulator start.
    • TREZOR_BRIDGE=true TREZOR_BRIDGE_URL=http://10.0.2.2:21325 ./gradlew installDevDebug.
  • Onboarded wallet with 0 sats in Savings; paired Trezor holding at least 10 000 confirmed regtest sats.

Journeys

  • J1: new 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 Trezor

Manual Tests

N/A

Automated Checks

  • added AppViewModelSendFlowTest.kt — a scan the hardware balance but not its spendable covers shows the Insufficient Savings toast and opens no Send sheet

Evidence

  • 363bf6c, local, macOS, JDK 17:
    • The new test fails without the fix (the Send sheet opens) and passes with it.
    • AppViewModelSendFlowTest.kt: 598/598 pass.
    • just compile, just test (4879 tests, 0 failures) and just lint pass.
  • J1 at 3118c17, Pixel 9 emulator (API 37.1), staging regtest, Bridge Trezor emulator, Savings 0, Trezor balance B = 19 928 006 sats:
  • J1 rerun at 2ee82c2 (merged with master, including fix: compose a hardware-funded request once #1785), same wallet and Trezor:
  • 2ee82c2 local: the new test fails without the fix and passes with it on the merged base. just compile, just test (4885 tests, 0 failures), just lint and the androidTest compile pass.

Gaps

Models used

  • Planning/scoping: claude-opus-5-5 (reasoning: medium)
  • Implementation: claude-opus-5-5 (reasoning: medium)
  • Review: Not performed

🤖 Generated with Claude Code

jvsena42 and others added 2 commits November 5, 2026 14:41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42 jvsena42 self-assigned this Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 6d644d3 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@jvsena42

jvsena42 commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

J1 device run: scan-onchain-amount-over-spendable.xml

Build 3118c17 (dev flavor, TREZOR_BRIDGE=true), Pixel 9 emulator on API 37.1.

Setup:

  • Staging regtest.
  • Fresh wallet with Savings 0.
  • Bridge Trezor emulator from bitkit-docker (seed all all …), paired through Settings › Hardware Wallets › Add.
  • 20 000 sats deposited to its hardware receive address with ./lsp POST /regtest/chain/deposit, plus 3 blocks mined.
  • The shared deterministic seed already held funds on staging, so the tile balance B was 19 928 006 sats.
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.

Insufficient Savings toast after scanning B − 30
Confirm from the Trezor for B − 20 000

Recording of the over-spendable scan (the toast appears near the end):

j1-over-spendable.mp4

@jvsena42
jvsena42 marked this pull request as ready for review November 5, 2026 18:30
@greptile-apps

greptile-apps Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

This PR shows Insufficient Savings when neither Savings nor a hardware wallet can cover a scanned Bitcoin amount after fees.

  • Scanned requests stop when no hardware wallet can cover the fees.

Reviews (3) · Last reviewed commit: "test: attempt the exact hardware compose..." · Reviewed by Greptile

@jvsena42
jvsena42 enabled auto-merge November 5, 2026 18:33

@talosmachina talosmachina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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, maxSendOnchain is maxSendOnchainSats, and the earlier amount > maxSendOnchain check already toasted every amount validateAmount would reject. The new branch only changes the raw-balance-versus-spendable gap it targets.
  • A wallet already selected: hardwareWalletId != null skips the new branch, so an active hardware wallet still routes as before.
  • State left behind by a failed selection: selectHardwareFundingSourceForAmount writes _sendUiState only 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 coreyphillips left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.xml and hardware-wallet/send-onchain.xml as not ported, but hardware-wallet/scan-onchain-amount-over-spendable.xml is 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.

@jvsena42
jvsena42 marked this pull request as draft November 5, 2026 18:49
auto-merge was automatically disabled November 5, 2026 18:49

Pull request was converted to draft

@jvsena42

jvsena42 commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

J1 rerun at 2ee82c2 (merged with master, including #1785)

Pixel 9 emulator (API 37.1), dev flavor with TREZOR_BRIDGE=true, staging regtest. Same wallet and Bridge Trezor as the first run: Savings 0, Trezor balance B = 19 928 006 sats.

Build Scan Result
master 25ce099 (before this PR) B − 30 (amount=0.19927976) Bitcoin Amount screen with 19 927 976 fixed, Available ₿ 0, Continue disabled: the reported dead end, still on current master. Opened 26–33 s after the scan (two runs).
this PR, 2ee82c2 B − 30 "Insufficient Savings / More ₿ needed to pay this Bitcoin invoice." toast about 32 s after the scan; no Amount or Confirm screen
this PR, 2ee82c2 B − 20 000 (amount=0.19908006) Confirm after 17 s with 19 908 006, From BITKIT TEST TREZOR, Normal fee 209 sats; closed without sending

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.

master: locked Amount screen for B − 30
this PR: Insufficient Savings toast for B − 30
this PR: Confirm from the Trezor for B − 20 000

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

@jvsena42

jvsena42 commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Reply to coreyphillips' nit (#1790 (review)): fixed in 8e1618e.

  • journeys/README.md's cross-platform table now lists hardware-wallet/scan-onchain-amount-over-spendable.xml as not ported.
  • The row notes the iOS behaviour: the same raw-balance check, then Confirm with a failing fee preview and a Try again toast. It also says to port the journey with that fix.

Also this round:

🤖 Generated with Claude Code

@jvsena42
jvsena42 marked this pull request as ready for review November 5, 2026 20:21

@coreyphillips coreyphillips left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.xml as 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)

@ovitrif

ovitrif commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Placed in 3.2.0: it fixes a regression from #1763, which is 3.2 work. I filed #1793 to track it under the milestone and added Closes #1793 to this PR. The iOS path you describe (Confirm with a failing fee preview) has no issue yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@coreyphillips coreyphillips left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 empty BlocktankState, so hardwareAvailableForPayment() skips the exact compose at AppViewModel.kt:4505 and 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() uses BlocktankState() (line 741), so resetSendState stores null rates (AppViewModel.kt:7459). hardwareAvailableForPayment then 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.

@jvsena42
jvsena42 marked this pull request as draft November 6, 2026 09:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42
jvsena42 marked this pull request as ready for review November 6, 2026 09:18

@coreyphillips coreyphillips left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-9542 now supplies fee rates, which resetSendState stores (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 resolve com.synonym, so I did not rerun the fails-without-the-fix check.

@jvsena42
jvsena42 requested review from a team, ben-kaufman and pwltr and removed request for a team November 6, 2026 11:53

@ben-kaufman ben-kaufman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@jvsena42
jvsena42 merged commit ef8cffa into master Nov 6, 2026
37 of 39 checks passed
@jvsena42
jvsena42 deleted the fix/scan-unaffordable-hardware-amount branch November 6, 2026 12:41
Sign up for free to 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: scan opens a locked amount screen when no wallet covers the fee

5 participants