Skip to content

fix: confirm fixed Paykit requests directly - #1014

Open
ben-kaufman wants to merge 31 commits into
masterfrom
fix/paykit-fixed-request-confirm
Open

ben-kaufman wants to merge 31 commits into
masterfrom
fix/paykit-fixed-request-confirm

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

  • Keeps requests with missing or expired conversion terms on the existing Amount rate-unavailable view, without binding payable terms.
  • Binds valid, above-minimum request terms before confirmation; lower quotes stay on Amount. Wallet-valued fiat display, quote expiry and changed-amount checks remain.
  • Keeps valid Spending/Savings switches on Confirm with the new quoted amount and refreshed fees. Missing, expired or below-minimum terms return to Amount.
  • Lets Back from manual coin selection stay on Confirm; swiping still requires coins before authorization.
  • Applies the saved automatic coin-selection algorithm before fee review, keeps manual coin selection, and preserves retry after fee or coin-preparation failures. Funding selection completes before preparing coins, so hardware fallback stays on Confirm. Pending setup blocks authorization.
  • Preserves asset selection when USDT is enabled and actually offered, and Savings/Spending selection for requests offering LNURL and on-chain Bitcoin. Single-method LNURL requests meeting their minimum open confirmation directly. Wallet sync, fees, authorization and duplicate-payment protections remain in place.
  • Orders backup observation requests, serializes subscription ownership and preserves newer listeners and writer-drain completion when stops overlap.
  • Adds duration-only preparation diagnostics (resolveMs and setupMs) to separate endpoint resolution from subsequent setup without logging payment details.

Out of Scope

Design

Reuses the existing send confirmation without changing its layout. Android's screen map identifies Send (Paste) (On-chain) → Confirm Send Onchain in the Bitkit design file. The exact current frame node is unverified.

Preview

Before: Preparing → Amount (locked) → Confirm.
After, when no separate funding choice is required: Preparing → Confirm. LNURL + Savings keeps its existing selection step.

Current validation — published aac68853

J1 action 14 now returns fixed/quoted requests to Amount after a post-selection fee failure, clears reviewed terms and closes safely on Back. The same regression failed before the fix and passed afterward, including authorization/submission tripwires.

Hosted attempt 1 on 8405acaf / tree 1123cafc (parents 831ec3e6 / aac68853): 2,954 unit + 39 UI passes, five fixture skips; 30 integration passes; E2E 13 passes and one hardware skip. Zero test failures or test-case reruns. E2E used its first workflow step, with one internal tap retry and four log-stream restarts. Full diagnostics and prior validation history are retained. Newer master 82925a86 is not covered by this combined-tree result.

Final local checks on the same tree: 2,993 passes/five skips, two independent source reviews, SwiftFormat 0/828 and translations zero errors/15 verified base warnings. All 31 publication-range signatures verified locally/on GitHub against base 831ec3e6. Hosted static checks passed too.

Both hosted XCTest builds retain 286 unique linker warnings/571 occurrences; keyboard/NaN/RGS/node notices remain. E2E retains 85 harness dependency findings (7 critical) and lacks media/structured test reports. No fresh master-runtime or warning-free claim. Historical entitlement/launch/E2E failures, skips, recoveries and missing local-only evidence retain their original attribution.

Bot QA reports stress-hook fee fallback/header Back/reopen; natural-rate shortfall and native interactive Back remain unverified. Zero current human approvals; all three named reviewers pending. Native interactive Back, full linked-request/provider/funded/physical-device and recovery acceptance remain incomplete. No gate is waived.

QA Notes

Setup

Use the existing fixed-quote journey prerequisites: two disposable identities, a fresh fixed $2.50 Bitcoin quote, funded regtest Savings and a usable Spending channel for live switching. For the minimum boundary, use controlled rates yielding exactly 546 and 547 Savings sats as specified in journeys/paykit-quoted-usdt.xml. Owner validation remains unfunded; the linked reviewer-reported regtest payments retain their recorded revision and environment. For the added J1 hardware subcase, reuse an explicitly owned, already funded regtest hardware-emulator wallet with empty Savings; check automatic/manual selection for both fixed BTC and quoted requests, then close without signing or sending.

The XCTest USDT fixture is separate: UsdtWalletUITests now defaults to unavailable loopback endpoints. An explicitly owned disposable fork must be selected through test-runner BITKIT_TEST_USDT_RPC_URL / BITKIT_TEST_USDT_BUNDLER_URL; its test funds its wallet. BITKIT_TEST_USDT_OFFLINE_RPC_URL must be unavailable. Never use another task's shared fixture. See journeys/README.md.

Journeys

  • J1: new journeys/payment-requests/fixed-quoted-confirmation.xml — LNURL/Savings selection, quoted amount and wallet valuation, saved/manual coin selection and initial hardware fallback, cancel/reopen, delayed preparation, insufficient funds, expiry and BTC/USDT choice. Partial linked-regtest execution is reported in Ovi’s d181 QA, including payment, wallet switching, manual coins, header Back/reopen and expiry/fresh terms. Native interactive Back remains unverified; earlier LNURL evidence retains its original revision. Manual failure, plain-BTC failure and fresh-review controls retain their earlier programmatic evidence.

  • J2: updated journeys/paykit-quoted-usdt.xml — iOS dot-decimal checks in comma-region settings preserve exact values, cents, trailing zeros and six-decimal precision. Partial d5e reviewer QA covers six-decimal review, request/subscription serialization and the 546/547-sat boundary; full J2 is not claimed. Incoming fixed USDT terms stay locked: action17 now separately checks editable USD creator amounts and fixture-provided six-decimal USDT review. Android retains confirmation but disables authorization below its equivalent minimum.

Manual Tests

N/A

Models used

For this repair and validation update (verified session metadata; earlier implementation attribution is not inferred):

  • Planning/scoping: gpt-6-astra (reasoning: xhigh)
  • Implementation: gpt-6-astra (reasoning: xhigh)
  • Review: gpt-6.1-sol (reasoning: high)
Earlier validation, failures and diagnostic history

Exact USDT review amounts — 629a874e (October 28): Fixed the six-decimal display finding from review 5678141014. The actual payer confirmation header and Show Details payment, fee, recipient amount and maximum total now use the existing exact USDT formatter. Request/subscription amount text also preserves all six decimals. Quote values, serialized amounts and payment authorization are unchanged; balance overviews retain their established formatting.

The rendered regression reproduced $0.12 / $<0.01 for 0.123456 / 0.000001 before the fix (one failed test, ten assertions). All ten focused amount/display tests now pass. Saved native screenshots show exact headers, fee 0.000001, and totals 0.123457 / 0.000002. These tests host the real production review builders directly and verify static rendering; they bypass full view lifecycle/navigation/quote loading and do not authorize or send a payment. Their new SwiftUI constant-binding warning is retained as an explicit test boundary, not a production-race or warning-free claim.

Final canonical local validation passed 2,934 unit and 39 UI executions, with four documented fixture skips and zero failures or retries. Complete diagnostics were inspected: two AppIntents warnings, ten structured QoS warnings plus the new static-builder State warning, eleven raw QoS backtraces, and 72 NaN notices remain explicitly retained alongside symbolication and simulator notices; this is not a warning-free claim. Full SwiftFormat 0.64.0: 0/846 files require formatting, 83 skipped. Translation validation: zero errors, 15 warnings matching the verified actual-base warning multiset. Two independent source perspectives covered the component and actual payer path; an initial component-only coverage gap was caught before publication and the actual payer regression added. Required source signatures: all 20 commits in the current-base PR range verified locally and matched the exact GitHub set with verified=true / reason=valid. Validation applies to local PR tree 88d9c2e3295b93490a3395510ed3605f1ce1b565, not a new combined master tree. Master 68bede62 added only disjoint profile journey documentation since the earlier base; no unrelated integration was performed.

J1/J2 retain their IDs. Their six-decimal request case now explicitly needs fixture-provided USDT terms, because the iOS editor creates BTC/USD terms. Full linked-request, native Back, fresh-terms retry, provider/funded/hardware/settlement acceptance remains incomplete. No pending human request was recreated; João's announced absence through November 1 is preserved. Current work: implementation gpt-6-astra / xhigh, scoped independent reviews and diagnostic audit gpt-6.1-sol / high, verified from execution metadata.

Hosted checks for 629a874e are complete on workflow attempt 1: unit passed 2,960 unit and 39 UI executions, with four fixture skips and no failures; integration passed 30 tests once each, with four existing Blocktank staging skips. Verified checkout a52bbce2 / tree efec63fb combines actual master 68bede62 with 629a874e; this differs from local PR tree 88d9c2e3. Complete logs, artifact digests, execution trees, case summaries and nested diagnostics were inspected. The payer display regression passed in 11.611 seconds. Its two introduced State/constant-Binding messages remain explicit: direct builder hosting proves static text, not the full view lifecycle or authorization flow. The 286 structured / 571 raw duplicate-symbol warnings match the verified prior warning multisets. Runtime/mock/fixture/tooling notices, symbolication limits, and two passing integration tests' internal sync/deposit recoveries remain recorded; no fresh-base runtime or warning-free claim.

E2E ultimately passed 13 unique cases across 14 executions: 12 passed initially, while Lightning timed out after 30 seconds waiting for ActivityShort-0 to show 9 000 after restored-channel closure. The selective second built-in step passed Lightning; step 3 was skipped. One existing hardware-wallet case remained disabled. This was not a first-pass clean run or a manual retry. Failure screenshots/video were generated on the runner, but their upload was skipped after recovery; no downloadable failed visuals, device/app logs or runtime report were available. Root cause remains unproven, with no actual-base reproduction or permanent-fix claim. All five job logs, pinned harness 4c0cbef0, cases and app-artifact provenance were inspected. The 76 stale-element notices, five log-stream restarts and pinned-harness dependency findings remain explicit. Formatter and translations were already inspected: 0/850 files needed formatting (87 skipped); zero translation errors and 15 verified base-matching warnings.

Automated reviewer QA at this head reports exact six-decimal and one-micro request displays, a six-decimal subscription, and Savings-only proposal selection using linked test wallets and a local fixture, without authorization. This is reviewer-reported evidence, not an independent rerun or human approval. Neither edge gesture established an interactive Back transition; expiry removed the request before fresh-terms retry could be exercised. Editing/serialization, live bridge, provider, hardware and settlement gaps remain. No new delta defect was reported. Shared-navigation ownership coordination and the existing remaining acceptance gates are still required.

Hosted results for 9dacc089: unit 40628484458 and integration 40628484601 succeeded on workflow attempt 1. Verified checkout b9e3793f / tree ed97b16a combines actual master 85e9d4c6 with this head; it differs from the local PR tree. Unit: 2,958 passes; UI: 39 passing executions and four fixture skips. Integration: 30 unique passes in 30 executions, four existing Blocktank staging skips, no failures or failed-test reruns. Full logs, archive digests, structured results, case/skip summaries and nested diagnostics inspected. The 286 structured build warnings and 571 build-log duplicate-symbol warnings match the previously inspected baseline; keyboard queue timeout/No stack, socket/haptic and negative-case notices remain, as do integration RGS/Taproot sync/node/teardown and version-probe broken-pipe notices. This is not a fresh actual-base runtime or permanent-fix claim. E2E 40628484848 also passed on workflow attempt 1: 13 unique passes in 13 executions, one existing hardware-transfer skip, zero failures; first test step passed and fallback steps 2/3 skipped. All five full logs, pinned skip source and artifact provenance were inspected on the same merge/tree. Harness 4c0cbef0; artifact 12790119756 SHA-256 dfd3344f1ced468140d65129a9cc51f3a1b23babb36b910e1df9c5837c95557e matches API/upload/runner download. Retained 74 stale-element notices, four log-stream restarts, one Applying-to-Updated toast wait, tool notices and 76 unresolved harness dependency findings. No local app ZIP or runtime-report/video artifact was inspected; no manual CI retry or warning-free claim. Native Back, fresh-terms retry and remaining J1/J2 acceptance gaps are unchanged.

Current documentation correction — 9dacc089 (October 28): Corrected stale J2 comma-decimal expectations to the existing iOS dot-decimal convention, including comma-region settings. All 19 actions and exact-value, trailing-zero, six-decimal and serialized-value checks remain. No app behavior changed. XML/action/reference checks, git diff --check, full SwiftFormat 0.64.0 (0/846), and one independent documentation review passed. All 19 current-base commits verify locally and on GitHub. App tests were not rerun for this documentation-only delta; earlier local/hosted results keep their original inputs. Automated review 5676643246 reports additional partial device evidence at 20cfeffa, not human approval or independent execution by this monitor. Native Back, expiry retry and full J1/J2 acceptance remain incomplete. The former comma-expectation result is preserved as a failure of the earlier journey wording, not converted into a completed new journey run.

Current repair — 20cfeffa (October 27): The initially-unreviewed Back assertion reported in automated review 5670593898 reproduced independently on unchanged b53e300e with Xcode 27.1/iOS 26.5. The four-line fix observes Amount being popped to an empty navigation path and reuses the existing invalid-terms, presentation-owned dismissal guard. All 15 unchanged lifecycle tests now pass. No assertion or production payment guard was weakened; the journey also names interactive Back as an acceptance check.

Final canonical validation on Xcode 26.1/iOS 26.1 passed 2,932 unit executions and 39 UI executions, four fixture skips, zero failures, including all 35 watch-only tests. Full SwiftFormat 0.64 passed (0/846; 83 exclusions); translations report zero errors and 15 warnings whose individual call/missing-language sets match actual master 543d9389. Complete structured/action/raw diagnostics and an independent frozen correctness/quality review were inspected. Eight aggregate QoS warning cases represent 19 warning-bearing executions; three raw Paykit/pubky initialization backtraces, two AppIntents build notices, simulator/negative-case/runtime messages and the unavailable-fork skip remain explicit. All 18 current-base commits verify locally and match GitHub's exact verified=true, reason=valid set. Tested local PR tree: e286b0d9; the new hosted unit and integration results below cover the combined tree. No warning-free or fresh-master-runtime claim.

Hosted checks at 20cfeffa: Unit 40559037614 and integration 40559037349 passed on workflow attempt 1. Unit: 2,953 unit + 39 UI executions passed, four fixture skips, zero failures, including all 15 confirmation lifecycle and 35 watch-only tests. The repaired initially-unreviewed Back regression passed. Integration: 30 unique passes in 30 executions, four existing Blocktank staging skips, zero failures or test-case reruns. Both checked out verified merge d1ec3668 (543d9389/20cfeffa), tree 370b70b7, which differs from the local PR tree; 21 additional upstream unit cases account for the count difference. Full archive hashes, test trees, all UI/integration case summaries, build/action/runtime diagnostics and job logs were inspected. The three Trezor opt-in cases and isolated Arbitrum case remain skipped; integration retains the four staging skips.

The 286 structured and 571 build-log duplicate-symbol warning multisets match the previously inspected hosted result. Runtime output still includes the keyboard queue timeout with Last Exception Backtrace: No stack!, socket/haptic/launch notices, injected negative-case diagnostics, integration RGS/node/address-search/teardown messages and the version-probe broken-pipe exception. Associated source comparisons are retained; this is not a warning-free run or a fresh actual-base runtime reproduction. E2E 40559038210 also passed on workflow attempt 1: 13 unique passes in 13 executions, one existing hardware-transfer skip, zero failures. The first built-in test step passed; steps 2/3 were skipped. Build and runtime used the same verified d1ec3668/370b70b7 combined tree. The executed harness was 4c0cbef0 (two commits beyond the previous harness), on iPhone 17/iOS 26.2. The exact pinned hardware spec retains its Blocktank-staging skip. All five full logs, case outcomes and artifact provenance were inspected. Artifact 12761095558 SHA-256 b59616212d303b5441c1053ac883fbd32ba4af1593cb9c98e93c85816accc1a6 matches API/upload/runner download; no local ZIP or runtime-report/video artifact was inspected. Retained diagnostics include 66 stale-element notices, four log-stream restarts, an address-type tap helper retry that reached its expected toast, Node/npm/tool/AppIntents notices and 76 pinned-harness dependency findings. No manual CI retry or warning-free claim. These hosted passes do not close the supplementary native gesture or other acceptance gaps below.

Supplementary native check remains partial: an unpublished animated test host used real Amount with fixed-quote terms that were valid at a historical time and expired before appearance. Header Back closed the owned sheet state with an empty path and no preparation, authorization or submission. The first diagnostic execution failed an exclusive-visible-route readiness assumption and timed out before an edge gesture was delivered. One bounded rerun corrected only that temporary readiness check; actual Amount/path assertions passed, but two edge swipes did not pop Amount and the diagnostic timed out. Both failures and full diagnostics/screenshots are retained. The cause is unproven; completed/cancelled interactive Back, real linked-request setup, dismissal animation and reopening are not accepted by this check. Temporary test code was restored byte-for-byte before signing. No funds, payment, wallet reset or shared-fork access occurred. The repaired existing lifecycle regression is green; this additional gesture gap is not presented as a pass.

A later bounded prerequisite check on the same isolated iOS 26.5 simulator found two native controllers, an enabled recognizer, allowSwipeBack=true and a key window. The temporary host's 390×844 window differed from the 402×874 screen. After correcting only that host geometry and observing native transition state, one cancel-shaped edge drag and one full edge swipe both produced recognizer failed state before any interactive transition. The 100-second diagnostic ended with one failed case/four assertions: Amount/path and sheet state remained, no completed or cancelled pop occurred, and routing/preparation counts stayed zero. The geometry change did not resolve the gap; the reason for gesture rejection is still unproven, and no production defect or fix is inferred. Complete xcresults, action logs, state trace and screenshots are retained. Temporary code was restored byte-for-byte; published source remains 20cfeffa. Further gesture acceptance needs the production sheet presentation to reach that boundary, rather than another unchanged synthetic-host retry.

At b53e300e, the bot reported rendered confirmation, saved/manual coin selection (143/212-sat fees), manual-picker Back, 546-sat fallback/Back/reopen, 547-sat confirmation, cancellation during preparation and LNURL/Savings selection, stopping before payment. Those remain reviewer-reported partial J1/J2 results at that head. Piotr's fee-refresh thread is externally resolved. On October 27, Ovi dismissed João's older changes request, noting its resolved threads and João's absence until November 1; this monitor did not dismiss it. Piotr/Ovi requests remain pending, João's comment retains assignment and his absence is respected, and there are zero current-head human approvals. A bot comment or resolved thread is not approval. Current shipped/provider/funded/physical-hardware/full-linked-request/two-wallet settlement acceptance remains incomplete.

Earlier validation — b53e300e (October 27): The conflict with then-current master d2d9d28d is resolved in a signed normal merge, tested tree 4516c8e1. Master's cache display and test-preference isolation changes are preserved; the USDT UI helper retains its explicit endpoint overrides and unavailable defaults. Final canonical validation passed 2,932 unit tests and 39 UI executions, four fixture skips, zero failures. The previously failing launch test passed locally, which does not establish the cause or a permanent fix for its earlier hosted timeout. Full SwiftFormat 0.64.0 passed (0/846); translations found zero errors and 15 warnings with all 20 inputs identical to actual master. Complete diagnostics and an independent frozen integration review were inspected. All 17 current-base commits verified locally and match GitHub's exact set with verified=true, reason=valid. Hosted unit 40519925128 and integration 40519925139 passed on workflow attempt 1. Unit/UI: 2,932 unit and 39 UI executions passed, four fixture skips, zero failures; all 35 watch-only account tests passed, and the earlier launch case passed in 13.385s. Integration: 30 unique passes in 30 executions, four existing Blocktank staging skips, zero test failures or test-case reruns. Both used verified checkout 3c59760a (d2d9d28d/b53e300e), tree 4516c8e1, exactly matching final local validation. Full artifact digests, execution trees, UI/skip summaries, structured diagnostics and compile/action/runtime logs were inspected. Hosted E2E 40519926046 also passed on workflow attempt 1: 13 unique passing cases in 13 executions, one existing hardware-case skip, zero failures. Its first test step passed; steps 2/3 were skipped. Build and runner checked out the same 3c59760a/4516c8e1 tree, with pinned harness ec01c02f. No manual CI retry. All three existing human requests are preserved; João's changes request remains. The later fee-thread disposition is recorded above. No approval, merge readiness or funded/provider acceptance is inferred.

Final diagnostics retain 16 structured QoS warnings, three AppIntents compile notices, 12 UIKit appearance events, 24 CoreGraphics NaN lines in app stdout, and platform/negative-case runtime messages. Full address stacks are saved; no raw symbolicated backtrace was exported. No fresh base runtime or warning-free claim. Effective runner/helper and built endpoints were verified at unavailable 295xx ports; the fork skipped before funding. J1/J2, physical hardware, full linked-request/two-wallet settlement and rendered 546/547 gaps remain as documented below.

Hosted diagnostics retain the exact prior 286 structured / 571 build-log duplicate-warning multisets. Integration's existing sync helper recovered from a first-attempt Taproot sync timeout inside a passing test; no whole test was repeated. Unit runtime logged a keyboard-queue timeout with Last Exception Backtrace: No stack!, plus socket/haptic and negative-case messages. These observations are retained without a cause or permanent-fix claim. Integration's 22 associated inputs match actual d2d9d28d; of 40 unit-runtime source files, 38 match and AppViewModel/SendConfirmationView retain this PR's changes. No fresh actual-base runtime or warning-free claim.

E2E diagnostics retain 66 stale-element notices, four log-stream restarts (statuses 143/137/124/149), Node/npm deprecations and the pinned harness's 76 dependency-audit findings (8 low, 17 moderate, 44 high, 7 critical). Build/action output retains AppIntents metadata, destination/tool and ignored-base-input notices. All five job logs and 14 case outcomes were inspected. App artifact 12741962657 (141,017,046 bytes) has SHA-256 81978458fa352095f683215ca76521bd97cd1c7fe1a8e73e62477a4e59891883, matching API metadata, upload and runner download logs; the archive was not downloaded locally. The successful workflow did not upload runtime-report/video artifacts. These results do not establish a warning-free run or erase earlier failures and acceptance gaps.

Actual master at the latest audit is 85e9d4c6: three commits and eight paths beyond the hosted base 543d9389. The only overlap with the PR diff is AppScene's foreground-notification hunk, disjoint from this PR's request-preparation timing changes. The PR remains mergeable; no integration was performed. Hosted unit/integration/E2E results cover 543d9389/20cfeffa; the newer 85e9d4c6 combined tree is unvalidated. Local results retain their separate repaired PR-tree attribution.

Earlier validation — 20f54449 (October 27): The unit CI crash at 811b4335 was traced to concurrent access in the watch-only test fake: an assertion read its tracking Set while the authorization task inserted into it. The two affected tests now await the existing actor-isolated gate after tracking has finished. All assertions and all 35 tests remain; production code is unchanged. The coordinated repair passed 35 focused tests, 2,928 unit executions and 39 UI executions, with four explicit fixture skips and zero failures. Full SwiftFormat 0.64.0 passed (0/845 files need changes); translation validation found zero errors and 15 warnings with unchanged actual-base inputs. Complete diagnostics and independent frozen review were inspected. All 16 commits against actual master b666ccb1 verify locally and match GitHub’s exact commit set with verified=true, reason=valid. Hosted integration 40501754529 passed on attempt 1: 30 unique tests passed in 30 executions, four existing Blocktank staging skips, zero failures or retries. Verified checkout a7cb2f52 (b666/20f54449) has tree c1e3cd27, exactly matching final local validation. The archive digest, all 34 full case summaries, tree, action/runtime output and compile/test log were inspected. Structured warning/error arrays are empty; retained 571 duplicate-symbol compile warnings match the prior integration multiset, alongside the version-probe broken-pipe exception, tool/destination notices and embedded RGS/node/address-search/teardown/fixture/launch-metric messages. All 22 associated source inputs match actual base; no fresh base runtime or warning-free claim. Hosted unit 40501754193 subsequently failed on attempt 1: all 2,928 unit executions (including all 35 watch-only tests) passed, but UI had 38 passes, four skips and one failure in BitkitUITests/testExample at app.launch(). XCTest timed out acquiring a background assertion; the failure video shows splash then Terms. Root cause remains unproven. E2E 40501754542 passed 13 unique cases in 13 executions on its first step, with one existing hardware skip and zero failures. The complete logs, artifact digests, test trees, failure video and diagnostics were inspected; no manual CI retry. Both use the verified a7cb2f52/c1e3cd27 checkout. The 286 structured/571 compile warnings match the preceding unit run; E2E retains 70 stale-element notices, log-stream restarts and pinned-harness dependency warnings. All three existing human requests are preserved; João’s changes request and Piotr’s fee-refresh thread remain in force.

Earlier validation at 9a807ef3 (October 27): Conflicts with actual master b666ccb1 are resolved in signed normal merge 9a807ef3, tested tree 0ce4fc92. The combined tree preserves fixed-quote amount/expiry/dust/fee and authorization safeguards, duration-only diagnostics, and master's SDK/request/proof lifecycle changes. Final local gates passed: 15 focused tests, 2,928 unit executions and 39 UI executions, four fixture skips, zero failures. Full SwiftFormat 0.64.0 reports 0/845 files needing changes (81 skipped); translations report zero errors and 15 warnings whose inputs match actual master. Complete test trees, compile/action logs, 19 structured QoS warnings, 21 raw backtraces and remaining platform notices were inspected and retained; this is not a warning-free claim or runtime baseline reproduction. All 14 commits against actual master verified locally and matched GitHub's exact set, with every signature verified=true, reason=valid. Hosted unit, integration and E2E workflows all passed on attempt 1 at the exact locally tested merge tree. Hosted unit/UI: 2,928 unit and 39 UI executions passed, with four fixture skips. Integration: 30 passed and four Blocktank skips. E2E: 13 passed and one existing hardware-case skip; its first test step passed and steps 2/3 were skipped. Full diagnostic and helper-retry limits are recorded below. Full evidence is recorded below. All three existing human review requests remain pending; no changes request was dismissed.

The canonical run verified built RPC/bundler endpoints at unavailable 29545/29546 and the fork skip before app launch or funding. No shared fork access, funding or transfer occurred. Funded/provider/hardware/two-wallet and rendered 546/547 acceptance gaps remain; the historical incidents and hosted retry/skip limits below retain their original attribution.

Earlier validation (October 26, retained at its original head): Conflicts with actual master b8b7f3d9 are resolved in signed normal merge 63ea2034 (tested tree 21fb1809). The combined tree preserves master's wallet-switch lifecycle fix and this PR's quoted-payment guards. Final local validation passed: 2,894 unit executions and 39 UI executions, with four fixture skips and no failures. All 13 current-base PR commits verify locally and on GitHub. Hosted unit/integration checks passed. E2E completed successfully after two built-in retries of the Lightning case; 13 unique cases passed and one hardware-transfer case stayed skipped. The two earlier Lightning timeouts remain unexplained. J1/J2 and the funded/provider/hardware acceptance gaps below remain open.

Fixed quoted Bitcoin requests open in the existing confirmation screen when no separate funding-source selection is needed. A fixed $2.50 Savings request skips the amount screen with disabled controls. Requests offering LNURL Spending alongside Savings retain the existing funding-choice screen because LNURL confirmation has no wallet selector.

Earlier master integration and local validation — 63ea2034

The normal integrations retain the previous dust and fixture-endpoint fix, merged #1003's Receive coverage, and merged #1098's single wallet-switch run and resumed-preparation behavior. Quote identity, exact amounts, expiry, selected-rail minimums, routing-fee cancellation and authorization checks remain intact. No assertions were removed or weakened, and no new exclusion or authored test was added by conflict resolution.

  • The affected confirmation lifecycle class passed 15/15. The final workflow-selected canonical run passed 2,894 unit executions and 39 of 43 UI executions; three opt-in Trezor cases and the unavailable local fork case skipped. The full execution tree includes 30 launch configurations grouped under one summary case. Receive/invalid-payment coverage passed in 56.723 seconds and comma-region manual entry in 44.006 seconds. There was no retry.
  • This was the combined b8b7f3d9 tree, using Xcode 26.1 and an iPhone 17 Pro Max simulator on iOS 26.1, one worker and private build/package paths. The six canonical integration exclusions match the workflow; separate integration, E2E, provider or hardware completion is not claimed by this local run.
  • Exact UI helpers, runner forwarding and built-app endpoints were checked. Unfunded endpoints were reserved without listeners at isolated ports; the runtime fork availability check failed at the configured port and skipped before app launch or funding. The shared fixture was not accessed. This safe skip does not validate the historical failed funded journey.
  • Final SwiftFormat 0.64 reports 0/844 files need changes (79 excluded). Translation validation reports zero errors and 15 warnings, whose full messages and source/input comparison match actual master. XML and whitespace checks passed.
  • Complete structured and raw diagnostics were inspected: 13 runtime QoS warnings remain (four unit and nine UI records), plus two AppIntents compile notices. SDK initialization/synchronization, UIKit appearance, locked-device discovery, simulator haptics and injected negative-case diagnostics are retained. No introduced lint, formatting or Swift compiler finding was identified; this is not a blanket warning-free claim or an actual-base runtime reproduction. The test-without-building report alone does not prove a warning-free compile.
  • Independent review of the frozen integrations found no actionable issue. All 13 commits against actual master b8b7f3d9 matched the local range and reported verification.verified=true, reason=valid on GitHub. Both integrations are additive signed commits, with authors and prior work preserved.

The earlier 536abb44 candidate also passed its own canonical run before master advanced, but that result is distinct from the final run above. The earlier shared-fork incident and incomplete rendered 546/547-sat boundary, provider, physical-biometric, hardware and two-wallet settlement journeys remain explicitly unvalidated.

Earlier dust/fixture fix validation — 9acc68e2

Quoted Savings terms now require at least 547 sats before confirmation can rebind them. Exact valid amounts, expiry, quote identity and authorization guards are preserved. This addresses Piotr's finding.

  • Original production failed the new 546-sat regression; the fixed confirmation lifecycle class passed 15/15, including exact 547-sat acceptance. These hosted SwiftUI tests prove navigation/terms state, not a rendered Amount/swipe/PIN journey.
  • Full canonical selection: 2,881 unit executions passed; 43 UI executions had 39 passes, three Trezor skips and one funded-fork failure. That older test hard-coded shared ports despite runner overrides, called test_fundWallet, and observed 1,000 fake USDT. Quote preparation failed before confirmation; no transfer/settlement is evidenced. This was an isolation mistake, not accepted funded QA. No fixture repair, reset or compensating write followed.
  • The test class now uses explicit selected endpoints and unavailable-loopback defaults. Every existing test and assertion remains. The final affected class rebuilt and ran with the actual isolated port confirmed in its runtime log: two unfunded UI cases passed, one unavailable-fixture case skipped. No complete canonical rerun followed in that earlier pass; the final combined-tree run is documented above, and the original failure remains recorded.
  • Final SwiftFormat 0.64: 0/843 files need changes. Translations: zero errors / 15 previously verified base-equivalent diagnostics. XML/whitespace checks passed. Complete structured/raw reports inspected: 19 canonical QoS warnings and three in the affected UI run remain; SDK, negative-test and simulator notices are retained, without a blanket warning-free claim.
  • Independent frozen review and bounded endpoint recheck passed. All 11 PR commits against actual master 2ce21025 verify locally and as verified=true, reason=valid on GitHub. This is an additive signed commit, with no history rewrite. At that point hosted CI was blocked by the master conflict; the current signed integration above supersedes that hold.

J1/J2 and live provider, physical biometric, hardware and two-wallet settlement gaps remain open. The final safe fixture skip does not validate the failed funded journey.

Automated Checks

  • updated PaymentNavigationHelperTests.swift — exact converted amount, expiry, real BTC/USDT choice, routing, unavailable/expired-rate fallback and reviewed-term mismatch protection.
  • updated PaykitPaymentRequestServiceTests.swift — hosted confirmation waits for wallet sync before rejecting a below-minimum quote.
  • updated SendConfirmationLifecycleTests.swift — hosted fixed-request preparation, rail-specific quoted amounts and fees, invalid-term fallback, manual picker Back/retry, UTXO failure and fee failure/recovery.
  • ran the final focused simulator pricing, navigation, confirmation and LNURL suites — 48 tests passed. The LNURL selection regression failed before the fix and passed afterward, including different 2,500-sat Lightning and 5,000-sat Savings quotes.

Historical validation before ad30f8be: the 48-test focused result above predates this integration. The prior all-target compile, whitespace-only ActivityRow formatting repair, 14 verified base translation warnings and CoreSimulator boot blocker remain historical evidence.

Historical author validation at ad30f8be (tested tree f76406a5, integrated master 53c5ec0d):

  • xcodebuildmcp simulator test --json focused selection: 439 passed, zero failures/skips. The canonical direct Xcode fallback used the same six CI integration exclusions and one worker: 2,881 units plus 43 UI executions (39 passed, four fixture skips), zero failures. The UI skips require the disabled Trezor emulator or local Arbitrum fixture; they are not passes. Existing unfunded iPhone 17 Pro simulator, iOS 26.1.
  • Complete xcresult and raw diagnostics inspected: zero build errors/warnings; 13 runtime QoS warnings during SDK initialization, with implicated tests, SDK wrappers and dependency pins verified unchanged from tested master. The first canonical CLI build succeeded but could not launch because its exported test products disappeared; its complete failure diagnostics are retained. The cause remains unexplained. The task-local direct Xcode run then completed successfully, including natural diagnostic collection.
  • Final SwiftFormat 0.64.0: 0/843 files require formatting. Translation validation: zero errors and the same 15 warning diagnostics as exported 53c5ec0d. Independent combined integration review found no verified issue; PR patches and master recovery/sign-out behavior are preserved.
  • Master subsequently advanced to e4bf44a2 through maintainer merges of fix: unify usdt balance and amount presentation #1077/fix: reuse received payment artwork layout #1092. Those 36 paths are disjoint from this PR's 13-path delta; a clean read-only merge retained the exact PR blobs and all current-base changes. Local runtime evidence applies to the published tree. Hosted unit and integration checks subsequently passed on CI merge a6deea4d, whose tree 053473bc exactly matches that reviewed combination with e4bf44a2.
  • The normal merge is signed. All 10 commits against actual base e4bf44a2 verified locally and the exact GitHub commit set reports verified=true, reason=valid for every commit.

Hosted validation for head ad30f8be:

  • Unit workflow 40030073120, workflow attempt 1: 2,880 unit tests and 43 UI executions (39 passed, four fixture skips), zero failures. The one-unit difference from the local run is the locale-input test removed by merged fix: unify usdt balance and amount presentation #1077 in the newer tested base. Complete logs, xcresult and build diagnostics were inspected. The structured report contains 286 duplicate-native-symbol warnings; its exact warning multiset and all 571 repeated build-log diagnostic nodes match actual master e4bf44a2 run 40028545050. AppIntents metadata notices also occur on both. Project, workflow and dependency pins match that base; no new warning was found. The hosted xcresult records no runtime warnings.
  • Integration workflow 40030072972, workflow attempt 1: 34 cases / 34 executions passed, zero failures/skips, with no test retry. Full log and xcresult inspected. This report is for test-without-building and does not independently establish a warning-free compile.
  • E2E workflow 40030073870 completed successfully on workflow attempt 1, with 14 unique cases / 15 executions across two built-in test attempts. The first attempt passed 13 cases, including LNURL msat pay/withdraw and all four hardware cases. @lightning_1 timed out after 30 seconds waiting for “Transfer Initiated” after closing a restored channel (lightning.e2e.ts:276 at pinned E2E revision bcf91fee). The second attempt reran only that case and passed; the other 13 were skipped and attempt 3 did not run. Complete workflow logs and per-case reporters were inspected. The close-confirmation view, transfer/wallet view models, Lightning service, toast components and E2E workflow match actual base e4bf44a2; this comparison does not prove that the timeout is pre-existing or establish its cause. After the successful retry, the workflow skipped runtime/screenshot/video artifact upload; only the built app artifact remains, so the saved failure visuals and app logs could not be inspected. Startup log-stream wrapper restarts are separate from the test retry. No manual workflow retry or code change was made, and the passing retry does not establish a permanent fix. All CI workflows for that earlier tested revision passed; this historical result does not describe the latest head. The full payment-journey limitations below remain.

External device QA at exact head ad30f8be is attributed to review 5635086275, which reports no introduced finding. On an iPhone 17 Pro / iOS 26.5 simulator with regtest funds, the reviewer checked direct 5,000-sat confirmation and a 143-sat fee, Largest First selection, manual-picker Back, cancellation during preparation, LNURL/Savings choice, LNURL-only direct confirmation, BOLT11/Savings switching at 2,500/5,000/2,500 sats, the fixed $2.50 BTC/USDT choice, and insufficient/expired-term guards. Swiping after expiry left both Bitcoin balances unchanged; fresh terms required confirmation. Hosted assertions covered invalid-term and fee/UTXO failure recovery. Videos and exact limitations are in that review.

This external subset does not report a completed two-wallet fixed-quote settlement. USDT funding/submission, subscription authorization and hardware-wallet signing were not exercised; provider and physical-biometric validation remain incomplete. The full journey above stays unchecked. That earlier author-validation pass made no funded payment or wallet reset. Simulator data was preserved and the native window released.

Hosted unit/integration completion — 63ea2034, 2026-10-26

Unit40410064946 and integration40410064928 succeeded on workflow attempt 1. Both checked out merge a9eb51a06724ccd6a0cbff62487fcd8b5bf0bc77, with verified parents actual master b8b7f3d9 / published 63ea2034 and tree 21fb1809bb1af4f626c437015f8780139a9320b7, exactly matching the locally tested published tree. Unit: 2,894 passed. UI: 43 executions, 39 passed / four fixture skips / zero failures. Integration: 30 passed / four existing Blocktank staging-retirement skips/zero failures, with no retry execution. The four integration guards are byte-identical to actual master. Skips are not successful provider/hardware journeys.

Artifact ZIP digests, all execution refs/skip reasons, complete logs and structured build/action reports were inspected. Unit has 286 structured duplicate-symbol warnings and 571 build-log warning messages; the structured multiset equals the historical b261 baseline. Project/pin/unit/integration workflow inputs match actual b8, but no actual b8 runtime baseline was run. Both full compile logs retain 571 duplicate-symbol warnings plus tool/multiple-destination notices; the integration version probe retains its broken-pipe exception. RuntimeWarnings/testFailures arrays and action warning/error nodes are empty. Integration's test-without-building status does not prove warning-free compilation. Xcode 16.4 compile paths, iPhone 16 / iOS 26.2.

E2E workflow 40410065359 completed on workflow attempt 1 using the same verified a9eb51a merge/tree above, Xcode 26.2 (17C52), iPhone 17 / iOS 26.2 and E2E revision ec01c02ff611797c112e65b5c87c936d25720bac. Of 14 selected cases, 13 unique cases passed across 15 executions and three built-in test attempts. Attempt 1: 12 passed, @lightning_1 failed, and hardware-wallet transfer-to-Spending stayed skipped. Attempt 2 reran only Lightning and failed; attempt 3 passed it. Both failures timed out after 30 seconds waiting for ActivityShort-0 to contain 9 000 after closing the restored channel (lightning.e2e.ts:287). No root cause or actual-base runtime reproduction is established; passing the third attempt does not establish a fix. The hardware skip is explicit in the pinned test source because Blocktank staging no longer creates orders.

All five job logs, per-case reporters and complete available diagnostics were inspected. The workflow marks the first two steps successful through continue-on-error; their raw exit-1 failures remain recorded here. Failure screenshots/videos were named in the runner log but were not uploaded after final success; only the app artifact remains, so those visuals and runtime reports could not be inspected. Log-stream restarts (143/137/124/124/143/143), 80 stale-element warnings, the address-type helper retry, dependency notices/audit output and the AppIntents/multiple-destination build notices remain retained. No new source change or manual CI retry was made. Rendered 546/547, funded/provider/hardware/two-wallet and other documented acceptance gaps remain explicit.

Hosted integration completion — 9a807ef3, October 27

Integration workflow40453530944 passed on attempt1: 30 unique tests /30 passing executions, four existing Blocktank skips, zero failures. All34 case summaries and the full execution tree confirm no retry execution. Empty runtimeWarnings/testFailures arrays and no structured action warning/error nodes. Embedded action output still contains RGS gossip failures, node/address-search/teardown diagnostics and the orphaned-RN-keychain fixture: all45 exact diagnostic groups were inspected, and all nine associated source/test files match actualb666. Hierarchy copies duplicate these log lines; this is not a warning-free or actual-base runtime reproduction claim. Skipped refund-address lifecycle/create-CJIT/orders/channel-purchase cases cite the closed staging service; their source and workflow match actual master.

The job checked out merge a27dd5a89eba7dd6c2fd9e753131800383c565cc, with verified b666ccb1 / 9a807ef3 parents and tree 0ce4fc92, exactly matching the locally validated tree. Artifact digest, full job log, test trees and structured diagnostics were inspected. Compile output retains571 duplicate-symbol warnings (286 unique), the same complete multiset as the earlier actual-cc5 baseline. Current project/pins match actualb666, but no new b666 runtime reproduction is claimed. Test-without-building buildStatus=notRequested/no exported build log is not warning-free evidence; the full separate compile output is retained. The version-probe broken-pipe exception and tool/destination notices remain explicit. Runner: Xcode16.4 compile environment, iPhone16/iOS26.2; local26.1 results remain distinct.

Unit and E2E completion is recorded in the following addendum; no manual retry. Funded/provider/hardware/two-wallet/rendered546/547 acceptance and historical retry/skip limits remain. Full report: /private/tmp/usdt-monitor-20261027-0534/ci/completion-report.md.

Hosted unit and E2E completion — 9a807ef3, October 27

Unit 40453530938 and E2E 40453531272 passed on workflow attempt 1. Unit, E2E build and E2E runtime logs identify merge a27dd5a89eba7dd6c2fd9e753131800383c565cc, with verified actual-master b666ccb1 / published 9a807ef3 parents and tree 0ce4fc929ce5dcb22d64b5800dc7451466705bb3, matching final local validation.

  • 2,928 unit executions passed; UI: 39 passed, four fixture skips, zero failures. Thirty launch configurations are grouped under one summary case. The three opt-in Trezor cases and the isolated Arbitrum-fork case were skipped; the fork skip occurred before app launch or funding. Receive passed in 88.878 s and comma-region manual entry in 53.286 s. These passes do not explain historical startup failures.
  • E2E: 13 unique cases passed in 13 executions, one existing hardware-case skip, zero failures. Test step 1 passed; steps 2/3 were skipped. Includes Lightning, LNURL msat pay/withdraw, boost, address-type, send and three Trezor-emulator cases. Hardware transfer-to-Spending remains explicitly skipped because Blocktank staging no longer creates orders, at E2E revision ec01c02f. This does not establish physical-hardware acceptance.

The unit archive digest, complete execution trees, all UI summaries, structured build/action diagnostics, all six unit/E2E job logs and available artifact metadata were inspected. Unit structured runtimeWarnings/testFailures are empty. Its 286 structured duplicate-symbol warnings and 571 build-log warning messages exactly match the recorded actual-cc5 multisets; current build inputs match b666, but no fresh b666 runtime baseline was run. Embedded action output retains negative-case offline/auth/storage/identity/hardware/durable-recovery and fee/UTXO diagnostics, synthetic Blocktank UUID-order failures, and simulator haptic/socket notices. Associated source comparisons and complete groups are retained; this is not a warning-free or blanket pre-existing claim.

E2E retains 68 stale-element notices, log-stream restarts 143/137/124/124, and one address-type tap/toast helper retry, separately from test retries. Tool/AppIntents/Node/npm notices remain; the pinned external E2E harness reports 76 dependency vulnerabilities (8 low, 17 moderate, 44 high, 7 critical), identical to its earlier same-revision log, with no uploaded advisory report. Only the app artifact was uploaded, and its metadata digest matches the build-upload and runner-download logs; no runtime-report or video artifact was available after success. Unit used Xcode 16.4 / iPhone 16 / iOS 26.2; E2E used Xcode 26.2 / iPhone 17 / iOS 26.2.

No manual CI retry or source change. Earlier Lightning failures/recovery and fixture incidents retain their original attribution. Funded/provider/physical-hardware/two-wallet settlement and rendered 546/547 acceptance remain incomplete. Full evidence: /private/tmp/usdt-monitor-20261027-0636/ci/completion-report.md.

Quoted review Back fix — 811b4335, October 27

Returning from fallback Amount to a fixed quoted request’s Confirm root now closes that presentation, so Back no longer immediately pushes Amount again. The first fallback—including initially unreviewed terms—still requires amount review; valid terms reset the marker. The existing presentation-scoped dismissal preserves a replacement sheet. Seven production lines changed; two existing lifecycle tests were extended with a true Confirm-root host, with no new/removed tests. The existing journey now includes the iOS fallback Back and manual-coin return steps.

Unchanged 9a807ef3 failed the Back/dismiss assertions for all four invalid quote variants; the frozen fix passed all 15 focused lifecycle tests and the workflow-selected canonical suite: 2,928 unit + 39 UI executions passed, four fixture skips, zero failures. Three Trezor cases remain opt-in; the isolated fork skipped at unavailable 29546 before app launch/funding. Exact helper/runner forwarding and built endpoints were checked. SwiftFormat 0.64: 0/845 files need changes; translations: zero errors/15 warnings, with all 21 relevant inputs identical to actual b666ccb1. Independent frozen-diff review found no verified issue. All 15 current-base PR commits verified locally and match GitHub’s exact set with verified=true / reason=valid. Normal additive push; no history rewrite.

Complete logs, execution trees and 47 warning/UI/skip summaries were inspected. The 13 structured QoS warnings and 16 raw backtraces are different diagnostic surfaces; raw traces cover Pubky/mainline initialization and LDK status/sync. Two AppIntents compile notices, 11 UIKit appearance notices, simulator haptic diagnostics and 48 CoreGraphics NaN notices during passing USDT Receive/manual-entry cases remain recorded. No NaN backtrace or fresh actual-base runtime reproduction is available; those cases do not enter the changed quoted Bitcoin Confirm path. The changed source’s UTXO-unavailable logs come from an unchanged negative-case method. No blanket warning-free/pre-existing claim. Test-without-building metadata is not compile-clean evidence.

The manual-coin regression also passed with Confirm as the actual navigation root: after Continue to the second confirmation and Back through the picker, the original root recalculated using the selected coin without a fee-rate change. This proves the controlled fee-preparation call, not rendered fee text or a funded/provider journey. New-head hosted CI is pending; full linked-request/provider/physical-hardware/two-wallet settlement and rendered 546/547 acceptance remain incomplete. Previous evidence retains its original head attribution. Planning/implementation/review for this fix: gpt-6-astra, reasoning xhigh, verified from actual execution metadata.

Hosted integration at 811b433

Integration 40486729719 passed on workflow attempt 1: 30 tests passed in 30 executions, four existing Blocktank staging skips, zero failures or retry executions. Checkout 156e44db has actual b666ccb1 / 811b4335 parents and tree e5409e4f, exactly matching final local validation. The archive digest, all 34 case summaries, test tree, action/runtime output and full compile/test logs were inspected.

Structured warning/error arrays are empty, but test-without-building is not proof of warning-free compilation. Retained 571 duplicate-symbol compile warnings (matching the prior hosted integration multiset), the version-probe broken-pipe exception, tool/destination notices and embedded RGS/node/address-search/teardown/fixture diagnostics. Associated inputs match actual base; no fresh base runtime reproduction is claimed. The subsequent unit failure and E2E completion for this parent are recorded in the crash-repair addendum below. Provider/funded/physical-hardware/full linked-request/two-wallet/rendered-fee acceptance limits and Piotr's open finding remain unchanged.

CI crash repair and diagnostic limits — 20f54449

At parent 811b4335, unit workflow 40486729701 built successfully but had 2,927 unit passes and one crash in testAuthorizationSerializesReconciliationUntilTrackingStateIsPersisted; UI had 39 passes/four skips. The crash report shows XCTAssertEqual reading trackedAccountIndexes concurrently with the fake’s insertion. The test and service inputs match actual b666ccb1, but no actual-base runtime reproduction is claimed. The narrow synchronization repair was coordinated with the file’s owning work before editing. Parent E2E 40486730476 passed 13 unique cases in 13 executions, with one existing hardware-case skip and no failures. Both checked out merge 156e44db / tree e5409e4f (b666/811b); these results retain that parent attribution, not the new head.

Final local repair validation used tree c1e3cd27, Xcode 26.1 and the owned iPhone 17 Pro Max / iOS 26.1 simulator with one worker and private products. The complete execution tree confirms 2,928 unit + 39 UI passes; the summary groups 30 launch configurations into one. Three Trezor opt-in cases and the unavailable isolated fork skipped. Built endpoints and runner/helper forwarding were verified; runtime fork availability failed at unavailable 29546 before app launch or funding. No shared fixture was accessed.

Full reports retain 16 structured QoS warnings and 16 raw backtraces (14 Paykit/Pubky initialization, two LDK status), two AppIntents compile notices, UIKit appearance and simulator keyboard/haptic/graphics/network notices, and negative-case storage/auth/recovery/fee diagnostics. Relevant QoS wrappers/pins match actual b666ccb1; no fresh base runtime or warning-free claim. The passing UI run emitted 24 CoreGraphics NaN notices; the earlier identical-production 811b run retains 48. No causal or permanent-fix claim is made for those notices. Hosted parent E2E retains 68 stale-element notices, helper/log-stream retries, pinned-harness dependency warnings and absent runtime-report upload. These checks do not complete J1/J2, production-provider, physical-hardware, funded settlement, full linked-request or rendered 546/547 acceptance.

Native Back prerequisite diagnostic — October 27, 21:24 UTC

A temporary, isolated diagnostic at 20cfeffa now mounted the production SendSheet through the same modal binding as MainNav, with setup-complete state seeded and unavailable fixture endpoints. This excludes real linked-request setup and persistence, authorization, and funded/provider acceptance. The modal, two-controller stack, enabled native recognizer, key window and hit-test were verified. One cancellation-shaped drag and one completion swipe still failed before any interactive transition began; the diagnostic failed all four relevant assertions. Adding the modal wrapper did not close the gap or establish a production defect.

A standard UIKit navigation control on the same iOS 26.5 simulator did reach and cancel a genuine interactive Back transition. Its first run and one bounded recovery both expired before the completion gesture could be delivered because of turn interruption/context compaction; both remain failed, with two assertions each. No completed native pop or J1 acceptance is claimed. Full logs, nested diagnostics, failed assertions and screenshots are retained, including UIKit transition fallback and platform/accessibility notices. The temporary source was restored byte for byte, the checkout is clean, and scoped simulator services are stopped with data preserved. No production edit, source push, CI rerun or additional review request was made.

Native Back control follow-up — October 27, 22:28 UTC

The final bounded recovery of the interrupted UIKit control passed using identical built products: both cancelled and completed native transitions were observed, and the stack returned to its root. Fresh UI and log guards kept both gestures inside one live test window. Earlier interrupted failures remain retained.

The remaining prerequisite also fails in minimal SwiftUI and UIKit modals with hidden navigation bars and no payment/request/wallet objects (four and three assertions respectively). Each received one cancellation drag; the guard withheld completion when no interactive transition began. Separate SendSheet observations found both the edge and iOS 26 content-pop recognizers enabled, with delegates permitting a start. These values were unchanged by temporarily showing the bar. Both shared swipe-extension files match current master 85e9d4c6; this is source comparison, not a base runtime test.

The controls do not yet separate navigation-bar visibility, modal presentation and runtime behavior, so no product cause/fix or completed J1 native Back acceptance is claimed. All logs, nested diagnostics, failure stacks and screenshots are retained. Temporary source is restored exactly and the checkout remains clean at 20cfeffa; no source push, CI rerun or reviewer notification. Existing provider, funded, physical-device and full linked-request limits remain.

Native Back visibility control — October 27, 23:28 UTC

The isolated UIKit modal control passed its one selected test in 8.209 seconds on the same owned iOS 26.5 / Xcode 27.1 setup after changing only the navigation bar from hidden to visible (plus its state assertion/log and test name). Native cancellation and completion callbacks were [true, false], and the stack returned to its root. No SendSheet/payment/request/wallet objects participated. The prior hidden-bar modal failures remain retained.

This isolates visibility-dependent behavior in that control; it does not establish the production cause/fix or complete SendSheet J1 native Back acceptance. Shared swipe code matches actual master 85e9d4c6, by source comparison only. A shared-navigation follow-up requires ownership reconciliation. Full build/test trees, nested diagnostics, raw logs and screenshots were inspected. Structured warning/error arrays are empty; SDK/accessibility notices and two UIKit transition-fallback notices remain. Temporary source is restored exactly, the checkout remains clean at 20cfeffa, and scoped simulator services are stopped with data preserved. No production edit, source push, CI rerun or reviewer request. Existing provider, funded, physical-device and linked-request limits remain.

@ovitrif ovitrif added this to the 3.1.0 milestone Oct 19, 2026
@ovitrif

ovitrif commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

Triage: milestone 3.1.0, the iOS twin of synonymdev/bitkit-android#1581. Fixed Paykit requests go straight to confirmation instead of a mostly disabled amount screen. It isn't a release blocker, and it closes no issue, so this PR carries the milestone itself.

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes reach incoming Paykit request preparation and payment confirmation.

The PR appears safe to merge, with a non-blocking issue where direct on-chain confirmation ignores the saved coin-selection algorithm.

Findings

  1. P2 Saved coin choice is ignored ▶

Summary

Fixed quoted Bitcoin requests now bind their exact payment terms before opening confirmation, skipping the locked amount screen when no enabled USDT choice is offered.

  • Fixed Bitcoin requests open straight to confirmation when no asset choice is needed.
  • Paykit request preparation records separate resolution and setup times.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Resolve incoming request] --> B{USDT enabled and offered?}
    B -->|Yes| C[Choose asset and review amount]
    B -->|No| D[Bind exact Bitcoin terms]
    C --> E[Confirmation]
    D --> E
    E --> F[Wait for wallet readiness]
    F --> G[Review fee and approve]
    G --> H[Recheck request and send]
Loading

Reviews (1) · Last reviewed commit: "fix: confirm fixed Paykit requests direc..." · Reviewed by Greptile

Comment thread Bitkit/ViewModels/AppViewModel.swift Resolved

@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.

1 finding (1 P2). Opens a fixed-quoted Bitcoin Paykit request straight on Confirm by preparing the payment terms during scan handling. Reviewed 72d89f3, full tier, reasoned from the code and CI (iOS does not build here).

What I checked, and 3 candidates I ruled out

Read in full: handleScannedData (asset-selection branch), PaymentNavigationHelper.openPaymentSheet / appropriateSendRoute / contactPaymentRoute, PaykitRequestPricing.payment(requested:endpoint:period:at:quoteId:), the incoming-request preparation block in AppScene.swift, SendSheet.shouldShowSyncOverlay and the post-setup validation
Call sites traced: every handleScannedData(_, claimedContactPaymentContext:) caller (AppScene, SubscriptionsView, SendSheet retry, SendContactSelectView, AddContactView, PaymentNavigationHelper)
CI: validate, detect-changes and Greptile green. Run Tests, Integration Tests and build-local were still pending when I reviewed.

Ruled out

  • Sync overlay bypassed on Confirm: shouldShowSyncOverlay now exempts asset selection only while currentRoot == .amount, so Confirm waits for the node. The renamed testBelowMinimumQuotedRequestWaitsForSyncBeforeFallback asserts that.
  • Stale terms after a wallet switch: the send path rechecks terms == app.paykitPaymentTerms and isValid before paying (SendConfirmationView.swift:1688), and resetSendState clears both fields (asserted in the new test).
  • USDT-capable requests losing the asset picker: the block is gated on !canSelectUsdt, and testDollarRequestOffersAssetSelectionOnlyWhenUsdtIsEnabled covers both settings.

Merge confidence: 4/5, one P2 on a narrow path, and the test suites had not finished at review time.

Comment thread Bitkit/ViewModels/AppViewModel.swift Resolved
ovitrif

This comment was marked as resolved.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Requesting changes: one MEDIUM (Back from the manual coin picker), inline with a failing test.

Checked and clean:

  • contactPaymentRoute maps .quickpay and .amount to Confirm for incoming requests, so QuickPay can't auto-pay a request whose terms are now bound.
  • Scanner and Enter Manually call appropriateSendRoute without a claimed context, so the terms branch never runs there.
  • Converted initial subscriptions stay manual (conversion == nil).
  • returnToAmountReviewIfNeeded re-validates terms on a wallet switch.
  • The rate-unavailable case is fixed by 963d810.

The android twin synonymdev/bitkit-android#1581 prepares coin selection at swipe, so this shape is absent there.

fixed-quoted-confirmation.xml is byte-identical to Android.

Device gate: not run (MEDIUM open).

Copy link
Copy Markdown
Contributor Author

@ovitrif Fixed the Spending/Savings switch in 6c79bae. Already-reviewed Bitcoin requests stay on Confirm with the selected rail’s exact quoted amount, refreshed fees and a required swipe. Missing/expired terms and amounts below the Lightning minimum return to Amount; BTC/USDT selection and hardware safeguards remain. The hosted test switches between 2,500 and 5,000 sats and verifies fee preparation, with separate fallback cases. The journey now matches Android and also covers Back from manual coin selection. All 47 focused tests pass; SwiftFormat and translation validation passed with no introduced diagnostics.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The MEDIUM is fixed at 6c79bae6. Back from the manual coin picker now stays on Confirm, and manual selection still runs before paying. I resolved the thread.

Wallet-switch rebind in the same commit, clean:

  • It needs USDT off, no hardware wallet, not submitting, the previous BTC terms equal to paykitReviewedAmount, valid new terms and meetsMinimum.
  • It sets paykitPaymentTerms, paykitReviewedAmount and sendAmountSats together.
  • acceptsPaymentAmount and acceptsLightningInvoiceAmount still re-check at pay time.
  • Unavailable or expired terms, or an amount below the minimum, return to Amount.

fixed-quoted-confirmation.xml is byte-identical to synonymdev/bitkit-android#1581.

Device gate (partial): not run yet. Pending: payment-requests/fixed-quoted-confirmation.xml, cross-device with synonymdev/bitkit-android#1581, plus Back from the manual coin picker on device. Code scan: no HIGH/MEDIUM open. The approval follows the device run.

@ovitrif

ovitrif commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

@ben-kaufman thanks, rechecked at 6c79bae. On a Spending ↔ Savings switch, returnToAmountReviewIfNeeded(rebindMethod:) now binds the new rail's valid BTC terms in place and recalculates fees. It falls back to Amount only for missing or expired terms or an amount below the LNURL minimum. The journey is identical to synonymdev/bitkit-android#1581. Nothing left from my review; I checked this from the code and did not run it on a simulator.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Fixed the Xcode 16.4 type-checking failure in b58c1964: the confirmation layout now has its own computed property, leaving the modifier order, task lifecycle and payment checks unchanged.

The custom-fee lifecycle test also now allows SwiftUI to cancel and restart preparation during navigation. It still requires exactly one successful preparation, rejects cancelled results, and checks the final manual-UTXO or confirmation route.

Validation: local simulator build and all 50 confirmation, swipe, lifecycle and pricing tests passed; SwiftFormat passed on all eight PR Swift files. Translation validation has no errors and no added warnings compared with master. Every PR commit verifies locally and on GitHub. Local validation used Xcode 27; the Xcode 16.4 CI rerun is pending. The cross-device journey remains a separate reviewer device gate.

@ben-kaufman
ben-kaufman requested a review from jvsena42 October 19, 2026 19:10
@ben-kaufman

Copy link
Copy Markdown
Contributor Author

The hosted Xcode 16.4 compiler still timed out on the remaining confirmation modifier chain. In 05beeb5 I split that expression into smaller private computed views. Inlining them reproduces the original expression byte-for-byte, preserving task IDs, modifier order, state and payment safeguards.

All 50 focused confirmation, lifecycle and pricing tests pass locally. SwiftFormat is clean for all eight changed Swift files; full translation/compiler diagnostics contain no findings introduced by this fix. Independent review is clean, and all six PR commits are verified. Reviews are re-requested; the new hosted CI run still needs to confirm the Xcode 16.4 build.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The delta since my last review, b58c196, is clean. It only moves code: confirmationContent → confirmationLayout with the same modifier chain in the same order, so payment and authorization behaviour is unchanged. The testCustomFeeWalletSwitchSurvivesFeeScreenNavigation relaxation (successfulCoinPreparations == 1) is safe, because cancelled preparations throw before they assign anything.

CI is red because of this change, so I'm holding the approval. Unit tests on Xcode 16.4 (run 39308712256) still stop at the compiler is unable to type-check this expression in reasonable time, now at confirmationLayout (SendConfirmationView.swift:393), so no test ran. The ~15-modifier chain is still one expression. Splitting the onChange/.task group into its own computed property or a ViewModifier should get it under the limit.

Device gate (partial): not run yet. Pending: payment-requests/fixed-quoted-confirmation.xml cross-device with synonymdev/bitkit-android#1581, and Back from the manual coin picker.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

@jvsena42 The latest commit 05beeb57 now passes the hosted Xcode 16.4 unit-test run and integration-test run. The failure in run 39308712256 was on the earlier revision. Re-requesting your review for the remaining device gate; the cross-device journey and manual coin-picker Back check still apply.

@ben-kaufman
ben-kaufman requested a review from jvsena42 October 19, 2026 20:59

Copy link
Copy Markdown
Contributor Author

Integrated actual master 04a0a1f8 in signed normal merge fc9164884bb5e86bb6ee2d3c350d43b007988804; tree 0a462801 preserves this PR's existing 22-file patch. This brings the pre-send Lightning hash journal and its restore regression into the confirmation path. Two independent scoped reviews found no introduced payment/recovery defect. All 28 commits in the current-base PR range verify locally and on GitHub.

Final local validation on Xcode 26.1/iOS 26.5 passed 2,880 unit and 39 UI executions, with four explicit fixture skips and no failures or retries. Total: 2,923 executions, including 30 launch configurations; 2,894 unique cases include the four skips. The new pre-send backup/restore regression, all 16 backup lifecycle cases and camera case passed. The protected native USDT recovery class was excluded before fixture contact and remains unvalidated. SwiftFormat reports 0/824 violations, 100 skips; translations report zero errors and 15 warnings whose inputs match actual master.

Complete structured, nested and runtime diagnostics were inspected. Retained: 21 structured QoS issues and one State issue, 22 raw QoS stacks, two raw State notices, 24 NaN notices, AppIntents metadata notices and symbolication limits. No introduced blocking finding was established; lower warning counts or passing tests do not establish a permanent fix for prior camera, launch, backup or runtime failures.

The prior 06d3bd2b E2E run also completed: 13 passes, one existing hardware-transfer skip, zero failed executions or retries on attempt 1. Verified checkout 9f9a0f5d/tree 0bea8ee0, base b23f305a, and corrected harness 611509ee. All published logs and artifact provenance were inspected; 74 stale-element notices, four log-collector restarts and 76 npm findings remain. The success run uploaded no native runtime log or video. This result is attributed to that earlier tree; new-head hosted checks are pending.

The existing native interactive Back, full J1/J2, fresh-install/channel/USDT recovery, provider and physical signing/biometric gaps remain explicit. Earlier human and automated QA keeps its reported revision and environment. Ovi's request and João's announced deferral through November 1 are preserved; Piotr's code-only review is not an approval. No merge or human-approval gate is claimed satisfied.

@piotr-iohk

piotr-iohk commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Focused device evidence for fc9164884bb5e86bb6ee2d3c350d43b007988804 (iPhone 17 simulator, iOS 26.0; verified current-head CI build, local regtest).

A fixed 10,000-sat, Bitcoin/Savings-only payment request, Savings 0 sats, and a paired Trezor emulator with 1,000,000 sats reproduce both funding-selection concerns. Paykit enabled, USDT disabled. The issuer uses the existing rc65-shared fixture; this is a fixed BTC request, not a USD-conversion quote or physical-device signing test.

  • Autopilot / Branch and Bound: request initially opens Confirm, then shows “Please try again — Insufficient funds” and falls back to Amount. Trezor becomes selected with 999,890 sats available. Continue returns to confirmation; no signing/payment was attempted. Reproduced on reopening.
  • Manual: the same request opens an empty Savings coin picker, with total required 10,000 and total selected 0, despite the funded Trezor. Header Back returns to confirmation. No payment was attempted.

Expected: funding selection should settle before Savings coin preparation; a payable hardware request should remain on usable Confirm without the Savings failure toast, Amount detour or empty Savings picker. These observations need fixing or explaining before sign-off. Baseline runtime was not tested, so the evidence alone does not establish the first affected revision.

Automatic selection Manual selection
Insufficient-funds toast while the funded Trezor is selected Empty Savings coin picker despite a funded Trezor

Automatic selection recording:

hardware-auto-evidence.mp4

Manual selection recording:

hardware-manual.mp4

Relevant runtime log:

Failed to set UTXO selection when switching to on-chain: AppError(message: "Insufficient funds", ... NodeError.InsufficientFunds ...)
SendConfirmationView.swift: onSwitchToOnchainWallet(context:) line 1908
SendSheet failed to estimate Savings availability: AppError(message: "Insufficient funds", ...)
SendSheet.swift: selectHardwareFundingSourceIfNeeded(amountSats:requestId:) line 933

The final QA review will consolidate these results with the passing controls and remaining gaps.

Logs from session: bitkit_logs_2026-10-30_12-23-07.zip

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Focused device result: two failed hardware-funding checks; approval is not established. The insufficient-Savings/no-hardware control and controlled background/foreground backup recovery passed. Native interactive Back remains unverified.

Tested fc916488, base 04a0a1f8, on the existing iPhone 17 / iOS 26.0 simulator with its wallet preserved. Installed the current CI app, artifact 12969464148, Bitkit 3.0.1 (202). Verified synthetic merge c1cbd167 and PR head have the same tree, 0a462801. Local Docker regtest; no app source modifications or payment authorization.

Actionable runtime finding: Savings coin preparation races hardware funding selection

Reproduction, screenshots, recordings and log excerpt.

Setup: Paykit enabled, USDT disabled, a fixed 10,000-sat BTC/Savings-only request, 0 sats Savings, and a paired Trezor emulator funded with 1,000,000 sats.

  • Autopilot / Branch and Bound — failed: Confirm briefly opens, then “Please try again — Insufficient funds” appears and navigation falls back to Amount. The Trezor subsequently becomes selected, showing 999,890 sats available. Continue reaches confirmation. Reopening reproduced the Amount detour.
  • Manual — failed: opening the same request pushes an empty Savings Coin Selection screen: required 10,000, selected 0, Continue disabled. Header Back reaches confirmation. A funded hardware source should not require this Savings picker.

The automatic log records onSwitchToOnchainWallet failing Savings UTXO selection at SendConfirmationView.swift:1908, alongside SendSheet.selectHardwareFundingSourceIfNeeded at line 933. This is consistent with confirmation starting Savings preparation before asynchronous hardware selection settles. That ordering is the suspected mechanism; no base-build runtime comparison was performed. Fix or explain these observed failures and retest both modes before sign-off.

Fixture boundary: these are real linked fixed-BTC requests from the existing rc65-shared issuer fixture. They are not USD-conversion quotes. The exact USD-quoted variants and physical hardware signing remain untested; the fixed-BTC failures are independently reproducible controls, not claimed completion of J1.

Other focused results

  • Insufficient Savings, no hardware — passed for fixed BTC: after removing the disposable Trezor association, opening the same request twice produced one “Insufficient Savings” message per open and closed the sheet. The request stayed available. No extra Amount detour or generic failure toast was observed in the recording.
  • Header Back / reopen / no payment — passed controls: header Back closed the automatic coin-failure fallback without looping; manual-picker Back returned to confirmation. Balances stayed Savings 0 and Spending 500,498 sats. The issuer still recorded the test request as Proposed, with no payment proofs. No signing or payment swipe was performed.
  • Backup during quick background/foreground — passed controlled recovery: used this build's existing E2E localhost VSS/auth endpoint overrides to delay backup transport. After observing a SETTINGS write in flight, changed Manual back to Autopilot, pressed Home, and tapped Bitkit's Home-screen icon to resume the same process. In the final eight-second-delay run, the SETTINGS request was held at 12:06:36.499 UTC; the second setting change began at 12:06:36.607, Home at 12:06:37.896, and resume completed at 12:06:42.797, before the held response completed at 12:06:44.662. Logs show the active SETTINGS backup succeeding, then another SETTINGS write succeeding for the later change. Data Backups showed Settings recently synced, with no stale failure banner. The transient “Required” label was not captured; pending work is evidenced by the subsequent write. An earlier launch-command attempt cold-restarted the app and is excluded from this foreground-resume result; a longer-delay run also exercised transport retries. These controls do not prove restore, wallet-switch or production-network acceptance.
  • Native interactive Back — blocked/unverified: short and longer edge-swipe attempts from the genuine automatic coin-failure Amount fallback did not establish a native transition. Touch-down hit target/interactive callbacks were not established. Neither cancellation nor completion is a pass, and no native-gesture product defect is inferred.

Retained code assessment and CI reconciliation

The saved full code assessment at fc916488 against 04a0a1f8 is retained: quoted-term binding, confirmation routing/lifecycle, coin and fee preparation, payment-time validation, backup observer generations/callers and exact amount formatting were reviewed. This device update does not claim a new full source review or erase earlier unverified concerns. The runtime finding above supersedes the earlier summary's implication that the proposed hardware cases had no demonstrated failure.

  • Current-head unit/UI and integration passed.
  • E2E #288, harness 611509ee, fixes the Connections-to-HeaderMenu navigation mismatch. It does not explain the current E2E run's two Lightning failures waiting 30 seconds for ~Channel to disappear after channel close and mining six blocks. That assertion is at lightning.e2e.ts:281, before #288's NavigationBack step. Lightning passed the third built-in test attempt on the same app/harness tree. This is an intermittent failure, not a proven permanent fix.
  • Failure screenshots/recordings were generated on the runner but not uploaded after the final pass; only the app artifact is available. App/backend/harness root cause remains unproven. Prior device evidence at 06d3bd2b keeps its original revision and limits.
  • The LOW journey-wording difference is already deferred to Android #1610, with the original discussion resolved. It is not an additional payment defect here.

Remaining gaps, without duplicate historical entries

  1. Fix/explain and retest both hardware-funding failures above; repeat their exact USD-quoted variants with a compatible issuer.
  2. Demonstrate cancelled and completed native interactive Back transitions after coin-preparation failure.
  3. Complete the outstanding J1/J2, provider USDT settlement, physical signing/biometrics and protected fresh-install/channel/USDT recovery coverage. The Trezor emulator and backup transport controls close only their stated cases.
  4. Reproduce the displayed-zero USDT toggle report with active eligibility/guard state captured. Unchanged guards and a successful fresh-fixture toggle do not diagnose that report.
  5. On another channel-removal failure, retain failure artifacts and channel/node state. Historical camera, launch and RBF-toast failures keep their original evidence; current passes do not establish their causes or permanent fixes.

The saved full-code-review model metadata below identifies that retained assessment. Focused device execution and this evidence reconciliation were performed in Codex. This is a COMMENT review, not approval or complete device acceptance.

Reviewer model: claude-opus-5-5
Reasoning effort: high

Copy link
Copy Markdown
Collaborator

Master comparison: both hardware-funding navigation failures are regressions in #1014; the generic “Please try again” toast is pre-existing.

This updates the baseline-attribution limit in the focused review and original reproduction/evidence.

Tested master ca98b813, then reinstalled #1014 fc916488 and repeated both cases. Same existing iPhone 17 / iOS 26.0 simulator, wallet data, funded Trezor emulator and fixed 10,000-sat BTC/Savings-only request. Savings: 0 sats; Trezor: 1,000,000 sats.

Check Master #1014 Result
Autopilot / Branch and Bound Stays on Confirm with Trezor selected and a 141-sat fee; also works when reopened. Falls back to Amount; Trezor then becomes selected with 999,890 sats available. Regression: unwanted Amount detour.
Manual coins Stays on Trezor Confirm with a 141-sat fee; no Savings picker. Opens an empty Savings picker: required 10,000, selected 0, Continue disabled. Back returns to Confirm. Regression: wrong Savings picker.
Generic “Please try again” toast Brief toast also occurs before hardware funding settles. Earlier automatic reproduction showed “Please try again — Insufficient funds.” Pre-existing toast; not independently a new #1014 defect.

The logs distinguish the paths:

  • Master: initial calculateTransactionFee() fails with OnchainTxCreationFailed (SendConfirmationView.swift:1964), but the flow stays on Confirm and settles on hardware funding.
  • fix: confirm fixed Paykit requests directly #1014: onSwitchToOnchainWallet() reports Failed to set UTXO selection when switching to on-chain: Insufficient funds (SendConfirmationView.swift:1908) and routes to Amount.

The likely common cause is Savings preparation starting before hardware fallback finishes: the new prepareRequestCoinsIfNeeded() checks !hwSend.isActive without waiting for setup to finish. That is a source-supported explanation, not an instrumented proof or a validated fix.

Keep the two navigation failures on this PR, with both modes required for retesting. Treat the generic initial fee-estimation toast separately as an existing issue. The other testing gaps in the focused review remain open.

Limits: master was locally built from clean pinned source; #1014 used its original CI artifact, so these are not identical-toolchain builds. Dependency pins and SendSheet.swift match. The request was fixed BTC, not a USD-conversion quote; physical signing and payment execution were not tested. No payment was signed or sent. Both tested heads were unchanged when rechecked; this comparison does not establish approval.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request changes — the reported hardware-funding failures remain unresolved; native interactive Back is still unverified.

Findings: No new findings in this delta. The current-head hardware-funding report remains actionable: automatic selection detours to Amount after Savings preparation fails, and manual selection opens an empty Savings picker despite a funded Trezor emulator. The subsequent master comparison reproduces both navigation regressions and separates the pre-existing generic toast. I checked the competing preparation paths in the source; I did not repeat those device cases or duplicate the finding inline.

QA for fc91648:

Built the pinned head with Xcode 27.0 and tested on an iPhone 17 Pro simulator running iOS 26.5. The linked-request check used Bitcoin regtest and a staging issuer. No payment was authorized. Videos play at 2×.

Restore a manual Lightning payment from its pre-send journal ✅ Hosted assertions reconcile the restored attempt as successful.

The exact-head XCTest run captures the journal before sending, restores it through the production backup conversion, supplies successful node history and verifies the recorded outcome. Related assertions preserve attempts while node history is unavailable and reject duplicate admission. The recording shows the test host; the result bundle establishes these states. Node history and the SDK are controlled test doubles, so this does not prove fresh-install or channel recovery.

recovery.2x.mp4

Hosted journal and recovery regression.

Interactive Back from fallback Amount ⏭️ Neither a cancelled nor a completed native transition was established.

Real coin selection failed for a fixed 699,819-sat quote against 700,000 sats, returning to Amount. Short, full-width and preset edge gestures left Amount visible; the mid-drag capture and accessibility snapshots did not establish a native transition. Header Back closed the sheet once, and the unpaid request reopened with the same quote, checked in accessibility snapshots. Logs establish the coin-selection failure. The issuer still reported Proposed with zero proofs. These attempts establish neither a native Back pass nor a new product defect.

interactive-back.2x.mp4

Edge-gesture attempts on the established fallback and the header Back control; reopening was checked separately in accessibility snapshots.

Retained evidence for behavior unchanged by this delta:

Reviewed the contiguous 06d3bd2..fc91648 delta, all four changed files and reached callers. The confirmation-to-journal hash forwarding was checked in code; the executor/restore behavior ran above. Physical signing/biometrics, production-provider USDT settlement and fresh-install/channel/USDT recovery were not driven. Full J1/J2 completion is not claimed.

Pair PR synonymdev/bitkit-android#1581 matches direct fixed-Bitcoin confirmation. synonymdev/bitkit-android#1638 also keeps USDT selection on confirmation; iOS retains Amount for USDT and mixed LNURL/Savings selection.


Reviewed by gpt-6.1-sol-high

Keep fixed and quoted request confirmation idle until initial or fallback
funding selection settles. Preserve saved Savings selection and hardware
preview behavior without premature Amount or empty UTXO navigation.

Preserve upstream backup subscription and metadata ordering safeguards,
including metadata upload drain for superseded observation stops.

Copy link
Copy Markdown
Contributor Author

Fixed the two hardware-funding navigation regressions from review 5712051079 in 659a7c07.

Confirmation now waits for initial/fallback funding selection before preparing Savings coins. When hardware is selected, it stays on Confirm and calculates the hardware preview; when Savings remains selected, its saved automatic/manual selection resumes. The existing amount, expiry, authorization and cancellation checks remain. The generic fee-unavailable toast reported on master is separate and unchanged.

A new controlled confirmation test reproduced the premature preparation before the payment fix on the staged integration (one failing method, four automatic/manual × fixed/quoted cases). The repaired candidate passed all 60 focused tests, including that regression and extended Savings/backup cases. Final canonical validation passed 2,945 unit and 39 UI executions, with five documented skips, no failures and no whole-test retries/restarts; XCTest retried one tap inside a passing USDT receive case. Complete logs and structured/nested diagnostics inspected; 28 structured QoS and one existing State notice, 32 raw QoS stacks, 72 NaN notices, three AppIntents metadata warnings and symbolication/runtime/simulator limits remain disclosed. SwiftFormat 0.64 reports 0/827 violations and 102 skips; translations report zero errors and 15 warnings with localization, validator and warning callsites identical to actual master 3f14ea05. Two scoped independent source reviews are complete.

This signed normal merge also integrates master 3f14ea05 and preserves its metadata/subscription safeguards; overlapping observation stops now always drain an admitted metadata upload. Tree 8bec58b7 is the tested tree. All 29 commits in the actual-base PR range verify locally and on GitHub. New-head hosted checks are pending and have not been retried.

The new hardware test uses controlled wallet/signer doubles and performs no PIN authorization, signing or broadcast. It does not replace the full SendSheet hardware, native interactive Back, provider/funded or full J1/J2 journeys. Those acceptance gaps and historical CI failures/recoveries remain disclosed. João’s deferral through November 1 and Ovi’s pending request remain intact.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⛔️ Request changes — pending funding setup still allows payment authorization before coin and fee preparation.

Findings:

  • MEDIUM — Block authorization until request setup finishes. Bitkit/Views/Wallets/Send/SendConfirmationView.swift:420: the new wait suppresses preparation while the explicit swipe remains usable. A controlled real-swipe test reached the payment-PIN callback with setup pending and no selected coins. The explicit submission path can proceed with an unreviewed fee and default coin selection. No payment was sent.

The previous hardware-navigation failures are fixed in the live checks below, including the added USD-quoted variants.

QA for 659a7c0:

Built 659a7c0 with Xcode 27.0 and tested an iPhone 17 Pro simulator on iOS 26.5. Live requests used linked disposable identities and Bitcoin regtest. Savings started empty; the paired Trezor emulator held 599,441 sats. No signing or payment was authorized. Videos play at 2×.

Payment stays disabled while funding setup is pending ❌ A real swipe reached the payment-PIN callback before setup finished.

An unpublished XCTest hosts the production confirmation with pending setup, valid quoted terms, a loaded fee rate and automatic Largest First selection. The recording shows the swipe and missing displayed fee; assertions establish the callback and state. The helper returns false at PIN, so it cannot send a payment.

setup=true pinRequests=1 preparations=0 selectedCoins=nil
XCTAssertTrue failed: Pending funding setup reached payment authorization before coin/fee preparation

This uses a controlled wallet and does not claim a completed send.

pending-authorization.2x.mp4

Unpublished diagnostic patch
diff --git a/BitkitTests/SendConfirmationLifecycleTests.swift b/BitkitTests/SendConfirmationLifecycleTests.swift
index d444e63e..599bb59b 100644
--- a/BitkitTests/SendConfirmationLifecycleTests.swift
+++ b/BitkitTests/SendConfirmationLifecycleTests.swift
@@ -867,6 +867,47 @@ final class SendConfirmationLifecycleTests: XCTestCase {
         XCTAssertTrue(isSubmittingPayment, "The original payment owns the retained submission flag")
     }
 
+    func testPendingRequestSetupMustNotAuthorizeDiagnostic() async throws {
+        snapshotAppDefaultsDomain()
+        snapshotAppGroupDefaults("home_screen_display_currency_code_v1", "home_screen_display_currency_symbol_v1")
+        let request = try onchainRequest()
+        let wallet = ControlledSendWallet(operation: .selection)
+        wallet.totalOnchainSats = 700_000
+        let app = AppViewModel()
+        let sheets = SheetViewModel()
+        sheets.activeSheetConfiguration = SheetConfiguration(id: .send, data: nil)
+        app.scannedOnchainInvoice = invoice
+        app.selectedWalletToPayFrom = .onchain
+        app.contactPaymentContext = ContactPaymentContext(publicKey: request.counterparty, incomingPaymentRequest: request)
+        app.paykitPaymentTerms = try request.payment(using: app.paykitPaymentMethod)
+        app.paykitReviewedAmount = app.paykitPaymentTerms?.amount
+        wallet.sendAmountSats = app.paykitPaymentTerms?.amount.atomic
+        let settings = SettingsViewModel.shared
+        let previousPin = settings.pinEnabled
+        addTeardownBlock { @MainActor in settings.pinEnabled = previousPin }
+        settings.pinEnabled = true
+        settings.requirePinForPayments = true
+        settings.useBiometrics = false
+        settings.coinSelectionMethod = .autopilot
+        settings.coinSelectionAlgorithm = .largestFirst
+        let setup = SendConfirmationTestSetup()
+        let navigation = SendConfirmationTestNavigation()
+        navigation.path = []
+        let window = host(app: app, wallet: wallet, sheets: sheets,
+                          path: Binding(get: { navigation.path }, set: { navigation.path = $0 }),
+                          navigation: navigation, setup: setup, confirmationIsRoot: true)
+        defer { close(window); wallet.resume(); ToastWindowManager.shared.hideToast() }
+        try await navigation.waitForAppearance(.confirm)
+        wallet.selectedFeeRateSatsPerVByte = 2
+        print("V4_PENDING_READY setup=\(setup.isPending) preparations=\(wallet.preparationCount)")
+        let deadline = ContinuousClock.now + .seconds(100)
+        while setup.pinChecks.isEmpty, ContinuousClock.now < deadline {
+            try await Task.sleep(for: .milliseconds(100))
+        }
+        print("V4_PENDING_RESULT setup=\(setup.isPending) pinRequests=\(setup.pinChecks.count) preparations=\(wallet.preparationCount) selectedCoins=\(wallet.selectedUtxos == nil ? "nil" : "set")")
+        XCTAssertTrue(setup.pinChecks.isEmpty, "Pending funding setup reached payment authorization before coin/fee preparation")
+    }
+
     private func host(
         app: AppViewModel,
         wallet: WalletViewModel,

The existing host denies its PIN callback. Production payment guards are unchanged.

Request funding waits for setup ✅ Hosted assertions passed for fixed BTC/USD requests in both coin modes.

The existing hardware-readiness regression confirms no Savings preparation before setup settles, then hardware fee preview without a Savings picker. Wallet and signer doubles are used; the result bundle establishes the assertions.

setup-readiness.2x.mp4

Fixed Bitcoin request selects hardware automatically ✅ The 10,000-sat request opened and reopened directly on Confirm.

Accessibility snapshots confirmed Trezor funding and the 141-sat fee. No Amount detour occurred; the request remained unpaid. The clip is the focused reopen.

hw-btc-auto.2x.mp4

Manual fixed Bitcoin request selects hardware ✅ Manual mode kept hardware Confirm without an empty Savings picker.

The coin-mode setting and accessibility snapshots confirmed Manual, 10,000 sats, Trezor funding and the 141-sat fee. Closing and reopening preserved the unpaid request; the clip shows the focused reopen.

hw-btc-manual.2x.mp4

USD quote selects hardware automatically ✅ The fixed 2.50-USD quote reopened directly at 10,000 sats.

Issuer terms fixed the BTC amount. Accessibility snapshots confirmed the selected Trezor and its 141-sat fee; no Amount detour occurred.

hw-usd-auto.2x.mp4

Manual USD quote selects hardware ✅ The same quote reached hardware Confirm without a Savings picker.

Manual mode was checked in Settings; accessibility snapshots confirmed 10,000 sats and the 141-sat hardware fee. The request stayed unpaid.

hw-usd-manual.2x.mp4

Empty Savings without hardware fails safely ✅ Both openings showed Insufficient Savings and closed.

After removing the disposable hardware association, the same plain-BTC request stayed available. No Amount detour or payment was observed.

no-hardware.2x.mp4

Savings preparation respects automatic and manual coins ✅ Saved selection and manual Back preserved the reviewed fee.

With 500,000- and 200,000-sat outputs, logs confirm Largest First selected the larger output; accessibility showed a 143-sat fee. Manual-picker Back retained Confirm and another swipe required coins. Selecting both outputs produced 212 sats on the second Confirm and after returning through the picker to the original root, checked in accessibility snapshots. No payment was submitted.

Automatic selection Manual selection
savings-auto.2x.mp4
savings-manual.2x.mp4
Merged backup and address safety regressions ✅ Current-composition backup assertions passed.

Foreground recovery, backup service and Android backup normalization ran at this head. These controlled tests cover the merged observer/metadata-drain composition; the recording shows the host and XCTest assertions establish the result. They do not prove fresh-install recovery.

backup-integration.2x.mp4

Interactive Back from preparation-failure Amount ⏭️ Neither native cancellation nor completion was established.

Real coin selection failed for a 699,819-sat quote against 700,000 sats. Preset and short/full coordinate edge drags left Amount visible in accessibility snapshots and did not establish a native transition. Header Back closed the sheet once, and the unpaid quote reopened. These attempts establish neither a native Back pass nor a new product defect.

native-back.2x.mp4

Physical authorization and complete recovery acceptance ⏭️ Physical signing, provider settlement and full recovery remain unverified.

This run used disposable simulator/regtest state and stopped before signing. Physical biometric/hardware acceptance, mainnet-provider USDT settlement and fresh-install/channel/USDT recovery were not driven. Earlier supported in-place restore reports retain their stated limits.

Earlier passing evidence retained for implementations this delta leaves unchanged:

Reviewed the contiguous fc91648..659a7c0 delta, all 62 changed files and reached callers, including the imported master changes and prior open hardware report. CI artifact-retention changes and journey wording were checked in code. Full J1/J2 completion is not claimed.

Twin comparison:


Reviewed by gpt-6.1-sol-high

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed to 659a7c0. No new findings.

  • 0801b4f8: a below-minimum initial quote stays unbound on an Amount root, and a coin-prep failure goes through returnToAmountReview(). Every Back and cancel path from that Amount screen ends either by hiding the sheet or on a Confirm whose terms equal wallet.sendAmountSats: header Back, edge swipe, Continue → Confirm → Back → Back, X, the Amount-root pop, and the funding tap.
  • 3d48ab10 and 8aabaf77: the backup observation lifecycle traces clean.
  • Merge 659a7c07: adds isRequestSetupPending, and takes master's BackupObservationSubscriptions in the BackupService conflict.

On ovi-reviewer's open MEDIUM at SendConfirmationView.swift:420 (the swipe isn't blocked while setup is pending): the fix should gate submitPayment itself. Once .pin is pushed, the setup-complete .task can no longer prepare coins, because confirmationContext needs path.last to be nil or .confirm.

Device gate (partial): 659a7c0, not run yet. fixed-quoted-confirmation.xml now has a new action 4, which renumbers the old 2–6 to 2, 3, 5, 6, 7.

  • My 629a874 results for 5, 6 and 7 stand.
  • 2 and 3 have to be re-run, because 659a7c07 re-times preparation.
  • Still pending: 4 (Trezor), 8–11, 13 (LNURL minimum), 14 (fee-shortfall Back), and all of paykit-quoted-usdt.xml.
  • 12 needs a conversion-capable issuer, so it is left to e2e CI.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Full reassessment of the entire PR at 659a7c07 against merge base 3f14ea05, because the comparison base changed. No earlier coverage was inherited.

Code review completed with unverified earlier concerns.

Validation: Inspected hosted unit-test logs and integration logs for merge 03ec70c5, whose parents match these pins and whose tree equals the reviewed head. The affected regression tests passed; they do not exercise swiping during pending request setup. Local diff checks and parsing of all changed journey XML passed. Android and E2E comparisons were limited to the affected contracts at 6d99ad38 and 611509ee.

Device testing: not performed in this review. Local Swift/Xcode execution was unavailable; authored J1/J2 runtime acceptance and visual equivalence were not verified. Prior-findings reconciliation is deferred to the separate stage.

Suggested additional test cases

  • iOS: with a running, funded Savings wallet, a fixed USD-to-BTC request, a non-default automatic coin algorithm and payment PIN disabled, hold the setup availability check after loading the fee rate. Attempt the confirmation swipe: no authorization or send should occur. Release setup and verify the saved coins and fee are prepared before the swipe enables; use a fee above 50% of the payment to verify its warning before sending.

Earlier findings reconciled at this revision: 1 still present, 14 verified fixed, 11 could not be verified.

Reconciliation incomplete: the following earlier concerns remain unverified. This report does not establish that they are fixed or invalid, or establish approval or device readiness.

Could not verify earlier concern — USDT toggle reportedly cannot be disabled at displayed zero balance: The original displayed-zero report still lacks its active eligibility state. Unchanged guards and the reported fresh-fixture success do not diagnose that state or disprove the symptom. Code-only execution cannot settle it; no introduction or fix is claimed.

Could not verify earlier concern — Native interactive fallback Back remains unverified: Committed navigation writes are handled, but current-head reports establish neither cancelled nor completed native interactive transitions. Failed gesture initiation proves neither a defect nor a pass. Device execution is prohibited here.

Could not verify earlier concern — Camera-permission CI failure has no established cause: The matching test passes on the pinned tree, but the historical failures retain an unproven cause. Current success does not establish a permanent fix, and code-only execution cannot recover the missing runtime state.

Could not verify earlier concern — Remaining provider and physical-hardware acceptance is incomplete: Current-head hardware fallback and backup reports narrow the gaps but do not complete J1/J2, provider settlement, physical authorization or full recovery. These remain execution limitations.

Could not verify earlier concern — Native interactive fallback Back remains unverified: This separately retained item remains unverified. Source covers committed path changes, while current reports establish neither native cancellation nor completion. Device execution is prohibited.

Could not verify earlier concern — Camera-permission CI failure has no established cause: This separately retained item still has no established cause. Passing current CI does not explain the historical camera failures or verify a permanent fix.

Could not verify earlier concern — Remaining provider and physical-hardware acceptance is incomplete: This separately retained item remains an execution limitation. Attributed hardware-emulator and controlled backup checks do not complete the outstanding journeys, provider settlement, physical authorization or recovery coverage.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: Historical launch and RBF-toast timeouts retain unproven causes. Current successes and retry recovery do not establish permanent fixes. The channel-navigation harness correction addresses a separate mechanism.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: This separately retained item remains unresolved. Later passes and the known harness correction do not recover the historical runtime state or establish the timeout causes.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: This separately retained item remains unresolved. Code-only inspection and later successful executions do not explain the earlier launch, RBF-toast or transient runtime failures.

Could not verify earlier concern — Contact-payment preparation latency remains unmeasured: The PR retains the latency concern without claiming a current reproduction or fix. Diagnostic timing hooks do not establish actual latency. No staging timing run occurred here.

Existing concerns

Reviewer model: gpt-6.1-sol
Reasoning effort: xhigh

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — native interactive Back remains unverified.

Findings: None in this delta. The pending-setup authorization finding is fixed at 1607a10.

QA for 1607a10:

Built this head with Xcode 27.0 and tested an iPhone 17 Pro simulator on iOS 26.5. Live requests used linked disposable identities and Bitcoin regtest; USDT entry used an unfunded test wallet. No payment or subscription was authorized. Videos play at 2×.

Payment waits for request setup ✅ Swipes during held setup reached no authorization.

An unpublished diagnostic hosts the production confirmation with valid quoted terms, a loaded fee rate and controlled wallet/setup state. Real swipes with payment PIN enabled and disabled left preparation at zero and selected coins unset. XCTest assertions establish those states; the recording shows the disabled/loading control.

pending-setup.2x.mp4

Payment waits for selected coins and fee ✅ The held preparation blocked authorization; a ready swipe reached the denied PIN callback.

While automatic coin selection was suspended, a swipe reached no PIN. After release, assertions verified Largest First, selected coins and fee preparation before the positive-control swipes reached the deliberately denied PIN callback. The callback always returned false, so this diagnostic could not authorize a payment.

coin-preparation.2x.mp4

Prepared high fee warns before payment ✅ The 1,500-sat fee on a 2,500-sat request triggered the over-50% warning.

With payment PIN disabled, Show Details displayed the controlled fee. A real swipe opened the fee warning, and Cancel returned to confirmation without sending.

high-fee-warning.2x.mp4

Manual selection and funding setup regressions ✅ The committed lifecycle and swipe assertions passed.

The result bundles cover automatic/manual and fixed-BTC/USD hardware setup, manual-picker Back, cancellation, minimum fallbacks and fee recovery. The focused replay shown here exercises the hardware setup wait and pending swipe-state test; XCTest assertions establish the broader result.

confirmation-regressions-focused.2x.mp4

Fixed request prepares the saved Savings coins ✅ The fixed $2.50 request opened and reopened directly at 5,000 sats with a 143-sat fee.

Settings and the device log confirmed Largest First selected the 500,000-sat output from the funded Savings wallet. The displayed fiat equivalent used the wallet rate. The request remained unpaid; the clip shows its reopen and details.

live-confirmation-focused.2x.mp4

Request denomination and receiving-method controls ✅ The USD/BTC control preserved 150.50 USD and method selection behaved independently.

Full-resolution captures verified one dark filled orange denomination control with its swap icon, and available Savings/USDT chips below expiry. Deselecting Savings changed its X to a dimmed plus while USDT stayed selected. Removing all methods disabled Send Request. The initial selection is shown separately; translated wrapping retains the earlier passing report below.

Denomination and selection Initial receiving methods
request-layout-focused.2x.mp4
USDT request sent state preserves exact USD amount ✅ Sent, the outgoing card and details all showed 150.50 USD.

The linked peer stored numeric 150.5 usd, only usdt-arbitrum-address, and zero proofs. A new creator retained typed 10.00 USD, verified in its accessibility snapshot. The recording shows the outgoing card/details and new entry; the still shows Sent.

Card, details and trailing zeros Sent
outgoing-request-focused.2x.mp4
Subscription defaults and receiving-method layout ✅ Creation and incoming review preserved 150.50 USD with USDT selected.

USD/BTC round trips and editing retained the exact amount. The creator placed USDT-selected/Savings-unselected chips below frequency, with description and icon side by side. Its sent proposal and peer record preserved the numeric amount. A separate incoming monthly proposal showed 150.50 USD on its card and review, USDT-only methods and a teal subscribe control. It was closed without subscribing.

Creator controls Incoming recurrence and review
subscription-create-focused.2x.mp4
subscription-incoming.2x.mp4
Private USDT amount matches direct Send layout ✅ Private entry offered only USDT and labeled the balance AVAILABLE.

The device log confirms a private payment list was opened. At 0.01 USDT, full-resolution captures verified matching shared amount, balance, chip, keypad and Continue typography and bottom layout against direct Send. No quote or payment was started.

private-usdt-layout-focused.2x.mp4

Interactive Back from fallback Amount ⏭️ Neither a cancelled nor a completed native transition was established.

Actual coin selection failed for a 699,819-sat quote against 700,000 sats and opened Amount. Short and full coordinate edge drags and the native gesture preset left Amount in place without an observable transition. Header Back closed the sheet. These attempts establish neither a native Back pass nor a new product defect.

native-back-focused.2x.mp4

Earlier passing evidence retained for implementations this three-file delta leaves unchanged:

Reviewed the contiguous 659a7c0..1607a10 delta, all three changed files and reached preparation, fee, authorization and cancellation callers, plus the prior open finding. The early setup-to-task submission guard was also checked in code. Native interactive Back remains the unrun journey subcase; full J1/J2 completion is not claimed. Physical authorization, provider USDT settlement and fresh-install recovery were not driven.

Twin comparison:


Reviewed by gpt-6.1-sol-high

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed to 1607a10. No new findings.

1607a10 gates submitPayment itself, as I asked in my last review. The guard now requires preparingRequest == nil, !isRequestSetupPending, walletSwitchContext == nil and prepared coins (or the automatic path) before any warning, PIN, biometrics or hardware validation.

Checked:

  • No payment before setup finishes. While isSetupPending holds, sendContext is nil, the swipe is disabled and loading, and a swipe that gets through throws CancellationError before isSubmittingPayment is set. SwipeButton resets, so a rejected swipe never leaves the view busy.
  • The pending state cannot stick. flow.isSetupPending is cleared in the setup task's defer. The task is cancelled only by cleanup() (the app-lock retain path re-runs setUpSend()) and by setUpSend() itself, which re-arms it. The fee-rate retry is bounded, and on failure the Confirm shows the retry row with the swipe disabled.
  • A swipe in the gap between setup and coin preparation sets walletSwitchContext and starts preparation once: startWalletSwitch dedupes the view .task run for the same context, so no second picker.
  • Displayed amount equals paid amount. The delta does not touch it, and the amount validation and terms-equality checks still run before the send.

The journey parity thread stays deferred to synonymdev/bitkit-android#1610. The new sentence in fixed-quoted-confirmation.xml action 2 belongs on that list.

Performance: this adds a user wait. For an incoming request the swipe stays loading until setup ends. The longest leg is the hardware-funding estimate when Savings cannot cover the quote. The gate times PaymentRequestConfirm shown → swipe enabled, with a paired Trezor and insufficient Savings.

Device gate (partial): 1607a10 — not run yet. Pending: payment-requests/fixed-quoted-confirmation.xml (action 2's new mid-setup swipe check included) cross-device with Android, paykit-quoted-usdt.xml, and the timing above. Device results will follow in a separate review.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Device gate result. Requesting changes for one MEDIUM, inline: journey action 14 fails, because a fee calculation that fails after coin selection leaves Confirm swipeable.

Device gate: 1607a10 — iPhone 17e simulator (payer, PIN required for payments) against Android master 1337f12 on Pixel_9 (requester), the two Paykit contacts linked both ways, Trezor emulator paired. perf: see below. l10n: n/a.

payment-requests/fixed-quoted-confirmation.xml, iOS paying Android requests:

  • 1: pass.
  • 2: pass, including the new mid-setup swipe. With Savings, 7 swipes during preparation opened nothing; the first after setup opened the PIN. With hardware, 5 swipes during pending setup opened no PIN and nothing was signed. 3,202 sats for $2.50, fee 143 shown before payment.
  • 3: pass. Largest First selection and fee; with manual selection the picker opens first, Back keeps Confirm, and returning through the picker recalculates the fee.
  • 4: pass. With Savings too small, BTC and USD-quoted requests settle on hardware Confirm with a fee and no Amount detour, in auto and manual modes; every request reopens.
  • 5, 6, 7, 9: pass.
  • 8: partial. A 1-hour request closed itself at expiry with nothing sent; Android uses the request expiry as the payment deadline, so the fresh-terms retry cannot be reached.
  • 10: partial. A Savings + BOLT11 request with USDT on opens straight to Confirm; switching rails and the Lightning minimum need a channel on the requester.
  • 12: adapted. 546 sats closes with the existing dust toast; 547 goes to Confirm; a $0.42 quote (538 sats) opens Amount with Continue disabled and stays open, and the Back arrow closes the sheet without reopening it.
  • 14: fail (inline).

Android paying an iOS $2.50 request: paid with one swipe (Android master has no PIN); parity is tracked in synonymdev/bitkit-android#1610.

paykit-quoted-usdt.xml: action 17 creator part passes ($150.50 keeps 150.50 through Edit). An Android USDT-only $150.50 request opens a locked Amount with USDT only and Continue disabled, and Back closes without any Bitcoin confirmation.

perf, Trezor paired with Savings too small, Confirm shown → swipe accepted:

  • head: 14.5–16.5 s over 6 samples (BTC and USD quotes).
  • master f3e97b0: ≤0.8 s, because master accepts the swipe while hardware setup is still running, which is what this PR gates.
  • The 15 s is two sequential hardware composeTransaction calls of about 7.5 s each; master runs the same two. The spinner ends after the first, and the arrow stays dimmed until the second ends. The duplicate compose is pre-existing, so it is not a finding here.

Skipped:

  • ⚠️ Actions 11 and 13: need an LNURL requester.
  • ⚠️ paykit-quoted-usdt.xml six-decimal step: needs paykit-fixture-sender; left to e2e CI. Funded USDT review, fee and comma-region checks were not run.
  • ⚠️ The interactive edge-swipe Back in actions 10 and 12: the simulator gesture never popped Amount in three tries, so it is not established.

Out of scope (same on master): with a Trezor paired, Savings max-sendable estimation fails by one sat (LDK "Available: 779999sats, Required: 780000sats"), so selectHardwareFundingSourceIfNeeded routes requests Savings could pay to the hardware wallet.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Follow-up review at 1607a10c. It re-reviews all changes since the previous reviewed head 659a7c07, plus the code paths those changes affect. Coverage of the unchanged rest of the PR carries over from the baseline review. Git confirms that the merge base is still 3f14ea05 and that the previous head is an ancestor of the current head. The update is one commit (1607a10c, "block payment while request setup is pending"). It changes three files: SendConfirmationView.swift, SendConfirmationSwipeTests.swift (new to the PR diff, which now has 23 paths) and the fixed-quoted-confirmation.xml journey.

Code review completed with unverified earlier concerns.

No new actionable code findings in the follow-up.

What the update changes:

  • Swipe blocked during request setup. For incoming requests, the payment swipe is now disabled and shows a loading state while SendSheet setup or fallback funding is still pending.
  • Manual swipe guard. submitPayment now rejects a manual swipe in three cases: request setup is pending, a coin preparation is in progress, or automatic coin selection is configured but the request has no coins selected yet. It throws CancellationError before the warnings, PIN, biometric, hardware-signing and broadcast steps. In the last case it starts the existing preparation run instead.

Why this holds up:

  • Swipe reset. SwipeButton resets after any thrown error.
  • Preparation scope. The preparation only runs for Savings payments of incoming requests, with a hardware wallet not active, a known fee rate and no selected coins.
  • No retry loop. A successful run always sets selectedUtxos, so the swipe cannot loop.
  • Manual coin selection. After Back from the coin picker, manualCoinsRequestedContext makes the swipe go through requireManualCoinSelection and reopen the coin picker.
  • Setup flag clears. isSetupPending goes back to false when the setup task finishes and is reset when setup is restarted, so the new swipe-disable condition does not add a new stuck state.
  • Single caller. The swipe is the only manual caller of submitPayment. The automatic path already waited on isSetupPending and is unchanged.

Validation: source analysis only. There is no Swift toolchain in this Linux environment, so nothing was compiled and no tests ran. That includes the updated SendConfirmationSwipeTests, which I only read. I did not check the matching CI run. Device testing: not performed in this review. I did not re-check the new swipe guard against Android; the Android comparison from the baseline review covers the earlier head only.

Suggested additional test cases:

  • iOS, automatic coin selection with a non-default algorithm. Open a fixed Savings-only BTC request on a payer with funded Savings. If a manual swipe happens before the coins are chosen (for example, right after a fee-rate change), verify that the swipe resets and no PIN or biometric prompt appears. Then verify the selected coins and fee update, and that a second swipe pays the quoted amount.
  • iOS, the same request with a hardware fallback funding source. While the fallback funding source is still being chosen, verify the swipe stays disabled with a spinner. Once a funding source is chosen, verify it becomes usable.

Earlier findings reconciled at this revision: 2 still present, 6 verified fixed, 12 could not be verified.

Reconciliation incomplete: the following earlier concerns remain unverified. This report does not establish that they are fixed or invalid, or establish approval or device readiness.

Could not verify earlier concern — USDT toggle reportedly cannot be disabled at displayed zero balance: The report at #1014 (comment) (USDT toggle cannot be disabled at a displayed zero balance) still lacks the active guard state that the author asked for in comment 6303103440. The toggle and eligibility guards are identical to base 3f14ea0, and the 659a7c0..1607a10 delta does not touch them. Unchanged code neither identifies which guard was active nor disproves the symptom. The comment count is still 40 and no newer comment supplies the guard booleans. Review 5722769833 exercised USDT entry only with an unfunded test wallet, not the original state. Code-only execution cannot recover that state. No introduction or fix is claimed.

Could not verify earlier concern — Native interactive fallback Back remains unverified: The source handles a committed pop from the fallback Amount to an empty path. Two current-head device reports still establish neither a cancelled nor a completed native interactive transition: ovi-reviewer review 5722769833 and jvsena42 review 5729110725. Both say edge drags and gesture presets never popped Amount, while header Back closed the sheet. Failure to start the gesture proves neither a defect nor a pass. Device execution is prohibited in this stage.

Could not verify earlier concern — Camera-permission CI failure has no established cause: The matching UI test passes again in current-head CI. The cause of the historical camera-permission failures is still unproven, and a later pass does not establish a permanent fix. That CI run merges the head into current master f3e97b0, not into the supplied base, so it is current-head evidence and not an exact-tree run of the supplied pins. No local Swift/Xcode execution was possible.

Could not verify earlier concern — Remaining provider and physical-hardware acceptance is incomplete: Current-head device reports narrow the gaps but do not complete acceptance. jvsena42's review 5729110725 passes J1 actions 1-7 and 9. Action 8 is partial, actions 10 and 12 are partial or adapted, and action 14 fails (see externalConcern ext-fee-calc-failure-swipe-enabled). Actions 11 and 13 were skipped because they need an LNURL requester, and the paykit-quoted-usdt six-decimal and funded USDT steps were not run. ovi-reviewer's review 5722769833 states that physical authorization, provider USDT settlement and fresh-install recovery were not driven. These are execution limitations. Code-only execution cannot close them.

Could not verify earlier concern — Native interactive fallback Back remains unverified: This separately retained item has the same unresolved limitation. The source covers committed path changes, but both current-head reports leave native cancellation and completion unestablished. It is not marked duplicate because the counterpart lineage is not open. Device execution is prohibited.

Could not verify earlier concern — Camera-permission CI failure has no established cause: This separately retained item still has no established cause. The current-head CI pass does not explain the earlier camera failures or prove a permanent fix.

Could not verify earlier concern — Remaining provider and physical-hardware acceptance is incomplete: This separately retained item is still an execution limitation. Current-head reports leave LNURL-requester actions, six-decimal and funded USDT steps, physical signing and biometrics, provider settlement and full recovery unverified, and journey action 14 fails on device.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: The historical launch and RBF success-toast timeouts still have unproven causes. The RBF symptom recurred at the current head. In E2E run 41198752500, the first invocation failed '@boost_2 - Can do RBF' with BoostSuccessToast not displayed after 30000 ms (12 passing, 1 failing), and it passed on the built-in second invocation. The run concluded success. Retry recovery does not diagnose the cause. Unit and integration runs at this head succeeded.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: This separately retained item is still unresolved. The current-head E2E run reproduced the RBF success-toast timeout on its first invocation and recovered only through the built-in retry, so neither the cause nor a permanent fix is established.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: This separately retained item is still unresolved. Code inspection and a current-head retry recovery do not explain the earlier launch, RBF-toast or transient runtime failures.

Could not verify earlier concern — Contact-payment preparation latency remains unmeasured: The contact-payment preparation latency concern in #868 (still open) has no staging measurement at this head. jvsena42's current-head timing (14.5-16.5 s from Confirm shown to swipe accepted, Trezor paired, Savings too small) measures the hardware-funding setup wait that this delta adds, not the shared-Paykit-runtime contact-payment latency of #868. The reviewer attributes it to two pre-existing hardware composeTransaction calls. No timing run occurred here.

Could not verify earlier concern — Contact-payment preparation latency remains unmeasured: This separately retained item is the same unresolved latency concern. Issue #868 is open, and no staging contact-payment timing exists at this head.

Existing concerns

Reviewer model: claude-opus-5-5
Reasoning effort: high

Integrate current master and route incoming payment requests back to Amount when fee calculation fails after coin selection. Clear reviewed terms before authorization and cover fixed/quoted requests plus Back dismissal.

Copy link
Copy Markdown
Contributor Author

Addressed the J1 action 14 fee-calculation failure in aac68853, integrating actual master 831ec3e6. When coin selection succeeds but the fee calculation fails, an incoming fixed or quoted request now returns to Amount with its reviewed payment terms cleared. Back closes that review without restarting coin preparation.

The unchanged regression reproduced the defect for fixed BTC and quoted USD: eight expected assertion failures in one execution. The same test passes after the fix, including Back dismissal and the existing authorization/submission tripwires. This uses controlled test UTXOs and the actual confirmation-root host; it does not prove a funded or physical-device journey.

Final validation: 2,954 unit and 39 UI executions passed; five fixture cases skipped, zero failures or test retries. The 2,993 passes include 30 distinct launch configurations. Complete logs and nested reports were inspected. Ten runtime warnings remain (nine QoS notices and one State-binding warning in an existing PR-added test), plus 72 ignored CoreGraphics NaN messages, stale-RGS/background failures and simulator/framework notices; no fresh master-runtime baseline is claimed. Xcode 27.2 (27B52), iOS 26.5 (23F77), isolated iPhone 17 simulator; frozen tree 1123cafc. SwiftFormat 0.64.0 reports 0/828 files needing changes (104 excluded); translation validation has zero errors and 15 warnings verified identical to master831. Two independent source reviews found no actionable issue. All 31 current-base commits verify locally and match GitHub's exact commit set, each with verified=true and reason=valid. New-head hosted CI is pending.

The first red invocation crashed before the fee boundary because its simulator build omitted app-group entitlements. After preserving and auditing that failure, one bounded signing correction reached the intended regression. A build-only invocation-option failure and an artifact-copy status-label mismatch are also retained; neither caused a test retry. All prior fixture, provider, funded, hardware and shared-navigation acceptance limits remain.

Previous Current validation section, preserved verbatim at its original revision

Current validation — published 1607a10c

Pending setup blocks swipe/submission before authorization. An early post-setup swipe starts missing-coin preparation and cancels submission. J1 action 2 covers this; older journeys do not prove it.

Local 73f1b9a5: 2,945 unit + 39 UI passes, five fixture skips, zero failures; one recovered UI tap retry. 33 focused passes; two independent scoped reviews passed. SwiftFormat 0/827; translations zero errors/15 verified actual-base warnings. 30 current-base signatures valid locally/on GitHub.

Earlier full run: exit 73 after 2,804 passes/one recovery skip, before UI. Disk exhaustion observed; cause unproven. Incomplete result and two diagnostics without a delivered swipe retained.

Prior 659a7c07: 2,984 hosted unit/UI passes/five skips; 30 integration passes; E2E 13 unique passes/14 executions after QRCode timeout/built-in recovery, one hardware skip. Failure visuals, warnings and unproven cause remain; no manual CI retry.

Hosted e36edc23/45e99e62 combines f3e97b08/1607a10c: formatting 0/828; translations zero errors/15 verified base warnings; unit/UI 2,992 passes/five skips; integration 30 passes/no skips. No failures/test retries. E2E: 13 eventual passes/14 executions after Boost toast-dismissal timeout/built-in recovery, one hardware skip; no manual CI retry. Local results on 3f14ea05 remain separate. Linker/runtime/fixture/State/NaN/QoS/frame/symbolication diagnostics and 85 reported harness findings (7 critical) remain; no base-runtime or warning-free claim.

Automated QA at 1607a10 reports held-setup/coin-selection blocked real swipes, ready swipes reached denied PIN, and high-fee Cancel returned safely. Earlier QA retains its revision. Native interactive Back, full J1/J2, physical/provider settlement and full recovery remain unverified. João deferred through Nov 1; zero current human approvals. Gates remain open.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — native interactive Back remains unverified.

Findings: No new defect found. The post-selection fee-failure finding is fixed at aac6885.

QA for aac6885:

Built this head with Xcode 27.0 and tested an iPhone 17 Pro simulator on iOS 26.5, using a linked disposable issuer and funded Bitcoin regtest. No payment was authorized. Videos play at 2×.

Fixed Bitcoin request fee failure ✅ A real fee error after selection returned the request to Amount.

For J1 action 14, the fixed 779,586-sat request selected real coins from a 780,000-sat wallet. An unpublished one-shot fee-rate stress hook then made LDK return InsufficientFunds. The request moved to locked Amount without a PIN prompt; one header Back closed it without another preparation attempt. Accessibility snapshots verified its unpaid reopen with the original amount and a 414-sat fee.

fee-btc-stress.2x.mp4

Post-selection fee failure and header Back.

Quoted dollar request fee failure ✅ The USD-to-BTC quote used the same safe fallback.

The fixed 2.50 USD request quoted exactly 779,586 sats. The same stress input caused a real LDK fee error after successful selection, followed by locked Amount and one header Back. Snapshots verified reopening at 779,586 sats with a 414-sat fee; the issuer retained the proposed request with zero proofs, and its regtest address received nothing.

fee-usd-stress.2x.mp4

Quoted request fee failure and header Back.

Fresh review after fee recovery ✅ A new swipe was required after reopening.

After restoring the unmodified head, snapshots verified that the previously failed quote reopened on Confirm at 779,586 sats with a 414-sat fee. The recording shows a fresh real swipe opening the existing over-half-balance warning; Cancel returned to unpaid confirmation.

fee-recovery.2x.mp4

Fresh swipe and warning cancellation on the unmodified head.

Native Back from fallback Amount ⏭️ Neither a cancelled nor a completed interactive transition was established.

On the unmodified head, real automatic selection failed for a 779,675-sat quote and opened Amount. Valid short, slow and fast left-edge gestures left Amount in place. Header Back closed the sheet without reopening it. This remains an unrun acceptance subcase; the attempts establish neither a native Back pass nor a new product defect.

native-back.2x.mp4

Fallback Amount, attempted edge gestures and header Back.

Confirmation lifecycle regressions ✅ The committed fee-failure, lifecycle, swipe and navigation assertions passed.

The exact-head XCTest results cover fixed BTC and quoted USD fee failures after successful selection, cleared review terms, Back without repeated preparation, cancellation, manual coins and funding setup. Assertions establish these results; the focused recording shows the hosted test execution rather than a full payment journey.

fee-regression-focus.2x.mp4

Focused replay of the new fee-failure regression.

Unpublished fee-rate test hook

The hook changes only a one-shot test input after real coin selection: 1 → 10,000 sat/vB. That stress rate makes both LDK's exact-payment calculation and its send-all fallback fail. It replaces no SDK result, fallback, navigation or authorization decision. The natural 779,586-sat quote was payable here at a 414-sat fee, so the stress run is not a literal reproduction of the earlier normal-rate shortfall. The committed regression separately covers the original error boundary with controlled wallet state. The hook was removed before the recovery and native Back checks.

diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift
index 940e92d2..276de8e1 100644
--- a/Bitkit/ViewModels/WalletViewModel.swift
+++ b/Bitkit/ViewModels/WalletViewModel.swift
@@ -757,6 +757,14 @@ class WalletViewModel: ObservableObject {
         }
         guard !Task.isCancelled, isCurrentSend() else { throw CancellationError() }
         selectedUtxos = utxos
+        #if DEBUG // test hook
+            let feeHookURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review.raiseFee") // test hook
+            if FileManager.default.fileExists(atPath: feeHookURL.path) { // test hook
+                try? FileManager.default.removeItem(at: feeHookURL) // test hook
+                Logger.info("REVIEW_FEE_HOOK: real selection completed; raising fee rate from \(selectedFeeRateSatsPerVByte) to 10000", context: "ReviewFeeHook") // test hook
+                self.selectedFeeRateSatsPerVByte = 10000 // test hook
+            } // test hook
+        #endif // test hook
 
         Logger.info("Selected UTXOs: \(String(describing: selectedUtxos))")
     }

The device log records the sequence for both denominations:

Quoted USD: real selection completed; fee rate 1 -> 10000
Failed to calculate actual fee: Insufficient funds
Hiding sheet send reason: Back from the request amount review
Fixed BTC: real selection completed; fee rate 1 -> 10000
Failed to calculate actual fee: Insufficient funds
Hiding sheet send reason: Back from the request amount review

Earlier passing evidence is retained only for behavior later pushes leave unchanged:

Reviewed the contiguous 1607a10..aac6885 delta, its three PR-specific files and reached fee, preparation, cancellation, manual and hardware callers, plus the prior open finding. Base-only allowance, contact-refresh and artwork changes received no separate device case. Full J1/J2 completion, physical signing and provider settlement are not claimed.

Cross-platform comparison:



Reviewed by gpt-6.1-sol-high

Copy link
Copy Markdown
Contributor Author

Hosted runtime CI for aac68853 has completed on workflow attempt 1. Complete logs, result archives, nested diagnostics, skips and retry records were inspected. All four artifact digests match GitHub.

XCTest jobs used Xcode 16.4 on iPhone 16 / iOS 26.2; E2E used Xcode 26.2 on iPhone 17 / iOS 26.2. These are different toolchains from local Xcode 27.2 / iOS 26.5 validation. Each runtime checkout is merge 8405acaf (parents 831ec3e6 / aac68853), tree 1123cafc, exactly matching the final local tree. Actual master has since advanced to 82925a86; this is not validation of that newer combined tree.

  • Unit/UI: 2,954 unit and 39 UI passing executions, five fixture skips, zero failures or test retries. There are 2,964 unique passing methods; 30 distinct launch configurations account for the additional 29 executions. The new fee-failure regression passed. The recovery-fixture, three disabled hardware-dashboard and isolated-fork skips remain explicit.
  • Integration: 30 passes, zero skips/failures or test-case reruns. Retry-on-failure was configured but not exercised. Two passing monitoring tests retained Taproot sync timeouts; their success does not establish a clean network sync.
  • E2E: 13 passes and one hardware-to-Spending skip. The first workflow test step passed; steps 2/3 were skipped. One internal address-type tap retry and four log-stream restarts remain. Harness 611509ee marks the skipped case as unavailable because Blocktank staging no longer creates orders; this is not a hardware/provider pass.

Both hosted XCTest builds retain 286 unique duplicate-symbol warnings / 571 occurrences. Project, dependency lockfile and package-action inputs match actual base 831ec3e6; no fresh baseline compiler/runtime execution is claimed. Unit logs retain a keyboard-queue timeout with Last Exception Backtrace: No stack in a passing test, 72 ignored CoreGraphics NaN messages and simulator/socket/haptic/negative-case notices. Integration retains stale-RGS/background and node-teardown messages. No blocking app source finding was identified; these are not warning-free runs.

E2E's external harness used Node 22.23.4/npm 10.9.9. npm ci reported 85 vulnerabilities (8 low, 24 moderate, 46 high, 7 critical); no detailed npm-audit artifact was supplied and no dependency-fix campaign was run here. E2E also retains 31 wrapped stale-element errors and 31 attribute-response errors, system haptic/audio/presentation notices, and AppIntents/tool warnings. Driver-internal retry counts are unavailable because Appium protocol logs were not uploaded. Successful-test media and a structured E2E test report were not uploaded; the saved runtime artifact contains simulator.log and trezor-emulator.json. The pinned harness's diagnostic settings and complete text logs are retained, so absent media is not replaced by a visual-verification claim.

New automated QA at this head reports fixed-BTC and quoted-USD post-selection fee fallback, header Back and unpaid reopening on an iPhone 17 Pro simulator. Its disclosed, unpublished one-shot fee-rate input (1 to 10,000 sat/vB) reached a real LDK InsufficientFunds catch; it did not literally reproduce the original normal-rate shortfall. Recovery on the unmodified head required a fresh swipe and allowed warning cancellation. Native edge gestures still established neither a completed nor a cancelled interactive Back transition. These are reviewer-reported results, not independent re-execution or fixture authority; the bot comment is not a human approval. Five video links are preserved in that review; no independent visual inspection is claimed here.

A read-only xcresult export initially hit a TestReport cache permission error; bounded reader recovery succeeded without rerunning any build or test.

All three named reviewers remain pending; no approval, repeated request or dismissal is inferred from green CI. Native interactive Back, full linked-request/provider/funded/physical-device and recovery acceptance still require their documented prerequisites. Prior failures, recovered E2E failures and missing local-only diagnostic disclosures retain their original attribution.

Previous Current validation section, preserved verbatim

Current validation — published aac68853

J1 action 14 now returns incoming fixed/quoted requests to Amount when fee calculation fails after successful coin selection. Reviewed terms are cleared; Back closes without repeating preparation. The unchanged regression failed in both variants before the fix and passes afterward, including authorization/submission tripwires.

Final local validation on tree 1123cafc, integrating actual master 831ec3e6: 2,954 unit + 39 UI passes, five fixture skips, zero failures or test retries. Xcode 27.2 / iOS 26.5; controlled test state, unavailable USDT endpoints. Two independent source reviews, full SwiftFormat (0/828; 104 excluded), translations (zero errors; 15 exact-base warnings), and 31 valid current-base signatures complete. New-head hosted CI is pending.

Ten runtime warnings remain (nine QoS and one State-binding warning in an existing PR-added test), plus ignored CoreGraphics NaN, stale-RGS and simulator/background notices. No fresh master-runtime or warning-free claim. Initial entitlement crash and bounded recovery are preserved in publication evidence and prior validation history.

Controlled tests do not complete funded/provider/physical-device, native interactive Back or full recovery acceptance. All earlier failures, skips, recovered E2E failures and missing local-only diagnostic disclosures retain their original attribution. Two current-head human approvals remain required; no approval is inferred from these checks.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Re-reviewed the full PR diff for bitkit-ios#1014 at aac68853 as a full reassessment. No verified baseline was supplied, and the saved comparison base changed from 3f14ea0 to 831ec3e6, which is also the merge base. Inherited coverage was not used.

Code review completed with unverified earlier concerns.

Valid fixed and quoted requests bind above-minimum terms and open confirmation directly. Missing, expired, and below-minimum quotes stay on Amount. A Spending/Savings switch rebinds that rail's quote before refreshing fees; coin preparation waits until funding setup finishes; fee or coin-preparation failure clears reviewed terms and returns to Amount; Back from that fallback closes the sheet. Exact USDT review text keeps six decimals. Backup start/stop requests are ordered so a newer request wins and an in-flight metadata upload and writer drain can finish.

Device testing: not performed in this review. The new lifecycle, navigation, amount, and backup tests were inspected and not executed. This pass did not compile the app.

Shared journey text was compared with bitkit-android 1337f121. Both open a valid fixed quote on confirmation and send unavailable, expired, or below-minimum Lightning terms back to Amount. iOS keeps LNURL-plus-Savings and Bitcoin-plus-USDT on Amount, and returns a below-minimum Savings quote there; Android keeps those choices on confirmation and disables authorization below its Savings minimum. Android fee-failure recovery was not traced in Kotlin.

Suggested additional test cases

  • iOS, funded regtest payer, fixed USD request offering Savings and BOLT11, with coin selection succeeding and the following fee calculation failing. From the fallback Amount, tap Continue, switch Spending/Savings, and swipe only after the new quote and fee are shown. Expected: one confirmation, the selected rail's exact quote and fee, a single coin selection, and no payment for the other rail's amount.

Earlier findings reconciled at this revision: 1 still present, 1 verified fixed, 12 could not be verified.

Reconciliation incomplete: the following earlier concerns remain unverified. This report does not establish that they are fixed or invalid, or establish approval or device readiness.

Could not verify earlier concern — USDT toggle reportedly cannot be disabled at displayed zero balance: The report at #1014 (comment) says the USDT toggle could not be turned off at a displayed zero balance. The toggle and eligibility guards at aac6885 are identical to base 831ec3e, so this pull request does not change them. Unchanged code neither identifies which guard was active nor disproves the symptom. The author asked for that guard state in comment 6303103440. All 42 conversation comments were scanned and none after that comment reports the guard booleans. A later fresh-fixture success remains limited to that fixture. This code-only pass has no Swift toolchain and cannot reproduce the missing runtime state. Focused open and closed issue search found no usdtDisableBlocked issue, so tracked_separately is unavailable. No introduction or fix is claimed.

Could not verify earlier concern — Native interactive fallback Back remains unverified: The source handles a committed pop from the fallback Amount to an empty path. The current-head report #1014 (review) still establishes neither a cancelled nor a completed native interactive transition: edge gestures left Amount in place, while header Back closed the sheet. Failure to start the gesture proves neither a defect nor a pass. Device execution is prohibited in this stage.

Could not verify earlier concern — Camera-permission CI failure has no established cause: The matching UI test passes on the current-head merge. The cause of the historical camera-permission failures is still unproven, and a later pass does not establish a permanent fix. The unit run merges the head into the supplied base and its tree equals HEAD, so it is current-head evidence. No local Swift or Xcode execution was possible.

Could not verify earlier concern — Remaining provider and physical-hardware acceptance is incomplete: Current-head reports narrow the fee-failure and header-Back cases but do not complete acceptance. Review 5730839316 at aac6885 does not claim a full J1/J2 pass, physical signing, or production-provider USDT settlement. The earlier current-lineage device report at 1607a10 left LNURL-requester actions, six-decimal and funded USDT steps, and native edge-swipe Back unverified. These are execution limitations. Code-only execution cannot close them.

Could not verify earlier concern — Native interactive fallback Back remains unverified: This separately retained item has the same unresolved limitation. The source covers committed path changes, but the current-head report leaves native cancellation and completion unestablished. It is not marked duplicate because the counterpart lineage is not open. Device execution is prohibited.

Could not verify earlier concern — Camera-permission CI failure has no established cause: This separately retained item still has no established cause. The current-head CI pass does not explain the earlier camera failures or prove a permanent fix. It is not marked duplicate because the counterpart lineage is not open.

Could not verify earlier concern — Remaining provider and physical-hardware acceptance is incomplete: This separately retained item is still an execution limitation. The current-head review leaves full J1/J2, physical signing, and provider settlement unverified. It is not marked duplicate because the counterpart lineage is not open.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: The historical launch timeout and the parent-head RBF success-toast timeout still have unproven causes. E2E run 41356864579 at this head completed successfully: the log records 13 passing and no failing count, and @boost_2 dragged BoostSuccessToast rather than timing out. An address-type toast retry and four log-stream restarts are separate from that historical timeout. Later success does not explain comments 6444464437, 6497736273, 6507674422, and 6510561206. The historical runtime state cannot be recovered from code.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: This separately retained item is still unresolved. The current-head E2E success does not explain the historical launch or RBF timeouts. It is not marked duplicate because the counterpart lineage is not open.

Could not verify earlier concern — Historical launch and E2E retry failures have unresolved causes: This separately retained item is still unresolved. Code inspection and a current-head E2E pass do not explain the earlier launch, RBF-toast, or transient runtime failures. It is not marked duplicate because the counterpart lineage is not open.

Could not verify earlier concern — Contact-payment preparation latency remains unmeasured: The contact-payment preparation latency concern in #868 remains open and has no staging measurement at this head. The 14.5-16.5 s timing in review 5729110725 measures the hardware-funding setup wait at 1607a10, which that reviewer attributes to two pre-existing composeTransaction calls. It is not the shared-Paykit-runtime staging measurement named by issue 868. No timing run occurred here.

Could not verify earlier concern — Contact-payment preparation latency remains unmeasured: This separately retained item is the same unresolved latency concern. Issue 868 is open, and no staging contact-payment timing exists at this head. It is not marked duplicate because the counterpart lineage is not open.

Findings

  • [LOW] Align fixed-quote journey wording across platforms — now at journeys/payment-requests/fixed-quoted-confirmation.xml:14. Still present from the earlier review.

Reviewer model: grok-4.7
Reasoning effort: high

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Focused device QA — fee failure, retry and wallet switching

Tested aac68853 on the existing iPhone 17 / iOS 26.0 simulator, using local regtest/Electrum/LND/Pubky and a distinct Android requester. Installed the CI artifact from run 41356864579; its merge checkout 8405acaf has the same source tree as this head. Wallets were preserved.

Outcome: the focused run FAILED the retry/single-preparation assertion. One MEDIUM finding is attached inline. Hold sign-off for a correction and retest of that sequence. No incorrect amount, duplicate payment, authorization bypass or funds loss was observed. This is the complete result of this focused session, not a full J1/J2 or whole-PR device pass.

Setup and controlled failure

Created one fixed $250 request offering Savings and Spending/BOLT11, note QA101-fee-retry, payable as 320,921 sats on both methods. Payer balances: Savings 474,714, Spending 502,998 sats. Opened the request on Spending, then switched to Savings with automatic Branch and Bound selection.

After real selection succeeded with one 474,714-sat UTXO, a one-call LLDB change raised only the LightningService.calculateTotalFee fee argument from 1 to 10,000 sat/vB. The real SDK returned InsufficientFunds. Amount, recipient, navigation and authorization were not mocked; no app source/binary patch was installed. The breakpoint was disabled immediately and the debugger detached before the switch comparison. Subsequent calculations used normal fees. This reproduces the error branch under controlled stress, not a natural fee-rate shortfall.

Executed results

Check Result Observation
Successful selection → fee failure → locked Amount PASS Real selection at 11:51:04 UTC, fee error at 11:51:31. “Please try again”; locked 320,921-sat Amount; no authorization prompt. The source clears reviewed terms; private state was not independently read from memory.
Continue and inspect selected-method fees PASS Recovered Savings showed 143 sats; Spending showed ~1 sat; returning to Savings restored 143 sats. Amount remained 320,921 sats.
One preparation/active confirmation after retry and switching FAIL — MEDIUM One Spending → Savings tap produced two selection starts, two selected-UTXO results and two real fee calculations at 11:52:30.648–.652 UTC. Header Back → Amount → Back revealed the older confirmation.
Fresh swipe and cancellation PASS within this boundary A new swipe was required and opened the “over 50% of your total balance” warning. Cancel returned to confirmation. No PIN/biometric or final payment was attempted.
Full dismissal/reopen control PASS Reopened the same unpaid request, then switched Spending → Savings: one selection and one 143-sat calculation at 11:55:12 UTC.
Unpaid state and cleanup PASS Both balances unchanged; requester still showed the same request pending. No duplicate request/payment observed. Dismissed only this test request afterward; requester showed Rejected.

The original post-selection fee-failure thread is marked resolved, and its fallback fix passes here. The new failure is the subsequent Continue → wallet-switch lifecycle; this does not re-report the former enabled-confirmation-after-error defect.

Evidence and where to look

Short Back-navigation clip (40 seconds): about 00:13, Back returns to Amount; about 00:31, the second Back reveals the older confirmation.

back-after-retry.mp4

Complete focused-run recording (5:39, normal speed): fee failure around 01:14, Continue around 01:31, switch back to Savings around 02:14, first Back at 03:18–03:19, second Back at 03:36–03:37, clean-reopen Savings control around 04:55.

fee-retry.gh.mp4

Timestamped app-log excerpt below — inspected, scoped app-log excerpt. Duplicate preparation is established by the logs, not visually distinguishable in the recording: the two fee calculations finish a millisecond apart. Compare 11:52:30 with the single preparation at 11:55:12.

Required retest and remaining scope

After correction, repeat successful selection → fee failure → Amount → Continue → Savings/Spending/Savings; assert one active confirmation/preparation owner, one preparation per switch, correct fees, and Back without resurfacing the older review. Add this exact continuation to the lifecycle regression: the existing test checks fallback → immediate Back, and its Amount destination is a placeholder, so it never exercises real Continue and switching.

Native interactive Back, natural fee shortfall, quote expiry/replacement, PIN/biometrics, settlement, hardware and the wider PR journey matrix were not tested in this session. Header Back is the navigation evidence. No new master runtime comparison was performed; the inline explanation identifies the newly added fee-error fallback as the source-level entry into this failing retry sequence.

Timestamped app logs: failure, retry, duplicate preparation and clean-reopen control
[2026-11-02 11:51:04.543 UTC] INFOℹ️: Selecting UTXOs with algorithm: branchAndBound, target amount: 320921 sats, fee rate: 1 sats/vbyte [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 742]
[2026-11-02 11:51:04.552 UTC] INFOℹ️: Selected UTXOs: Optional([LDKNode.SpendableUtxo(outpoint: LDKNode.OutPoint(txid: "b5cc711319dc7595156a1143d0aab81ee7d59a998878adc29d071cecf4e90bd0", vout: 0), valueSats: 474714)]) [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 761]
[2026-11-02 11:51:31.235 UTC] ERROR❌: Failed to calculate actual fee: AppError(message: "Insufficient funds", debugMessage: Optional("The available funds are insufficient to complete the given operation."), paymentFailureReason: nil, underlyingError: Optional(LDKNode.NodeError.InsufficientFunds(message: "The available funds are insufficient to complete the given operation."))) [SendConfirmationView.swift: calculateTransactionFee() line: 2057]
[2026-11-02 11:51:47.942 UTC] INFOℹ️: Selecting UTXOs with algorithm: branchAndBound, target amount: 320921 sats, fee rate: 1 sats/vbyte [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 742]
[2026-11-02 11:51:47.944 UTC] INFOℹ️: Selected UTXOs: Optional([LDKNode.SpendableUtxo(outpoint: LDKNode.OutPoint(txid: "b5cc711319dc7595156a1143d0aab81ee7d59a998878adc29d071cecf4e90bd0", vout: 0), valueSats: 474714)]) [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 761]
[2026-11-02 11:51:47.971 UTC] INFOℹ️: Calculated transaction fee: 143sats for sending to address bcrt1q7nhtndqq07h602qwc8qv0qequsjcdgyl793npw - [LDK] [ldk_node::wallet:1931] [Logger.swift: log(record:) line: 173]
[2026-11-02 11:52:30.648 UTC] INFOℹ️: Selecting UTXOs with algorithm: branchAndBound, target amount: 320921 sats, fee rate: 1 sats/vbyte [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 742]
[2026-11-02 11:52:30.648 UTC] INFOℹ️: Selecting UTXOs with algorithm: branchAndBound, target amount: 320921 sats, fee rate: 1 sats/vbyte [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 742]
[2026-11-02 11:52:30.650 UTC] INFOℹ️: Selected UTXOs: Optional([LDKNode.SpendableUtxo(outpoint: LDKNode.OutPoint(txid: "b5cc711319dc7595156a1143d0aab81ee7d59a998878adc29d071cecf4e90bd0", vout: 0), valueSats: 474714)]) [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 761]
[2026-11-02 11:52:30.650 UTC] INFOℹ️: Selected UTXOs: Optional([LDKNode.SpendableUtxo(outpoint: LDKNode.OutPoint(txid: "b5cc711319dc7595156a1143d0aab81ee7d59a998878adc29d071cecf4e90bd0", vout: 0), valueSats: 474714)]) [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 761]
[2026-11-02 11:52:30.651 UTC] INFOℹ️: Calculated transaction fee: 143sats for sending to address bcrt1q7nhtndqq07h602qwc8qv0qequsjcdgyl793npw - [LDK] [ldk_node::wallet:1931] [Logger.swift: log(record:) line: 173]
[2026-11-02 11:52:30.652 UTC] INFOℹ️: Calculated transaction fee: 143sats for sending to address bcrt1q7nhtndqq07h602qwc8qv0qequsjcdgyl793npw - [LDK] [ldk_node::wallet:1931] [Logger.swift: log(record:) line: 173]
[2026-11-02 11:53:53.809 UTC] INFOℹ️: Calculated transaction fee: 143sats for sending to address bcrt1q7nhtndqq07h602qwc8qv0qequsjcdgyl793npw - [LDK] [ldk_node::wallet:1931] [Logger.swift: log(record:) line: 173]
[2026-11-02 11:55:12.217 UTC] INFOℹ️: Selecting UTXOs with algorithm: branchAndBound, target amount: 320921 sats, fee rate: 1 sats/vbyte [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 742]
[2026-11-02 11:55:12.221 UTC] INFOℹ️: Selected UTXOs: Optional([LDKNode.SpendableUtxo(outpoint: LDKNode.OutPoint(txid: "b5cc711319dc7595156a1143d0aab81ee7d59a998878adc29d071cecf4e90bd0", vout: 0), valueSats: 474714)]) [WalletViewModel.swift: setUtxoSelection(coinSelectionAlgorythm:isCurrentSend:listSpendable:selectForAmount:) line: 761]
[2026-11-02 11:55:12.221 UTC] INFOℹ️: Calculated transaction fee: 143sats for sending to address bcrt1q7nhtndqq07h602qwc8qv0qequsjcdgyl793npw - [LDK] [ldk_node::wallet:1931] [Logger.swift: log(record:) line: 173]

Logger.error("Failed to calculate actual fee: \(error)")
transactionFee = 0
if app.contactPaymentContext?.incomingPaymentRequest != nil {
returnToAmountReview()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MEDIUM — keep a single active confirmation after fee-failure retry

On aac68853, after real automatic selection succeeds and the subsequent fee calculation fails, this new fallback clears terms and sets the path to [.amount] while the original Confirm remains the root. Real Amount Continue then appends .confirm (SendAmountView:441), leaving Confirm(root) → Amount → Confirm(retry). Both confirmations share the path and presentation; confirmationContext/sendContext (lines 65–85) accept either instance when the path ends in .confirm. Wallet-switch deduplication is per-view @State, so it cannot prevent both instances preparing coins.

Device result: FAIL. After Continue → Spending → Savings, one tap caused two UTXO selections and two SDK fee calculations at 11:52:30.648–.652 UTC; a full dismissal/reopen control selected/calculated once. Back → Amount → Back exposed the older confirmation. The valid-terms path also resets hasShownAmountReview at line1813, so the immediate-failure dismissal guard does not cover this recovered sequence. See the accompanying review's recordings and the timestamped log excerpt in the review summary. The duplicate work is log evidence; no duplicate payment or funds loss was observed.

Ensure only the active review owns preparation when retrying this fallback (retire/reuse the old confirmation or enforce instance ownership), and add a real fee failure → Continue → switch methods → Back regression. testRequestFeeFailureAfterCoinSelectionReturnsToAmountReview currently tests immediate Back only; its host renders Amount as Color.clear, so it misses the failing continuation. The original resolved fee-fallback finding is fixed; this is the newly exercised retry path reached by the added catch branch.

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.

5 participants