Repository navigation
fix: compose a hardware-funded request once - #1785
Conversation
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
[Medium impact] The PR appears safe to merge; no new actionable issues were found. SummaryHardware-funded requests now reuse one exact-payment fee compose while preparing confirmation.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Request exceeds Savings] --> B[Try largest hardware balance]
B --> C[Compose exact payment fee]
C --> D{Compose succeeds?}
D -->|Yes| E[Cache shared fee result]
D -->|No| F[Check maximum spendable amount]
E --> G[Open confirmation]
G --> H[Reuse selected speed fee]
H --> I{Open speed picker?}
I -->|Yes| J[Estimate remaining speeds]
H --> K[User confirms]
K --> L[Compose transaction for signing]
Reviews (4) · Last reviewed commit: "fix: keep request amounts off the hardwa..." · Reviewed by Greptile |
|
Placed in 3.2.0 with its twin: a non-blocking fix found after the 3.1 cut. I filed #1786 to track it under the milestone and added |
pwltr
left a comment
There was a problem hiding this comment.
Reviewed c7e51ba against 21f0b40.
Looks good. Fee composes for the same hardware payment are now shared and survive a cancelled caller, failures are not cached, and entries are dropped when the wallet balance changes. Choosing a hardware wallet proves the exact payment once, and the confirm screen reuses that fee instead of composing every speed up front. No findings.
Review details
Claude Opus 5.5, effort high
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Composes a hardware-funded payment request once: the exact-amount compose that picks the funding wallet is cached in HwWalletRepo (keyed by wallet, payment, fee rate and funding balance) and reused by the confirm screen, and a hardware send now estimates only the selected speed until the picker opens. Reviewed c7e51ba, full tier. Static review: on this track I do not build the Android branch, so this is reasoned from the code and from CI. Twin: synonymdev/bitkit-ios#1221.
What I checked, and 5 candidates I ruled out
Read: the HwWalletRepo diff with composeFundingOffline, getFundingAccount and TrezorRepo.composeTransactionOffline. In AppViewModel: selectHardwareFundingSourceForAmount and its only caller in the payment-request path, hardwareAvailableForPayment, refreshOnchainSendIfNeeded, refreshOnchainFeeUi, getFeeEstimate, prepareHardwareSendFee, the SpeedAndFee handler, the speed-change path, validateAmount and handleSanityChecks. Also SendFeeViewModel.init and SendAmountScreen.
CI at review time: build, build-and-test, lint, detekt green. compose-ui and the local e2e shards still running.
Ruled out
- Dropping
refreshOnchainSendIfNeededfrom the selection loses the confirm fee: the one caller runsrefreshOnchainSendIfNeeded(walletGeneration)right afternavigateToSendRoute(... Confirm ...), and the toast path returns before navigating. - Speed picker opens with no other speeds:
SendEvent.SpeedAndFeeruns the fullrefreshOnchainFeeUiwheneverestimatesis empty, which is the stateselectedSpeedOnlyleaves.SendFeeViewModelis the only reader ofestimates. hardwareAvailableSatsset to balance minus this payment's fee instead of the max spendable: the compose succeeding means the inputs cover amount plus fee, so it is never belowamount. A speed change recomputes it throughhardwareMaxSpendable, andwasHardwareMaxonly matches when the amount equals it.- Cache serving a fee for a changed UTXO set: entries for the wallet are dropped as soon as its funding balance moves. The balance standing in for the UTXO set is the documented design, not an accident.
- A failed compose cached after its only waiter is cancelled: already raised in Greptile's open thread on
HwWalletRepo.kt:805, so not repeated here.
Merge confidence: 4/5. The new tests cover the shared compose and the single selection, but the e2e shards had not finished, and the cancelled-waiter failure path in the open Greptile thread has no test.
Reviewer model: claude-opus-5-5
Reasoning effort: Unknown (the runner does not report the setting to the review session)
pwltr
left a comment
There was a problem hiding this comment.
Reviewed commit c7e51ba.
Medium: invalidate hardware fee estimates when spending inputs change. The new cache keys successful estimates by total wallet balance, so spending and replenishing an account to the same total can reuse a fee calculated for different inputs. Confirmation and app fee warnings can then use the stale estimate, while signing freshly composes a transaction with a different fee. This is introduced by retaining completed estimates across payment preparations. Invalidate on transaction/input changes or scope reuse to a single payment preparation. Hardware-device transaction approval still occurs; this is an app fee-preview and warning correctness issue.
Review details
General review: GPT-6.1 Sol / high.
Finding verification: GPT-6.1 Sol / high.
|
@pwltr on the fee estimates keyed by balance: fixed in 2f09e74. The cache key is now the funding account's state, its balance plus the sorted txids of the native-segwit watcher's on-chain activities, not the balance alone. Any coin change (a spend, a top-up, a replaced or dropped transaction) changes the key and composes again, even when the total ends up the same. Proof: new test Ported to synonymdev/bitkit-ios#1221 in 1f8643f45, which keys the stored preview on balance plus txids the same way. The fix for Greptile's cancelled-caller finding is in 46d849d (reply in that thread). |
pwltr
left a comment
There was a problem hiding this comment.
Reviewed commit 2f09e74.
Medium candidate: changing transaction speed after automatic hardware funding selection may change a fixed payment request’s amount. hardwareAvailableForPayment stores the balance minus the exact-payment fee; setTransactionSpeed treats equality with that value as send-max intent and replaces the amount with the newly computed maximum. For a valid no-change exact payment, the fixed request can satisfy that equality even when it is below the true maximum, and request validation then rejects the changed amount.
The native-composer behavior needed for this case remains unverified. Can the composer return that no-change exact-payment result? If so, fixed request amounts need to stay independent of the send-max heuristic.
Review details
General: GPT-6.1 Sol / High.
|
@pwltr on the speed change rewriting a fixed request amount: it reproduces, and it is fixed in 5929075. Yes, the composer can return that result. Branch-and-bound accepts a change-free selection and adds the leftover below the change cost to the fee, so for an exact payment that spends every coin, Fix: Proof: new test iOS (synonymdev/bitkit-ios#1221) does not have this: |
Choosing the hardware wallet for a payment request now composes the exact payment instead of a send-max, and HwWalletRepo shares exact-fee composes for the same payment and balance, so the confirm fee reuses it. Hardware sends estimate only the selected speed; the other speeds are estimated when the speed picker opens. The source selection no longer starts a fee refresh that the confirm navigation cancels. Twin of synonymdev/bitkit-ios#1221 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5929075 to
3fdea9c
Compare
|
Rebased on master at 3fdea9c to resolve a conflict with #1763. Both changed the hardware send-max guard in Local: |
ben-kaufman
left a comment
There was a problem hiding this comment.
Reviewed 3fdea9c708f906b755bc0fc19d0ed15bddd765d8. No new material findings. Independent security passes remain incomplete (HTTP 401), and there was no device run after the rebase.
Reviewer model: claude-opus-5-5, gpt-6.1-sol
Reasoning effort: high for both
Closes #1786
Twin: synonymdev/bitkit-ios#1221
This PR cuts the hardware composes for a payment request funded from a hardware wallet down to one before the confirm screen is ready. Today it runs a send-max compose per wallet, then up to four exact composes (one per fee speed) on the single core queue, plus composes from a refresh that the confirm navigation cancels. Same problem as synonymdev/bitkit-ios#1187.
Description
AppViewModel.selectHardwareFundingSourceForAmount: tries hardware wallets largest balance first and composes the exact payment at the selected speed. A successful compose proves the wallet can pay. When it fails, the old max-spendable compose still runs, so the insufficient-funds toast is unchanged.HwWalletRepo.estimateFundingMiningFee: one compose per wallet, address, amount, fee rate and funding balance (standing in for the UTXO set), shared by concurrent callers and owned by the repository scope, so a cancelled caller leaves its result for the next one. Failures are not kept; entries for an older balance are dropped.refreshOnchainFeeUi: hardware sends estimate only the selected speed and leaveestimatesempty, soSendEvent.SpeedAndFeeestimates the other speeds when the speed picker opens. Signing then does not queue behind three extra composes.onScanOnchainstarts one after navigating to Confirm.Review round 1
Review round 2
Out of Scope
onchain_compose_transaction: each call opens a new Electrum client and fully syncs a fresh wallet; reusing a synced wallet is a separate bitkit-core follow-up.AppViewModelquoted requests (USD with fixed rate): the quoted path (AppViewModel.kt:4000-4043) never callsselectHardwareFundingSourceForAmount, so a quoted request Savings cannot cover opens on Savings with the swipe disabled instead of falling back to the hardware wallet. Seen in the device run; the path is unchanged here.SendFeeViewModel.getFeeLimit(): the fee cap uses the Savings balance (totalOnchainSats), so with empty Savings every preset speed is disabled on a hardware send and cannot be selected. Seen in the device run; unchanged here.Design
N/A — no UI changes.
Preview
N/A
QA Notes
Setup
bitkit-docker, Trezor emulator over Bridge as in hardware-wallet.Journeys
N/A — not drivable; see Manual Tests.
Manual Tests
composeTransactionOfflinebefore Confirm shows a fee, and the swipe is enabled about one compose after Confirm appears — compose count from logcat is not in CapabilitiesAutomated Checks
HwWalletRepoTest.kt— the same payment composes once and a different rate composes again; a failed compose is retriedHwWalletRepoTest.kt— coins changing at the same balance compose again; a cancelled caller's failed compose is retried by the next callerAppViewModelSendFlowTest.kt— a hardware-funded request keeps its amount when the speed changesAppViewModelSendFlowTest.kt— a request Savings cannot cover picks the Trezor without a send-max compose and estimates only the selected speedEvidence
HwWalletRepoTest(115) andAppViewModelSendFlowTest(583) at a471907: 0 failures../gradlew detektat a471907: passed.Device run at c7e51ba (devDebug, staging regtest,
TREZOR_BRIDGE=true), Android emulator (arm64), Trezor emulator over Bridge (Savings 0, Trezor 23,242,326 sats), requests fromtools/paykit-fixture-sender(synonymdev/bitkit-e2e-tests). Composes counted from the cold Electrum sync each offline compose runs (onefound tx_detailsfor the account's oldest tx per compose):Round 1 at 2f09e74:
HwWalletRepoTest117/117 andAppViewModelSendFlowTest583/583; the two new repo tests fail against c7e51ba. Fulljust testat the previous round-1 head: 4855 tests, the one failure fixed since. detekt,lintDevDebugand the androidTest compile pass.Round 2 at 5929075:
AppViewModelSendFlowTest584/584,HwWalletRepoTest117/117; the new test fails without the fix (expected:<2500> but was:<48000>). compile,just lintand the androidTest compile pass.Gaps
Models used
claude-opus-5-5(reasoning: Not exposed)claude-opus-5-5(reasoning: Not exposed)🤖 Generated with Claude Code