Repository navigation
feat: share paykit state across apps - #856
Conversation
|
There was a problem hiding this comment.
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)
piotr-iohk
left a comment
There was a problem hiding this comment.
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.
jvsena42
left a comment
There was a problem hiding this comment.
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, withrawUrlre-checked inapproveAuthRequest. The requesterclientIDandrelayOriginare displayed. A Paykit-only claim skips watch-only account allocation, while a combined claim still goes through the watch-only consent step.PubkyAuthClaim.encoderefuses 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 toacceptedPaymentEndpointIdentifiers, and the post-broadcast lookup reuses the capturedcontactPaymentContext. - No app-group or keychain-access-group changes, and
Env.keychainGroupstays private. - Biometric and PIN checks run in
submitPaymentbeforeperformPaymenttakes the execution claim, so declining auth leaves no claim. - Not raised, because nothing reaches them today: claims are never released on abandon (no
releasePaymentRequestExecutionClaimcall 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.
There was a problem hiding this comment.
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 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)
|
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 |
piotr-iohk
left a comment
There was a problem hiding this comment.
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.
jvsena42
left a comment
There was a problem hiding this comment.
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.ensurePaymentAllowedrequiresisApprovedForPaymentand re-checks generation, identity and approval after the asynclinkedPeerscall. A stale proposal on the second install fails at the SDK accept, which re-validatesProposedinside the locked transaction. - Backfill is skipped while the identity, contact snapshot, activity revision and reservation revision are unchanged. Every
ActivityServicewrite 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.xmlmatches 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.
23d6ddb to
fe281dd
Compare
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
@ben-kaufman please resolve the merge conflicts. QA review has not been performed for this request.
piotr-iohk
left a comment
There was a problem hiding this comment.
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.
jvsena42
left a comment
There was a problem hiding this comment.
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.
|
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. |
|
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. |
There was a problem hiding this comment.
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
|
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. |
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 Relaunch (terminate, launch), first log line →
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 ( Fast launch, B2 ( The 363 s launch, A2 ( 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 |
Device gate (partial 2) — b633684 (Paykit rc69)Payment request round trip, both directions, UTC 2026-10-07. No
Both payments completed and both sides show the activity. Payer log for the second one ( Seen in passing on the payer after the first payment: |
jvsena42
left a comment
There was a problem hiding this comment.
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.
piotr-iohk
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
I agree on merge now and improve the performance on follow up PRs
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.






























































































Closes #815
This PR moves Bitkit to Paykit's identity-wide shared state using the published
0.1.0-rc69SDK.SDK: https://github.com/pubky/paykit-rs/releases/tag/v0.1.0-rc69
Companions: Android #1401, Paykit Server #46.
Description
Out of Scope
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.xmlwith 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
PaykitReceivedPaymentContactsTests.swift- combined attribution, cache invalidation, and a database-backed test of backfill retry, saved contact attribution, and skipped completed scans.AddressSearchCoordinatorTests.swift- companion-account lookup, isolated search indexes, and conservative handling of unknown outputs.PaykitSdkClientConfigTests.swiftandPubkyProfileManagerTests.swift- shared identity setup, cached key reuse, rotation and rollback rejection, and identity switching.PaykitBackupStateTrackingTests.swift- cached backup fingerprints and rechecking uncertain writes.PubkyAuthRequestTests.swift,PubkyAuthApprovalSheetTests.swift, andWatchOnlyAccountServiceTests.swift- independent claims, combined consent, and malformed request rejection.PrivatePaykitServiceTests.swift,PaykitContactLifecycleTests.swift, andContactPaymentsServiceTests.swift- publication ordering, deferred work, contact cleanup, and attribution.PaykitPaymentRequestServiceTests.swift,PaykitPaymentProofServiceTests.swift, andPaykitPaymentStateBackupTests.swift- request destinations, execution ownership, fresh snapshots after state changes, and retained wallet payment state.PaykitPaymentActivityTests.swiftand updatedPaykitSdkOperationLockTests.swift- deferred proof refresh and delivery, payment-priority barriers, cancellation, and backup admission across wallet changes.PaykitReceiverNoiseKeyStoreTests.swift- keys belong to the identity, not individual receivers; authorizer coverage is inPaykitSdkClientConfigTests.swift.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.