Skip to content

feat: share paykit state across apps - #856

Merged
ovitrif merged 90 commits into
masterfrom
codex/paykit-shared-runtime-local-20260930
Oct 7, 2026
Merged

ovitrif merged 90 commits into
masterfrom
codex/paykit-shared-runtime-local-20260930

Conversation

@ben-kaufman

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

Copy link
Copy Markdown
Contributor

Closes #815

This PR moves Bitkit to Paykit's identity-wide shared state using the published 0.1.0-rc69 SDK.

SDK: https://github.com/pubky/paykit-rs/releases/tag/v0.1.0-rc69

Companions: Android #1401, Paykit Server #46.

Description

  • Reconciles hardware payments against their own wallet activity, preserves that wallet scope through backup/restore, and completes confirmed sends without rebroadcasting after expiry. Recovery results stay available across view recreation.
  • Supports one-time absolute payment deadlines, with expiry checks at submission, separate acceptance deadlines, and late proof delivery.
  • Deletes contacts with one bulk block/remove operation, pauses background contact preparation during profile deletion, and batches local cleanup. Active subscriptions, busy peer leases and public contact markers remain protected.
  • Explicit contact re-add and import save contacts and unblock their selected peers atomically, without per-contact writes or compensating re-block loops. Label edits do not unblock peers.
  • Uses encrypted homeserver state and one Encrypted Link per contact identity, shared by authorized Paykit apps.
  • Stores wallet reservations, pending payment proofs, and recovery backups locally while following shared request state and execution claims.
  • Retains broadcast transaction IDs before remote identity reads, defers publication for unavailable contact links, and preserves manually detached activity contacts through sync.
  • Saves one-time acceptance intent before the remote call and includes it in wallet backups, so interrupted acceptance and wallet restore can resume safely, including shared-state lock conflicts. Execution rechecks subscription state after asynchronous lookups, and acceptance IDs are removed only when shared state confirms payment, cancellation, or rejection.
  • Uses atomic claim-and-accept after preparation, with interactive queue priority. Acceptance is durable before proceeding; peer delivery runs separately and remains retryable. Proof-triggered refreshes and backup exports wait until payment submission finishes, without dropping pending work or crossing identity changes.
  • Lets Bitkit authorize Paykit access, a watch-only account, or both as independently requested claims, without sharing wallet spending keys.
  • Shows the requested access in the authorization sheet. Server reconnect requests Paykit access without allocating another watch-only account.
  • Pays the exact endpoint supplied by a Payment Request instead of substituting a later private list, permits a fixed on-chain destination for unpaid recurring periods, and attributes received activity through its matching receiving address, including companion accounts.
  • Combines reservation and request attribution, leaves ambiguous transactions unlabeled, and skips unchanged history backfills.
  • Handles contact publication, requests, and subscriptions using app-owned endpoints inside the shared identity.
  • Saves contact-sharing OFF and cleanup-pending before withdrawal, preventing new endpoint publication during cleanup. Serializes private/public cleanup with sharing changes and coalesces foreground retries. Retries failed withdrawals and registry updates, and discovers shared-state recipients even while links are recovering, and keeps unfinished withdrawals pending.
  • Includes app ownership fields when validating subscription proposals against the transport size limit. One-time and subscription proposals revalidate and deliver only to the selected saved recipient, without draining unrelated peers.
  • Reuses validated Paykit keys and backup fingerprints for unchanged state, and refreshes keys after identity errors without replaying failed writes.
  • Checks incoming private messages during the ten-second foreground poll without draining outbound work. Full synchronization runs on startup, explicit refreshes, and elapsed-time maintenance after 30 seconds, then every 60 seconds. Notification handling can reload saved requests without network intake. Temporary session-restoration transport and lock failures retain the saved session for retry instead of starting another auth flow.
  • Temporary restoration failures retain credentials and use a bounded short retry window in the existing foreground loop before normal backoff. Identity changes and cancellation stop pending retries; SDK safety waits remain unchanged. Invalid credentials still require recovery.
  • Completes public payment setup before preparing private contacts in the background. Coalesces repeated preparation, reuses established links, backs off unavailable lookups, and invalidates pending publication during cleanup. Unchanged contact keys do not trigger preparation when SwiftUI rebuilds the view.
  • Reads public request capabilities up to eight contacts at a time outside the SDK mutation queue, without waiting for all-contact preparation. Private-message retries drain only the affected peers; idle cleanup skips unrelated work.
  • Shows incoming requests in the existing payment sheet while preparing, with payment disabled until validation finishes. Closing the sheet prevents a late result from reopening it; failed payments remain retryable.
  • Prioritizes selected-recipient and request-delivery work over queued background reads while preserving active SDK calls and identity-change barriers.
  • Retains due-reminder targets through failed, canceled or stale refreshes. Manual Pay and payment retries take priority without dropping an unrelated reminder or interrupting an active payment.
  • Normalizes uppercase Bech32 request addresses for attribution while preserving validation and ambiguity checks.
  • Runs Dev Settings Paykit-disable cleanup through the same coordinator as contact-sharing changes.

Out of Scope

  • Guaranteed private-list withdrawal on contact deletion. Deletion blocks immediately even if withdrawal fails. The old list can remain at the peer, and registry cleanup can remain pending until the contact is explicitly re-added.
  • Migration from receiver-folder data. Paykit has not launched, so that development data is unsupported.
  • Homeserver lock-finalization safety: the SDK cooldown is a mitigation, not a fix for a write completing after lock expiry.

Design

N/A — no design available.

Preview

QA Notes

Journeys

  • J1 updated delete-profile.xml - bulk deletion with 62 contacts; active-preparation runs completed on both platforms, with a settled/idle repeat still pending.

  • J2 updated import-all-contacts.xml - Continue completes public setup without waiting for every contact to link; retest with 61 contacts and unavailable profiles.

  • J20 Repeat import-all-contacts.xml with the same 62-contact identity after profile deletion. Three imports and two overlapping deletions completed on rc68; fully idle deletion and private-link readiness remain open.

  • J3 new cancellation-during-confirmation.xml - a subscription canceled while confirmation is open cannot be paid after its cancellation is received.

  • J4 new fixed-onchain-destination.xml - later unpaid subscription periods can use the request's fixed on-chain address, while paid periods remain unavailable.

  • J5 new contact-payment-sharing.xml - disabling contact payments stays off after leaving and returning to Settings.

  • J6 updated automatic-presentation.xml - linked contacts on separate identities show new requests in the payment sheet, keep payment disabled during preparation, and defer presentation while another sheet is open.

  • J7 new accepted-device-ownership.xml - only the accepting install can resume a one-time request after restart.

  • J8 new paykit-only-approval.xml - approves Paykit access without creating a watch-only account.

  • J9 new paykit-reconnect.xml - renews server access without replacing its account or invoices.

  • J10 updated contact-request-or-pay.xml - contact payments and requests use identity-wide state.

  • J11 updated delete-and-readd-contact.xml - deletion blocks private requests and refreshes the list without waiting for another poll.

  • J12 updated definite-pre-broadcast-retry.xml - a failed request can retry immediately using fresh state.

  • J13 updated issuer-interoperability.xml - requests from another app retain their exact endpoint and request context.

  • J14 updated request-summary.xml - request details show the shared request and endpoint correctly.

  • J15 updated open-watch-only-link.xml - the OS handoff opens the requested authorization flow.

  • J16 updated wallet-leg.xml - authorizes the server, pays its exact request destination, attributes the received payment, and preserves manual detachment after sync and restart.

  • J17 updated create-and-propose.xml - oversized proposals are rejected before sending, and shorter proposals use the shared identity and app ownership.

  • J18 updated requested-resolution-failure.xml - progress is visible while preparing and clears after failure.

  • J19 new absolute-payment-deadline.xml - accepted requests remain payable until their payment deadline; expiry during callbacks, fee/PIN entry, signing or queue waits prevents submission, while earlier uncertain broadcasts and late proofs remain recoverable. The lost-response/expiry/reconciliation case passed on b633684/rc69 in the normal app with a Trezor emulator and local regtest. The remaining deadline and reattachment cases are still unverified.

  • J21 Updated payment-deadline-history.xml - expired one-time and unsupported recurring deadlines remain visible without enabling payment.

  • J22 new delete-newly-saved-contact.xml - deletion from Contact Saved returns to Contacts; Back does not reopen the deleted contact. Verified by the reviewer in three staging deletion/re-add runs on 291328d; see the device report.

Manual Tests

  • 1 With network fault injection, overlap foreground/connectivity recovery requests while restoration fails, then recover and retry. Waiting callers must share the active attempt; a later attempt must remain available.

  • 2 Tap a due subscription reminder during background contact preparation, both with Bitkit open and on cold launch. Follow the reminder checks and record tap-to-sheet timing separately from authentication and SDK lock waits.

  • 3 Hold sharing withdrawal in progress, foreground the app, then request sharing on again. Cleanup must not overlap, and publication must wait for it to finish. Repeat with foreground cleanup already active, and with Paykit UI disabled/re-enabled before Contact Payments is enabled; this requires fault injection.

  • 4 Drop the acceptance response after its durable commit, restart Bitkit, then refresh and retry the accepted one-time request. The accepting installation must retain ownership, and retry must not duplicate a payment. This requires fault injection.

  • 5 Inject private withdrawal and public/app-registry update failures, then disable contact payments. Both sharing settings must stay off and cleanup must remain pending until recovery, without re-sharing cleared endpoints. Repeat with only the public/app update failing, and with a recipient removed by another authorized app while Bitkit has no local contact cache, including Linking and RecoveryRequired recipients.

  • 6 Back up an accepted but unpaid one-time request, stop the original wallet, then restore on a replacement install and retry. Automated wallet backup/restore is not a journey capability. Running the same wallet on multiple devices concurrently is unsupported.

  • 7 Hardware broadcast recovery: follow the manual fault-injection checklist. Drop a successful broadcast response, let the deadline expire, and verify reconciliation completes without rebroadcasting, restores navigation, and survives Activity/view recreation. Lost-response/expiry/reconciliation passed on b633684/rc69 with no extra broadcast after expiry, normal completion/navigation and one matching proof. View reattachment, process death and other failure combinations remain unverified; these require controlled fault injection and are not automated journey capabilities.

Automated Checks

  • added PaykitReceivedPaymentContactsTests.swift - combined attribution, cache invalidation, and a database-backed test of backfill retry, saved contact attribution, and skipped completed scans.
  • added AddressSearchCoordinatorTests.swift - companion-account lookup, isolated search indexes, and conservative handling of unknown outputs.
  • updated PaykitSdkClientConfigTests.swift and PubkyProfileManagerTests.swift - shared identity setup, cached key reuse, rotation and rollback rejection, and identity switching.
  • updated PaykitBackupStateTrackingTests.swift - cached backup fingerprints and rechecking uncertain writes.
  • updated PubkyAuthRequestTests.swift, PubkyAuthApprovalSheetTests.swift, and WatchOnlyAccountServiceTests.swift - independent claims, combined consent, and malformed request rejection.
  • updated PrivatePaykitServiceTests.swift, PaykitContactLifecycleTests.swift, and ContactPaymentsServiceTests.swift - publication ordering, deferred work, contact cleanup, and attribution.
  • updated PaykitPaymentRequestServiceTests.swift, PaykitPaymentProofServiceTests.swift, and PaykitPaymentStateBackupTests.swift - request destinations, execution ownership, fresh snapshots after state changes, and retained wallet payment state.
  • added PaykitPaymentActivityTests.swift and updated PaykitSdkOperationLockTests.swift - deferred proof refresh and delivery, payment-priority barriers, cancellation, and backup admission across wallet changes.
  • removed PaykitReceiverNoiseKeyStoreTests.swift - keys belong to the identity, not individual receivers; authorizer coverage is in PaykitSdkClientConfigTests.swift.
  • ran the iOS/Android/server regtest flow on published rc58: a 17,000-sat payment used the request's exact address, the server confirmed it, and iOS showed Received from Buyer. All 37 simultaneous-sync readiness samples stayed healthy.
  • verified SwiftPM resolves rc69 from the published release tag and its downloaded framework archive matches the manifest checksum.

Local validation against published rc69: the simulator build and 551 focused native tests passed, including session recovery, private Paykit, requests, proofs, contact navigation and backups. Three new scheduler tests passed ten repetitions each. SwiftPM uses the published release without a local override. Complete compiler, formatter and translation reports have no introduced diagnostics against the base; the 18 existing format findings remain unchanged. These tests do not replace the unchecked device journeys or establish payment-flow latency.

Performance is not signed off. Paired mobile measurements on the rc66/rc67 runtime recorded Send Request at 13.68s, confirm/swipe to native send at 15.56s, first-returned LINKED at about 80s, and private sharing withdrawal at 30.63s (not full cleanup). These single samples predate the final queue fix and do not establish current-head latency. Cold start, backup stalls, sharing cleanup, reminder failure recovery and full content unlock remain open in #868 and the unchecked journeys above.

The reminder cold-launch network-failure journey is not yet device-tested.

The rc68 62-contact staging retest completed three imports and two deletions of the same identity, without reproducing the previous contact-save/re-import stall. Preview took 15.89-16.44s, Import All 1.358-1.830s and Continue 8.993-9.530s. Deletion reached onboarding in 17.068s and 15.324s. These are individual UI-polled observations. Preparation retries remained active, so fully idle deletion and private-link readiness are unverified; no payment was attempted.

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 0/5

[High risk] Updates payment SDK and refactors payment state sharing across apps.

The PR should not merge until received-payment attribution and compatibility with existing persisted payment state are addressed.

Findings

  1. P1 Security Unrelated payments gain payer labels ▶
  2. P1 Older wallet backups cannot restore ▶
  3. P1 Existing pending proofs become unreadable ▶
  4. P1 Legacy reservation keys lose contacts ▶

Summary

This PR moves Paykit integration from receiver-specific local state to identity-wide shared state, adds independent authorization claims, and uses shared requests for payment and received-activity attribution.

  • The new attribution path can assign an unrelated historical receipt to a request counterparty.
  • Existing wallet backups, local pending proofs, and reservation ledgers need compatibility handling for their changed persisted formats.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Shared Paykit requests] --> B[Request endpoint resolution]
  B --> C[Payment and proof]
  A --> D[Endpoint-to-contact index]
  D --> E[Historical received activity backfill]
  F[Local proof and reservation state] --> C
  G[Wallet backup] --> F
Loading

Reviews (1) · Last reviewed commit: "docs: clarify paykit integration contrac..."

Comment thread Bitkit/Services/PaykitReceivedPaymentContacts.swift Outdated
Comment thread Bitkit/Models/PaykitPaymentStateBackup.swift
Comment thread Bitkit/Services/PaykitPaymentProofService.swift
Comment thread Bitkit/Services/PrivatePaykitAddressReservationStore.swift

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

Advice: ✅ Approve

Review: diff 72 files.
Pair PR synonymdev/bitkit-android#1401: equivalent.

Findings:
3 inline (1 MEDIUM, 2 LOW)

QA:
Tests running: 7 of 9 passed.


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

Comment thread Bitkit/Services/CoreService.swift Outdated
Comment thread journeys/pubky-marketplace/wallet-leg.xml Outdated
Comment thread Bitkit/Services/PrivatePaykitService+Backup.swift

@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 review of the complete PR diff against merge base ab88d1c9, at 4c7f705.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

Compatibility with the unreleased receiver-path backup, pending-proof, and reservation formats is an intentional out-of-scope break. Paykit has not launched, and this PR does not claim those development documents remain readable. Authorization still shares only the requested watch-only account and generation-bound Paykit secret, and payment requests resolve through the request endpoint rather than a later private list.

The new marketplace wallet-leg consent step does not match the on-screen Paykit access copy or the Android companion's action text.

GitHub reports unit tests and integration tests succeeded on this revision. This review did not run them. The local e2e job was still running and is not evidence. bitkit-android#1401 was compared only for the updated consent journey step, not reviewed in full.

Recommended before device testing: correct the wallet-leg Paykit access action so that journey checks the localized consent copy.

Device testing: not performed in this review.

Findings

  • [LOW] Wallet-leg journey checks the wrong Paykit access copy — inline at journeys/pubky-marketplace/wallet-leg.xml:19.

Comment thread journeys/pubky-marketplace/wallet-leg.xml 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.

One MEDIUM (two installs can pay one request twice) and two LOWs inline. Paykit is ungated on main, so these are user-facing from the next release.

Checked and clean:

  • Auth sheet: approval is pinned to the immutable config.request, with rawUrl re-checked in approveAuthRequest. The requester clientID and relayOrigin are displayed. A Paykit-only claim skips watch-only account allocation, while a combined claim still goes through the watch-only consent step. PubkyAuthClaim.encode refuses mismatched payload and claim combinations.
  • The exported Paykit secret is a one-way blake3 derivation of root and generation, signed and encrypted to the relay channel. Nothing logs the payload.
  • No new auto-start payment path. Amounts are still gated by validateIncomingPaymentRequestAmounts, endpoints are limited to acceptedPaymentEndpointIdentifiers, and the post-broadcast lookup reuses the captured contactPaymentContext.
  • No app-group or keychain-access-group changes, and Env.keychainGroup stays private.
  • Biometric and PIN checks run in submitPayment before performPayment takes the execution claim, so declining auth leaves no claim.
  • Not raised, because nothing reaches them today: claims are never released on abandon (no releasePaymentRequestExecutionClaim call site), and a missing registry counts as generation 1 against the saved floor (PubkyService.swift:1051). Both start to matter once a second executor app, or key rotation, exists. Same on synonymdev/bitkit-android#1401.

Non-blocking: is there a Figma frame for the new PubkyAuthPaykitAccess block in the approval sheet? Link it and I'll diff the implementation against it on the next pass.

Comment thread Bitkit/Services/PaykitPaymentRequestService.swift
Comment thread Bitkit/AppScene.swift Outdated
Comment thread Bitkit/Utilities/Keychain.swift

@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

Retest for the review: journey J8 fails; journey J4 passes now.

QA:
Tested on two iOS 26.5 simulators (iPhone 17 Pro), regtest.
Tests J1, J2, J3, J5, J6, J7, J9 already done in review.

🟢 Test J4
Test J4

Passed.

J4-retry-104009.mp4
J4-retry-104009-buyer.mp4

🔴 Test J8
Test J8

Written review Back control unavailable.

J8-retry-104009.mp4
J8-retry-104009-buyer.mp4
J8-retry-104009-resume.mp4
J8-retry-104009-buyer-resume.mp4
log
Timed out after 3000ms waiting for UI predicate exists for identifier NavigationBack.


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

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Updated in 71904a7. For the reported J8 failure, an automatically opened review is the root of the send sheet, so it has no Back button. The journey and README now use a downward swipe from the drag indicator. The consent step also matches Android. I have not rerun the full marketplace journey, so it remains unchecked.

For the design question, no Figma frame was supplied for this authorization UI. The PR keeps N/A — no design available.

@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 of the changes since 4c7f705, at 36ed45d. The inherited baseline is the full review of the PR diff against merge base ab88d1c. That base and merge base are unchanged, and 4c7f705 is an ancestor of this head. This pass covered the payment-ownership, received-payment attribution, reservation, keychain, and journey delta, plus the callers those paths use.

No new actionable code findings.

A one-time request stays payable on the install that stored its acceptance. Once that accepted state is visible, another install's refresh leaves the request out of pending and auto-presentation, and payment, retry, and send authorization require the local acceptance id. testOnlyAcceptingInstallCanResumeOneTimePayment covers the stale proposal and the restarted accepting install. The two-install payment comment matches this gate: the SDK execution claim still succeeds again for app id bitkit. Received-payment labeling requires the wallet receiving output and one contact across the transaction's mapped outputs, and it stops when the identity or reservation revision changes during lookup. The wallet-leg consent step now asks for private Paykit data and messages without sharing identity or spending keys, matching pubky_auth__paykit_access_description, and the automatic review is dismissed with a downward swipe. That resolves the previous consent finding.

This review did not run the simulator tests. Unit tests and integration tests were still running on this revision. Device testing was not performed. accepted-device-ownership and wallet-leg remain unchecked on the PR. The PR description's regtest payment report was not re-executed here.

ovi-reviewer[bot]

This comment was marked as resolved.

@ben-kaufman
ben-kaufman requested a review from piotr-iohk October 1, 2026 12:33

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

Follow-up at 36ed45d. One MEDIUM is still open: recurring periods are double-payable across installs. I replied on the existing two-install thread rather than opening a new one.

Resolved:

  • Two-install double pay for one-time requests. Every entry needs the local acceptance id: auto-present, notifications, list and detail, retry, finishPayment, SendConfirmationView, LnurlPayConfirm, quickpay and the hardware path. ensurePaymentAllowed requires isApprovedForPayment and re-checks generation, identity and approval after the async linkedPeers call. A stale proposal on the second install fails at the SDK accept, which re-validates Proposed inside the locked transaction.
  • Backfill is skipped while the identity, contact snapshot, activity revision and reservation revision are unchanged. Every ActivityService write invalidates it, and an incomplete scan is not cached.
  • Attribution requires the receiving address to be an actual output and a single contact across all mapped outputs, and conflicts stay unlabelled. The live path re-checks auth, identity and snapshot after the async lookup.
  • The ledger is keyed by normalized identity, removed by wipeEntireKeychain(), and kept out of backups.
  • accepted-device-ownership.xml matches the Android copy apart from identifiers.

Not raised:

  • An accepted one-time request that no install owns stays blocked until the payee cancels. That is the stated trade-off.
  • Activation failing closed on a ledger read error matches the existing subscription-store behaviour.
  • ovi-reviewer's open J8 thread is not repeated here.

@ben-kaufman
ben-kaufman requested a review from jvsena42 October 1, 2026 13:05
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-shared-runtime-local-20260930 branch 2 times, most recently from 23d6ddb to fe281dd Compare October 1, 2026 13:52

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

Advice: ✅ Approve

Reaudit: diff 13 files.
No new findings; the rest is in the review.
Pair PR synonymdev/bitkit-android#1401: equivalent.

QA:
Tests wait for CI.


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

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

@ben-kaufman please resolve the merge conflicts. QA review has not been performed for this request.

@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 at d99d5a67, covering the entire delta since 05ec22d3, affected callers and tests, and the complete current PR inventory. Unchanged coverage is inherited from the completed baseline; ancestry, base and merge base were verified locally, and HEAD was rechecked before returning.

1 actionable finding — resolve or provide an evidence-backed rebuttal.

The expired hardware-retry navigation concern is fixed in source. Started proofs retain reconciliation and duplicate-payment protection after dismissal. The contact-deletion regression was independently confirmed and reconciled with its existing canonical thread. Startup and linking performance remain unresolved.

Validation: changed Swift files passed syntax parsing. A standalone Swift harness using the pinned navigation helper confirmed that Contact Saved remains on the stack after deletion; it does not exercise SwiftUI. Native XCTest assertions were inspected, not executed here. The author's 439-test report is attributed evidence; matching unit and integration CI remained in progress when collected. Structured code-result and diff-anchor validation passed. Targeted comparison used Android 804e7547, whose older expiry path still locks dismissal; this does not establish current Android parity or a full Android review.

Device testing: not performed in this review. Hardware lost-response, no-reconciliation and retained-resolution view-recreation checks still require the author's controlled issuer and fault-injection setup. Concurrent wallet/shared-Pubky installations and pre-2.6.0 profile compatibility remain excluded by the supplied product rules.

Findings

  • [MEDIUM] Return to Contacts after deleting from Contact Saved — inline at Bitkit/ViewModels/NavigationViewModel.swift:209.

Comment thread Bitkit/ViewModels/NavigationViewModel.swift Outdated
jvsena42
jvsena42 previously approved these changes Oct 7, 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.

Re-reviewed d99d5a6..291328d. No findings. The Contact Saved delete MEDIUM is fixed and resolved, verified on the simulator: three deletes from the Contact Saved screen all returned to Contacts with no error screen, Back did not restore the deleted contact, and each re-add reached Contact Saved. The Edit-screen delete still returns by itself.

Also in this commit: HwFundingSigner.swift:622 now drops the pending payment only on a definite pre-broadcast failure (InvalidHex, InvalidTransaction) with no prior attempt, matching Android 50811b2a5; Electrum and unclassified errors keep the signed transaction for retry.

Device gate: two simulators on staging at 291328d, contacts only (timeline and log lines in the resolved thread). Payments, launches and sharing were last driven at d99d5a6 and 5dd730b. The hardware deadline, reconciliation and failure paths are verified in code and unit tests only; the manual fault-injection checklist is unexecuted. The localized error toast from d99d5a6 was not seen on a device.

Open and tracked in #868: about one minute to restore the session in roughly half of launches, and the delete and Contacts-reload waits noted in the thread.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Updated to published Paykit rc69 in b633684, on top of the hardware and contact-navigation fixes in 291328d. SwiftPM resolved the release artifact and 610 focused native tests passed.

The SDK now returns temporary lock contention after one bounded acquisition batch instead of repeating it as a revision conflict. The staging SDK test reduced a blocked call from about 26s to 3s, but still recovered only when the 60s lock expired. This is not a new mobile startup or linking speed claim; those timings remain open.

The reviewer’s three Contact Saved deletion runs are now recorded as passed. Hardware fault-injection and the other unchecked device scenarios remain open.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

The matching hardware lost-response case now passed on b633684 / published rc69, using the normal iOS app, a Trezor emulator and disposable local regtest funds.

Core independently accepted the signed payment while the proxy dropped the response and held reconciliation reads. Core made two initial attempts with identical bytes. Retry after the actual deadline added no broadcast attempt. Releasing the reads automatically reached Bitcoin Sent, returned Home, and delivered exactly one matching proof to the issuer. The attempt count remained two.

This verifies that recovery path, not the whole checklist. View reattachment, process death and other injected failure combinations remain unverified. It is not a latency result or physical-hardware test, and no public-network payment was made.

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

Suggestion: 👍 Approve

Reaudit: diff 12 files.
No new findings; the rest is in the review.
The hardware pre-submission classifier matches synonymdev/bitkit-android#1401 at 7bc3ccb; J22 has identical filename, name and action prose on both platforms. Published rc69 changes shared lock backoff for both mobile consumers without a new mobile latency claim.

QA:
Tests queued.

Replies:

Error toast: not exercised. (comment)

The localized toast remains a device QA gap. Contact Saved deletion is fixed in 291328d and separately confirmed in the canonical thread. The rc68 relaunch and payment waits remain open in #868; rc69 changes contention backoff without establishing current mobile latency.


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

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

The latest CI unit failure was the test’s two-second wait for contact loading to start; its sign-out/publication assertions did not fail. 480b8d2 gives that setup wait ten seconds, with the existing load/sign-out gates and assertions unchanged. All 22 tests in the class passed ten repetitions after the edit. This changes only the test, not app delays or safety timeouts. CI is rerunning.

@jvsena42

jvsena42 commented Oct 7, 2026

Copy link
Copy Markdown
Member

Device gate (partial) — b633684 (Paykit rc69)

Two simulators on staging, build installed over the rc68 state from 291328d. UTC 2026-10-07. Still running: payment request round trip, contact delete and re-add.

Upgrade in place: the session restores on both from rc68-written state. No failed validation, recovery_required or concurrent_update line in any of the 14 logs; every lock error is now shared_state_busy.

Relaunch (terminate, launch), first log line → Paykit session restored:

# first log line Deferred session restoration lines restored total
A1 13:39:02.907 13:39:10.514, 13:39:28.729, 13:39:51.934 13:40:39.480 96.6 s
A2 13:41:08.074 13:41:15.564, 13:41:34.205 13:47:11.099 363.0 s
A3 13:47:15.634 none 13:47:26.323 10.7 s
A4 13:48:38.706 13:48:46.187, 13:49:04.525, 13:49:29.790 13:50:26.357 107.7 s
A5 13:51:08.700 none 13:51:19.372 10.7 s
A6 13:52:28.070 13:52:35.570, 13:52:54.092, 13:53:24.402 13:54:09.367 101.3 s
B1 13:39:17.454 13:39:25.557, 13:39:44.275, 13:40:12.401 13:40:57.372 99.9 s
B2 13:41:43.678 none 13:41:54.607 10.9 s
B3 13:47:22.847 13:47:30.854, 13:47:48.398, 13:48:18.512 13:49:00.785 97.9 s
B4 13:49:54.116 none 13:50:05.811 11.7 s
B5 13:51:23.220 13:51:31.161, 13:51:49.840, 13:52:18.887 13:53:17.530 114.3 s
B6 13:53:58.291 13:54:06.275, 13:54:28.357, 13:54:53.478 13:55:42.403 104.1 s

8 of 12 launches were deferred. Not deferred: 10.7–11.7 s. Deferred: 96.6–114.3 s, and one at 363 s. At d99d5a6 and 291328d (rc68) on the same simulators the deferred launches took 60–68 s with a single deferral, so in this run the slow case is about 35–45 s longer and takes three deferrals instead of one. The first launch after the install showed the same split (A 58.0 s with two deferrals, B 11.7 s).

Slow launch, B5 (B_bitkit_foreground_2026-10-07_13-51-23.log):

13:51:23.220 PERF: init(walletIndex:) took 0.0 seconds on core queue
13:51:28.900 WARN: Stopped waiting for Pubky identity republishing - PaykitSdkService [waitForRepublish line: 486]
13:51:31.161 WARN: Deferred session restoration, keeping saved session - PubkyProfileManager [resolveSessionInitialization line: 1786]
13:51:43.331 WARN: Failed to refresh public paykit endpoints after receive refresh: SharedStateBusy(code: "shared_state_busy", context: "Pubky shared state remains locked; retry later") - WalletViewModel [refreshBip21 line: 1496]
13:51:49.840 WARN: Deferred session restoration, keeping saved session - PubkyProfileManager
13:51:59.428 WARN: Failed to refresh public Paykit endpoints on foreground: SharedStateBusy(code: "shared_state_busy", ...) - WalletViewModel [line: 1358]
13:52:02.691 ERROR: Backup failed for: 'WALLET': SharedStateBusy(code: "shared_state_busy", ...) - BackupService [triggerBackup line: 262]
13:52:18.887 WARN: Deferred session restoration, keeping saved session - PubkyProfileManager
13:52:18.891 DEBUG: paykit_session loaded from keychain
   -- 48.9 s without a paykit line, no WARN/ERROR --
13:53:07.824 DEBUG: paykit_session loaded from keychain
13:53:08.293 INFO: Updated paykit_session - Keychain
13:53:17.530 INFO: Paykit session restored for pubkydso8zn5... - PubkyProfileManager [line: 322]
13:53:19.122 INFO: Loaded 1 contacts - ContactsManager

Fast launch, B2 (B_bitkit_foreground_2026-10-07_13-41-43.log):

13:41:43.678 PERF: init(walletIndex:) took 0.0 seconds on core queue
13:41:49.184 WARN: Stopped waiting for Pubky identity republishing - PaykitSdkService
13:41:54.607 INFO: Paykit session restored for pubkydso8zn5... - PubkyProfileManager [line: 322]
13:41:56.069 INFO: Loaded 1 contacts - ContactsManager

The 363 s launch, A2 (A_bitkit_foreground_2026-10-07_13-41-08.log): after two deferrals a third attempt started at 13:41:58 and logged neither Deferred nor restored for 5 min 9 s. The app stayed responsive, but Contacts opened as a screen titled "Profile" with only a spinner (13:45:13, unchanged at 13:46:42). It restored by itself.

13:41:15.564 WARN: Deferred session restoration, keeping saved session - PubkyProfileManager [line: 1786]
13:41:28.101 WARN: Failed to refresh public paykit endpoints after receive refresh: SharedStateBusy(code: "shared_state_busy", ...) - WalletViewModel [refreshBip21 line: 1496]
13:41:34.205 WARN: Deferred session restoration, keeping saved session - PubkyProfileManager
13:41:43.323 WARN: Failed to refresh public Paykit endpoints on foreground: SharedStateBusy(...) - WalletViewModel [line: 1358]
13:41:46.588 ERROR: Backup failed for: 'WALLET': SharedStateBusy(...) - BackupService [line: 262]
13:41:58.655 DEBUG: Upserting paykit_session - Keychain
13:42:01.254 DEBUG: paykit_session loaded from keychain
   -- 304.8 s with no paykit/pubky line (the ~1/s `paykit_session loaded` polling stops too), no WARN/ERROR --
13:47:06.042 DEBUG: paykit_session loaded from keychain
13:47:11.099 INFO: Paykit session restored for pubkyyks9epx... - PubkyProfileManager [line: 322]

Notes on the method: each kill was issued 38–127 s after the previous restore (A3 1 s after, B3 325 s after), and in every deferred launch the last keychain poll line was 0.1–1.2 s before the kill. In every deferred launch the WALLET backup fails once with shared_state_busy at about +38 s. The Deferred session restoration line carries no error code on iOS; the code is only on the neighbouring lines.

@jvsena42

jvsena42 commented Oct 7, 2026

Copy link
Copy Markdown
Member

Device gate (partial 2) — b633684 (Paykit rc69)

Payment request round trip, both directions, UTC 2026-10-07. No shared_state_busy, concurrent_update, failed validation or recovery_required line in either log during these flows. Still running: contact delete and re-add.

step B requests from A A requests from B
Send Request tap → "Sent" 12.7–17.1 s 18.3–23.1 s
"Sent" → sheet opens by itself on the payer (idle on Home) 6.9–9.0 s 14.9–17.5 s
sheet open → Opened private Paykit payment 3.7 s 3.8 s
swipe → Onchain send result txid 18.6 s 19.2 s

Both payments completed and both sides show the activity. Payer log for the second one (B_bitkit_foreground_2026-10-07_13-53-58.log), no WARN/ERROR apart from No UTXO selected:

14:02:08.416 DEBUG: Showing sheet send - SheetViewModel
14:02:12.216 INFO: Opened private Paykit payment for pubkyyks9epx... using payment list version 71 - PrivatePaykit
14:02:12.250 INFO: Updated paykit_presented_payment_requests - Keychain
   (swipe 14:02:31.5–14:02:32.7)
14:02:34.652 INFO: Saved paykit_pending_payment_proofs - Keychain
   -- 8.2 s --
14:02:42.893 INFO: Consumed private Paykit payment list version 71 for pubkyyks9epx... - PrivatePaykit
14:02:42.896 INFO: Updated paykit_accepted_payment_requests - Keychain
   -- 4.9 s --
14:02:47.768 INFO: Updated paykit_presented_payment_requests - Keychain
14:02:47.769 WARN: No UTXO selected, using default selection algorithm.
14:02:51.894 INFO: Sending 1000 sats to bcrt1qkpcl… with fee rate 1 sats/vbyte
14:02:51.986 INFO: Onchain send result txid: 6ab228a8…

Seen in passing on the payer after the first payment: Synced LDK payments - Added: 2 - Updated: 7 logged 17 times within 14:00:18.586–.605.

@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 291328d..480b8d2: the rc69 bump (b633684) and a test timeout (480b8d2). One MEDIUM, inline: rc69 makes the slow launch slower.

rc69 otherwise: the session restores from rc68-written state on both simulators. No failed validation, recovery_required or concurrent_update line in any of the 14 logs; every lock error is shared_state_busy, which the app already handles wherever it handles ConcurrentUpdate (PubkyProfileManager.swift:1820, PaykitPaymentRequestService.swift:1787).

Payments: a request and payment in each direction completed, swipe → broadcast 18.6 s and 19.2 s, no lock errors. Contacts: delete from the Edit screen returned to Contacts and the re-add reached Contact Saved in under 5 s. Numbers and log lines: #856 (comment).

480b8d2 only raises a test wait from 2 s to 10 s (ContactPaymentsServiceTests.swift:514).

Device gate: b633684 — relaunch 10.7–11.7 s when not deferred, 97–114 s when deferred (8 of 12, one at 363 s); payments 18.6 s and 19.2 s; contact delete and re-add pass; no crash. 480b8d2 is test-only and was not driven. Hardware paths are code-only as before.

Comment thread Bitkit.xcodeproj/project.pbxproj

@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 at 480b8d25, covering the entire delta since d99d5a67, affected callers and tests, the rc68-to-rc69 SDK delta, and the complete current PR inventory. Supported unchanged coverage is inherited from the completed baseline. Ancestry, base and merge base were verified locally; HEAD was rechecked before returning.

No new actionable code findings.

The Contact Saved deletion concern is fixed in source, including stale Back history and newer-route preservation. Hardware failures now retain signed transactions and pending proofs unless local decoding definitively failed before any submission. Earlier uncertain attempts remain protected.

Validation: changed Swift files passed syntax parsing; a standalone harness using the exact pinned navigation helper and key normalizer passed 10 route assertions. Native XCTest assertions were inspected, not executed. Code-result and publication-preview validation passed. Pinned-head unit and integration CI were still running when collected. Dependency inspection used Paykit dd97fc9a and Core b53fa54a. Targeted comparison at Android 804e7547 retains older hardware error and expiry behavior; this does not establish current Android parity or a full Android review.

Startup, linking and backup delays remain unresolved; attributed rc69 relaunch measurements do not establish improved latency. The author's hardware lost-response recovery pass applies to b633684 and that single scenario. View reattachment, process death and other unchecked fault cases remain unverified.

Device testing: not performed in this review. Remaining author journeys and fault-injection cases remain required runtime work. Concurrent wallet/shared-Pubky installations and pre-2.6.0 profile compatibility remain excluded under the supplied product rules.

jvsena42
jvsena42 previously approved these changes Oct 7, 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.

I agree on merge now and improve the performance on follow up PRs

@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

Reaudit: diff 12 files.
No new findings; the rest is in the review.
The hardware pre-submission classifier matches synonymdev/bitkit-android#1401 at 7bc3ccb; J22 has identical filename, name and action prose on both platforms. Published rc69 changes shared lock backoff for both mobile consumers without a new mobile latency claim.

QA:
Tested on 6 iOS 26.5 simulators (iPhone 17 Pro).

🟢 Tests 1, 4, 6, J1, J3, J4, J5, J7, J8, J9, J10, J14, J15, J17
Test 1

With network fault injection, overlap foreground/connectivity recovery requests while restoration fails, then recover and retry.


Test 4

Drop the acceptance response after its durable commit, restart Bitkit, then refresh and retry the accepted one-time request.

4-final-0.mp4

Test 6

Back up an accepted but unpaid one-time request, stop the original wallet, then restore on a replacement install and retry.


Test J1

Delete-profile.


Test J3

Cancellation-during-confirmation.


Test J4

Fixed-onchain-destination.

J4-final-0.mp4

Test J5

Contact payment sharing stays off after returning through Home.

J5.mp4

Test J7

Accepted-device-ownership.


Test J8

Paykit-only-approval.

J8.mp4

Test J9

Paykit-reconnect.

J9-final-0.mp4

Test J10

Contact-request-or-pay.

J10-flow.mp4
J10-request.mp4
J10-terminal.mp4
J10.mp4

Test J14

Request summaries match sender and note.

J14-requester.mp4
J14.mp4

Test J15

Open-watch-only-link.

J15.mp4

Test J17

Create-and-propose.

J17-final-0.mp4

🟠 Tests 2, 3, 5, 7, J2, J6, J11, J12, J13, J16, J18, J19, J20, J21, J22
Test 2

Blocked: Warm and cold reminder taps were not proved.

2-r1-104308.mp4

Test 3

Blocked: Held-withdrawal ordering was not fully certified before interruption.

3-interrupted-r1.mp4

Test 5

Blocked: Uncached recipient cleanup variants could not be prepared.

5.mp4

Test 7

Incomplete: the run ended before this test finished.


Test J2

Failed, and nothing shows whether our setup or the app caused it: Staging contact fixture creation failed.

J2-1.mp4

Test J6

Blocked: Funded, linked setup was not established.

J6-r1-131105-part2.mp4
J6-r1-131105.mp4

Test J11

Failed, and nothing shows whether our setup or the app caused it: Final private connection and After readd request unproved.

J11-r1-104308.mp4

Test J12

Blocked: Fixture cannot offer the required payable LNURL endpoint.

J12-r1-104308.mp4

Test J13

Blocked: Automatic issuer confirmation was not proved.

J13-r1-104308.mp4

Test J16

Blocked: Fixture relay failed during combined authorization.

J16.mp4

Test J18

Blocked: Native profile setup timed out before the request could be seeded.

J18-r1-104308.mp4

Test J19

Blocked: Required deadline and invoice controls are unavailable.

J19-r1-104308.mp4

Test J20

Incomplete: the run ended before this test finished.


Test J21

Failed, and nothing shows whether our setup or the app caused it: Deadline-history fixture was not prepared.

J21-1.mp4

Test J22

Incomplete: the run ended before this test finished.


Tip

Tests 1, 4, 6, J4, J5, J8, J10 worth a journey
Test 1
  • Launch with the native transport fault enabled
  • Press Home and return to Bitkit twice while restoration is pending
  • Cut and restore the device network while the same attempt is pending
  • Verify Try Again is available after failure
  • Heal the native endpoint and tap Try Again
  • Verify the original profile and pubky return and stay visible

Test 4

  • Create a one-time request in the second wallet and open it in the funded payer wallet.
  • Pause immediately after acceptance commits and verify the persisted acceptance event.
  • Hold the payer's shared-storage lock, resume the response read, and verify lock contention prevents payment.
  • Release the lock, detach the debugger, and restart the payer normally.
  • Refresh payment requests and open the accepted request.
  • Retry payment and verify Bitcoin Sent, Paid, and one acceptance plus one payment proof.

Test 6

  • Open an incoming one-time request
  • Swipe to pay while the invoice callback fails
  • Verify Transaction Failed and wait for Data Backups to show All Synced
  • Stop the original wallet
  • Restore its recovery phrase on the replacement install
  • Verify the accepted request returns
  • Restore the invoice callback and swipe to pay
  • Verify Bitcoin Sent

Test J4

  • Propose a monthly 5000-sat subscription with no deadline and a fixed P2WPKH endpoint; verify the full terms
  • Accept the proposal; verify the unpaid period opens the on-chain send confirmation
  • Cancel from the issuer; keep Bitkit foregrounded and verify confirmation closes after cancellation arrives
  • Verify the send control is absent after confirmation closes
  • Verify zero received at the fixed address and no payment proof
  • Open the expired subscription details; verify there is no Pay action

Test J5

  • Open the menu and Settings.
  • Open General.
  • Verify contact payments are on.
  • Turn contact payments off.
  • Wait for the update and verify the preference is off.
  • Return Home.
  • Reopen Settings.
  • Open General.
  • Verify contact payments remain off.

Test J8

  • Record the current wallet's watch-only account names, derivation paths, and tracking settings
  • Open the fixture Paykit-only auth URL in the wallet
  • Verify the authorization screen shows exactly /pub/paykit with READ, WRITE access
  • Verify Paykit access (id "PubkyAuthPaykitAccess") describes access to private Paykit data and sending Paykit messages, without wallet spending keys
  • Verify watch-only consent (id "PubkyAuthWatchOnlyConsent") is not shown
  • Tap Cancel (id "PubkyAuthCancel")
  • Verify the authorization sheet is dismissed
  • Verify the current wallet's watch-only accounts and tracking settings are unchanged

Test J10

  • Open bitkit://contact?pubky= on the payer
  • Verify Contact Detail opens with the requester's name (id "ContactViewName")
  • Tap Pay (id "ContactPay")
  • Verify the Request or Pay sheet (id "RequestOrPaySheet") appears with a Pay and a Request button
  • Tap Pay on the sheet
  • Verify the sheet stays open, its Pay button shows a loading spinner and the Request button is disabled
  • Verify the send amount screen (id "SendAmount") opens within 3 seconds and the Request or Pay sheet is gone
  • Close the send amount screen without paying
  • Verify Contact Detail is visible and Pay (id "ContactPay") is enabled
  • Tap Pay (id "ContactPay")
  • Verify the Request or Pay sheet (id "RequestOrPaySheet") appears
  • Tap Request on the sheet
  • Verify the Payment Request amount screen (id "PaymentRequestAmount") opens

Replies:

Error toast: not exercised. (comment)

The localized toast remains a device QA gap. Contact Saved deletion is fixed in 291328d and separately confirmed in the canonical thread. The rc68 relaunch and payment waits remain open in #868; rc69 changes contention backoff without establishing current mobile latency.

Note

Retest Suggested 1, 3, 5, 7, J1, J3, J5, J7, J9, J10, J11, J12, J16, J17, J19, J22

@ovi-reviewer retest 1,3,5,7,J1,J3,J5,J7,J9,J10,J11,J12,J16,J17,J19,J22

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

piotr-iohk
piotr-iohk previously approved these changes Oct 7, 2026
@ben-kaufman
ben-kaufman dismissed stale reviews from piotr-iohk and jvsena42 via 3420760 October 7, 2026 14:56

@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 at 3420760c, inspecting the entire delta since 480b8d25, affected recovery callers/tests, and the complete current PR inventory. Supported unchanged coverage is inherited from the completed baseline; ancestry and unchanged base/merge base were verified locally.

No new actionable code findings.

Temporary restoration failures receive eight short retry delays before exponential backoff; invalid-credential failures retain ordinary backoff. Cancellation and identity revision changes end pending retries without shortening SDK safety waits. Targeted contract checks used Paykit dd97fc9a and Android 804e7547; this does not establish a full Android review or current-platform runtime equivalence.

Validation: both changed Swift files passed syntax parsing. A standalone Swift harness using the exact scheduler body passed timing, jitter/cap, mixed-failure and exit checks with restoration stubbed. Native XCTest assertions were inspected, not executed. Code-result and publication-preview validation passed. Pinned-head unit and integration CI were still running when collected. The author reports 551 focused tests and repeated scheduler tests passing.

The startup-latency concern remains open for a same-device staging retest: the inter-attempt delay is addressed in source, while the underlying lock hold and reported five-minute stall remain unverified. Broader performance work and remaining author fault-injection journeys remain outstanding; older scoped runtime reports do not establish a pass at this head.

Device testing: not performed in this review. Concurrent wallet/shared-Pubky installations and pre-2.6.0 profile compatibility remain excluded under the supplied product rules.

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

lgtm

@ovitrif
ovitrif merged commit 51df7e9 into master Oct 7, 2026
17 checks passed
@ovitrif
ovitrif deleted the codex/paykit-shared-runtime-local-20260930 branch October 7, 2026 17:02
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.

chore: update paykit to the pubky 0.14 release

4 participants