Skip to content

feat: separate public payment sharing - #1095

Merged
ovitrif merged 14 commits into
masterfrom
codex/ios-public-sharing-privacy-20261021
Oct 30, 2026
Merged

ovitrif merged 14 commits into
masterfrom
codex/ios-public-sharing-privacy-20261021

Conversation

@ben-kaufman

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

Copy link
Copy Markdown
Contributor

Twin: synonymdev/bitkit-android#1668

This PR:

  1. Reserves public Bitcoin addresses separately from ordinary Receive and private-contact addresses.
  2. Splits private contact payments and public payments into independent settings, with public sharing opt-in.
  3. Preserves known address exclusions across rotation, opt-out, cleanup, restart and backup restoration.

Description

  • Allocates a dedicated public Bitcoin address because publishing the ordinary Receive address unnecessarily links manual receiving to the public profile.
  • Persists reservations before exposing them, retains retired indexes, and rotates a used public address after activity persistence without rotating ordinary Receive.
  • Adds independent private and public sharing controls because consent to exchange contact-specific details is not consent to publish payment details for everyone.
  • Keeps private onboarding private-only; public sharing starts off and requires a separate explicit choice.
  • Migrates the old combined choice to public-off with removal pending, retaining private consent because the old UI did not obtain separate public consent.
  • Quarantines known legacy public addresses and merges backup exclusion ceilings so an older restore does not release already-known reservations.
  • Carries the initiating session through publication, withdrawal, registry admission and acknowledgement so stale operations cannot modify a replacement session; admitted writes retain their existing drain behavior.

This is a proposed Bitkit application policy, not a Paykit protocol change. The two controls permit all four combinations. Private-only sharing keeps the private App Registry capability but publishes no public endpoints; public-only sharing does not authorize private contact lists. Turning either off preserves the other choice.

Address separation reduces unnecessary reuse; it does not provide anonymity. Public details still identify the profile, and removing an endpoint cannot erase copies already obtained. Optional backup metadata preserves known exclusions; seed-only restoration and old backups cannot reconstruct unknown exposure history.

Out of Scope

Design

N/A - no design available. Reuses the existing General Settings controls and typography.

Preview

The current row has no permanent explanatory paragraph. A fresh disposable-wallet Settings capture verified the updated presentation; the attachment below predates that change.

Historical preview before the settings-row follow-up private-only-settings

QA Notes

Setup

Use this PR's pinned Package.resolved dependencies and a dedicated unfunded simulator. Build Debug with SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_BUILD, E2E_BACKEND=network, and E2E_NETWORK=regtest; use a concrete simulator with XcodeBuildMCP. Do not erase an existing wallet. In Advanced Developer Settings enable Paykit, then create a local disposable Pubky identity through the normal UI and complete private-contact onboarding. Leave public payments off initially. Resolve the unrelated USDT introduction without funding or sending.

J1 needs one wallet and working Pubky/network access. J2 additionally needs a saved, linked disposable contact. J3 requires the separate compatible marketplace fixture described in its suite README, seller and buyer identities, grants and funded regtest wallets; ordinary staging alone is not sufficient. It is listed because its sharing setup changed, not because a funded checkout was run.

Journeys

  • J1: new public-private-sharing.xml - independent switches, explicit public opt-in, restart persistence and unchanged ordinary Receive.
  • J2: updated contact-payment-sharing.xml - private sharing stays off after leaving and returning to Settings.
  • J3: updated wallet-leg.xml - marketplace setup explicitly enables private contact sharing.

Manual Tests

  • 1. Inspect remote public endpoints and a linked contact's private list through all four sharing combinations, including zero contacts -> public, ordinary Receive and contact-specific Bitcoin addresses differ, and capability/withdrawal match the selected controls - remote storage/SDK inspection not in Capabilities.

  • 2. Hold or fail publication/removal, opt out, cancel or replace the identity, then release -> stale work cannot mutate or acknowledge the replacement session, admitted writes drain, repair stays pending and exposed reservations remain excluded - controlled SDK/write gates not in Capabilities.

  • 3. Upgrade a pre-split wallet and restore current/older backups -> private consent remains, public consent does not resume implicitly and known address exclusions remain - legacy data injection and controlled backup restore not in Capabilities.

  • 4. With a separately authorized operator-supplied funded regtest fixture, publish address P, disable public sharing, batch-pay ordinary Receive R and P so activity displays R, confirm and spend P, restart, then re-enable public sharing -> publish a fresh address, never P; ordinary Receive and all prior exclusions remain unchanged - controlled batch funding/spending and durable history inspection are not in Capabilities.

Automated Checks

  • added PublicPaykitAddressReservationTests.swift - public/private/ordinary separation, save-before-exposure, retirement, backup exclusions and post-persistence rotation with ordinary Receive unchanged.
  • updated AppDataMigrationsTests.swift - one-time public-off migration preserves private consent and later explicit opt-in.
  • updated ContactPaymentsServiceTests.swift - independent controls, cancellation/rollback, identity changes and zero-contact capability recovery.
  • updated PaykitSdkIdentityCheckTests.swift - live registry wrappers reject queued stale sessions and preserve admitted-write draining, including private cleanup's actual caller.
  • updated PublicPaykitServiceTests.swift - cleanup retry/acknowledgement ownership and publication modes.
  • updated PrivatePaykitServiceTests.swift, ReceiveInvoiceRefreshTests.swift, WalletViewModelReceiveTests.swift - private-only setup and reserved-address exclusion without changing the ordinary invoice.

Evidence

Current Integration, October 28

Signed 63ac124ea integrates actual master 681f5aaa9, retaining public-sharing restore admission, wallet-before-metadata exclusions and the lock-owned backup subscriptions alongside upstream independent USDT recovery. Temporary wallet-backup contention now remains retryable at the metadata dependency boundary; the existing bounded retry cadence is unchanged. Both parent fixture behaviors and the subscription regressions are preserved. Released dependency pins are inherited exactly from master, not discretionary changes.

Validation is deliberately qualified: the full composed scheme ran 2,854 passing native methods plus one failing auto-selected localhost fixture method, and 10 passing / four skipped unique UI methods (43 expanded UI executions). A stale FFI preflight failure and the fixture failure remain retained. Ben then authorized an isolated owned fixture and an explicit loopback-only test selector, removing automatic contact with the protected service. All existing assertions remain intact. On the final selector-only delta, that one native method passed six separate prepare/resume/restore invocations across rejection and commit-with-lost-acknowledgement scenarios, with zero failures, skips or retries. Signed operations remain identical and each recovery acknowledgement precedes submission; this is unfunded local protocol evidence, not deployed-contract or manual sharing QA. All other 1,641 inputs are unchanged from the full-scheme run; there was no duplicate whole-suite rerun or erased original failure.

Final full SwiftFormat: zero findings on 822 files, with 89 configured skips; translations retain zero errors and 15 actual-base-matched warnings. The final E2E-configured network/regtest build passed, compile-only. Complete diagnostics, structured results and fixture event ledgers were audited once, with AppIntents, priority-inversion, CoreGraphics and artifact/API limitations retained. Independent combined/recovery source reviews and the bounded selector review are clean. All 11 actual-base PR commits verify locally and GitHub reports verified=true, reason=valid for each.

Historical hosted 4441a404 results are now completely audited: 3,000 native passes, 10 UI passes/four UI skips, integration 30 passes/four explicit Blocktank skips, and selected E2E 13 unique passes/one explicit skip with no case retry. Those results use target 682acdea1, not this 681f5aaa9 composition; missing E2E media/runtime artifacts and runtime warnings remain documented. New-head hosted checks are pending, not passes. Ready-for-review / 3.2.0, existing QA IDs and checkbox states, legitimate-environment/legacy/session acceptance limits and the separate deferred, unrun J3/GCS case remain unchanged. No PR merge, CI retry, history rewrite, real funds, cloud or device reset.

Target reconciliation at 16:50 UTC: the integration/validation recorded below used b666ccb18; live master has advanced to 3d16b6200. Product head c30393de2 remains unchanged and mergeable. The live-target range remains the same six commits, all locally and GitHub verified. No new master merge or history rewrite was performed. Existing validation belongs to its recorded source/merge checkout, not a new runtime validation of 3d16b6200; paired/upgrade/controlled-write acceptance remains incomplete. This follow-up changes description/triage only.

Author validation on signed commit 1b1fd6a5 (committed binary diff 818ca726 exactly matches the tested candidate), based on 4530ff7, using iPhone 17 / iOS 26.5 and the old-Xcode toolchain:

  • Native suite: 2,758 unique passes, zero failures/skips; full CLI and xcresult inventories reconcile. The six existing CI integration exclusions were unchanged. Focused live-cleanup/admission checks: 165 passes.
  • Unfunded UI allowlist: 10 distinct cases / 39 passing executions, including 30 launch configurations, zero failures/skips. This is separate from the manual journey below; three hardware cases and the funded USDT-fork case were not selected.
  • Full SwiftFormat 0.64.0 passed (0/834 files need formatting; 57 configured skips). Translation validation: zero errors and 15 warnings exactly matching the freshly checked base output. Diff check passed. Rebuilt source retained warnings-as-errors with the existing deprecation exception; final native run was incremental, not another full compile.
  • J1: 17/17 actions passed on a dedicated unfunded, normally created profile. All four sharing combinations behaved independently; private-on/public-off survived app restart; ordinary Savings Receive remained unchanged.
  • Partial manual case 1: anonymous public storage reads showed public Bitcoin P different from ordinary Receive R; private-off preserved P and changed the private capability to false. Public-off returned 404 for both published BTC/USDT endpoint files, also after private-on/restart. Explorer directory entries remained stale, so absence was verified by direct file reads. No linked-contact C comparison is implied by this zero-contact result.
  • Independent correctness/repository-native/polish review and bounded correction rechecks completed without unresolved findings. Initial compiler fixture errors were corrected before the final passing runs; no assertions, exclusions, safety waits or checks were weakened.

Runtime diagnostics retained: startup node/balance-not-ready messages, stale staging RGS data, geo restriction, two USDT refresh warnings, a Pubky republish wait warning and simulator accessibility/IOSurface notices. The affected logging paths were compared with the base; this is not a matching-base runtime reproduction or a clean-runtime claim. No new public-sharing failure appeared in the completed J1 observations. The source patch applies to actual master 35655ae8; local tests ran the 4530ff7-based branch, not a new merge with that target.

Original Published Validation

Signed implementation 1b1fd6a5 is followed only by the exact content-preserving changelog rename c1632841 (1095.security.md). At that publication, both commits in the complete PR range verified locally and on GitHub (verified=true, reason=valid) against actual target 35655ae8; no target merge or history rewrite was performed. The Preview uses the approved disposable-wallet screenshot, not personal wallet data.

Before the settings-row follow-up, hosted SwiftFormat and translation validation passed. The full logs and annotations were inspected: SwiftFormat 0.64.0 reported 0/841 files needing formatting with 61 configured skips; translation validation reported zero errors and 15 warnings already inspected in the local/base comparison. Hosted checkout af0660d1 combines this head with 35655ae8. Seventeen draft-skipped build/test/E2E jobs or wrappers are not passing tests; the local app tests above did not run a newly merged target.

Settings Row Follow-up

Signed follow-up 825250d1 removes the permanent explanation beneath Public payments so it matches the neighboring label-and-switch settings. The unused warning is removed from all 15 locales; only J1 action 5 changes, with all 17 actions retained. Public sharing remains opt-in. Independent private consent, reservations, authorization, cancellation, loading/disabled/error handling and safety waits are unchanged. No unit tests were added, removed or weakened for this presentation-only delta.

Fresh canonical native validation passed 2,758 unique tests with zero failures or skips, independently reconciled against xcresult, with the same six existing live/integration exclusions. SwiftFormat 0.64.0 passed with 0/834 files requiring formatting; translation validation reported zero errors and the same 15 verified-base warnings. The E2E simulator app build passed, with app warnings-as-errors flags preserved. Complete diagnostics were inspected: source compiler errors/warnings were zero; AppIntents metadata notices, fixture failures, asynchronous mock diagnostics and simulator haptics messages remain qualified. Their 38 source files are unchanged by this follow-up, not proof of full base-runtime equivalence.

A bounded Settings capture on the preserved disposable iOS wallet verified the label-and-switch presentation without the paragraph or overlap, with private sharing on and public sharing off. Neither sharing switch nor a payment action was exercised; this is not a new full J1/J2/J3 pass. Startup node/balance messages, RGS gossip failure and the identity-republication wait-cap warning were retained, not diagnosed as fixed or base-reproduced. Both owned simulators were shut down with data directories preserved.

One independent combined correctness/repository-native/polish review found no actionable issue in the frozen follow-up. All three commits in the complete PR range verified locally and on GitHub (verified=true, reason=valid) against actual target ec2d68a5 at push. At that historical head, hosted SwiftFormat passed (0/842 files, 63 configured skips), and translation validation passed with zero errors and 15 hidden warnings; hosted checkout 2c502ba7 combines this head with ec2d68a5. Draft-skipped test/build/E2E wrappers are not passing tests. Local tests ran the 4530ff7-based feature branch, not a newly merged target; master subsequently advanced to e968e218 without a branch update or new combined-target test claim. This PR stays draft and all six QA checkboxes remain unchanged.

Master Update And Address Repair

October 24: signed merge 6fece930 brings in master f7074d59, preserving both conflict branches, all assertions, master cleanup deferral and the feature's injected SDK/original-session guards. Independent combined review also found an existing feature-path race: a delayed older public Bitcoin snapshot could overwrite a newer reservation and clear pending repair. The correction binds snapshots to the reservation owner's revision, performs guarded acknowledgement inside the existing publication lock, and re-arms pending before an admitted write. Original identity/consent and cancellation/drain behavior remain; no persisted schema, SDK/protocol, dependency choice or safety wait changed.

Final validation on the committed tree: 248 focused and 2,906 unique CI-selected native tests passed with zero failures/skips; the six existing live/integration exclusions are unchanged. The final E2E-configured simulator app build passed (compile-only, no journey or payment execution). SwiftFormat 0.64.0 checked the full tree: 0/844 files require formatting, 75 configured skips. Translation validation has zero errors and 15 warnings whose complete output exactly matches verified actual master. All 1,654 frozen input hashes and full patch were verified after checks and commit.

Not first-attempt clean: the initial final native run failed one Ring-approval wait case (Core returned after the existing two-second expectation). The unchanged method and 11-test class passed on both candidate and actual master; one subsequent unchanged full-suite confirmation passed. Cause/shared-process contribution remains unproven, not baseline reproduction or a root-cause fix. No assertion, timeout or exclusion was weakened, and no hosted CI retry was dispatched.

Complete xcresult inventories and diagnostic logs were inspected. Final native logs retain injected failure/cancellation, mock persistence/order, shared-keychain, geocheck timeout and simulator haptics diagnostics; source equality is not complete base-runtime equivalence. Final build retains the AppIntents metadata warning, with no Swift source compiler warnings/errors and warnings-as-errors/deprecation exemption unchanged. Four regression methods were added and existing assertions retained. The original race was reproduced with signature-adapted behavior-equivalent production, not an untouched-source red; the later cancellation variant was not separately run red. Independent combined correctness/repository-native/polish review and bounded final readback found no unresolved issue. The delayed-ack scheduling and successful-return-after-cancellation variants are statically checked, not separate deterministic runtime cases.

All four PR commits verified locally and on GitHub (verified=true, reason=valid) against actual master f7074d59 after the normal push. Prior hosted results are historical; new-head hosted checks are tracked separately. The PR remains draft, all six QA checkboxes are unchanged, and none of this establishes fresh J1/J2/J3, funded, physical-device or performance acceptance.

Restore Safety Follow-up

October 26, signed commit 423cd22c6f2c78ddf18c3a7f7c196b48673e4c7a: legacy Android METADATA.cache.onchainAddress is imported into the existing public-address exclusion set. Restore retains the WALLET backup obligation before importing the address, and METADATA replacement waits for a successful WALLET backup containing that exclusion. In-flight and cancellation-insensitive METADATA uploads drain before restore imports new exclusions. No SDK, protocol, dependency, remote backup format, timeout or safety-wait change.

Final validation on the committed tree: 39 focused tests and 2,919 unique CI-selected native tests passed with zero failures/skips; all six existing live/integration exclusions remain. The native run includes all 29 QuickPaySpendStoreTests. SwiftFormat 0.64.0 passed on the full tree (0/844 files needing formatting; 75 configured skips). Translation validation reported zero errors and 15 fully inspected warnings, exactly matching a fresh validator run on actual master 65d0ae38. The network/regtest E2E-configured app build passed, compile only. All 1,651 regular tracked inputs and four tracked symlink targets were verified; committed patch SHA256 59dcc4f46d7307de23a82eb3706192c99bac4ad859214e6e8f75f0f362fa5297 matches the frozen reviewed and tested repair.

Thirteen regression methods were added without removing tests or weakening assertions. Unchanged-production regression runs reproduced legacy-address loss, premature remote METADATA replacement and early return while a cancelled admitted upload remained active. A separate initial simulator-access failure occurred before compilation and was resolved through approved access, without resetting a device. Independent combined correctness/repository-native/polish review and bounded correction rechecks found no remaining actionable finding on the final patch.

Complete structured test inventories and diagnostics were inspected. Native fixture/network/keychain/mock/geocheck/haptics messages and build AppIntents metadata warnings remain qualified; automatic discovery of a locked physical device did not involve a physical-device test. App warnings-as-errors and the existing deprecation exception remain enabled. This is not a warning-free runtime or matching-base runtime claim. Master advanced without a new merge; tests validate this feature tree, not a newly combined tree with 65d0ae38.

All five commits in the current PR range verified locally and on GitHub (verified=true, reason=valid) against actual master 65d0ae38, after a normal push. New-head hosted SwiftFormat and translation validation passed on GitHub's merge checkout 08451536 (423cd22c plus 65d0ae38). Complete 194/193-line logs and all check annotations were inspected: formatting reported 0/845 files needing changes with 77 configured skips; translation validation reported zero errors and 15 hidden warnings whose texts were inspected in the local candidate/base runs. Runner Ubuntu migration notices are retained. Seventeen draft-skipped jobs are not native, integration, build or E2E passes; no hosted retry was dispatched.

Current device prerequisites remain explicit: the fresh dedicated iPhone 16 Pro / iOS 26.5 simulator is at Terms/Privacy acceptance, which the human operator must complete personally. No wallet, fresh J1/J2 execution, linked C/P/R comparison, controlled device restore/failure acceptance, funded J3, physical-device or performance acceptance is claimed for this head. All six stable QA checkboxes remain unchecked. The PR stays draft pending the remaining review-readiness work; Ovi (ovitrif), Joao (jvsena42) and Piotr (piotr-iohk) are already requested.

Current-master Integration, October 27

Signed merge c30393de2e976d44ce9f9c79ad94f6698f140009 integrates actual master b666ccb183837d9b49cf01809dad41166bd04702 without rebasing or rewriting history. The PaykitSdkIdentityCheckTests.swift conflict resolution retains both cleanup and signed-in-peer fixture behaviors in a single linkedPeers implementation, all 26 tests and all 132 assertions. Only the three cleanup tests select the original empty-peer fixture explicitly. Production integration follows master without a discretionary dependency, SDK/protocol or safety-wait change.

Final combined-tree validation passed: 26 focused identity tests; 2,967 unique CI-selected native tests, zero failures/skips; full SwiftFormat 0.64.0 (0/846 needing formatting, 81 configured skips); translations with zero errors and 15 fully inspected warnings exactly equal actual master; and the final network/regtest E2E-configured app build. The six existing live/integration exclusions are unchanged. All 1,666 tracked inputs, including four symlinks, match the frozen reviewed/tested tree. Build success is compilation, not journey execution.

Complete xcresult inventories and diagnostic reports were inspected. Retained output includes injected failures, fixture/network/keychain messages, GeoCheck403, UI-test appearance transitions, old test-log deletion races, simulator haptics and automatic locked-device discovery failures. The build retains one BitkitNotification AppIntents metadata warning; app warnings-as-errors and the existing deprecation exception remain enabled. No physical-device action occurred. These qualifications are not a warning-free-runtime or fresh base-runtime claim. Independent combined correctness/repository-native/polish review is clean, source review rather than independent runtime.

All six actual-base PR commits verify locally and on GitHub (verified=true, reason=valid). Hosted SwiftFormat 122085610146 / run 40473384629 and translation 122085609975 / run 40473384605 passed. Complete 194/193-line logs and all annotations were inspected: 0/846 formatting changes with 81 configured skips; zero translation errors with the same 15 warnings inspected locally. Hosted merge d7558c2a has exact parents c30393de and b666ccb1, and its tree matches the validated tree. Ubuntu runner notices remain. Seventeen draft-skipped jobs/wrappers are not native, build, integration or E2E passes; no CI retry was dispatched.

Fresh device journeys remain incomplete. The preserved QA devices were last observed at personal Terms/Privacy acceptance; no agent acceptance or bypass occurred. Historical J1, the J2 linked-contact prerequisite, manual cases 1-3 and the separately funded J3 fixture retain their stated limitations. This integration does not satisfy those device acceptance gaps. All six QA boxes remain unchecked and all three named reviewers remain requested. October 27 parent orchestration and independent review metadata verify gpt-6-astra / xhigh; earlier unknown metadata remains unknown.

Gaps

J2 mutual-contact setup completed through the normal Save flow on both disposable wallets. Empty endpoint caches after Save are not proof of failed message delivery: the app populates those caches during payment resolution, and its published flag denotes queued publication rather than peer receipt. A single normal Contact Pay preview at 23:28 UTC, with no amount, confirmation or submission, finished back on the Contact screen. The log reports recovery_required/recovery_required; the exact-peer Bitcoin cache was still empty afterward. Earlier logs also contain handshake-checkpoint, Pubky write-lock, republish and backup errors. Their cause and any PR contribution are not established or base-reproduced. No retry, forced recovery, wallet reset or safety-wait change was used. J2's eleven private-off actions and linked C/P/R comparison remain unrun because the linked prerequisite was not established.

J3 was not run: it needs the compatible marketplace fixture and funded wallets, outside this unfunded validation. Manual case 1 remains partial; cases 2-3 are native-regression coverage, not controlled device failure/upgrade/restore acceptance. No payment-triggered device rotation, real identity-replacement device test, physical-device test, general performance claim or seed-only recovery of unknown reservations. This PR remains a draft rather than claiming complete manual acceptance.

October 27 Device Follow-up

This is current-head author evidence, superseding earlier unresolved setup statements without erasing their diagnostics. Both dedicated wallets are unfunded, normally created disposable profiles, mutually saved and linked. Human legal consent was completed before these checks; no consent preferences, seed export, wallet reset or geographic bypass was used.

  • J1 and J2 are complete on the installed current-head artifacts: independent switches, process-restart persistence, unchanged ordinary Receive, and linked private-off persistence after leaving/returning to Settings. Android completed after the preserved ANR/uncertain-touch episode was reconciled; this is not a clean first-attempt or proven root-cause/base-runtime result.
  • Manual 1 is partial: exact-SDK anonymous public reads passed all four combinations on both platforms, without credentials or writes. Ordinary Receive, public Bitcoin and linked-contact private Bitcoin addresses are distinct on each wallet. The linked fixture is now established, with both apps having opened private Payment List version 2 and later versioned updates; no amount, financial approval or payment was submitted.
  • J3 is deferred by Ben and remains unrun, not passed. Isolated compatible fixture source/configuration was prepared, but no verified GCS-backed Homeserver deployment or configured cloud account was available. Ben elected to skip the authentication/setup handoff. No cloud resources, billing changes, payment, funds or mining have occurred.
  • Manual 3 has additional instrumented supported current-backup runtime coverage: all six VSS categories were read back first, with local-seed identity, SDK recovery state and reservation ceiling guards. The supported full restore returned successfully with the same identity, private consent on, public consent off and no remaining wallet-restore obligation. An app-only restart retained the profile and those controls. A subsequent live SDK resolution received Android's private Bitcoin endpoint at list version 5 without payment. This is not older-backup/startup-upgrade coverage, an unmodified UI-only restore journey, or complete post-restore exclusion coverage.
  • Remaining acceptance: reciprocal private-list withdrawal for every combination in manual 1; controlled cancellation/session replacement/admitted-write release in manual 2; and older/pre-split and complete post-restore exclusion acceptance in manual 3. J3 is separately deferred by Ben. Native regression results do not replace the remaining device gates.

The exact app input tree remains unchanged from the signed integration and its final native/format/lint/build/signature gates. Full runtime diagnostics retain the identity-republish wait warning, price-response failures and earlier fixture/transport qualifications; no warning-free runtime, funded rotation or physical/performance pass is claimed. Marked ready for review at Ben's explicit request on October 27. Unchecked QA, the regional blocker and the J3 deferral remain documented; review status is not a QA pass or merge/release approval.

Post-restore/restart in-process readback confirms the same identity, private consent on, public consent off, no pending wallet restore and a native-Segwit reservation ceiling of 3 (at least the pre-restore floor of 2). A separate anonymous SDK read confirms both QA identities still publish no public endpoints, with no endpoint-load failures. These results complete the current-backup floor/control readback, not the older/pre-split scenario or every manual case.

Current-target Integration, October 27

Signed 1aece6bf2 resolves the new BackupService conflict and integrates target 543d9389e without rewriting history. Both metadata backup/readiness checks and master's late cancellation/scheduled-suppression checks are retained; cancellation preserves every pending category's backup obligation. Other incoming changes are preserved. No new SDK/protocol/pin, safety-wait, device-reset or payment change is bundled.

Exact frozen tree e199ea2ca, 1,676 tracked inputs (1,672 regular files and four symlinks): 85 focused backup/recovery cases and 2,992 unique full native cases passed, zero failures/skips. The inherited inventory delta is 26 additions minus one removed display-rate case, net 25; this integration added no new test methods. Full SwiftFormat 0.64.0: zero of 850 files require formatting, 86 reported skips. Translation validation: zero errors and 15 warnings matched against the actual integrated target. The E2E-configured build passed compilation only, with zero E2E tests executed. The six canonical unit exclusions and UI journeys remain unrun, not passes.

Two independent source passes cover combined correctness/native/polish and the material backup recovery/cancellation risk. Complete raw logs, action records, all nested test diagnostics, warning stacks and inventories were audited once and the structured report verified by the owner. Native final build-for-testing was incremental; its strict compiler policy reuses the earlier focused build on identical inputs (15 strict project-target driver rows). The separate E2E compile has nine strict project-target driver rows with E2E_BUILD. Results are not transferred between those binaries.

Diagnostics are retained: four real priority-inversion warnings, UI appearance/preference/presentation warnings, AppIntents/simulator/haptics notices, background GeoCheck 403/fixture lookup limitations, expected injected failures, and one unresolved forex missing-data error. Passing tests are not a warning-free, performance, hardware or fresh base-runtime claim. The forex source/logging/test blobs match the integrated target; that is not proof of historical runtime occurrence or cause. No verified integration-introduced defect justified a speculative patch.

Master advanced to 85e9d4c6d during validation with separate allowance-banner changes. That delta was inspected and merges without conflict; no further source merge or whole-suite rerun was added for that unrelated update. Local runtime evidence is for the tested 543d9389e composition, not an executed 85e9d4c6d merge. All seven PR commits against the actual current target verify locally, and GitHub reports verified=true / reason=valid for each. Current-head hosted checks are now audited on synthetic merge edab0ee67 (tree 6b368aaa1, parents 85e9d4c6d + 1aece6bf2), not the pure-head tree. Native/UI run: 2,997 unique native passes plus 10 unique UI-method passes and four explicit UI skips; launch repetitions are not extra unique methods. All 2,992 local native IDs remain, with five target-inherited foreground-notification cases added. Integration run: 30 passes and four explicit unavailable Blocktank purchase/CJIT/refund lifecycle skips. Local E2E run: 13 unique passes and one explicit hardware-wallet skip on executed suite 4c0cbef019220efed01ed3080c1f0ccc1fb627a3, first attempt only, zero failed attempts or completion-lock skips; no Paykit case is selected by this gate. SwiftFormat reports zero of 851 files requiring formatting, with 87 configured/reported skips; translations report zero errors and the same 15 archived-base warnings, expanded on the exact hosted tree. Complete logs, annotations, structured test references and exported diagnostics were inspected. Duplicate-symbol linker, setup/runtime/fixture and npm warnings remain retained, not a warning-free claim. Report artifacts were extracted and local evidence hashed; remote report ZIP digests were not independently verified and the app artifact digest is runner-attested only. These results do not complete unchecked public/private/session/older-backup QA or the user-deferred J3.

The remote push also retained GitHub's default-branch notice of one high-severity dependency advisory; it was not independently triaged or attributed to this PR, and no dependency change was authorized. Ready-for-human-review and 3.2.0 triage remain as directed, not merge readiness or approval. Prior device evidence stays at its original revisions; all unchecked QA/legitimate-environment limits and the user-deferred, unrun J3/GCS remain unchanged.

Latest Monitoring Repair And Integration, October 28

Signed 806b35f7c fixes the unmonitored public-address case and integrates exact master 682acdea1 without rewriting history. Master's published LDK 0.7.0-rc.78 pin and other upstream changes are preserved; no discretionary dependency or protocol choice is introduced.

An existing public assignment now needs rotation when used or no longer monitored. Monitoring changes and post-sync maintenance reach the same guarded publisher; the existing private-contact refresh is queued first. Still-monitored unused assignments remain quiet. Ordinary Receive, reserved-address history, session/consent/deletion/cancellation admission and pending repair are retained. Earlier source passes missed this monitoring case. One behavioral test was added, none removed; the baseline failed to rotate, and the final suite includes the corrected address-type-aware fixture without weakened assertions. The profile README adds the corresponding remote-storage check within manual case 1.

Frozen tree 2d8b7d4de, 1,683 tracked inputs (1,679 regular files and four symlinks): 2,998 unique native tests passed, with no failed or skipped tests. Full SwiftFormat 0.64.0 reports zero of 851 files requiring formatting, with 88 reported skips; translations report zero errors and the same 15 verified-base warnings. The E2E-configured build passed compilation only, with zero E2E tests executed. Six canonical integration exclusions remain distinct from the unit pass. Fifty focused cases and one existing unfunded monitoring integration smoke passed before the final scheduling-only correction; the smoke used wallet:nil, not a public SDK fixture, and is not fresh device acceptance.

One combined correctness/native/polish review and a distinct session/admission risk review are clean after a bounded ordering correction. Complete logs, structured test inventories and nested diagnostics were audited once and verified against the frozen inputs. Earlier red, compile and fixture failures remain retained. Five nested priority-inversion warnings in passing tests remain un-attributed to introduced source; runtime/fixture/GeoCheck, UI/haptics, simulator/toolchain and AppIntents qualifications are retained. This is not a warning-free or base-only runtime claim. All eight current-base PR commits verify locally and GitHub reports verified=true / reason=valid for each.

Hosted results on synthetic merge 8b6f1dd01 (parents 682acdea1 + 806b35f7c, exact validated tree 2d8b7d4de) are now audited for three completed jobs. The original native/UI job remains failed: all 2,998 unique native cases passed, but BitkitUITests.testExample failed in XCUIApplication.launch() while acquiring a background assertion. UI totals are nine unique method passes, one failure and four explicit skips; 43 expanded executions are not 43 unique methods. The recording reaches Terms of Use, and system logs show delayed assertion processing after the test timed out; the ultimate cause is unresolved, not labeled infrastructure-only, flaky, introduced or pre-existing. The identical generic launch test passed once on actual master, which is not causal proof. A separate single-case local diagnosis passed in 5.125 seconds on Xcode 26.1/iOS 26.5 with the exact committed inputs, whereas CI uses Xcode 16.4/iOS 26.2; that local pass does not replace the failed gate. Ben explicitly authorized one diagnostic rerun of only the failed job. That second attempt also failed: 2,997 native cases passed and one backup-foreground case crashed with SIGSEGV in AnyCancellable.store(in:) / Set insertion at BackupService.swift:762. Its replacement test host passed the remaining 2,875 cases; that resumed summary does not erase the crash, and the crashed case had no passing retry. The original UI launch method passed in 13.173 seconds; UI totals are ten unique method passes and four skips (39 passing expanded executions). The complete artifact digest and nested case/crash/diagnostic inventory are verified. This differs from the original launch failure. The single diagnostic rerun grant is consumed; no further job rerun or check weakening was performed.

The integration job passed 30 unique cases with four explicit unavailable Blocktank lifecycle skips and no observed recovered retry. The E2E build passed compilation only. Complete logs, all 33 annotations, structured cases/attempts, nested diagnostics and retained media were inspected; all three downloaded artifact ZIP digests were verified. Linker/runtime/fixture/toolchain warnings, unsupported spindump and four opaque ancillary diagnostic-store limits remain documented. The subsequent E2E job is now completely audited for its available evidence: built-in attempt 1 had 12 passes, one failure and one explicit skip. boost_2 stopped during wallet restore waiting 180 seconds for GetStartedButton, before its RBF assertions. Attempt 2 passed that one failed case while 12 previously successful cases were completion-lock skips and the same hardware case remained explicitly skipped. Thus there are 13 eventually passing unique cases and one explicit skip, not a clean first attempt or a second full-suite pass. The actual executed suite is 4c0cbef019220efed01ed3080c1f0ccc1fb627a3, independently verified from this job checkout. The app archive digest matches the verified build artifact. Failure screenshots/video and nested logs were saved only on the runner and not uploaded after final recovery; they could not be inspected. Full available logs, both annotations and all case/attempt records were inspected. The restore-button view/identifier matches actual master, but the failure cause and introduction remain unproven; it is distinct from the XCTest background-assertion failure. Dependency/runtime warnings and incomplete runtime-version/fixture details remain qualified. The aggregate job executes no tests. No manual E2E retry or product/check/assertion change was made. Prior-head hosted passes remain historical. Prior J1/J2 and device evidence remain at their original installed revisions. Remote monitoring/publication, reciprocal private withdrawal, session/admitted-write and older/pre-split acceptance remain unchecked, and J3/GCS remains user-deferred, unrun and not passed. READY and the 3.2.0 milestone are preserved, not merge/release approval. No protected QA device, wallet reset, geographic bypass, payment or funded fixture was used.

Backup Subscription Follow-up, October 28

Signed 4441a4047 addresses the reachable unsynchronized subscription storage exposed during the second native attempt. Backup listener setup stores subscriptions from both background and MainActor work; a service-local lock now serializes collection insertion and detachment. Cancellation runs after releasing that lock so callbacks can safely register another subscription. Observer selection, upload/restore admission and draining, pending backup obligations, failure timing, dependencies and safety waits are unchanged. This is a collection memory-safety repair, not a lifecycle-generation redesign.

The unsafe registration pattern also exists in actual target 682acdea1. The confirmed crash site supports this focused correction, but the competing writer at the crash instant, historical introduction and matching-base runtime occurrence are not established. Prior hosted failures remain retained; they are not relabeled passing or infrastructure-only.

Final committed tree 6c74847d9, all 1,683 tracked inputs (1,679 regular files and four symlinks) verified: 3,000 unique native passes, zero failures/skips/retries in the selected unit suite. Exactly two tests were added and none removed: concurrent registration/cancellation and reentrant cancellation. The original crashing method is included. The six canonical integration exclusions remain unexecuted, not passes. Twenty-nine focused backup/recovery cases passed before only the final declaration-comment normalization; the full suite covers that exact final source. The E2E-configured network/regtest app build passed compilation only, with zero E2E tests executed.

Full pinned SwiftFormat 0.64.0: zero of 851 files require formatting, 88 reported skips. Translation validation: zero errors and the same 15 warning-specific findings verified against actual target. Complete CLI, structured cases/attempts, every nested diagnostic reference and retained attachments were inspected once and their hashes verified by the owner. Four runtime priority-inversion warnings, AppIntents, simulator/UI/fixture/geographic diagnostics remain retained; passing tests and source equality do not establish runtime pre-existence or a warning-free result. Initial non-Sendable compiler errors and formatting failures were corrected and retained, without suppressing compiler checks or weakening assertions.

One combined correctness/native/polish review and a distinct concurrency/recovery risk pass found no unresolved introduced finding. A bounded recheck covered the subsequent explicit lock-owned Sendable contracts and comment-only normalization, reusing unaffected review coverage. Existing deferred-registration/start-stop generation limitations remain outside this narrow repair. All nine commits in the complete actual-target PR range verify locally and GitHub reports verified=true / reason=valid for every commit after the normal push; no history rewrite or PR merge.

Current-head hosted checks are tracked separately; local validation is not a claim that they all passed. Earlier device and hosted evidence stays at its original revisions. All six QA IDs and checkbox states remain unchanged, including the remaining remote monitoring/private-withdrawal, controlled session/admitted-write and older/pre-split acceptance. J3/GCS remains user-deferred, unrun and not passed. READY for human review and the 3.2.0 milestone are preserved, not merge/release approval. No protected QA device, reset, consent/geographic bypass, payment or new fixture was used.

Batch-output Usage, October 29

  • Fixed in signed commit ae5a2c71: the existing queued address-usage utility retains its activity fast path and checks every stored transaction output. Read failures propagate. One real temporary-Core-database regression passed for secondary batch outputs in default and hardware-wallet scopes; no new state, protocol, API or dependency pin.
  • Full scheme: 2,870 unique logical cases = 2,865 passes, zero failures, five explicit skips. Expanded launch parameters/repetitions give 2,894 passes plus the same five skips, not failure retries. The new batch-output method passed. Skips are the opt-in native USDT fixture, three disabled Trezor UI cases and the unavailable local-Arbitrum-fork UI case; four configured unit-workflow exclusions remain distinct, never passes.
  • Full SwiftFormat lint: 0/822 require formatting, 89 configured skips, no edits; the earlier unsupported-formatter refusal is retained. Strict compiler flags stayed enabled. E2E network/regtest build passed compilation only; it was not installed on the preserved manual-QA devices. Complete logs, case identities, nested diagnostics and available exports were inspected by one accountable auditor. All 17 runtime priority-inversion occurrences and AppIntents messages remain qualified, not a warning-free or baseline-cause claim; no diagnostic node points to the new regression.
  • Independent combined correctness/native/polish and separate privacy/queue checks are complete. All 12 actual-target PR commits verify locally and GitHub reports verified=true, reason=valid. Local validation uses integrated master 681f5aaa; publication target 25f8225b merges cleanly in object-only analysis but was not locally runtime-tested as a combined target. New-head hosted CI is not claimed passed here.
  • New manual case 4 is UNRUN, not passed; no funded batch/spend/restart device journey or new fixture work occurred. Existing manual cases 1-3, legitimate-environment/legacy prerequisites and user-deferred J3/GCS remain. Ready for 3.2.0 human review does not mean merge-ready or complete manual acceptance.
  • Matched execution metadata: this parity/implementation used gpt-6.1-sol / high; parent implementation used xhigh; independent source/risk and complete diagnostic review used high. Historical verified pairs and unknown fallbacks are retained without retroactive inference.

Published-head CI, October 29

  • Hosted checks for ae5a2c71 used app checkout 907f23ae, tree e3e0378f, with actual target parent c0b4bc1c. Native-suite results: 2,890 logical passes and five explicit skips, expanded to 2,919 passes plus the same five skips by launch configurations; zero failures or XCTest reruns. This includes 2,880 unit passes and 10 unique UI passes. The separately selected integration suites passed 30 unique cases, zero failures/skips, retaining one recovered internal sync timeout. Each runtime job retains 571 duplicate-symbol warning occurrences, not a warning-free claim. Complete available case objects, activity logs, diagnostics and annotations were inspected. Hosted formatting reports 0/823 findings with 92 configured skips; translation validation has zero errors and 15 exact-actual-base warnings.
  • The local E2E job passed on independently verified suite 4c0cbef0: 14 selected cases, 13 executed once and passed, one explicit hardware_wallet_3 skip, zero emitted case failures or E2E retries. Reporter runtime was 52m 22s. Other disabled Blocktank purchase cases were excluded by the selector, not extra runtime passes/skips. Four simulator log-stream failures/restarts, 70 stale-element warnings and the suite's dependency/deprecation diagnostics remain retained. No E2E media, detailed simulator/Appium logs or original structured case-report artifact was uploaded; the audit's case inventory is reconstructed from the complete available reporter log, not independent visual acceptance.
  • The consumer identifies producer artifact 12856934862 with the independently verified ZIP digest. The archive's embedded ad-hoc signature is present, but strict deep verification fails because upload omitted a sealed hidden build marker. The responsible upload/project configuration is identical to the actual target. No installed-consumer binary hash, explicit re-signing evidence, matching-base binary reproduction, source-cause or no-runtime-effect claim is available. Original archive and failed verification are preserved, not repaired to obtain a clean result.
  • These results supplement the recorded local gates and original-revision device evidence. Conditional migration/retarget skips are not passes. Manual cases 1-4, legitimate-environment/legacy/session prerequisites and Ben-deferred, unrun J3/GCS remain unchanged. Ready for 3.2.0 human review is not complete QA, human approval or merge authorization.

Current Integration and Private Consent, October 29

  • Signed history-preserving merge c43402858 integrates master 32ad1977; validated tree 5d30084a. Backup observer generation checks, reentrant subscription cancellation, wallet-before-metadata durability, admitted-write drains and original-session cleanup remain composed. No rebase, force-push or PR merge.
  • A related inherited implicit-consent race is also fixed: the wrapper no longer captures private consent before SDK admission. Its implicit preference is resolved under the existing SDK operation lock after identity lookup, before the final session guard; explicit true/false overrides and identity capability remain intact. The new Swift app-service/fake-binding regression observed both stale directions before the correction and passes afterward, with a separate explicit-override test. This is not a completed-toggle API or real-network reproduction; no new state, lock, binding, protocol or dependency choice was added.
  • Final exact-input checks passed: 29 focused tests; full scheme 2,915 unique passes (2,905 native and 10 UI), zero failures, and five explicit skips. Parameterized/repeated launch execution expands this to 2,944 passes, not extra unique cases or failure retries. E2E-configured network/regtest compile-only passed. Full SwiftFormat has 0/825 requiring changes with 96 configured skips; translations have zero errors and 15 warnings independently verified against actual master. All 1,666 tracked inputs/four symlinks and pinned package inputs matched the reviewed candidate. Two independent reviewers checked the integration and affected consent delta; complete available raw/structured/nested diagnostics were audited.
  • Original red assertions and the pre-test sandbox failure remain preserved. AppIntents/tooling output and 25 nonfatal QoS warnings in pinned Pubky/LDK paths remain documented; matching source is not a fresh master-runtime reproduction or a warning-free claim. The five skips require the owned recovery fixture, AI-only Trezor setup or Arbitrum-fork fixture. The four workflow-excluded integration suites were not run locally. Simulator Debug artifacts are retained, not a release archive or distribution-signing claim.
  • All 13 actual-base PR commits verify locally and GitHub reports verified=true, reason=valid. New-head hosted CI is separately pending audit, not an inherited pass from ae5a2c71. Historical device evidence retains its installed revision; unchecked manual cases 1-4, legitimate-environment/legacy/session prerequisites and Ben-deferred, unrun J3/GCS remain unchanged. READY/3.2.0 is for human review, not completed QA, human approval or merge authorization.

Published-head CI on c434028, October 29

  • The hosted unit/UI job passed 2,915 unique cases (2,905 native and 10 UI), with five explicit fixture skips and zero failures or retries. Launch configurations expand this to 2,944 passing executions, not extra unique cases. The separate integration job passed 30 unique cases with zero failures/skips/retries. Actual hosted app merge 5f4664b8 has validated tree 5d30084a and actual base 32ad1977. Complete available structured/nested diagnostics were audited; linker/runtime warnings, fixture skips, disabled runtime-issue detection and attachment limitations remain qualified, not a warning-free or full-QA claim.
  • The selected E2E job failed on independently verified shared suite 4c0cbef019220efed01ed3080c1f0ccc1fb627a3: 14 selected logical cases = 12 passed, one failed (lightning_1) and one explicit hardware_wallet_3 skip. The same failing test executed in all three built-in workflow attempts; the 12 completed passes were locked out on later attempts, not repeated passes. The aggregate propagates that failure. No agent CI rerun was made.
  • All three failure screenshots show Lightning Connections with a back arrow while the test waits for HeaderMenu. The executed test performs its navigation-back step only on Android. Six selected native navigation files and the close-routing method match actual base 32ad1977; successful close routes back to Connections. This supports a shared-suite/native-navigation contract mismatch, not a proved introduced application defect, conclusive helper-only cause or passing base-runtime reproduction. No accessibility/page-source snapshot was uploaded. The final post-close activity assertion was not reached. The failure is not waived, and no app control, selector, assertion or timeout was changed to obtain green CI.
  • The new E2E evidence archive 12933875782 was size/digest verified and safely inspected, including available simulator logs, all three failure images and videos. Original failures, retries, warnings and missing structured evidence remain recorded. The separate build archive 12928152605 is still incompletely materialized after its retained transport failure; its partial bytes are not binary, UUID or strict-signature verification. Unit/integration evidence cannot substitute for that missing producer audit.
  • This is an evidence-only update. Unchecked manual cases 1-4, legitimate-environment/legacy/session prerequisites and Ben-deferred, unrun J3/GCS remain unchanged. READY/3.2.0 is for human review, not completed QA, human approval, CI waiver or merge authorization.

Migration backup and integration, October 30

  • Signed fix 7c112a4d5 addresses the migration-backup finding. Migration 4 persists a required WALLET backup in the existing status envelope before clearing the quarantined Receive cache. Startup observation can schedule the upload without relying on a migration-time event; no new persisted flag, consent change or SDK/protocol change.
  • The focused regression first failed at the missing backup requirement, then both migration/backup suites passed 34 cases. Final scheme: 2,927 unique passes (2,917 native and 10 UI), zero failures, five explicit fixture/AI-only skips; no failure retries. Four workflow-excluded integration suites remain separate. The E2E-configuration build passes as compile-only, not a device journey.
  • Full SwiftFormat 0.64.0: zero of 825 files require formatting, 100 configured skips. Translation validation: zero errors and 15 verified actual-base warning occurrences. Complete available logs/nested xcresult inventories were audited; App Intents metadata warnings, 24 nonfailing QoS records and 48 CoreGraphics NaN messages remain recorded. The latter runtime causes are not proven pre-existing or migration-introduced. No warning-free claim.
  • Local gated tree d2e735a5 composes master 04a0a1f8; all 14 PR commits verify locally and on GitHub (true/valid) against live master ca98b813. Its later loaded-account address-display optimization is not claimed as local runtime coverage. Combined correctness/native-fit/polish and distinct persistence reviews are clean, with unaffected integration coverage reused. History and other authors are preserved.
  • Earlier c434 hosted E2E attempt 2 passed 13 cases with one explicit skip at changed suite 611509ee; the original attempt-1 failures remain historical. That suite change and old producer handoff are not new-head passes or an independently inspected producer binary. Current-head hosted CI is recorded below.
  • READY / 3.2.0, existing QA IDs and checkboxes remain unchanged. Legitimate-environment/legacy prerequisites, remaining manual acceptance and this owner's J3/GCS deferral are not waived by source review, native tests or review-ready status.

Hosted CI at 7c112a4, October 30

  • The audited build/test cohort has nine successful jobs and twelve guard skips, not twelve test passes. Actual checkout fdd6d76c, tree da5a0864, has parents ca98b813 / 7c112a4d; it is distinct from the local d2e735a5 gate. Unit results: 2,929 unique passes (2,919 native and 10 UI), five explicit fixture skips, zero failures/retries; 2,958 expanded passes are not unique tests. Separately selected integration: 30 native passes, zero failures/skips/executed retries. Full available logs, annotations, nested results and retained attachments were audited. Linker/runtime error logs, NaN/symbolication output, disabled runtime checking and unavailable binary provenance remain qualified, not benign or proved pre-existing. Format/translation receipts are unchanged.
  • E2E run executed suite 611509ee: 13 eventual unique passes and one explicit hardware-transfer skip. First attempt: 12 passes, one Channel-close wait failure and one skip; the built-in second attempt passed that one failed case and skipped the twelve previous passes plus the explicit skip. Cause/introduction remain unproven. The original failure annotation remains; screenshots/video/native logs were created but not uploaded after recovery. No manual CI rerun or selector/timeout change occurred; aggregate status adds no cases.
  • Producer 12975074749 independently matches ZIP SHA-256 788b61fd97e9aa0f5968c99860ce680e887c600d399bbf4d56d16aa0af3a2994; nine native identities were inspected and the consumer identifies this exact archive. Strict signature verification FAILS: sealed empty .static-ffi-stubs-removed is missing. Upload hidden-files=false and the workflow exactly matches actual base ca98; this proves configuration parity, not base-runtime failure or PR introduction. Simulator test success does not approve or repair the archive seal.
  • Existing QA IDs, unchecked manual/environment/legacy cases, deferred/unrun J3, READY/3.2.0 and separate human-approval requirements are unchanged. This is not complete device QA or merge authorization.

Models used

  • Planning/scoping: gpt-6-astra (reasoning: xhigh), gpt-6.1-sol (reasoning: xhigh), Unknown (reasoning: Unknown; October 26 parent orchestration), gpt-6.1-sol (reasoning: high)
  • Implementation: gpt-6-astra (reasoning: xhigh), gpt-6.1-sol (reasoning: xhigh), gpt-6.1-sol (reasoning: high)
  • Review: gpt-6-astra (reasoning: xhigh), gpt-6.1-sol (reasoning: xhigh), gpt-6.1-sol (reasoning: high)

Metadata note: implementation and independent review session records verify gpt-6-astra / xhigh for this follow-up. Accessible parent execution records did not identify the October 26 orchestration turn; its model/effort are not inferred from configuration or historical turns.

Archived device-QA/fixture parent model and reasoning metadata remain unavailable. Earlier October 27 maintenance/parity work used gpt-6.1-sol / xhigh; the October 29 integration implementation and both source reviews used that pair, while that diagnostic auditor used gpt-6.1-sol / high, verified from matched execution records. No historical fallback is replaced or applied retroactively, and no independent device execution is implied. Prior verified pairs are retained.

October 30 implementation used gpt-6.1-sol (reasoning: xhigh); combined review, integration review and complete diagnostics used gpt-6.1-sol (reasoning: high), with the distinct migration persistence review using gpt-6.1-sol (reasoning: xhigh). Actual matched execution records were verified; historical fallbacks remain unchanged.

@ben-kaufman
ben-kaufman marked this pull request as ready for review October 27, 2026 14:00
@ovitrif ovitrif added this to the 3.2.0 milestone Oct 27, 2026
@ovitrif

ovitrif commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Triage: 3.2.0, with its twin synonymdev/bitkit-android#1668. It's a large new feature (opt-in public payment sharing with its own address) that was only just marked ready, and 3.1.0 is cut on 28 Oct, too soon for review and a device run.

@talosmachina talosmachina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

iOS counterpart of bitkit-android#1668. It splits private contact payments from public payment sharing: ContactPaymentsService.setPublicEnabled handles the public side, and the private toggle now syncs only the private capability. AppDataMigrations migration 4 switches public publishing off, arms public cleanup and moves the cached onchainAddress into publicPaykitLegacyAddresses. Public endpoints get per-key reserved addresses (currentOrRotatedPublicAddressSnapshot). Legacy addresses are carried through the wallet backup and through AppCacheData.onchainAddress on metadata restore.

No findings. Reviewed at c30393de, oversized tier: 45 files and about 3,400 changed lines.

Build: not run. iOS does not build on this reviewer's Linux host, so the review is static and relies on CI, which was still running when I posted (lint green, Run Tests pending).

Merge confidence: 3/5. This is the cap for an oversized review, not a sign of any open defect. Only the migration, the backup and restore paths and the address reservation store were read in full.

What was checked
  • Migration 4. It runs once behind versionKey and quarantines the cached address only when there was public state: hasPublicPublishedState, the old hasConfirmedPublicPaykitEndpoints combined flag, or a pending cleanup. It then clears sharesPublicPaykitEndpoints and the published BOLT11 for every user. The old combined flag was dropped from hasPublicPublishedState and added back explicitly in migration 3's footprint check, so migration 3 behaves as before.
  • Reservation store. nextAvailableAddressInfo reserves each legacy-address hit, reveals past it, and throws unless the next index advances. Every allocation path (nextNonReservedReceiveAddress, private assignment, public assignment) goes through it. isUnavailableForReusableReceive checks the legacy list before the format guard. Restoring a backup unions both the legacy addresses and the ceilings with local state, so an older backup cannot release an address that was already exposed.
  • Public assignment. It excludes the current reusable receive address, both before and after prepareReusableReceive, and then advances once more.
  • Not reviewed: the WalletViewModel refresh orchestration, the PubkyService and LightningService hunks, the GeneralSettingsView binding details, and test-only files beyond spot checks.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Reviewed the full PR diff against its merge base, at c30393de.

No new actionable code findings.

Public sharing stays opt-in and independent of private contact payments. A dedicated public Bitcoin address is reserved before publication, kept out of ordinary Receive and contact addresses, and rotated after persisted on-chain activity without moving ordinary Receive. Migration turns the old combined choice public-off, keeps private consent, and quarantines the cached receive address. Backup restore merges reservation ceilings and imported legacy addresses so an older snapshot does not release them. Session checks on publication, withdrawal, and app-registry updates keep a finished stale operation from acknowledging a replacement session.

Compared that sharing and address-reservation contract with bitkit-android#1668 at 10794577. iOS blocks wallet start while a backup restore is pending, which keeps a known metadata address excluded until restore finishes.

Hosted unit tests, SwiftFormat, and translation validation succeeded for this revision. This review did not run those checks or a simulator.

Device testing: not performed in this review.

Ready for device testing. J3 still needs the compatible marketplace fixture described in the PR.

Reviewer model: grok-4.7
Reasoning effort: high

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code scan at c30393d: no HIGH or MEDIUM.

Checked and clean:

  • Migration from v3.0.1 and the keychain changes.
  • Backup and restore compatibility of the new settings.
  • Separation between public, private and ordinary addresses.
  • Republishing after a sync is gated on isPublicPaykitAddressUsed (WalletViewModel.swift:1490-1499). That is the behaviour I asked synonymdev/bitkit-android#1668 to copy: Android republishes after every SyncCompleted.

Twin parity: this PR changes profile__pay_contacts_description and adds the profile__payments_pending hint in General settings, and Android has neither. I raised it on synonymdev/bitkit-android#1668. If the difference is meant to stay, record it in journeys/profile/README.md here too.

Device gate (partial): c30393d, not run yet. Pending: public-private-sharing.xml, contact-payment-sharing.xml paired with Android in both directions, and an upgrade from v3.0.1.

Copy link
Copy Markdown
Contributor Author

Follow-up to #1095 (review): out of scope: tracked in synonymdev/bitkit-android#1717 for3.2.0, because the LOW sharing-introduction/pending-hint parity observation is presentation/localization work separate from the Android correctness repair. Both twins' Out of Scope sections link this issue and the source reviews; no intentional-parity or completed-UI claim is implied. There is no inline LOW thread to resolve.

The Android twin's idle-sync finding is repaired in signed6c0bed1aabb3236c9bf74712da9607c7dfefdfe5 with native regression evidence. This iOS product head remains c30393d, out of draft at milestone3.2.0. Your source-only comment is not counted as an approval or device execution, and the remaining paired/upgrade/controlled-write acceptance limits are retained. No duplicate iOS review request is being generated from this unchanged source comment.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — the remaining device cases were not completed.

Findings: None confirmed.

QA for 1aece6b:

Test 1: Marketplace wallet leg ⏭️ Not run; the author deferred this journey and a compatible isolated fixture was not established.

The available fixture uses an older Paykit SDK. No purchase was attempted.

Test 2: Reciprocal private withdrawal ⏭️ Not run; a second linked wallet and reciprocal SDK inspection were not established.
Test 3: Session replacement and admitted writes ⏭️ Not run; controlled SDK write gates were not set up.
Test 4: Pre-split upgrade and older backup ⏭️ Not run; a pre-split wallet and older backup were not prepared.
Test 5: Address monitoring change ⏭️ Partial: the local reservation eventually rotated, but remote publication was not checked.

On a disposable regtest simulator, public sharing was enabled with Taproot selected. After switching to Native SegWit and disabling Taproot monitoring, the stored public assignment initially remained Taproot with reconciliation pending, then became Native SegWit with pending cleared, as checked in the app's stored reservation state. The recording had excessive stutter. This does not establish the remote endpoint's state.

The author reports J1 and J2 device journeys at the prior integration head in the PR evidence; the sharing and journey code is unchanged through this head. The earlier source review also covers the sharing contract. Reviewed the full 543d9389..1aece6bf diff; backup/session failure and marketplace changes were not driven in this run.

Twin: bitkit-android#1668 implements the same split; the documented introduction and pending-hint presentation differences are deferred to bitkit-android#1717.

Reviewer model: Unknown
Reasoning effort: Unknown


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

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Full reassessment of the complete PR diff against merge base 543d9389, at 1aece6bf. No verified baseline was supplied, and the comparison base changed from b666ccb to 543d938, so nothing was inherited.

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

A dedicated public Bitcoin address stays published after its address type is removed from monitoring. Private contact addresses rotate on that settings change. The new post-sync check still republishes only when the current public address is used, so an unused monitored address is left as published.

Hosted unit tests and integration tests succeeded for this head. This review did not re-run them. Device testing: not performed in this review. Sharing and address reservation were compared with bitkit-android#1668 at 748f856a; that PR was not reviewed as a whole. Pending-hint copy remains bitkit-android#1717. Recurring-request address reuse in bitkit-ios#1148 is outside this PR's payment-rail scope.

Suggested additional test cases

  • Platform: iOS simulator, unfunded regtest, Paykit enabled, signed-in local profile. Set the address type to Taproot, enable Public payments, switch the selected type back to native SegWit, then disable Taproot monitoring. Expected: the published public Bitcoin address changes to a still-monitored type, ordinary Savings Receive stays unchanged, and a wallet sync does not leave the Taproot address published.

Findings

  • [MEDIUM] Rotate public addresses when their type is no longer monitored — inline at Bitkit/ViewModels/WalletViewModel.swift:1499.

Reviewer model: grok-4.7
Reasoning effort: high

Comment thread Bitkit/ViewModels/WalletViewModel.swift Resolved

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Re-reviewed the full pull request at 806b35f after the comparison base changed from 543d9389 to 682acdea and no coverage baseline was available. This is a full reassessment of the diff from that merge base, with empty inherited coverage.

No new actionable code findings.

Public and private payment sharing are independent. Public sharing stays off until an explicit choice. The one-time migration of the old combined preference keeps private consent and queues removal of previously published public endpoints. A dedicated public Bitcoin address is reserved before publication, excluded from ordinary Receive, and rotated after it is used or its address type is no longer monitored. That rotation leaves ordinary Receive unchanged. Publication and withdrawal carry the initiating session through admission and acknowledgement, and a replaced address or session leaves pending repair in place.

Compared with bitkit-android#1668 at 748f856: journeys/profile/public-private-sharing.xml matches, and rotation of an unmonitored public address follows the same rule. Android clears public sharing on every settings restore because that flag is part of the restored settings document. iOS keeps the sharing flags out of the settings backup, so restore leaves the on-device choice in place.

Validation: reservation, migration, backup-exclusion, sharing-rollback, and session-admission tests were inspected at this head and not executed. Xcode is unavailable in this environment. Device testing: not performed in this review. Simulator results described in the pull request are from earlier revisions, not 806b35f. The monitoring check in journeys/profile/README.md and manual cases 1–3 are still unchecked at this revision.

Earlier findings reconciled at this revision: 1 verified fixed.

Reviewer model: grok-4.7
Reasoning effort: high

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — device acceptance remains incomplete.

The PR moved to 4441a40 after this run; these results are for 806b35f.

Findings: None confirmed.

QA for 806b35f:

Test 1: Public address after disabling monitoring ⏭️ Partial: the local assignment rotated, but remote publication was not verified.

On an unfunded regtest simulator, I enabled public payments with Taproot selected, switched to Native SegWit and disabled Taproot monitoring. The app's stored reservation changed from Taproot to Native SegWit and the pending marker cleared. Ordinary Receive and the remote endpoint were not checked. The recording had excessive stutter.

Test 2: Independent public and private sharing (J1) ⏭️ Not run after the address and wallet dependency changes.
Test 3: Marketplace wallet leg (J3) ⏭️ Not run; the two-wallet fixture setup was incomplete.
Test 4: Reciprocal private withdrawal ⏭️ Not run; a second linked wallet was not prepared.
Test 5: Session replacement and admitted writes ⏭️ Not run; controlled write gates were not prepared.
Test 6: Pre-split upgrade and older backup ⏭️ Not run; an older wallet and backup were not prepared.

The head moved during Test 1, so the remaining cases were left for the new revision. The author's earlier device report covers J2; reciprocal withdrawal remains open. The source review at this head also confirms the monitoring repair. I reviewed the 1aece6b..806b35f delta and the earlier open monitoring finding; the other merged changes were not driven on a device.

Twin: bitkit-android#1668 implements the same sharing split and unmonitored-address rotation. Presentation differences are tracked in bitkit-android#1717.

Reviewer model: Unknown
Reasoning effort: Unknown


Reviewed by gpt-6.1-terra-high

@piotr-iohk

Copy link
Copy Markdown
Collaborator

@ben-kaufman please resolve conflicts.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ⚠️ Not verified — remote withdrawal, session-race device acceptance, and backup restore remain incomplete.

Findings: No new actionable finding confirmed.

QA for 4441a40:

Test 1: Public address after disabling monitoring ⏭️ The assignment and remote endpoint rotated, but the full Receive and idle-sync checks remain open.

Starting with a Taproot public assignment, switching to native SegWit and disabling Taproot monitoring replaced the assignment and retained the old reservation. A read-only Paykit diagnostic from the peer resolved a payable native SegWit endpoint. The assignment and reservation were checked in local state; the diagnostic checked remote publication. Ordinary Receive before and after and an idle-sync no-republication check were not established. The recording stutters.

monitor-2x.2x.mp4
Test 2: Independent public and private sharing ✅ All four switch combinations worked independently; private on and public off persisted after restart, and the Receive address matched by clipboard comparison.

The recording stutters despite a repeat capture. The final Receive screen is from the earlier capture of the same wallet.

j1-repeat-2x.2x.mp4
j1-end-2x.2x.mp4
Test 3: Private contact sharing persistence ✅ Private sharing remained off after leaving and reopening settings with a linked peer.

A read-only Paykit diagnostic independently confirmed the peer was linked. The recording stutters; the second clip completes the return to settings.

j2-2x.2x.mp4
j2-retry-2x.2x.mp4
Test 4: Marketplace wallet leg ✅ The buyer sent 10,000 sats, the open seller received them, and the fixture confirmed the payment.

The buyer and seller were linked through the local regtest marketplace fixture. The fixture reported one transaction in the mempool, mined it, and reported the matching payment confirmed with one confirmation. The seller's receipt persisted after reopening. The isolated fixture used a test-only Electrum port override. The side-by-side recording stutters, particularly on the buyer; frame retention in the combined copy was not verified.

j3-marketplace-2x.2x.mp4
j3-reopen-2x.2x.mp4
Test 5: Remote public and reciprocal private endpoints ⏭️ The complete publication and withdrawal matrix was not driven.

The linked-peer and public-resolution observations above do not establish reciprocal private endpoint and capability withdrawal across all four combinations.

Test 6: Session replacement and admitted writes ⏭️ The full controlled device scenario and pending-repair observation were not driven.

Focused native tests passed for stale-session guards and admitted writes draining under cancellation, using controlled mock gates. They are partial evidence for the device acceptance case.

Test 7: Pre-split upgrade and backup restoration ⏭️ A pre-split upgrade preserved the expected local state, but backup restores and remote cleanup remain open.

An actual pre-split wallet with combined sharing enabled was upgraded to this head. Private sharing remained on, public sharing was off, and the old public address was retained in the local exclusion set, checked in persisted state. Older and current backup restores and remote cleanup were not verified. The recording stutters.

upgrade-2x.2x.mp4

The focused native run also passed the new concurrent registration/cancellation and reentrant cancellation tests. Its first attempt failed before test execution when the simulator rejected the app launch; the retry passed.

Reviewed the delta 806b35f..4441a40 and the earlier monitoring fix; the backup subscription change was covered by native tests and review, not a device-specific backup journey. The previous review at 806b35f covers the earlier change. The Android twin, bitkit-android#1668, matches the independent sharing and unmonitored-address rotation behavior; its presentation follow-up is tracked in bitkit-android#1717.

Reviewer model: Unknown; reasoning effort: Unknown.


Reviewed by gpt-6.1-terra-high

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed to 63ac124. No new findings.

  • 806b35f7: needsPublicAddressRotation (store :229) sends an unmonitored public assignment through the existing guarded publisher. The old reservation stays excluded, and an address that is monitored and still unused doesn't rotate.
  • 4441a404: BackupObservationSubscriptions (BackupService.swift:1431-1455) serializes the two Task { @MainActor } registrations that raced on the plain Set. Cancel runs outside the lock and is idempotent, and the new test covers it.
  • The three merges: I compared each resolution with a clean auto-merge, and none drops master's behaviour or the PR's. 63ac124e keeps metadata retryable on walletTemporarilyUnavailable. 033ae20a drops master's duplicate restoreAppCacheData call, which the PR's wrapper at :712 already covers.

Device gate (partial): 63ac124, not run yet. Pending:

  • public-private-sharing.xml and contact-payment-sharing.xml, paired with synonymdev/bitkit-android#1668.
  • The monitoring rotation check in journeys/profile/README.md.
  • One backup and restore run.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Re-reviewed the full PR at 63ac124e after the comparison base changed from 682acdea to 681f5aaa (also the merge base). This is a full reassessment of the complete diff with no inherited coverage.

No new actionable code findings.

Public payments are opt-in and separate from private contact payments. Onboarding enables private sharing only. Turning either switch off leaves the other unchanged. The public Bitcoin address is a dedicated reservation, saved before publication and kept out of ordinary Receive and contact addresses. Retired public indexes stay reserved. The public address rotates after on-chain activity is persisted, after a sync, or when its address type is no longer monitored, without changing ordinary Receive. Migration 4 turns public sharing off, keeps private consent, queues removal of anything already published and quarantines the previously shared Receive address. Backup restore merges exclusion ceilings and legacy public addresses, so an older backup cannot release addresses that were already exposed. Metadata uploads now wait for a successful wallet backup; deferred and temporary wallet outcomes stay retryable. Publication, removal, App Registry writes and acknowledgements are tied to the session that started them. A delayed acknowledgement is also checked against the current public reservation.

The earlier finding about public addresses not rotating when their address type is no longer monitored is fixed in the source at this head. needsPublicAddressRotation now returns true for unmonitored assignments, and both the monitoring-disable path and post-sync reach the guarded publisher.

Parity with bitkit-android#1668 at 60f29720: Android explicitly turns public sharing off when restoring settings. iOS reaches the same result because its settings backup does not include the sharing keys. Pending-hint and introduction parity remains out of scope, tracked in bitkit-android#1717.

Validation: this was a source review only. No local build or test was run. Hosted unit tests, integration tests, SwiftFormat and translation validation passed at this head. The local E2E job was still running. The LDK node source was not fetched: ordinary Receive and public allocation are assumed to always get a new address, as the already-shipped private contact allocation relies on, and an explicit collision guard exists. Device testing: not performed in this review. The author's manual cases 1–3 and J3 remain unchecked.

Suggested additional test cases

  • iOS, restore from an Android backup: on Android, create a wallet whose metadata backup cache contains an ordinary Receive address and record that address. Restore the wallet on iOS. Open Receive → Savings: the recorded address is never shown. Metadata backup status is cleared only after the wallet backup has completed.
  • iOS, toggle failure: turn off network access to the homeserver, then turn Public payments on in Settings → General. Expected: an error toast appears, the switch returns to off, and Private contact payments keeps its previous state. After the network is back and the app returns to the foreground, nothing is left published publicly.

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

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Re-reviewed changes since 63ac124e, including affected code paths and prior findings, at ae5a2c71. This is a follow-up. It inherits the full-reassessment coverage of the previous QA review. The base and merge base are unchanged at 681f5aaa, and the previous head is an ancestor of the current head.

No new actionable code findings.

No new actionable code findings in the follow-up.

The only change is ae5a2c71. UtilityService.isAddressUsed used to check only the address shown on an on-chain activity. If that check finds nothing, it now also searches the outputs of every stored transaction. So an address paid by one output of a batch transaction now counts as used, even when the activity shows a different address. This only ever changes a result from unused to used. That makes these callers rotate more readily and never less:

  • private contact address rotation
  • public address rotation and the rotation check after sync
  • filtering of used remote on-chain endpoints
  • refreshing the ordinary Receive address

I found no path where the new fallback marks an address as used without an output that paid it. The TxOutput and TransactionDetails shapes and the core queries were checked against bitkit-core 22e0acc3 (0.8.0-rc11, the version pinned in Package.resolved).

Limitation: the fallback reads only transaction details stored in the database. It does not use the node lookup that getTransactionDetails falls back to for restored history. A restored wallet whose batch-transaction details were never cached keeps the earlier behaviour. This is a limit of the new fix, not a regression.

Parity: bitkit-android#1668 at 7fbc022b makes the same change in CoreService.isAddressUsed. Android pending-hint and introduction parity is still tracked in bitkit-android#1717. The earlier rotation finding is still fixed, because the delta does not touch that path.

Validation: this was a source review only. No local build or test was run, because there is no Swift toolchain in this environment. At this head, hosted unit tests, integration tests and the local E2E job passed. The unit-test workflow does not skip OnchainActivitySyncTests, but the job log could not be retrieved, so I could not confirm that the new test ran. Device testing: not performed in this review. The author's manual cases 1–3 and J3 are still unchecked.

Suggested additional test cases

  • iOS (regtest): enable Public payments and record the published public address. From an external wallet, send one transaction that pays both the ordinary Receive address and the public address, then confirm it. Expected: the activity shows one of the addresses. After sync, the public address rotates and is republished. Ordinary Receive also moves to a new address, and neither old address is offered again.

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

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Re-reviewed the full PR at c4340285 because the comparison base moved from 681f5aaa to 32ad1977, which is also the merge base. This is a full reassessment of the complete diff. No earlier coverage is carried over.

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

Public and private sharing are now separate switches. Onboarding and the automatic first enable in Settings turn on private sharing only and only update the App Registry capability. Public payments change only through the new Public payments switch, and turning one switch off leaves the other as it was. The public Bitcoin address has its own reservation. It is saved before it is exposed, it never matches the ordinary Receive or contact addresses, and it rotates after on-chain activity is saved, after a sync, or when its address type stops being monitored. Ordinary Receive does not change when it rotates. Publication, removal and acknowledgements check the session that started them. The private-consent read for the App Registry write now happens inside the SDK operation lock. Restoring a backup merges the reserved-address limits and the list of previously published addresses, so restoring an older backup cannot make known addresses usable again. The earlier MEDIUM finding about public addresses whose type is no longer monitored is still fixed at this head.

The one finding (LOW) is in the upgrade migration. It moves the previously published Receive address into the excluded list but never marks the wallet backup as needing an upload. The restore path and Android both do.

Parity with bitkit-android#1668 at 6ad6440d: both platforms enable private sharing only (and refuse when private access is unavailable), make public sharing opt-in, and migrate to public-off with removal queued. Both treat an address as used if any stored transaction output paid it. Android turns public sharing off when restoring settings. iOS gets the same result because its settings backup does not include the sharing keys. Pending-hint and introduction parity is tracked in bitkit-android#1717. The companion bitkit-e2e-tests suite at 4c0cbef0 does not use the removed ContactPaymentsToggle identifier. All 15 locales have the three new strings.

Validation: this was a source review only. No local build or test was run. At this head, hosted unit tests, integration tests, SwiftFormat and translation validation passed. The local E2E job was still running; the author reports that an earlier attempt failed in lightning_1. LDK Node source was not fetched. Whether a restored wallet can hand out the same unused Receive address again is inferred from the source, not executed. Device testing: not performed in this review. The author's manual cases 1–4 and J3 are still unchecked.

Suggested additional test cases

  • iOS, upgrade then restore (regtest): install a pre-split build, turn on the combined contact payments setting with a linked contact, and record the ordinary Receive address A. Upgrade to this build and do not create contacts, toggle sharing or receive funds, so no other wallet backup runs. Open Settings → Backup and confirm the wallet backup is in sync. Then restore the wallet on a fresh install. Expected: Receive → Savings never shows A, and turning on Public payments publishes an address other than A.

Findings

  • [LOW] Back up the address quarantined by migration 4 — inline at Bitkit/Utilities/AppDataMigrations.swift:82.

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

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Full reassessment of the whole PR at 7c112a4d, compared with merge base 04a0a1f8. A full reassessment was needed because the comparison base moved from 32ad197 to 04a0a1f, and that change touched Bitkit/AppScene.swift and Bitkit/Services/PubkyService.swift, which this PR also changes. No earlier coverage is inherited.

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

Covered: reserving a public address separately from ordinary Receive and private-contact addresses, and rotating it after a payment is recorded; carrying the session through publication, withdrawal and acknowledgement; independent private and public switches, including rollback and cancellation; the one-time migration (step 4) that turns public sharing off; legacy address quarantine and merging of backup exclusions; ordering of metadata backups after wallet backups during restore; the changed address-usage check; and the merge with the new base in AppScene and PubkyService. Nothing defective was found outside the one finding.

Parity with the Android twin #1668 at 8c6f2bb3, checked for the changed flows only: enabling private sharing fails in the same way when private access is unavailable, the toggle identifiers PrivateContactPaymentsToggle / PublicPaymentsToggle match, and the backup field publicPaykitLegacyAddresses has the same name. Android made the identical address-usage change, so the finding applies there too; that belongs to its own review.

Validation: source analysis only. The address-usage cost was traced into bitkit-core at the pinned 22e0acc3 and was not measured. Hosted Run Tests and Run Integration Tests passed at this head. No Swift build or test was run in this review. A GitHub code search found no remaining use of the removed ContactPaymentsToggle selector in bitkit-e2e-tests; the search index may be incomplete. The PR description mixes requirements with long fix and validation notes. Only the Description, Out of Scope and QA Notes sections were used as requirements; the author's evidence was not treated as checked by this review.

Device testing: not performed in this review. Manual cases 1–4 and J3 remain open, as the author lists.

Suggested additional test cases:

  • iOS, wallet with a large transaction history (for example a connected hardware wallet with hundreds of transactions) and 20 or more saved contacts with private sharing on: bring the app to the foreground and pull to refresh on Home. Expected: the activity list and Receive stay responsive, and the isAddressUsed timing logged for the core queue stays small for each contact.

Earlier findings reconciled at this revision: 1 verified fixed, 1 not substantiated.

Findings

  • [LOW] Address-usage check now loads every stored transaction on each call — inline at Bitkit/Services/CoreService.swift:2480.

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

Comment thread Bitkit/Services/CoreService.swift Resolved

@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

Findings: No new confirmed defects. The migration-backup finding is fixed at 7c112a4. The existing LOW address-usage performance concern is deferred to bitkit-ios#1174; the bounded measurement below does not establish its larger-scale cost.

QA for 7c112a4: iPhone 17 Pro simulators, iOS 26.5, regtest. The upgrade ran on the unmodified head build; the controlled cases used the debug hooks included below. No Paykit SDK response was mocked.

Back up the address quarantined during upgrade ✅ Migration schedules an exclusion-bearing wallet backup.

The focused AppDataMigrationsTests and BackupServiceTests ran on the unmodified head: 34 passed, zero failures or skips. The regression covers migration before backup observation and the exclusion-bearing upload; production WALLET payload inclusion was also source-traced.

Independent public and private sharing ✅ Independent switches and restart persistence passed at c434028.

Carried from c434028: independent public/private switches, explicit public opt-in, private-on/public-off after restart, and ordinary Receive equality checked through the clipboard. The only subsequent change schedules the migration backup; these paths are unchanged.

Full controls and restart Repeat controls

Recordings at 2×.

Private contact sharing persistence ✅ Private sharing stayed off after reopening Settings at c434028.

Carried from c434028: a real SDK-linked contact, private sharing off, leave/reopen General Settings, still off. ⚠️ The private-off update took about 12 seconds with a visible spinner.

Private sharing off Persistence recheck

Recordings at 2×.

Remote public and reciprocal private endpoints ✅ Peer SDK reads confirmed independent remote publication at c434028.

Carried from c434028: zero-contact and reciprocal linked-wallet matrices. Real peer SDK registry, public resolution and private-list reads verified all four switch combinations. Public, ordinary Receive and contact-specific addresses were compared and differed. Videos show the controls; SDK readbacks establish the remote state.

Zero contacts Linked wallet A
Linked wallet B

Recordings at 2×.

Restore current and older backups without releasing public addresses ✅ Upgrade and supported in-place restores retained known exclusions.

A real pre-split wallet upgraded with private sharing retained, public sharing off and the former Receive quarantined. Remote VSS WALLET readback contained that address, while METADATA had no cached Receive. Supported full current and older-backup restores on the existing disposable wallet retained known reservation ceilings and legacy exclusions; after restart, Receive and explicitly re-enabled public sharing used different addresses. A second fresh pre-split-wallet upgrade repeated the consent, quarantine and remote VSS checks on the clean head. The restores used the app API through a debug hook; this is not a fresh-install seed-only recovery claim.

Upgrade Backup capture
Restore and restart Explicit public opt-in
Upgrade repeat

Recordings at 2×.

Rotate an unmonitored public address without moving Receive ✅ Remote rotation preserved ordinary Receive, checked by SDK and clipboard.

Selecting native SegWit while Taproot remained monitored retained the remote Taproot endpoint. Disabling Taproot monitoring replaced it with native SegWit. SDK resolution establishes the remote replacement; stored-state and clipboard comparisons establish unchanged ordinary Receive. The retired Taproot index remained excluded. An idle refresh logged zero public-endpoint SDK syncs and retained the replacement. A focused repeat records the switch transition clearly.

Original transition Focused repeat

Recordings at 2×.

Keep stale publication out of a replacement session ✅ Held work and real HTTP faults preserved session ownership and repair.

Explicit debug holds before publication exercised same-key session replacement, caller cancellation and opt-out. Stale work could not acknowledge the replacement session; exclusions and pending repair remained. For an already admitted real HTTP write, the proxy recorded PUT 201 after caller cancellation, then queued DELETE 204. Injected PUT 500 rolled public sharing back off without changing private consent; DELETE 500 left public consent off with repair pending and the remote endpoint still present, then normal restart recovery withdrew it after the fault cleared. SDK resolution and persisted-state readback establish those assertions. The first admitted-write attempt reached its native timeout before release; the successful immediate-release repeat establishes write/drain ordering.

Pre-admission holds HTTP failures and recovery
Admitted write and cleanup

Recordings at 2×.

Rotate a public address paid by a secondary batch output ✅ RPC and app readback verified secondary-output retirement after spending.

With public sharing off, a batch paid ordinary Receive R first and public P second; Activity showed R. RPC and Core output readback confirmed both outputs. An app MAX send spent both outpoints (confirmed by RPC). After restart, re-enabling public sharing published a fresh address and retained P in exclusions. Receive clipboard values before/after re-enable matched. The focused restart/re-enable recording supplements the long funding/spend capture.

Batch and spend Restart and re-enable

Recordings at 2×.

Rotate a public address received in a batch transaction ✅ SDK and clipboard reads verified both paid addresses rotated.

With public sharing enabled, a confirmed batch again paid ordinary Receive and public P while Activity showed ordinary Receive. After synchronization, SDK resolution showed a new public endpoint, and clipboard readback showed a fresh ordinary Receive; neither paid address was offered again. RPC and stored transaction outputs establish the secondary-output payment.

Original batch Focused repeat

Recordings at 2×.

Pay the marketplace wallet request ✅ Both real wallets and the fixture confirmed matching payments.

Two real wallets approved the seller watch-only/Paykit setup and marketplace grant, saved each other, and exposed reciprocal private lists through real SDK reads. The buyer paid one 10,000-sat request: transaction 48f545a1496a3cc0b83843af990cd1f598f1527480536befdcb5d3c52a0ef608. The request stayed dismissed after Details. The seller remained active at Home and detected the receipt without manual sync or restart; attribution survived restart, and detach remained effective after another restart. The fixture independently reported the same transaction, exact amount and one confirmation. A separate 10,001-sat repeat completed once as 057ade951ff1892e66170aa2cbf91c7d037a14b15ff5bbad4a81690bd5b6bf42, matching both apps and the fixture. Buyer is left, seller right in the aligned videos. The initial pair uses MP4 creation timestamps (one-second precision); the repeat uses recorded wall-clock starts. This uses the compatible controlled local marketplace/Paykit Server, pinned to the app's Paykit SDK revision.

Seller setup Full paired payment
Focused paired repeat

Recordings at 2×.

Refresh addresses with a large transaction history ✅ The measured scan stayed below half a second at the tested size.

A debug setup saved 20 contacts through the real Paykit SDK, reserved their private addresses, and inserted 500 Core transaction rows with 10 outputs each. The production reserved-address usage scan took 0.401, 0.395 and 0.394 seconds. Contacts loaded; wallet refresh and Receive remained usable. These are synthetic persisted rows and offline contact identities on a simulator, with empty transaction inputs; physical-device costs, witness-heavy histories and a large remote peer network remain unmeasured. This adds bounded evidence to the LOW follow-up; the batching optimization remains deferred.

History and controls Focused scan repeat

Recordings at 2×.

All planned cases have passing evidence; the three c434028 cases are carried at their recorded head. Some original captures stutter; focused repeats are included. The 2× copies and aligned pairs passed the changed-frame retention check. Unrelated features merged from master were not separately driven. No physical-device or fresh-install seed-only recovery claim is made.

Review coverage: contiguous delta 4441a40..7c112a4 plus the open migration and address-usage concerns, including their backup, consent, session, reservation and wallet-sync callers. I relied on the previous review at 4441a404 for the unchanged earlier scope; the carried case recordings above are from the later c434028 run.

Twin: bitkit-android#1668 feat: separate public payment sharing at 8c6f2bb3 matches the independent-consent and reservation behavior checked here; presentation parity is deferred in bitkit-android#1717.

Debug hooks used for controlled setup and readback

Not pushed. The current superset below adds operation holds/cancellation, same-key session revision replacement, production backup capture/restore/readback, real Paykit SDK observation, an isolated local Electrum port, and the performance data setup. Staging cases omit the local port and performance additions. The initial upgrade itself used the unmodified head. The hooks stage inputs or expose state; production publication and restore decisions remain intact.

diff --git a/Bitkit/AppScene.swift b/Bitkit/AppScene.swift
index 46f4671f..1e959378 100644
--- a/Bitkit/AppScene.swift
+++ b/Bitkit/AppScene.swift
@@ -522,6 +522,11 @@ struct AppScene: View {
 
     var body: some View {
         appEventContent
+            .task { // test hook
+                #if DEBUG
+                await ReviewQA.run(wallet: wallet, profile: pubkyProfile, contacts: contactsManager) // test hook
+                #endif
+            } // test hook
     }
 
     private var configuredContent: some View {
diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift
index 90aed6ed..21ce87e4 100644
--- a/Bitkit/Constants/Env.swift
+++ b/Bitkit/Constants/Env.swift
@@ -223,7 +223,7 @@ enum Env {
 
     static var electrumServerUrl: String {
         if isE2E, e2eBackend == "local" {
-            return "tcp://\(e2eLocalHost):60001"
+            return "tcp://\(e2eLocalHost):60011" // test hook: isolated fixture Electrum port
         }
         return electrumServerUrl(for: network)
     }
diff --git a/Bitkit/Managers/PubkyProfileManager.swift b/Bitkit/Managers/PubkyProfileManager.swift
index 65fbb6bf..05683d7f 100644
--- a/Bitkit/Managers/PubkyProfileManager.swift
+++ b/Bitkit/Managers/PubkyProfileManager.swift
@@ -1159,6 +1159,9 @@ class PubkyProfileManager: ObservableObject {
             return try await saveProfileEdit(name: name, bio: bio, links: links, tags: tags, avatarImage: avatarImage, session: session)
         }
 
+        func reviewReplaceSession() { // test hook
+            do { try Self.beginSessionMutation(); Self.endSessionMutation() } catch {} // test hook
+        } // test hook
         func clearAuthenticatedStateForTesting() {
             clearAuthenticatedState()
         }
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
index 677f5321..a867e99c 100644
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -536,6 +536,54 @@ class BackupService {
         }
     }
 
+
+    #if DEBUG
+    func reviewCapture(_ name: String) async throws { // test hook
+        let data = try await getBackupDataBytes(category: .wallet) // test hook
+        let folder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-qa") // test hook
+        try data.write(to: folder.appendingPathComponent(name + ".backup"), options: .atomic) // test hook
+        Logger.info("REVIEW_QA captured wallet backup", context: "ReviewQA") // test hook
+    } // test hook
+    func reviewRestoreOlder(_ name: String) async throws { // test hook
+        let folder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-qa") // test hook
+        let data = try Data(contentsOf: folder.appendingPathComponent(name + ".backup")) // test hook
+        await stopObservingBackups() // test hook
+        try await uploadBackup(BackupCategory.wallet.rawValue, data) // test hook
+        try await performFullRestoreFromLatestBackup() // test hook
+        Logger.info("REVIEW_QA restored older wallet backup", context: "ReviewQA") // test hook
+    } // test hook
+    #endif
+
+    #if DEBUG
+    func reviewBackupFacts() async throws -> [String: Any] { // test hook
+        var value: [String: Any] = [:] // test hook
+        for category in [BackupCategory.wallet, .metadata] { // test hook
+            if let item = try await VssBackupClient.shared.getObject(key: category.rawValue), // test hook
+               let object = try JSONSerialization.jsonObject(with: item.value) as? [String: Any] { // test hook
+                if category == .wallet { // test hook
+                    value["walletVersion"] = item.version // test hook
+                    value["ceiling"] = object["privatePaykitHighestReservedReceiveIndexByAddressType"] ?? NSNull() // test hook
+                    value["legacy"] = object["publicPaykitLegacyAddresses"] ?? NSNull() // test hook
+                } else { // test hook
+                    value["metadataVersion"] = item.version // test hook
+                    value["cachedReceive"] = (object["cache"] as? [String: Any])?["onchainAddress"] ?? NSNull() // test hook
+                } // test hook
+            } // test hook
+        } // test hook
+        return value // test hook
+    } // test hook
+    func reviewInjectLegacyMetadata(_ address: String) async throws { // test hook
+        await stopObservingBackups() // test hook
+        guard let item = try await VssBackupClient.shared.getObject(key: BackupCategory.metadata.rawValue), // test hook
+              var object = try JSONSerialization.jsonObject(with: item.value) as? [String: Any], // test hook
+              var cache = object["cache"] as? [String: Any] else { throw UsdtError.InvalidBackup } // test hook
+        cache["onchainAddress"] = address // test hook
+        object["cache"] = cache // test hook
+        try await uploadBackup(BackupCategory.metadata.rawValue, JSONSerialization.data(withJSONObject: object)) // test hook
+        try await performFullRestoreFromLatestBackup() // test hook
+        Logger.info("REVIEW_QA imported Android-format cached receive metadata", context: "ReviewQA") // test hook
+    } // test hook
+    #endif
     func hasPendingWalletRestore() -> Bool {
         if UsdtRecoveryRestoreGate.isPending {
             return true
diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift
index 1751e439..e8c89adf 100644
--- a/Bitkit/Services/PubkyService.swift
+++ b/Bitkit/Services/PubkyService.swift
@@ -981,6 +981,24 @@ actor PaykitSdkService {
         }
     }
 
+
+    #if DEBUG
+    func reviewObserve(_ peer: String) async throws -> [String: Any] { // test hook
+        try await withSdk { sdk in // test hook
+            var value: [String: Any] = [:] // test hook
+            value["peer"] = peer // test hook
+            let registry = try await sdk.paykitAppRegistry(publicKey: peer) // test hook
+            value["registry"] = registry?.apps.map { ["app": $0.appId, "private": $0.capabilities.privatePayments] as [String: Any] } ?? [] // test hook
+            let resolution = try await sdk.resolvePublicContactPayment(counterparty: peer, amount: nil) // test hook
+            value["publicStatus"] = String(describing: resolution.status) // test hook
+            value["publicFailures"] = resolution.failures.count // test hook
+            value["public"] = resolution.payableEndpoints.map { ["app": $0.appId, "identifier": $0.identifier, "payload": $0.payload.exportText()] } // test hook
+            value["private"] = try await sdk.currentPrivatePaymentLists(counterparty: peer).map { ["app": $0.appId, "version": $0.latestStreamItemId ?? 0, "endpoints": $0.paymentEndpoints.map { ["identifier": $0.identifier, "payload": $0.payload.exportText()] }] as [String: Any] } // test hook
+            value["peers"] = try await sdk.linkedPeers().map { ["key": $0.counterparty, "state": String(describing: $0.state)] } // test hook
+            return value // test hook
+        } // test hook
+    } // test hook
+    #endif
     func syncPaykitApp(
         privatePaymentsEnabled: Bool?,
         priority: PaykitSdkOperationLock.Priority = .ordered,
@@ -1001,7 +1019,10 @@ actor PaykitSdkService {
     func syncPublicEndpoints(
         _ endpoints: [PublicPaykitService.Endpoint], isSessionCurrent: (@MainActor () -> Bool)? = nil
     ) async throws -> EndpointSyncReport {
-        try await withStateRevisionTracking(priority: endpoints.isEmpty ? .ordered : .background) { sdk in
+        #if DEBUG
+        try await ReviewQA.hold("public-before-admission") // test hook
+        #endif
+        return try await withStateRevisionTracking(priority: endpoints.isEmpty ? .ordered : .background) { sdk in
             if let isSessionCurrent {
                 guard await isSessionCurrent() else { throw PubkyServiceError.sessionNotActive }
                 try Task.checkCancellation()
diff --git a/Bitkit/Utilities/ReviewQA.swift b/Bitkit/Utilities/ReviewQA.swift
new file mode 100644
--- /dev/null
+++ b/Bitkit/Utilities/ReviewQA.swift
@@ -0,0 +1,120 @@
+
+#if DEBUG
+import Foundation // test hook
+import Paykit // test hook
+import BitkitCore // test hook
+
+@MainActor enum ReviewQA { // test hook
+    static var operation: Task<Void, Never>? // test hook
+    static var folder: URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-qa") } // test hook
+    static func write(_ value: [String: Any], _ name: String) { // test hook
+        try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) // test hook
+        if let data = try? JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted, .sortedKeys]) { // test hook
+            try? data.write(to: folder.appendingPathComponent(name + ".json"), options: .atomic) // test hook
+        } // test hook
+    } // test hook
+    static func hold(_ name: String) async throws { // test hook
+        let file = folder.appendingPathComponent(name + ".hold") // test hook
+        guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+        Logger.info("REVIEW_QA hold entered \(name)", context: "ReviewQA") // test hook
+        write(["gate": name, "entered": true], "gate") // test hook
+        while FileManager.default.fileExists(atPath: file.path) { try await Task.sleep(for: .milliseconds(100)) } // test hook
+        Logger.info("REVIEW_QA hold released \(name)", context: "ReviewQA") // test hook
+    } // test hook
+    static func state(_ profile: PubkyProfileManager) async -> [String: Any] { // test hook
+        let defaults = UserDefaults.standard // test hook
+        var value: [String: Any] = ["key": profile.publicKey ?? "", "session": String(describing: profile.currentSession), "head": "7c112a4d"] // test hook
+        for key in ["sharesPublicPaykitEndpoints", "sharesPrivatePaykitEndpoints", "hasConfirmedPublicPaykitEndpoints", "publicPaykitCleanupPending", "paykitContactSharingCleanupPending", "onchainAddress", "publicPaykitLegacyAddresses", "appDataSchemaVersion"] { // test hook
+            value[key] = defaults.object(forKey: key) ?? NSNull() // test hook
+        } // test hook
+        if let data = defaults.data(forKey: "privatePaykitAddressReservations") { // test hook
+            value["reservations"] = (try? JSONSerialization.jsonObject(with: data)) ?? NSNull() // test hook
+        } // test hook
+        value["ceiling"] = await PrivatePaykitAddressReservationStore.shared.backupSnapshot() ?? [:] // test hook
+        value["profileInitialized"] = profile.isInitialized // test hook
+        value["walletRestorePending"] = BackupService.shared.hasPendingWalletRestore() // test hook
+        value["backupWalletRequired"] = BackupService.shared.getBackupStatus(category: .wallet).isRequired // test hook
+
+        do { // test hook
+            let activities = try await CoreService.shared.activity.get(filter: .onchain, walletId: WalletScope.default) // test hook
+            value["activities"] = activities.compactMap { activity -> [String: Any]? in // test hook
+                guard case let .onchain(row) = activity else { return nil } // test hook
+                return ["txid": row.txId, "address": row.address, "confirmed": row.confirmed, "value": row.value] // test hook
+            } // test hook
+            value["transactions"] = try await ServiceQueue.background(.core) { // test hook
+                try BitkitCore.getAllTransactionDetails().map { details in // test hook
+                    ["txid": details.txId, "wallet": details.walletId, "outputs": details.outputs.map { ["address": $0.scriptpubkeyAddress ?? "", "value": $0.value, "n": $0.n] as [String: Any] }] as [String: Any] // test hook
+                } // test hook
+            } // test hook
+        } catch { value["activityError"] = String(describing: error) } // test hook
+        return value // test hook
+    } // test hook
+    static func performanceSeed(profile: PubkyProfileManager, contacts: ContactsManager) async throws -> [String: Any] { // test hook
+        var updates: [Paykit.ContactUpdate] = [] // test hook
+        for index in 0..<20 { // test hook
+            let key = try PubkyProfileManager.publicKeyFromSecretKey(String(format: "%064x", index + 50000)) // test hook
+            updates.append(Paykit.ContactUpdate(publicKey: key, label: "review-perf-\(index)")) // test hook
+            _ = try await PrivatePaykitAddressReservationStore.shared.currentOrRotatedAddress(for: key) // test hook
+        } // test hook
+        let saved = try await PaykitSdkService.shared.saveContacts(updates: updates, expectedIdentity: profile.publicKey) // test hook
+        let rows = (0..<500).map { index in // test hook
+            BitkitCore.TransactionDetails(walletId: "trezor:review-performance", txId: String(format: "%064x", index + 10000), amountSats: 5000, inputs: [], outputs: (0..<10).map { output in // test hook
+                TxOutput(scriptpubkey: "", scriptpubkeyType: "v0_p2wpkh", scriptpubkeyAddress: "bcrt1qreviewunowned\(index)output\(output)", value: 500, n: UInt32(output)) // test hook
+            }) // test hook
+        } // test hook
+        try await ServiceQueue.background(.core) { try BitkitCore.upsertTransactionDetails(detailsList: rows) } // test hook
+        let clock = ContinuousClock(); let start = clock.now // test hook
+        let used = await PrivatePaykitAddressReservationStore.shared.contactsWithUsedReservedAddresses() // test hook
+        let elapsed = start.duration(to: clock.now) // test hook
+        Logger.info("REVIEW_QA performance 20 reservations / 500 transaction rows / 10 outputs: \(elapsed), used=\(used.count)", context: "ReviewQA") // test hook
+        return ["savedContacts": saved.count, "reservations": 20, "transactionRows": 500, "outputsPerRow": 10, "duration": String(describing: elapsed), "used": used.count] // test hook
+    } // test hook
+    static func run(wallet: WalletViewModel, profile: PubkyProfileManager, contacts: ContactsManager) async { // test hook
+        try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) // test hook
+        let command = folder.appendingPathComponent("command.json") // test hook
+        while !Task.isCancelled { // test hook
+            if let bytes = try? Data(contentsOf: command), let value = try? JSONSerialization.jsonObject(with: bytes) as? [String: String], let action = value["action"] { // test hook
+                try? FileManager.default.removeItem(at: command) // test hook
+                let tag = value["tag"] ?? action // test hook
+                if action == "cancel" { operation?.cancel(); write(["cancelled": true], tag) } // test hook
+                else if action == "session_roll" { profile.reviewReplaceSession(); write(await state(profile), tag) } // test hook
+                else if action == "state" { write(await state(profile), tag) } // test hook
+                else if action == "observe" { // test hook
+                    Task { // test hook
+                        do { write(try await PaykitSdkService.shared.reviewObserve(value["peer"] ?? profile.publicKey ?? ""), tag) } // test hook
+                        catch { write(["error": String(describing: error)], tag) } // test hook
+                    } // test hook
+                } else { // test hook
+                    operation = Task { // test hook
+                        do { // test hook
+                            switch action { // test hook
+                            case "public_on", "public_off": // test hook
+                                _ = try await ContactPaymentsService.setPublicEnabled(action == "public_on", pubkyProfile: profile, operations: .live(wallet: wallet)) // test hook
+                            case "private_on", "private_off": // test hook
+                                _ = try await ContactPaymentsService.setEnabled(action == "private_on", pubkyProfile: profile, contactsManager: contacts, operations: .live(wallet: wallet)) // test hook
+                            case "backup": // test hook
+                                for category in BackupCategory.allCases where category != .usdt { _ = await BackupService.shared.triggerBackup(category: category) } // test hook
+                            case "performance_seed": write(try await performanceSeed(profile: profile, contacts: contacts), tag); return // test hook
+                            case "performance_clear": await PrivatePaykitAddressReservationStore.shared.clearContactAssignments() // test hook
+                            case "signout": try await profile.signOut() // test hook
+                            case "inspect_backup": write(try await BackupService.shared.reviewBackupFacts(), tag); return // test hook
+                            case "inject_legacy": try await BackupService.shared.reviewInjectLegacyMetadata(value["name"] ?? "") // test hook
+                            case "metadata": // test hook
+                                let result = await BackupService.shared.triggerBackup(category: .metadata) // test hook
+                                guard result == .completed else { throw NSError(domain: "ReviewQA", code: 1, userInfo: [NSLocalizedDescriptionKey: "Metadata backup did not complete: \(result)"] ) } // test hook
+                            case "capture": try await BackupService.shared.reviewCapture(value["name"] ?? "older") // test hook
+                            case "restore": try await BackupService.shared.performFullRestoreFromLatestBackup() // test hook
+                            case "restore_older": try await BackupService.shared.reviewRestoreOlder(value["name"] ?? "older") // test hook
+                            case "sync": await wallet.refreshAndSyncState() // test hook
+                            default: break // test hook
+                            } // test hook
+                            var result = await state(profile); result["completed"] = true; write(result, tag) // test hook
+                        } catch { write(["error": String(describing: error), "completed": false], tag) } // test hook
+                    } // test hook
+                } // test hook
+            } // test hook
+            try? await Task.sleep(for: .milliseconds(200)) // test hook
+        } // test hook
+    } // test hook
+} // test hook
+#endif

Deciding readbacks:

Migration: private=true, public=false, schema=4; remote WALLET legacy exclusion contains old Receive; METADATA cached Receive=null.
Monitoring: remote btc-regtest-p2tr retained while monitored, then btc-regtest-p2wpkh after disable; ordinary Receive unchanged; idle public SDK sync count=0.
Admitted write: PUT 201 after caller cancellation (3184 ms), followed by DELETE 204; remote endpoint absent.
Disabled-public batch: fee4eec8... outputs R=200000, P=100000; spend79ed374c... consumes both output0 and output1.
Marketplace: 48f545a1... amount_matched=true, confirmations=1, purchase_state=completed; repeat057ade95... same result.
Performance: reservations=20, savedContacts=20, transactionRows=500, outputsPerRow=10; durations0.401/0.395/0.394 seconds.
Observation hook for carried c434028 cases

Not pushed. Reads actual persisted reservation state, peer registry, public resolution and private lists; invokes the production sharing controls.

diff --git a/Bitkit/AppScene.swift b/Bitkit/AppScene.swift
index 122b3a15..3bfff592 100644
--- a/Bitkit/AppScene.swift
+++ b/Bitkit/AppScene.swift
@@ -522,6 +522,11 @@ struct AppScene: View {
 
     var body: some View {
         appEventContent
+            .task { // test hook
+                #if DEBUG
+                await ReviewQA.run(wallet: wallet, profile: pubkyProfile, contacts: contactsManager) // test hook
+                #endif
+            } // test hook
     }
 
     private var configuredContent: some View {
diff --git a/Bitkit/Managers/PubkyProfileManager.swift b/Bitkit/Managers/PubkyProfileManager.swift
index 65fbb6bf..4061ce49 100644
--- a/Bitkit/Managers/PubkyProfileManager.swift
+++ b/Bitkit/Managers/PubkyProfileManager.swift
@@ -1159,6 +1159,10 @@ class PubkyProfileManager: ObservableObject {
             return try await saveProfileEdit(name: name, bio: bio, links: links, tags: tags, avatarImage: avatarImage, session: session)
         }
 
+        func reviewReplaceSession() { // test hook
+            try? Self.beginSessionMutation() // test hook
+            Self.endSessionMutation() // test hook
+        } // test hook
         func clearAuthenticatedStateForTesting() {
             clearAuthenticatedState()
         }
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
index 1ada955e..7d780565 100644
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -536,6 +536,23 @@ class BackupService {
         }
     }
 
+
+    #if DEBUG
+    func reviewCapture(_ name: String) async throws { // test hook
+        let data = try await getBackupDataBytes(category: .wallet) // test hook
+        let folder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-qa") // test hook
+        try data.write(to: folder.appendingPathComponent(name + ".backup"), options: .atomic) // test hook
+        Logger.info("REVIEW_QA captured wallet backup", context: "ReviewQA") // test hook
+    } // test hook
+    func reviewRestoreOlder(_ name: String) async throws { // test hook
+        let folder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-qa") // test hook
+        let data = try Data(contentsOf: folder.appendingPathComponent(name + ".backup")) // test hook
+        await stopObservingBackups() // test hook
+        try await uploadBackup(BackupCategory.wallet.rawValue, data) // test hook
+        try await performFullRestoreFromLatestBackup() // test hook
+        Logger.info("REVIEW_QA restored older wallet backup", context: "ReviewQA") // test hook
+    } // test hook
+    #endif
     func hasPendingWalletRestore() -> Bool {
         if UsdtRecoveryRestoreGate.isPending {
             return true
diff --git a/Bitkit/Services/PubkyService.swift b/Bitkit/Services/PubkyService.swift
index 70b306e3..b8595a9c 100644
--- a/Bitkit/Services/PubkyService.swift
+++ b/Bitkit/Services/PubkyService.swift
@@ -981,6 +981,24 @@ actor PaykitSdkService {
         }
     }
 
+
+    #if DEBUG
+    func reviewObserve(_ peer: String) async throws -> [String: Any] { // test hook
+        try await withSdk { sdk in // test hook
+            var value: [String: Any] = [:] // test hook
+            value["peer"] = peer // test hook
+            let registry = try await sdk.paykitAppRegistry(publicKey: peer) // test hook
+            value["registry"] = registry?.apps.map { ["app": $0.appId, "private": $0.capabilities.privatePayments] as [String: Any] } ?? [] // test hook
+            let resolution = try await sdk.resolvePublicContactPayment(counterparty: peer, amount: nil) // test hook
+            value["publicStatus"] = String(describing: resolution.status) // test hook
+            value["publicFailures"] = resolution.failures.count // test hook
+            value["public"] = resolution.payableEndpoints.map { ["app": $0.appId, "identifier": $0.identifier, "payload": $0.payload.exportText()] } // test hook
+            value["private"] = try await sdk.currentPrivatePaymentLists(counterparty: peer).map { ["app": $0.appId, "version": $0.latestStreamItemId ?? 0, "endpoints": $0.paymentEndpoints.map { ["identifier": $0.identifier, "payload": $0.payload.exportText()] }] as [String: Any] } // test hook
+            value["peers"] = try await sdk.linkedPeers().map { ["key": $0.counterparty, "state": String(describing: $0.state)] } // test hook
+            return value // test hook
+        } // test hook
+    } // test hook
+    #endif
     func syncPaykitApp(
         privatePaymentsEnabled: Bool?,
         priority: PaykitSdkOperationLock.Priority = .ordered,
@@ -1001,7 +1019,10 @@ actor PaykitSdkService {
     func syncPublicEndpoints(
         _ endpoints: [PublicPaykitService.Endpoint], isSessionCurrent: (@MainActor () -> Bool)? = nil
     ) async throws -> EndpointSyncReport {
-        try await withStateRevisionTracking(priority: endpoints.isEmpty ? .ordered : .background) { sdk in
+        #if DEBUG
+        try await ReviewQA.hold("public-before-admission") // test hook
+        #endif
+        return try await withStateRevisionTracking(priority: endpoints.isEmpty ? .ordered : .background) { sdk in
             if let isSessionCurrent {
                 guard await isSessionCurrent() else { throw PubkyServiceError.sessionNotActive }
                 try Task.checkCancellation()
diff --git a/Bitkit/Utilities/ReviewQA.swift b/Bitkit/Utilities/ReviewQA.swift
new file mode 100644
index 00000000..e9185ce7
--- /dev/null
+++ b/Bitkit/Utilities/ReviewQA.swift
@@ -0,0 +1,77 @@
+
+#if DEBUG
+import Foundation // test hook
+import Paykit // test hook
+
+@MainActor enum ReviewQA { // test hook
+    static var operation: Task<Void, Never>? // test hook
+    static var folder: URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-qa") } // test hook
+    static func write(_ value: [String: Any], _ name: String) { // test hook
+        try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) // test hook
+        if let data = try? JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted, .sortedKeys]) { // test hook
+            try? data.write(to: folder.appendingPathComponent(name + ".json"), options: .atomic) // test hook
+        } // test hook
+    } // test hook
+    static func hold(_ name: String) async throws { // test hook
+        let file = folder.appendingPathComponent(name + ".hold") // test hook
+        guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+        Logger.info("REVIEW_QA hold entered \(name)", context: "ReviewQA") // test hook
+        write(["gate": name, "entered": true], "gate") // test hook
+        while FileManager.default.fileExists(atPath: file.path) { try await Task.sleep(for: .milliseconds(100)) } // test hook
+        Logger.info("REVIEW_QA hold released \(name)", context: "ReviewQA") // test hook
+    } // test hook
+    static func state(_ profile: PubkyProfileManager) async -> [String: Any] { // test hook
+        let defaults = UserDefaults.standard // test hook
+        var value: [String: Any] = ["key": profile.publicKey ?? "", "session": String(describing: profile.currentSession), "head": "c4340285"] // test hook
+        for key in ["sharesPublicPaykitEndpoints", "sharesPrivatePaykitEndpoints", "hasConfirmedPublicPaykitEndpoints", "publicPaykitCleanupPending", "paykitContactSharingCleanupPending", "onchainAddress", "publicPaykitLegacyAddresses", "appDataSchemaVersion"] { // test hook
+            value[key] = defaults.object(forKey: key) ?? NSNull() // test hook
+        } // test hook
+        if let data = defaults.data(forKey: "privatePaykitAddressReservations") { // test hook
+            value["reservations"] = (try? JSONSerialization.jsonObject(with: data)) ?? NSNull() // test hook
+        } // test hook
+        value["ceiling"] = await PrivatePaykitAddressReservationStore.shared.backupSnapshot() ?? [:] // test hook
+        value["walletRestorePending"] = BackupService.shared.hasPendingWalletRestore() // test hook
+        value["backupWalletRequired"] = BackupService.shared.getBackupStatus(category: .wallet).isRequired // test hook
+        return value // test hook
+    } // test hook
+    static func run(wallet: WalletViewModel, profile: PubkyProfileManager, contacts: ContactsManager) async { // test hook
+        try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) // test hook
+        let command = folder.appendingPathComponent("command.json") // test hook
+        while !Task.isCancelled { // test hook
+            if let bytes = try? Data(contentsOf: command), let value = try? JSONSerialization.jsonObject(with: bytes) as? [String: String], let action = value["action"] { // test hook
+                try? FileManager.default.removeItem(at: command) // test hook
+                let tag = value["tag"] ?? action // test hook
+                if action == "cancel" { operation?.cancel(); write(["cancelled": true], tag) } // test hook
+                else if action == "session_roll" { profile.reviewReplaceSession(); write(await state(profile), tag) } // test hook
+                else if action == "state" { write(await state(profile), tag) } // test hook
+                else if action == "observe" { // test hook
+                    Task { // test hook
+                        do { write(try await PaykitSdkService.shared.reviewObserve(value["peer"] ?? profile.publicKey ?? ""), tag) } // test hook
+                        catch { write(["error": String(describing: error)], tag) } // test hook
+                    } // test hook
+                } else { // test hook
+                    operation = Task { // test hook
+                        do { // test hook
+                            switch action { // test hook
+                            case "public_on", "public_off": // test hook
+                                _ = try await ContactPaymentsService.setPublicEnabled(action == "public_on", pubkyProfile: profile, operations: .live(wallet: wallet)) // test hook
+                            case "private_on", "private_off": // test hook
+                                _ = try await ContactPaymentsService.setEnabled(action == "private_on", pubkyProfile: profile, contactsManager: contacts, operations: .live(wallet: wallet)) // test hook
+                            case "backup": // test hook
+                                for category in BackupCategory.allCases where category != .usdt { _ = await BackupService.shared.triggerBackup(category: category) } // test hook
+                            case "capture": try await BackupService.shared.reviewCapture(value["name"] ?? "older") // test hook
+                            case "restore": try await BackupService.shared.performFullRestoreFromLatestBackup() // test hook
+                            case "restore_older": try await BackupService.shared.reviewRestoreOlder(value["name"] ?? "older") // test hook
+                            case "sync": await wallet.refreshAndSyncState() // test hook
+                            default: break // test hook
+                            } // test hook
+                            var result = await state(profile); result["completed"] = true; write(result, tag) // test hook
+                        } catch { write(["error": String(describing: error), "completed": false], tag) } // test hook
+                    } // test hook
+                } // test hook
+            } // test hook
+            try? await Task.sleep(for: .milliseconds(200)) // test hook
+        } // test hook
+    } // test hook
+} // test hook
+#endif

Reviewed with gpt-6.1-sol, reasoning effort high.


Reviewed by gpt-6.1-sol-high

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

@ovitrif

ovitrif commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

@ben-kaufman Merging this PR: approved on head 7c112a4 with a device run of its cases on that head, checks green and no open threads. It merges before the 3.1.0 cut, so it ships in 3.1.0.

@ovitrif
ovitrif merged commit 3f14ea0 into master Oct 30, 2026
23 checks passed
@ovitrif

ovitrif commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

@ben-kaufman Merged, as it was approved and green.

@ovitrif
ovitrif deleted the codex/ios-public-sharing-privacy-20261021 branch October 30, 2026 15:06
@ovitrif

ovitrif commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Placing this PR in 3.1.0: it merged before the 3.1.0 cut, so it ships in 3.1.0.

@ovitrif ovitrif modified the milestones: 3.2.0, 3.1.0 Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants