Repository navigation
Conversation
A BIP21 request with an amount now opens Confirm instead of Amount when scanned, pasted or opened as a link. Confirm runs the saved automatic coin selection when it is reached without Amount and opens Amount if selection fails. In-sheet scans that open Confirm directly drop the amount and coin choices left by an earlier recipient. Zero-amount requests and contact payments still open Amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Payment Request priced in another currency, or with fixed conversion rates, was always routed to Amount while no terms were bound, even when USDT could not pay it. The preparing confirmation then resolved to a locked Amount screen. Bind the issuer's Bitcoin terms when USDT is not a real choice so the request opens Confirm after preparation. Requests that also offer LNURL and on-chain keep Amount, since LNURL confirmation has no funding selector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Added to the 3.2.0 milestone with its Android twin synonymdev/bitkit-android#1763: this fix was opened after the 3.1.0 cut and closes no issue, so this PR carries the milestone itself. |
[Medium impact] The PR appears safe to merge; no new blocking issue was found. SummaryFixed-amount Bitcoin requests now open Confirm directly.
Previous finding #1 is addressed for the new direct-Confirm route. jvsena42 deferred the older Amount-to-Confirm fee window as a separate issue. Reviews (2) · Last reviewed commit: "fix: keep swipe disabled while direct co..." · Reviewed by Greptile |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
talosmachina
left a comment
There was a problem hiding this comment.
1 finding (1 P1). Routes a BIP21 request with an amount, and a converted Payment Request that only Bitcoin can pay, straight to Confirm. Reviewed f751a46, full tier, statically: iOS does not build on this reviewer's Linux host, so nothing was compiled or run here.
What I checked, and 5 candidates I ruled out
Read: the full diff, plus PaymentNavigationHelper (all three route functions), AppViewModel.handleScannedData (contact and BIP21 branches), SendSheet setup, cleanup and performPaymentValidationAfterSync, SendConfirmationView task, fee calculation and wallet switch, SendAmountView.onContinue, WalletViewModel.setUtxoSelection
Call sites traced: appropriateSendRoute (ScannerManager, SendEnterManuallyView, AppScene), openPaymentSheet (scanner, clipboard, deep link, Shop), paykitPaymentTerms readers
CI: lint, translations and build-local green; unit tests, integration tests and the @ios_gate e2e shard still running at review time
Ruled out
- Stale amount from an earlier recipient on the deep link, clipboard and Shop paths: each opens a new sheet whose
setUpSendcallswallet.resetSendState, and Confirm'sensureSendAmountFromScannedInvoicesIfNeededthen fills the invoice amount. Only the in-sheet scan reuses wallet state, and that is the pathprepareFixedAmountOnchainSendcovers. Manual entry resets before decoding. - Hardware wallet send skips the preview Amount used to build:
calculateTransactionFeecallshwSend.preparePreviewon Confirm itself, and the new coin selection guards on!hwSend.isActive. - Bound Bitcoin terms rejected later by the amount checks:
fixedBitcoinPaymentTermsreturns exactlyrequest.payment(using:)for the same method, soacceptsPaymentAmount(..., paymentTerms:)andreturnToAmountReviewIfNeededcompare equal values; an expired quote is refused byisValidand falls back to Amount. - Terms bound while USDT can still pay: the guard requires USDT off or no USDT endpoint, and the binding only runs when
paykitUsesUsdtis false; the tests cover both sides. - Swipe racing the new coin selection: real, but already raised in Greptile's thread on
SendConfirmationView.swift:428, so not repeated here.
Merge confidence: 3/5, one P1 (the @ios_gate e2e spec still expects the Amount step), and unit and e2e CI had not finished.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the review runtime does not report an effort setting)
|
|
||
| /// A BIP21 request with an amount skips Amount; the balance was already checked when it was scanned. | ||
| /// Contact payments keep Amount because it records the reviewed amount for the payment. | ||
| static func onchainInvoiceRoute(app: AppViewModel) -> SendRoute { |
There was a problem hiding this comment.
P1: the @ios_gate e2e spec still expects Amount here, and no matching e2e branch exists
With this route, a BIP21 with an amount opens Confirm, but bitkit-e2e-tests test/specs/send.e2e.ts on main still drives the old screen in two @ios_gate cases, which is the shard this repo's e2e-tests.yml runs on every PR:
@send_1: after typingunified1(bitcoin:...?amount=0.0001) it tapsAddressContinue, thenverifyAmountToSend(10_000)and waits forContinueAmount.@send_2:unified2has a 200 000 sat Lightning invoice the wallet cannot pay, sohandleScannedDatafalls back to on-chain and clearsscannedLightningInvoice; this function then returns.confirm, and the spec'stap('ContinueAmount')has nothing to tap.
determine-e2e-branch looks for a bitkit-e2e-tests branch named after this PR's head (fix/scan-amount-to-confirm), and that branch does not exist, so the run falls back to main's specs. The PR's Gaps section already names both cases; the fix is a companion e2e branch with the same name, so this PR's gate runs the updated specs instead of failing on them.
There was a problem hiding this comment.
Agreed, this would fail the gate. The companion branch fix/scan-amount-to-confirm now exists in bitkit-e2e-tests, with PR synonymdev/bitkit-e2e-tests#290 (1b28071):
@send_1: afterAddressContinue, the spec expects the review screen at 10 000. iOS pops back to manual entry with one Back. Android goes through Amount first.@send_2:unified2opens the review screen at 20 000 directly. TheAssetButton-savings/ContinueAmountsteps are removed.
Prettier and tsc are clean for the spec. Leaving this open until the @ios_gate shard has run against that branch, since it was not driven locally.
|
Direct Confirm from direct-confirm-swipe.mp4 |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Amount → Confirm after edefd08 (local regtest): Continue, then an immediate swipe. Coins were chosen at 14:20:49.933 and the 143-sat fee at .974, and the send went out at 50.955. Tx amount-confirm-early-swipe.mp4 |
| // A swipe before the fee is known would skip the fee warnings and record a zero fee. | ||
| isCalculatingOnchainFee = true | ||
| defer { | ||
| if feeCalculationId == requestId { |
There was a problem hiding this comment.
If the funding source changes to Spending or hardware while a Savings fee calculation is suspended, the newer calculateTransactionFee increments feeCalculationId but returns through a path that never clears isCalculatingOnchainFee. The older call then skips this defer, leaving the swipe disabled on the new funding source. Could we make every new calculation take ownership of and reset the flag, including the Lightning and hardware paths?
pwltr
left a comment
There was a problem hiding this comment.
Reviewed commit edefd08 through independent general and security review, targeted finding verification and peer comparison. The Debug simulator build, lint and 39 scoped tests passed.
[P2] Switching funding sources during fee calculation can leave sending disabled
If a Savings fee calculation is still running when the user switches to Spending or hardware, the newer calculation advances feeCalculationId without clearing isCalculatingOnchainFee. The old calculation then refuses to clear it because its ID is stale. The retained flag disables both the swipe and manual submission. Clear or transfer the loading state when a calculation is superseded, including early-return paths. This is the same issue already reported by ben-kaufman, confirmed separately by source tracing.
[P3] Direct confirmation skips the app’s minimum-amount check
A positive plain BIP21 amount now routes directly to Confirm, bypassing Amount’s 547-sat minimum. With manual coin selection, neither Confirm nor the picker restores that validation gate before submission. Apply the same minimum validation to direct confirmation. This confirms the missing application check; it does not establish how the native library will reject a sub-minimum transaction.
Review details
General review
Model: Claude Opus 5.5
Reasoning effort: xhigh
Security review
Model: Kimi K3
Reasoning effort: high
Finding verification, peer comparison and scoped validation
Model: GPT-6.1-sol
Reasoning effort: xhigh
Related: #1014 (open) fixes the same Paykit routing root cause with a wider scope. The Paykit commit here is separate so it can be dropped if #1014 merges first.
This PR:
Description
requiresAssetSelectionsent every converted request to Amount while no terms were bound, even with USDT off.Motivation for the Paykit fix, from Slack:
Reproduced on iOS with the fixture issuer: the preparing confirmation sheet resolved its route to
.amountbecause the converted request had no bound terms.Out of Scope
Bitkit/Views/Subscriptions,SendSheetembedded retry: initial subscription payments with converted terms keep their Amount review.Design
N/A — no UI changes. Existing Amount and Confirm screens are reused.
Preview
BIP21 with amount opens Confirm and sends 10 000 sats:
01-bip21-amount-opens-confirm-and-sends.mp4
Editing the amount from Confirm, then sending:
01b-bip21-amount-edit-amount-from-confirm.mp4
Zero-amount BIP21 still opens Amount:
02-bip21-no-amount-opens-amount.mp4
Full-balance BIP21: coin selection fails on Confirm and Amount opens with Continue disabled:
04-bip21-full-balance-falls-back-to-amount.mp4
USD-priced Payment Request ($2.50), USDT off, before the fix (locked Amount):
06-before-fix-usd-priced-request-opens-amount.mp4
Same request after the fix (Confirm, 2 500 sats, Savings):
05-usd-priced-request-opens-confirm.mp4
QA Notes
Setup
bitkit-dockeron local regtest,E2E_BUILDdebug build on a fresh simulator, wallet funded with 500 000 sats from the localbitcoind. Clipboard set withxcrun simctl pbcopy.tools/paykit-fixture-senderfrom bitkit-e2e-tests linked as a contact. Requests sent with./sender send <payer> --asset usd --amount 2.5 --rate btc=0.00001 --address <unused bcrt1q>and theusd/btcpresets.Journeys
scan-onchain-amount-to-confirm.xml— pasted BIP21 with an amount opens Confirm, the amount can still be edited, and the send completesscan-onchain-no-amount-to-amount.xml— pasted BIP21 without an amount still opens Amountonchain-accepted-result.xml— the fixed-amountbitcoin:link opens Confirm directlyrbf-own-send.xml— thebitcoin:link with an amount opens the review screen directlyfixed-price-bitcoin.xml— USD-priced and rate-priced requests open confirmation after preparation, without AmountManual Tests
N/A
Automated Checks
PaymentNavigationHelperTests.swift— on-chain BIP21 with an amount opens Confirm from both entry points, zero amount opens Amount, contact payments keep Amount, incoming on-chain requests open ConfirmPaykitPaymentRequestServiceTests.swift— converted requests bind Bitcoin terms only when USDT cannot pay them, and route to Confirm once boundSendConfirmationSwipeTests.swift— the swipe stays disabled while on-chain coins or the fee are still being preparedSendConfirmationSwipeTests.swift,SendConfirmationLifecycleTests.swift,PaymentNavigationHelperTests.swiftandSendSheetLifecycleTests.swiftre-run, 63 passed, 0 failedPaymentNavigationHelperTests.swift,PaykitPaymentRequestServiceTests.swift,SendConfirmationLifecycleTests.swift,SendSheetLifecycleTests.swiftandScannerManagerTests.swift: 485 passed, 0 failed. The full suite was not run locally; its integration tests start a node and were too slow here.SendConfirmationLifecycleTestsfirst failed two wallet-switch cases because the new coin selection also ran during a Savings/Spending switch; it now skips while a switch is running.Evidence
d313a2…, 143 sats fee in the mempool). Video in the PR comments.2df859…, 143 sats fee). Video in the PR comments.usdpreset → 21 000 sats,btcpreset → 50 000 sats. All opened Payment Request confirmation with Savings selected and no Amount step. The same $2.50 request with the routing fix removed reproduced the locked Amount screen.Gaps
send.e2e.ts@send_1and@send_2are updated in test: expect review screen for bip21 with an amount bitkit-e2e-tests#290, on the same-named branch the gate picks up; not driven locally.send.e2e.tsexpects Amount after a BIP21 with an amount in@send_1(unified1, manual entry) and@send_2(unified2, Lightning-insufficient fallback to on-chain). Both need updating for this change.Models used
claude-opus-5-5(reasoning:Not exposed)claude-opus-5-5(reasoning:Not exposed)🤖 Generated with Claude Code