Skip to content

feat(swift-sdk): add async off-main createWallet(mnemonic:) overload - #4483

Merged
QuantumExplorer merged 3 commits into
v4.2-devfrom
feat/swift-sdk-async-create-wallet
Aug 26, 2026
Merged

feat(swift-sdk): add async off-main createWallet(mnemonic:) overload#4483
QuantumExplorer merged 3 commits into
v4.2-devfrom
feat/swift-sdk-async-create-wallet

Conversation

@llbartekll

@llbartekll llbartekll commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

The synchronous createWallet blocks the calling thread for the entire native create — per-account key derivation plus a synchronous persistence flush, seconds of work — and every production caller is @MainActor, so wallet creation freezes the UI for its full duration (measured ~1.1–2.2s per network in dashwallet).

What was done?

Additive async overload with identical semantics that runs the blocking FFI on the shared destroyQueue (same rationale as async shutdown() from #4469: park a plain GCD thread, never the main thread or a Swift Concurrency cooperative-pool thread):

  • MainActor prologue (ensureConfigured + shutdown-in-progress admission check + handle/call-table snapshot) and a direct continuation (deliberately no Task {} wrapper — the dispatch happens synchronously at the suspension point, so an admitted create is enqueued FIFO-before any later shutdown()'s teardown block; teardown can never overtake an in-flight create).
  • shutdown() drains admitted creates before taking the handle: it closes admission (shutdownRequested, new creates throw up front before any native work), then awaits every in-flight create's FULL transaction — native create and MainActor epilogue (publish) — before the take-once. An admitted create whose FFI already persisted wallet data is therefore never failed retroactively by a concurrent teardown (that would make the caller roll back its mnemonic and orphan the persisted rows). The epilogue handle re-check remains as defense in depth (with assertionFailure) but is unreachable from the production shutdown path.
  • setWalletName and the wallets publish live in the MainActor epilogue.
  • performCreateWallet telemetry in parity with performNativeTeardown: one log line native create finished in Xms offMain=Y network=Z (verified on-device: 1512ms offMain=true network=1, 1076ms offMain=true network=0). The measured time includes the create's trailing identity().sync() inside the Rust call — per-stage splits would need Rust changes and are deliberately out of scope.
  • New PlatformWalletNativeCreateCalls test seam (the teardown table can't express create) + PlatformWalletCreateWalletTests: off-main execution, error mapping, create-after-shutdown, shutdown-during-create drain (handle stays live until the gated create finishes; shared event log proves create:end precedes the first teardown step), create-during-drain rejection, and concurrent creates with a measured maxInFlight == 1 (the queue provably serializes).
  • destroyQueue doc updated: it now serializes create+destroy; its job is thread-parking + deterministic FIFO (memory safety comes from the Rust registry — create holds a read guard for its whole call, destroy's map removal takes the write lock and block_on(shutdown()) runs after releasing it).
  • Example app: CreateWalletView now uses the async overload too (its per-network create loop ran synchronously inside MainActor.run, freezing the UI); WalletDetailView.enableNetwork adopted it and dropped its 50ms paint-a-frame sleep hack; ContentView.recoverWallet adopted it.

No Rust changes.

How Has This Been Tested?

  • bash build_ios.sh --target tests --profile dev && swift test: full suite green (6 create tests).
  • dashwallet-ios (dashpay scheme, arm64 sim) builds against this branch; on-device smoke of the consuming Add Wallet flow shows both networks' creates off-main.

Breaking Changes

Source-compatibility caveat (no ABI/behavior break for existing binaries): adding a same-name async overload changes overload resolution — per SE-0296, try await manager.createWallet(...) call sites silently re-resolve to the new overload (one such site in this repo: SpvLateWalletBackfillIntegrationTests.swift, behavior equivalent), and try manager.createWallet(...) written inside an async context stops compiling until marked try await (two such sites existed in SwiftExampleApp and are fixed here). External Swift consumers with async-context call sites may need the same one-word await fix. Sync contexts are untouched.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (N/A)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Wallet creation now runs asynchronously, improving responsiveness during creation and recovery.
    • Wallet operations are coordinated safely during shutdown, allowing in-progress creations to finish before teardown.
    • New wallet creation requests are rejected once shutdown begins.
  • Bug Fixes

    • Removed an unnecessary delay when creating wallets and improved handling of asynchronous creation errors.
  • Tests

    • Added coverage for concurrent creation, shutdown behavior, execution context, error handling, and teardown ordering.

The synchronous createWallet blocks the calling thread for the whole
native create - key derivation for every account plus a synchronous
persistence flush, seconds of work - and every production caller is
@mainactor, so wallet creation froze the UI for its full duration.

Add an additive async overload with identical semantics that runs the
blocking FFI on the shared destroyQueue (same rationale as the async
shutdown(): park a plain GCD thread, never the main thread or a
cooperative-pool thread), with:

- a MainActor prologue (ensureConfigured + handle/call-table snapshot)
  and a direct continuation (no Task wrapper), so an admitted create is
  enqueued FIFO-before any later shutdown's teardown block;
- a MainActor epilogue that re-checks the handle: a shutdown that landed
  during the off-main window discards the created wallet (its wrapper
  destroy is a registry no-op after manager teardown) instead of
  publishing into a torn-down manager;
- performCreateWallet timing + offMain logging in parity with
  performNativeTeardown, behind a new PlatformWalletNativeCreateCalls
  test seam;
- tests covering off-main execution, error mapping, create-after-
  shutdown, the shutdown-during-create race (FIFO proven via a shared
  event log), and concurrent creates.

The sync overload stays for sync contexts (loadFromPersistor recovery,
MainActor.run test bodies). Async call sites resolve to the new
overload per SE-0296: SpvLateWalletBackfillIntegrationTests:56 switches
silently (behavior equivalent, now off-main); ContentView.recoverWallet
and WalletDetailView.enableNetwork needed try await - which also
retires enableNetwork's 50ms paint-a-frame sleep hack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 747f1e0e-9e00-41bb-911c-8cda9e0d6d6d

📥 Commits

Reviewing files that changed from the base of the PR and between 3b3fd45 and 5b80766.

📒 Files selected for processing (3)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletCreateWalletTests.swift

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


📝 Walkthrough

Walkthrough

The Swift SDK now tracks wallet-creation admission during shutdown. Async creation completes native work and publication before teardown. Example app creation and recovery flows now await asynchronous wallet creation.

Changes

Async wallet creation

Layer / File(s) Summary
Native creation contract and shutdown admission
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
The manager injects native calls, serializes creation and teardown, rejects new requests after shutdown admission closes, and drains admitted async creations before teardown.
Async creation and application wiring
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
Async mnemonic creation maps native errors, publishes wallets, and checks the native handle after creation. Example app creation and recovery flows await the async API.
Creation and shutdown validation
packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletCreateWalletTests.swift
Tests cover execution context, publication, typed errors, shutdown draining, admission rejection, teardown ordering, and serialized concurrent creation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 5b807

This change moves wallet creation off the main thread while preserving shutdown ordering and adds coverage for the relevant concurrency cases; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: quantumexplorer, shumkov, zocolini

Sequence Diagram(s)

sequenceDiagram
  participant CreateWalletView
  participant PlatformWalletManager
  participant NativeWalletCalls
  participant WalletCollection
  CreateWalletView->>PlatformWalletManager: await createWallet(...)
  PlatformWalletManager->>NativeWalletCalls: create mnemonic wallet
  NativeWalletCalls-->>PlatformWalletManager: wallet ID or native error
  PlatformWalletManager->>WalletCollection: publish wallet
  PlatformWalletManager-->>CreateWalletView: return wallet or mapped error
  PlatformWalletManager->>NativeWalletCalls: await admitted creates before teardown
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.09% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding an asynchronous, off-main createWallet(mnemonic:) overload to the Swift SDK.
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.
  • 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/swift-sdk-async-create-wallet

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.

@thepastaclaw

thepastaclaw commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 5b80766)

Review follow-up (P1): the epilogue-throw design could fail an admitted
create RETROACTIVELY - the native create had already persisted wallet
data through the persister when a concurrent shutdown() took the handle,
so the invalidHandle throw made the app-side caller roll back its
mnemonic and orphan the persisted rows (watch-only wallet without a
mnemonic; retry hits WalletAlreadyExists). The destroy-queue FIFO only
ordered the native calls, not the MainActor epilogue.

shutdown() now closes admission first (shutdownRequested - new creates
throw invalidHandle up front, before any native work) and then waits for
every in-flight create's FULL transaction (native create + publish
epilogue) to finish before the take-once. The idempotency/no-op checks
re-run after each drain await. The epilogue handle re-check stays as
defense in depth (assertionFailure) but is unreachable from the
production shutdown path.

Tests reworked to prove the new properties: the shutdown-during-create
race asserts the handle stays live until the gated create finishes and
that create:end precedes the first teardown step in a shared event log;
a new case pins create-during-drain rejection; the concurrent-creates
test now measures maxInFlight == 1 through the seam.

Also adopts the async overload in the example app's CreateWalletView
(its per-network create loop ran synchronously inside MainActor.run,
freezing the UI - the second reviewer note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Preliminary review — Codex only

The async path correctly moves native wallet creation off the main thread and drains admitted async transactions before teardown, but shutdown still admits both legacy synchronous creation APIs while the MainActor is suspended. The tests also fabricate live-looking wallet handles whose deinits call the real Rust destructor, creating a test-isolation issue. Source: Codex general, security-auditor, and FFI-engineer reviewer lanes (exact backend model ID was not supplied); Claude final verifier (exact backend model ID was not supplied); openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift:546-550: Shutdown admission remains open for synchronous wallet creates
  After setting `shutdownRequested`, `shutdown()` suspends while an admitted async create finishes, making the MainActor reentrant while `isConfigured` and `handle` remain live. During that interval, callers can enter the synchronous mnemonic overload at line 828 or the synchronous seed overload at line 982 because both only call `ensureConfigured()`, which does not inspect `shutdownRequested`. Those calls can perform native creation and publish another wallet after shutdown has begun; they are also absent from `activeCreateCount`. This behavior was introduced by moving the handle take after the new drain and contradicts the PR's closed-admission guarantee. Reject shutdown-in-progress from both synchronous creation overloads, preferably through a common creation-specific guard shared with the async overload.

In `packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletCreateWalletTests.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletCreateWalletTests.swift:62-68: Fake wallet handles can destroy real Rust registry entries
  The injected success result is wrapped in `ManagedPlatformWallet`, whose `deinit` unconditionally calls the live `platform_wallet_destroy` FFI. Rust removes that numeric key from the process-global `PLATFORM_WALLET_STORAGE`, and real handles come from a process-global monotonic counter, so fake handles 101 and 102 can collide with live wallets owned by another test and remove their registry entries. This also contradicts the test's claim that the seam does not call FFI. Use `NULL_HANDLE`; destroying handle zero is a harmless registry miss, and the distinct wallet IDs already provide the differentiation these assertions require.

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

Final validation — Codex/Sol only (Phase 2 disabled)

At exact head 5b80766, the supplied Codex evidence is clean and no remaining in-scope defects were found. Both prior findings are fixed: every wallet-creation overload now uses the shutdown-aware admission guard, and injected successful creates use NULL_HANDLE rather than collision-prone fake registry handles. Validation passed with the iOS example build succeeding under warnings-as-errors and all 6 targeted PlatformWalletCreateWalletTests passing; git diff checks were also clean.
Source: Codex reviewer backend gpt-5.6-sol (general, FFI-engineer, and security-auditor); Claude verifier backend exact model ID was not supplied; openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and not reviewer evidence.

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — ffi-engineer (completed), gpt-5.6-sol — security-auditor (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

@QuantumExplorer
QuantumExplorer merged commit 2f3cff1 into v4.2-dev Aug 26, 2026
18 checks passed
@QuantumExplorer
QuantumExplorer deleted the feat/swift-sdk-async-create-wallet branch August 26, 2026 16:37
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.

3 participants