Repository navigation
fix: validate minimum paykit request amounts - #913
ben-kaufman wants to merge 1 commit into
Conversation
|
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Rejects incoming on-chain Paykit requests under 547 sats with the Amount Too Low toast at both validation points: scan routing in AppViewModel and the deferred check after sync in SendSheet. It also generalises the rejection flag so a below-minimum request is marked presented rather than deferred. Reviewed 932ed17, light tier.
What I checked, and 5 candidates I ruled out
Read: the commit's diff, handleScannedData's .onChain branch, SendSheet's post-sync validation and selectHardwareFundingSourceIfNeeded, showPaymentSetupFailure, Env.dustLimit (547), the four new tests, the journey and README rows.
CI at review time: validate, detect-changes and Greptile green; Run Tests, Run Integration Tests and build-local still running. iOS does not build on this reviewer's host, so this review is reasoned from the code and CI.
Ruled out
- Off by one / platform drift:
amount < 547here andamount <= 546on synonymdev/bitkit-android#1457 reject the same set, and the journey file is byte-identical across the two PRs. - A path that skips the check: the node-running Lightning fallback goes through
validateOnchainBalance, the on-chain fallback has the explicit guard, and the deferred case (channels present but unusable, or node not running) reachesvalidateOnchainBalanceAndDismissIfInsufficient, which now checks first.testBelowMinimumUnifiedRequestClosesSheetAfterSyncFallbackcovers the last one. - Rejecting a payable Lightning request: the guard reads only the incoming request and runs only on on-chain routes;
testLightningRequestBelowOnchainMinimumCanOpenPaymentcovers it. - Rename missing a caller: no reference to
didRejectScannedPaymentForInsufficientBalanceorinsufficientBalance:remains in the tree. - Behaviour change from the generalised flag: on any rejection,
handleUnavailablePaymentRoutenow marks the request presented instead of deferring it. That matches the AndroidretryIncomingRequest = falsecleanup and the PR's intent: no automatic reopen, manual retry still works.
Merge confidence: 4/5, no findings and the change has tests, but the unit and integration runs had not finished at review time.
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Reviewed the full PR diff against its merge base, at 932ed17.
No new actionable code findings.
On-chain Paykit requests under 547 sats are rejected during scan routing and again when deferred send preparation falls back to on-chain. The minimum toast uses the existing invoice copy (PaymentRequestAmountTooLowToast, ₿ 547), and the unpaid request stays available for manual retry with the same presentation cleanup as an insufficient balance. Lightning still opens for a request under that amount. The boundary matches bitkit-android#1457 at 43a184ec: iOS rejects amounts below 547 and Android rejects amounts through 546, and both show 547. journeys/payment-requests/minimum-onchain-amount.xml is the same file on both pins. That journey uses an on-chain-only payer; a unified request Lightning can pay opens confirmation, and the minimum check runs when preparation falls back to on-chain.
Validation: Run Tests and Run Integration Tests passed on this revision. This review inspected the new PaykitPaymentRequestServiceTests cases and the journey.
Device testing: not performed in this review.
Ready for device testing.
piotr-iohk
left a comment
There was a problem hiding this comment.
QA tested on iOS at 932ed17c using staging regtest and a dedicated fresh wallet/profile.
Device checks passed:
- 100- and 546-sat on-chain contact requests show the 547-sat minimum feedback and close preparation.
- Unpaid requests remain retryable; manual retry repeats the minimum feedback.
- A fresh 547-sat request reaches enabled confirmation; dismissing it creates no payment.
- An above-minimum 500,000-sat request shows the genuine balance shortfall.
- Unpaid state survives app restart, and retry still clears preparation correctly.
Manual follow-up by Piotr: 100-sat unified contact payment requests paid successfully over Lightning in both directions (Android → iOS and iOS → Android). This is user-reported testing, without an agent recording.
Deferred Lightning preparation falling back to on-chain below the minimum remains untested on both platforms. No confirmed product defect was found in the tested scope. Approving as explicitly requested by Piotr with that coverage gap disclosed.
jvsena42
left a comment
There was a problem hiding this comment.
No finding at 932ed17.
Checked and clean:
- Validated amount = paid amount. Both validation sites (
AppViewModelscan routing and the deferredSendSheetfallback) readincomingPaymentRequest.amountSats, the value the sheet pays. A request without an amount skips the check and keeps the zero-amount flow. - Every route to on-chain is covered. Node running with no channels, pure on-chain with any node state, channels present but unusable (sync timeout fallback) and node not running (lifecycle fallback) all reach the minimum check before the balance check. Lightning and LNURL routes are untouched, so a Lightning request under 547 sats still opens.
- Rejection cleanup.
didRejectScannedPaymentleads tohandleUnavailablePaymentRoute(paymentRejected: true), which marks the request presented, clears the retry attempt and deadline, and ends the preparation. The request stays pending, is not auto-presented again and can be retried from the bell, which repeats the same path. Nothing is accepted and the private payment list is not consumed. - Cancel and Back. A swipe-down before the timeout goes through the same
cleanupas the coded dismissal;finishPaymenttouches only approved or accepted records. - Reentrancy.
didRejectScannedPaymentis reset at the start ofhandleScannedDataand set synchronously before eachreturn; there is noawaitbetween the set and the read inAppScene. - Journey.
PaymentRequestsBellandPaymentRequestAmountTooLowToastexist at head and the strings match;minimum-onchain-amount.xmlis byte-identical to the one in synonymdev/bitkit-android#1457.
CI is green at this head.
Device gate: 932ed17 — journeys: 0 driven by me; perf: not relevant; Figma: no screen changed; Android: same journey file on synonymdev/bitkit-android#1457.
Skipped — already driven this round: minimum-onchain-amount.xml (iOS, by @piotr-iohk at 932ed17, passed: 100, 546, 547 and 500,000 sats, manual retry and restart, in the approving review of 2026-10-12)
Skipped — covered by CI at 932ed17: unit tests, integration tests, e2e (all green)
This PR fixes misleading balance feedback for on-chain Paykit requests below 547 sats.
Description
Out of Scope
Design
N/A — no design available. Reuses the existing minimum-invoice toast.
Preview
QA Notes
Journeys
minimum-onchain-amount.xml— 100 and 546 sats show the 547-sat minimum, preparation closes, unpaid requests remain retryable, and 547 sats reaches confirmation.Manual Tests
N/A
Automated Checks
PaykitPaymentRequestServiceTests.swift— four regressions cover below-minimum scan routing and manual retry, deferred sync fallback with sheet dismissal, the payable boundary, and a small valid Lightning alternative.PaykitPaymentRequestServiceTests.swift— existing balance-rejection assertions use the generalized rejection signal.xcodebuild testwith the unit target, CI integration exclusions, pinned packages, and iOS 26.1 — 2,367 passed with zero failures or skips; simulator diagnostics collection timed out after the tests completed, and the result bundle finalized as Passed.minimum-onchain-amount.xmlon an iOS 26.1 regtest simulator with a disposable staging issuer — minimum feedback and manual retry verified; a prepared 547-sat confirmation was dismissed without paying.