Skip to content

feat(wallet): run Add Wallet's create FFI off-main and serialize it on the lifecycle queue - #1068

Merged
llbartekll merged 2 commits into
developfrom
feat/add-wallet-offmain-create
Aug 26, 2026
Merged

feat(wallet): run Add Wallet's create FFI off-main and serialize it on the lifecycle queue#1068
llbartekll merged 2 commits into
developfrom
feat/add-wallet-offmain-create

Conversation

@llbartekll

@llbartekll llbartekll commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Etap B of the Add Wallet freeze fix — a partial fix, by design. Etap A (#1064) put the blocking overlay up from the first moment, but the MainActor was still genuinely frozen underneath: the synchronous SDK createWallet FFI ran on the main thread for both networks (~1.5s + ~1.1s measured), so the spinner stalled and the app was dead to the touch.

What was done?

Adopts the SDK's new async createWallet(mnemonic:) overload (dashpay/platform#4483) for the interactive Add Wallet flow and serializes that flow on the runtime's lifecycle chain:

  • MnemonicFirstWalletCreation.run takes an async createWallet closure (single version — no sync/async pair, to avoid the overload-resolution hazard inside app code). Rollback-after-await semantics unchanged, pinned by a suspension-path test.
  • createAndPersistasync throws with an offMainCreate switch:
    • true — interactive add: the async overload; the blocking FFI leaves the MainActor. Safe because performAddWallet runs the whole add as one link of the serial lifecycle chain, so a queued refresh/fullReset can never interleave with the suspension the off-main create introduces.
    • false — onboarding/migration (createOrImportWallet): the sync SDK overload, forced by an explicitly typed non-async closure. An await between the mnemonic persist and the wallet rows appearing would let a concurrently scheduled startIfReady/refresh observe the half-state (mnemonic present, no rows), build a competing runtime, and the two flows could overwrite each other's publish or leak a manager to the deinit-fallback teardown. Onboarding can't ride the lifecycle queue instead (refresh awaits the key migrator, and the migrator calls createOrImportWallet — enqueueing would self-deadlock), so it keeps the MainActor-atomic critical section and still blocks main during onboarding create, as before this PR. Its off-main adoption needs its own serialization design (etap C candidate).
  • recoverPersistedWallet (launch recovery, sync context) stays on the sync SDK overload.
  • SerialAsyncLifecycleQueue gains a value-returning enqueueAwaitable<T> (same chain, full barrier semantics); the runtime gains performAddWallet. The post-add switch stays outside the chain (sequential op — nesting would self-await-deadlock the queue, same hazard documented at switchNetwork).
  • The 100ms overlay-commit sleep stays: on the mirror-repair path the first provisioning work is still the other network's synchronous SDK build on the MainActor with no suspension point before it — without the sleep the overlay window would not be committed until that block ends. Removal is tied to etap C.

SDK-side, #4483's shutdown() drains admitted creates before taking the handle, so an add whose FFI already persisted wallet data can never be failed retroactively into a mnemonic rollback that orphans the persisted rows.

Honest scope — what still blocks main: the mirror-network leg's SDK(network:) + configure + loadFromPersistor (~1.6–2s, one continuous stretch) during Add Wallet, and the whole onboarding create (unchanged from before). Both are etap C candidates once the new create telemetry accumulates. The Add Wallet loader shows for a similar total time as before — the work is the same, it just no longer freezes the UI during the create phases.

How Has This Been Tested?

  • swift-sdk: full suite green, 381 tests incl. 6 create tests (see #4483).
  • App: clean dashpay build (arm64 sim). Testnet simulator smoke of Wallets → Add Wallet → Create New Wallet:
    • SDK log shows native create finished in 1512ms offMain=true network=1 and 1076ms offMain=true network=0 — both legs off-main;
    • composite Creating → Switching → Home with no idle flicker; new wallet active;
    • error path (host not running) drops the overlay and surfaces the alert with the sheet still open;
    • network switch M→T under its overlay unchanged (feat(wallet): unify wallet lifecycle loaders behind an app-wide overlay #1062 regression check).
  • Fresh-onboarding smoke (wipe → create from the seed screen, launch-time startIfReady racing): wallet created and bound, single runtime.
  • App test target remains broken repo-wide; updated MnemonicFirstWalletCreationTests + new enqueueAwaitable test are compile-ready per the repo's current posture.

Build requirement: the local ../platform checkout must include dashpay/platform#4483 (local SPM path dependency — no pin to bump; no FFI rebuild needed, Rust untouched).

🤖 Generated with Claude Code

…n the lifecycle queue

Etap B of the Add Wallet freeze fix (a PARTIAL fix - see scope note
below). Adopts the SDK's new async createWallet(mnemonic:) overload
(platform feat/swift-sdk-async-create-wallet) so the blocking native
create - both networks' key derivation + persistence flush, the bulk of
the ~5s MainActor freeze - runs on the SDK's dedicated queue instead of
the main thread. The overlay's spinner now animates through the create
windows.

- MnemonicFirstWalletCreation.run takes an async createWallet closure
  (single version - no sync/async pair); rollback-after-await semantics
  unchanged and now covered by a suspension-path test.
- createAndPersist is async; its three call sites await it. The
  loadFromPersistor recovery path (recoverPersistedWallet) deliberately
  stays on the sync SDK overload.
- SerialAsyncLifecycleQueue gains a value-returning enqueueAwaitable<T>
  (same chain, full barrier semantics), and the runtime gains
  performAddWallet: the interactive add now runs as ONE link of the
  serial lifecycle chain, so a queued refresh/fullReset can no longer
  interleave with the multi-network provisioning. The post-add switch
  stays OUTSIDE the chain (sequential op - nesting would self-await-
  deadlock the queue). Onboarding's createOrImportWallet deliberately
  stays off the chain (its launch-time race-by-design is unchanged).
- The 100ms overlay-commit sleep STAYS: on the mirror-repair path the
  first provisioning work is still the other network's synchronous SDK
  build on the MainActor with no suspension point before it.

Scope note: the mirror-network leg's SDK(network:) + configure +
loadFromPersistor (~1.6-2s) still blocks the MainActor under the
visible overlay - known limitation, candidate for etap C after the new
create telemetry (offMain + duration logs) is in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@llbartekll
llbartekll requested a review from romchornyi August 26, 2026 09:28
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e62da32-fa74-4469-a51a-4cde2e912ecc

📥 Commits

Reviewing files that changed from the base of the PR and between 0e243a9 and c38748d.

📒 Files selected for processing (5)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletRuntime.swift
  • DashWallet/Sources/UI/Menu/Security/Wallets/WalletsViewModel.swift
  • DashWalletTests/SwiftDashSDKCoreLifecycleTests.swift
  • DashWalletTests/WalletWipeSerialExecutorTests.swift

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Wallet creation now awaits asynchronous SDK operations. Additive provisioning runs as one serialized lifecycle operation. Tests cover queue results, error propagation, ordering, and mnemonic rollback.

Changes

Wallet lifecycle updates

Layer / File(s) Summary
Async wallet creation and rollback
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKHost.swift, DashWalletTests/WalletWipeSerialExecutorTests.swift
Wallet creation and cross-network provisioning now await asynchronous SDK operations. Mnemonic rollback and cleanup behavior remain covered by async tests.
Awaitable lifecycle queue
DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletRuntime.swift, DashWalletTests/SwiftDashSDKCoreLifecycleTests.swift
SerialAsyncLifecycleQueue returns operation results and errors. Additive provisioning uses this queue as one serialized operation.
Add-wallet UI integration
DashWallet/Sources/UI/Menu/Security/Wallets/WalletsViewModel.swift
The view model invokes serialized additive provisioning and retains overlay, run-loop, and post-provisioning switch behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to c3874

The change moves wallet creation off the main thread and serializes interactive lifecycle work; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant WalletsViewModel
  participant SwiftDashSDKWalletRuntime
  participant SerialAsyncLifecycleQueue
  participant SwiftDashSDKHost
  WalletsViewModel->>SwiftDashSDKWalletRuntime: performAddWallet
  SwiftDashSDKWalletRuntime->>SerialAsyncLifecycleQueue: enqueueAwaitable
  SerialAsyncLifecycleQueue->>SwiftDashSDKHost: addWallet
  SwiftDashSDKHost-->>SerialAsyncLifecycleQueue: AddWalletResult or error
  SerialAsyncLifecycleQueue-->>WalletsViewModel: provisioning result or error
Loading

Suggested reviewers: romchornyi, quantumexplorer, jeanpierreroma

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: moving Add Wallet creation FFI off the main thread and serializing it on the lifecycle queue.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-wallet-offmain-create

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Review follow-up (P1): adopting the async SDK create everywhere put a
REAL suspension point in the middle of createOrImportWallet's
transaction - between the mnemonic persist and the wallet rows
appearing. During that window the MainActor could run a concurrently
scheduled startIfReady/refresh, which would see the mnemonic (hasSDKWallet)
with no wallet rows yet, build a competing runtime, and the two flows
could overwrite each other's publish or leak a manager to the deinit
fallback teardown. The old sync FFI blocked the MainActor exactly there,
so the ordering was NOT unchanged as previously claimed.

createAndPersist gains offMainCreate: the interactive add keeps the
async overload (safe - performAddWallet serializes it on the lifecycle
queue, so refreshes queue behind it), while the onboarding/migration
path (createOrImportWallet) forces the SYNC SDK overload via an
explicitly typed non-async closure - no suspension between persist and
create, restoring the atomic critical section. Onboarding cannot go on
the lifecycle queue instead: refresh awaits the key migrator, and the
migrator calls createOrImportWallet - enqueueing would self-deadlock.

Onboarding therefore still blocks main for the create's duration (as
before this PR); its off-main adoption needs its own serialization
design and stays out of this etap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@llbartekll
llbartekll merged commit 1afd502 into develop Aug 26, 2026
3 checks passed
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.

1 participant