Skip to content

fix: refresh Paykit views and local completion - #914

Merged
ovitrif merged 3 commits into
masterfrom
codex/paykit-observation-proof-handoff
Oct 12, 2026
Merged

ovitrif merged 3 commits into
masterfrom
codex/paykit-observation-proof-handoff

Conversation

@ben-kaufman

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

Copy link
Copy Markdown
Contributor

This PR:

  1. Refreshes existing Paykit display projections with one validated observation.
  2. Completes verified hardware-payment recovery locally before remote proof delivery or acknowledgement finishes.

Related: SDK #183, Server #78. Pins published rc73; neither PR needs to merge for this consumer build.

Description

  • Reuse existing refresh coalescing and display projections with identity/generation-checked observations, retaining previous data on observation failure while authoritative eligibility, authentication and action getters remain unchanged.
  • Publish hardware-recovery resolution after exact transaction verification and durable proof/activity/follow-up persistence, before serialized remote delivery; normal iOS proof delivery was already asynchronous.
  • Handle Pending resolution on the main queue and navigate before acknowledgement, preserving payer/wallet/transaction guards, actual SDK identity verification, retained proof on failure and restart retry without another payment.
  • Pin exact rc73 revision 0eff3d4e694ba0dbbea2d47852fe796e456671b0; the actual SwiftPM archive matches published SHA256 511715acf3ca84e322d19ea6651c767c98b5325845d571abe2a9527b1eb6ae26, with no local override or unrelated dependency change.

Out of Scope

Design

N/A - no UI layout or copy changes.

Preview

The J1 QA report and recording on 532eb159 reports a completed regtest payment on an iPhone 17 Pro simulator running iOS 26.5: Trezor signed one 100,000-sat payment, and the original hardware transaction matched the issuer's exact proof, with no second payment. This is reviewer-reported simulator/Bridge evidence; it does not establish physical USB/BLE validation.

QA Notes

Journeys

  • J1 updated shop-onchain-proof.xml - reviewer reports the common hardware payment/proof journey passed on 532eb159, including signing and the original transaction's exact issuer proof without a second payment. This supersedes the earlier pre-signing selector/input blockers. The report does not establish injected broadcast/response-loss faults, held-delivery/restart ordering, payer-switch or later activity-edit recovery checks; those remain separate from this reported pass. Held-delivery/restart ordering retains deterministic unit coverage, not new device evidence.

Manual Tests

N/A.

Automated Checks

  • added ContactsManagerTests.swift, PaykitPaymentRequestServiceTests.swift - preserve prior displays on failed/wrong-identity observations and keep cached display separate from action authority.
  • added PaykitSdkIdentityCheckTests.swift - enforce observation identity and cancellation without overlapping native operations.
  • added PaykitPaymentProofServiceTests.swift, TransferServiceActivityTests.swift - prove durable held-delivery/restart recovery and main-thread Pending completion before held acknowledgement, with exact-context rejection and proof retention.
  • updated ContactsListViewTests.swift, ProfileDestinationViewTests.swift, PaykitPaymentRequestServiceTests.swift - cover the explicit display loader and full-observation failure.
  • ran a clean published-package build and full CI-selected unit suite with locked versions on the dedicated simulator: 2,371 passed, zero failures (BASE: 2,363); eight tests added, none removed, same six CI exclusions, no funded/UI target. The test-only follow-up 3d2283e2 replaces fixed navigation/acknowledgement sleeps with bounded completion expectations; all 2,371 tests passed again with no introduced diagnostics and a clean bounded independent review. Production code and the measured read path are unchanged.
  • ran complete BASE diagnostic comparisons: all 79 compiler warnings and 102 SwiftFormat findings are pre-existing; zero introduced. Translation output is identical: zero errors, 1,898 existing warnings. Diff, project and journey XML checks passed.
  • ran bounded repetitions before the final published-package suite: 30/30 hold/recovery cases, 10/10 revised live-event Pending cases, and 137/137 contact-boundary cases passed; final local and published SDK artifacts are byte-identical.

Measurement Limits

  • Actual request-display refresh reads: two to one; contact display: one to one. Message processing is unchanged; broader SDK getter comparisons are not the iOS pipeline.
  • Matched 250 ms holds show existing warm-display isolation, and the hardware-recovery fix now publishes durable local resolution before delivery releases, retaining exact proof for restart retry. These are synthetic local isolation/recovery checks, not measured network or unlock speedups.
  • Published rc72/rc73 Swift bindings against the actual staging Homeserver: 60/60 native samples passed in ABBA blocks. Warm stored-request refresh measured midpoint medians 1,639.432 ms versus 814.080 ms (24 per arm); new handles measured 1,639.525 ms versus 828.013 ms (6 per arm). Fixture: 32 contacts, empty request/peer/receipt collections, no injected delay or shorter safety wait. This measures the service refresh, not rendered UI, cold app startup, populated history, linking, payment or unlock latency. Contacts-only and already-cached display have no demonstrated improvement.

@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High impact] Updates payment proof delivery and contact observation logic.

The PR appears safe to merge, with a non-blocking improvement needed in the acknowledgement test.

Findings

  1. P2 Test can pass too early ▶

Summary

Refreshes Paykit displays through one identity-checked observation and lets verified hardware-payment recovery reach Success before remote delivery or local acknowledgement finishes.

  • Contacts and request refreshes use one identity-checked observation for display data.
  • Verified hardware payments can reach local Success before proof delivery finishes.

Diagram

sequenceDiagram
    participant Recovery
    participant Store as Local proof store
    participant Activity as Local activity
    participant Pending
    participant SDK as Paykit SDK
    Recovery->>Recovery: Verify original transaction
    Recovery->>Store: Save verified proof
    Recovery->>Activity: Save local follow-up
    Recovery->>Store: Save completion awaiting consumption
    Recovery-->>Pending: Publish resolution on main queue
    Pending->>Pending: Check payer, request, wallet and transaction
    Pending->>Pending: Navigate to Success
    par Delivery
        Recovery->>SDK: Submit proof
    and Acknowledgement
        Pending->>SDK: Check active identity
        Pending->>Store: Save consumption when checks pass
    end
Loading

Reviews (1) · Last reviewed commit: "fix: refresh Paykit views and local comp..." · Reviewed by Greptile

Comment thread BitkitTests/TransferServiceActivityTests.swift

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

No findings. Swaps the request and contact display reads for one identity- and generation-checked observeState observation, and publishes verified hardware-payment resolution before proof delivery on the reconcile path. Reviewed abc28553, full tier, reasoned from the code, the pinned paykit-rs source and CI; iOS is not built here.

What I checked, and 4 candidates I ruled out

Read in full: completeHardwareOnchainPayment, consumeOnchainPaymentResolution, submit/submitInBackground (PaykitPaymentProofService.swift), PaykitSdkService.observeState, withSdk, refreshPaykitKey, PaykitSdkOperationLock (PubkyService.swift), ContactsManager.loadContacts, PaykitPaymentRequestService.synchronize, PaykitPaymentRequestManager.refresh/performRefresh, SendPendingScreen.applyOnchainPaymentResolution
SDK side: observe_state, contact_records, list_payment_requests, load_remote_state at paykit-rs 0eff3d4 (tag v0.1.0-rc73, matches Package.resolved)
Call sites traced: onchainPaymentResolutionPublisher (AppScene, HwSendSignView, SendPendingScreen), loadContacts(for:) (AppScene, ContactsManager), service.synchronize (only performRefresh)
CI: validate, detect-changes and Greptile green; Run Tests, Run Integration Tests and build-local still running at review time

Ruled out

  • Duplicate proof submission when resolution is published before submit on the reconcile path: consumeOnchainPaymentResolution can now interleave with the foreground submit(completed) and start a second submit, both doing a check-then-submit against paymentRequests(). The same interleave already exists on the default deliverInBackground path, and the SDK documents a repeated proof for the same occurrence as evidence that implies no second payment.
  • observeState failing where the old getters returned data: signed-out, no Paykit identity secret, and uninitialized identity all errored on the old contact_records() / payment_requests() path too, because the shared storage transaction needs the same session and secret. A missing blob with no registry noise key still yields default state, so missingDataIsEmpty: false loses no empty-list case.
  • performRefresh now requires activeIdentity before syncing: the only state this skips is a manager whose stored acceptance or subscription state failed to load. Outbound proof delivery still runs from PaykitPaymentProofService.submit and the other processPendingMessages callers.
  • Dropped identity guard after consume in SendPendingScreen: applyOnchainPaymentResolution already matches resolution.identity against pubkyProfile.publicKey on entry, and navigation now runs on the main queue via the added .receive(on:).

Merge confidence: 4/5, the unit and build jobs were still running and the iOS target was not built here.

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

Code scan at 3d2283e: no HIGH or MEDIUM finding and nothing to raise inline. The device result follows in a second review; not approving until it has run.

Checked and clean:

  • Durable before publish. completeHardwareOnchainPayment verifies the exact transaction and persists proofData and onchainAcceptanceVerified, runs the activity follow-up, persists onchainLocalFollowupComplete and onchainResolutionAwaitingConsumption, and only then publishes the resolution and submits. A crash between publish and submit re-enters on the next reconcile(), skips lookup and follow-up, republishes and submits: no second signing or broadcast.
  • No second payment. The request stays off the payable list while the row exists, and hardwareCompletionsInProgress blocks a concurrent completion for the same key across the actor's awaits.
  • Consume against submit. consume flips awaitingConsumption under mutationLock and submits that copy; the reconcile's submit holds the stale copy, so its removeRequestProofs predicate does not match and cannot delete the row. isAlreadyQueued prevents a duplicate proof.
  • SendPendingScreen. The resolution is matched against the current identity and now also against hardwareTransactionId; delivery is on the main queue; the .task replay and onReceive replay end in one consume. Close hides the sheet and leaves the row and the resolution in place, so reopening replays it. Back and swipe-back are disabled.
  • Observation binding. observeState runs under the operation lock after refreshPaykitKey() and checks identity and key generation; performRefresh re-checks activeIdentity after the await; ContactsManager re-checks loadGeneration and contactsRevision, and sign-out bumps the generation.
  • A failed observation keeps the previous display for both contacts and requests.
  • SDK pin. 0.1.0-rc73 resolves to 0eff3d4, whose parent is the rc72 commit; the request filter is the same function the old getter used.

⚠️ Not verified: the rc73 xcframework binary; the tag sits on a commit of an unmerged SDK branch.

CI is green at this head.

Device gate (partial): 3d2283e — not run yet. Pending, against a master build on the same simulator: time from opening Contacts to the list publishing now that it reads the full state observation (contacts, linked peers, requests and receipts) where master read contact records only, with an empty and with a populated request history; request-refresh time; and shop-onchain-proof.xml if the Trezor emulator and issuer fixture can be stood up for the simulator. Performance-relevant: the PR states a speed purpose and changes what runs under the SDK operation lock.

@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 result for 3d2283e. No finding from the run. Not approving yet: shop-onchain-proof.xml, the only journey that exercises the Pending → Success change, has not been driven by anyone.

Device gate (partial): 3d2283e — perf: Contacts reload 1.02 s median, range 1.00–3.74, 8 of 8 completed (master 4e0c8ba: 1.40 s median, 1.07–2.14, with 2 of 8 not finishing in the 5 s on screen); request refresh 3.60 s median, 2.69–3.98, n=12 (master 4.86 s, 3.75–6.61, n=11); refresh on opening Subscriptions 5.92 / 3.78 / 3.71 s (master 4.74 / 4.80 / 15.66 s); five quick switches in and out of Contacts start five loads on both builds with no "already loading" storm; no hang or crash report. iPhone 17 simulator, iOS 26.5, regtest, staging backends.

Read with this limit: the identity has 2 contacts, 1 pending request and one retained hardware proof. That is close to an empty history, so the run does not stress the part of the change that could cost more, which is Contacts now reading the full state observation (requests and receipts included) where master read contact records only. At this size the PR is not slower than master on any path measured.

The first Contacts open after a cold launch took about 21 s on both builds whenever it landed before the Paykit session was restored (about 64 s after launch, after Deferred session restoration warnings). Same on master, so it is not this PR.

Refresh was timed with breakpoints from performRefresh entry to persistSubscriptionState, since it logs nothing at either ref.

⚠️ Not run — shop-onchain-proof.xml: it needs a linked paykit-server issuer fixture, an E2E Bridge build of this head and the Trezor emulator. None of the three was up; I will retry it on the next pass.

jvsena42
jvsena42 previously approved these changes Oct 12, 2026

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

No HIGH or MEDIUM finding at 3d2283e, in the code scan or on the device. Approving; the code trace and the measurements are in my two earlier reviews today.

Device gate: 3d2283e — journeys: 0 driven by me; perf: Contacts reload 1.02 s median (master 1.40 s), request refresh 3.60 s (master 4.86 s), with 2 contacts and 1 request; no re-entry storm, no hang report; Figma: no screen changed.
Skipped — covered by CI at 3d2283e: unit tests, integration tests, e2e (all green)

⚠️ Not run: shop-onchain-proof.xml. It needs a linked paykit-server issuer fixture, which is not available on my setup; hardware Shop payment coverage is left to the e2e CI. The Pending → Success ordering rests on the code scan and testHardwareReconciliationPublishesDurableResolutionBeforeProofDelivery.
⚠️ perf: not measured at scale — the identity used has a nearly empty request and receipt history, so the cost of Contacts reading the full state observation with a large history rests on reading the code.

@ovitrif
ovitrif self-requested a review October 12, 2026 11:00
@ovitrif

ovitrif commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

I added myself as reviewer because:

  1. fix: refresh Paykit views and local completion #914 (review) indicates Joao's setup didn't run device tests, IIRC on my qa setup agents can run these tests now (requiring paykit-server).
  2. bitkit-devs may include Talos as reviewer, bypassing the requirement for 2 human reviewers (or agents with a human behind), I will fix this by excluding Talos from the auto load-balancing.

ovi-reviewer[bot]

This comment was marked as outdated.

ovi-reviewer[bot]

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Addressed the stale J1 selectors from this QA report in signed commit 532eb15.

The journey now uses the existing iOS HardwareSendSign screen and exact HardwareSendSignAmount container, and checks the original issuer destination under To address (confirm on device). The README records the Android/iOS difference. These selectors were stale on the base too; no app/UI identifiers or payment behavior changed. The 100,000-sat check and all signing, original-wallet, exact-transaction and issuer-proof assertions remain intact.

Validation: XML parses; all 12 actions remain, with only the platform lookup in action 6 changed. The complete 102 SwiftFormat findings and 1,898 translation warnings match the previously base-verified reports, with zero introduced findings. A fresh frozen correctness/native-pattern/polish review found no actionable issue. This documentation-only follow-up did not rerun native tests or devices.

J1 remains unchecked: the reported run stopped before signing, and the corrected journey still needs signing and proof-delivery validation. Ovi's pending review is preserved; no device pass or approval is implied.

ovi-reviewer[bot]

This comment was marked as outdated.

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

Approving at 532eb15. The one commit since my last approval changes journey text only; the code review and measurements from 3d2283e stand.

  • HardwareSendSign (HwSendSignView.swift:113) and the HardwareSendSignAmount prefix (:36) exist, and "To address (confirm on device)" is the hardware__send_confirm_address string, so the reworded action names what the screen has. No HardwareSendAddress identifier exists on iOS, which is what the new README row records.
  • The Android names in the new README rows (HardwareSendAmount, HardwareSendAddress) match HwSendSignScreen.kt on Android master.

⚠️ Device gate: not run — shop-onchain-proof.xml needs the paykit-server issuer fixture, which is not on my setup; it is left to the e2e CI and to the QA run already on this PR. perf: unchanged from my review at 3d2283e (no code changed).

@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: ✅ Approve

QA for 532eb15:
Tests for the review: journey J1 passes.

Tested on iOS 26.5 simulator (iPhone 17 Pro), regtest.
Request: Ovi asked to use the available local issuer setup; J1 completed signing and verified the original hardware transaction and its exact issuer proof.
Request: The author asked for corrected J1 signing and proof checks; J1 completed signing and verified the original hardware transaction and its exact issuer proof.

🟢 Test J1
Test J1

Trezor signed one 100,000-sat payment; the original hardware transaction and issuer proof matched, with no second payment.

J1.mp4

Note

J1 checked in the PR description.


Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest

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

utAck

@ovitrif
ovitrif enabled auto-merge October 12, 2026 16:37
@ovitrif
ovitrif merged commit 9521413 into master Oct 12, 2026
21 checks passed
@ovitrif
ovitrif deleted the codex/paykit-observation-proof-handoff branch October 12, 2026 17:16
@ovitrif ovitrif added this to the 2.6.0 milestone Oct 12, 2026
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.

4 participants