Skip to content

fix: separate usdt recovery backups from paykit - #1681

Draft
ben-kaufman wants to merge 5 commits into
masterfrom
fix/usdt-recovery-parity
Draft

ben-kaufman wants to merge 5 commits into
masterfrom
fix/usdt-recovery-parity

Conversation

@ben-kaufman

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

Copy link
Copy Markdown
Contributor

Twin: synonymdev/bitkit-ios#1118

This PR separates acknowledged USDT recovery backups from the full wallet backup, so an ordinary transfer does not export Paykit state or acquire a Pubky lock before broadcast.

Description

  • Adopts published Core 0.8.0-rc11, including the restore-order fix from fix: preserve USDT application identity across backup merges bitkit-core#179 and its regenerated bindings.
  • Writes encrypted, immutable, content-addressed USDT recovery records and acknowledges the required backup before broadcasting. Failed uploads retain the original signed transfer.
  • Keeps complete wallet backups separate, retains pending work through temporary failures and retries capture with exponential delays capped at five minutes.
  • Gates network recovery during restore, retains the selected legacy payload before journal import, validates every selected record and resumes interrupted migration. Concurrent writers cannot replace one another's acknowledged records or overwrite a complete wallet backup with partial data.
  • Advances recovery feedback only after an acknowledged backup callback, retaining Retry after a successful no-op refresh.
  • Reconciles automatic recovery and manual Retry against the retained transfer ID. Retry never creates another quote/payment, and feedback explains that closing the screen does not cancel the operation.
  • Preserves genuine Paykit authorization, request/deadline checks and started-payment guards. Imported proof evidence must be revalidated before it can satisfy a request.

Out of Scope

Native Pubky authentication recovery is tracked in pubky/pubky-homeserver#741. This PR does not release SDK artifacts, replay uncertain contact mutations or establish the historical contact-error cause.

Design

N/A — no new design supplied; Retry uses the existing send-screen controls.

Preview

Unavailable for the backup-failure screen: the controlled funded device fixture is not available. Unit tests are not presented as visual verification.

QA Notes

Setup

Use the committed app dependency pins for automated tests. The complete device journey requires an owned disposable test wallet and recipient, controlled USDT RPC/bundler, encrypted VSS endpoint and Pubky homeserver with fault injection. Standard staging does not provide this combined fixture. Do not use production funds or reset an existing wallet. Record operation IDs, signed bytes, acknowledgement and broadcast counts; see the committed USDT recovery backup document and journey prerequisites.

For the native protocol subset, follow the pinned loopback fixture recipe in synonymdev/bitkit-core#180. It uses a public test mnemonic without funds, real native signing and encrypted VSS, and separate prepare/resume/restore processes. The earlier combined run below used local Core c393f8e0, Paykit e79b7bbb and Pubky 65a331e0 bindings; these overrides are not the committed release pins.

For the deployed-contract UI subset, use the Core fork provider at c393f8e0 with Anvil v1.8.6, chain42161, Prague hardfork and fork block517943110. Build with USDT_RPC_URL=http://127.0.0.1:18545 USDT_BUNDLER_URL=http://127.0.0.1:18546; install on a new dedicated emulator and reverse only its ports18545/18546. Create a disposable wallet, use the provider's test_fundWallet on its receive address, then send one synthetic USDT to 0x1111111111111111111111111111111111111111. Cancel once, review again, confirm and inspect the receipt. Remove only that emulator's18545 reverse mapping before restarting to check saved activity offline. All funds and mutations remain in the local fork; normal VSS is used and no backup/Pubky faults are injected in this subset.

Journeys

  • J1: new usdt-independent-recovery.xml — Pubky outage with working VSS, actual backup failure before broadcast, lost acknowledgements, repeated/automatic retry, closing/restarting, interrupted restore/migration and concurrent writers; uses the same actions on both platforms.

Manual Tests

N/A — the controlled device cases are specified in J1; backend fault injection is an explicit prerequisite.

Automated Checks

  • added UsdtNativeRecoveryIntegrationTest.kt — real encrypted backup rejection/lost acknowledgements, separate-process restart, fresh-database restore and the original signed operation before and after retry.
  • added UsdtRecoveryBackupStoreTest.kt — independent uploads, immutable concurrent writers, lost acknowledgements, byte-order/hash compatibility and fail-closed restore.
  • updated BackupRepoTest.kt, PaykitUsdtPaymentRepoRecoveryTest.kt, UsdtRepoTest.kt, WalletViewModelTest.kt and UsdtViewModelTest.kt — restart/migration gates, retained payment progress, proof revalidation and same-transfer Retry/automatic recovery.

Evidence

At cc14496ed, required just compile, just test and just lint passed on macOS ARM64 with the committed library coordinates. Complete JUnit reports contain 4,602 passing tests, zero failures or skips. Detekt SARIF and Android Lint XML contain zero findings. The final test/lint run had unchanged source content.

Validation caught and repaired a restore-gate early-return type error and a stale test expectation that trusted imported proof. The repaired tests preserve payment-started guards while requiring proof revalidation. Journey XML parses. Independent review and bounded repair rechecks found no remaining actionable issues.

At test commit 2d8f8747, required just compile, just test and just lint passed again with committed dependency coordinates: 4,602 tests, zero failures/skips, Detekt SARIF zero findings and Android Lint XML zero findings. With the combined local SDK build, native rejection and lost-acknowledgement sequences passed prepare, resume and restore; bidirectional iOS/Android encrypted snapshot restore also passed. The instrumented native case calls the recovery store directly; BackupRepo orchestration is covered separately by unit tests.

Each native failure phase verifies zero broadcast attempts before its own recovery acknowledgement, unchanged signed operation bytes on retry and one unique payment. Full-wallet sentinel bytes remain unchanged. This verifies the native protocol subset, not the complete J1 journey.

At adoption commit 258bf9e5b, normal Gradle resolution uses published Core RC11. The downloaded AAR SHA256 is 9d5d54f5761b7773d2cfaaf0be77eeb914348d52e31ebf81684d7e761ce68b5b, matching Gradle's resolved artifact and the installed app's native library. All four ABI build IDs match the published debug-symbol archive. Required compile/lint checks passed with zero Detekt SARIF or Android Lint XML findings. The initial unit invocation reused cached output, so an explicit fresh execution was run: 4,602 passed, zero failures/errors/skips across 253 suites. Existing Gradle plugin deprecation and JVM CDS diagnostics are recorded separately.

The normal published-dependency app and instrumentation APKs were installed on the owned Android emulator and their installed bytes verified. Native rejected-backup and lost-acknowledgement cases each passed prepare, resume and restore. Fresh iOS-to-Android and Android-to-iOS encrypted restores passed with iOS 646670df; all 16 released-dependency native phases preserved the original operation and acknowledgement-before-submission ordering. All four PR commits are signed and GitHub reports verified=true, reason=valid. Current-head hosted Detekt and Android Lint also passed; their uploaded SARIF/XML reports contain zero findings. Draft-only build/E2E jobs were skipped and are not counted as executed tests.

At acknowledgement fix dc5a47d90, a successful refresh with no backup callback no longer clears recovery feedback. The UI reads live acknowledgements from the native backup adapter, including when a prior payment was already acknowledged. Failed persistence and no-op automatic/manual retry retain the error and Retry action. Regressions fail on unchanged 258bf9e5b and pass with the repair. Final required just compile, just test and just lint passed: 4,604 tests across 254 suites, zero failures/errors/skips; complete Detekt SARIF and Android Lint XML/SARIF reports contain zero findings. Source inputs were unchanged through validation and independent re-review is clean. All five commits against the live base are signed and GitHub reports verified=true, reason=valid. This follow-up has component/adapter coverage; the earlier native protocol evidence remains tied to the commits above.

Hosted lint for dc5a47d90 passed in run 40123893660. The downloaded artifact checksum matches GitHub metadata; complete Detekt SARIF and Android Lint XML/SARIF contain zero findings. Build and E2E jobs remain skipped by the draft guards and are not counted as passes.

A fresh normal-dependency app and instrumentation build at dc5a47d90 also passed. Both installed APK hashes were verified on the owned Android emulator, without clearing wallet data. All ten UsdtPaymentContentTest device cases passed, including submitted/confirmed transitions, closing the payment screen and comma-region input. This is screen-component coverage; it does not execute a full payment against the backend.

At dc5a47d90, a separate full-app run on a newly created Pixel 8 / API 36 ARM emulator used published dependencies with only USDT RPC/bundler URLs pointed to the owned loopback Anvil fork. The exact installed APK SHA256 was 02606962f5493af2e32c05219df6888e42f0c10e7ea9d2789aebafb490902cc7. After synthetic funding, canceling the first fee warning submitted no operation. Repeating review and explicitly confirming sent one USDT; the UI showed Confirmed. The local deployed-contract receipt contains one successful user operation and exactly one recipient token transfer, with zero native ETH in the sender. Restarting with the chain RPC unavailable preserved the saved activity. The full build log had no compiler diagnostics; the sole structured Gradle deprecation exactly matched the prior normal-dependency build. This exercises the real app and BackupRepo success path, but does not inject VSS/Pubky faults or complete J1. Android returns to Amount after canceling the fee warning; iOS retains Review. Both preserve the amount and submit nothing before approval.

Gaps

J1 has not run as a complete UI/Paykit/deployed-contract journey. The protocol fixture does not execute deployed contracts or exercise a real Pubky outage. Full wallet migration and concurrent state changes remain covered by component tests rather than this native fixture; Android's native store test does not exercise all BackupRepo orchestration.

This remains draft pending the remaining integrated validation and published Paykit/Pubky adoption. Core #179 and RC11 preparation #181 are merged; published Core RC11 is adopted and validated in both native recovery PRs. Local combination builds and native tests passed with Paykit lease-expiry safety from pubky/paykit-rs#211 and authentication recovery from pubky/pubky-homeserver#741; compatible published Paykit/Pubky artifacts are still unavailable. No app release or deployment is part of this PR.

The later device-sleep lease repair at Paykit 461864a4 passed 1,557 Rust tests, both native binding builds and full generated Android checks, but has not been adopted in this app. The combined app/native evidence above used Paykit e79b7bbb. Core RC11 does not contain the Pubky authentication or Paykit lease fixes.

The legacy complete wallet format remains supported. Older apps cannot read journal-only operations; downgrade recovery is limited to the last complete wallet backup. Records are not pruned, and immutable writes do not make multi-page listing atomic or coordinate simultaneous spending by multiple devices.

Models used

  • Planning/scoping: gpt-6-astra (reasoning: xhigh)
  • Implementation: gpt-6-astra (reasoning: xhigh)
  • Review: gpt-6-astra (reasoning: xhigh)

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

ovitrif commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Triage: 3.1.0. It fixes shipped USDT recovery backups, and 3.1.0 is due 28 Oct.

This branch has not been deployed

No deployments
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.

2 participants