Skip to content

feat(swift-sdk): add mnemonic storage to WalletStorage#3477

Merged
QuantumExplorer merged 8 commits into
v3.1-devfrom
feat/swift-sdk-mnemonic-storage
Apr 14, 2026
Merged

feat(swift-sdk): add mnemonic storage to WalletStorage#3477
QuantumExplorer merged 8 commits into
v3.1-devfrom
feat/swift-sdk-mnemonic-storage

Conversation

@llbartekll

@llbartekll llbartekll commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

dashwallet-ios is migrating from DashSync to SwiftDashSDK. The backup seed phrase display (Settings → View Recovery Phrase) needs to read the 12-word mnemonic
from SwiftDashSDK instead of DashSync. Today WalletStorage only stores the post-PBKDF2 64-byte seed via storeSeed, which is one-way — you cannot recover the
human-readable words from it. This PR adds the missing mnemonic storage capability.

What was done?

Added three public methods to WalletStorage:

  • storeMnemonic(_ mnemonic: String) — stores the BIP39 phrase as UTF-8 in iOS keychain at account wallet.mnemonic under the existing org.dash.wallet service.
    Idempotent (delete-before-add).
  • retrieveMnemonic() -> String — reads the mnemonic back. Throws WalletStorageError.mnemonicNotFound if absent.
  • deleteMnemonic() — removes the entry. Idempotent (accepts errSecItemNotFound).

Added WalletStorageError.mnemonicNotFound enum case.

Storage uses plain keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly — no PIN-derived encryption. This is parity with how DashSync stores mnemonics
(plaintext in keychain with OS-level protection). iOS keychain is hardware-backed (Secure Enclave), encrypted at rest, device-locked, and never backed up to
iCloud. Adding PIN encryption on top provides marginal value given a 4-digit PIN is brute-forceable in seconds at 10k PBKDF2 iterations.

How Has This Been Tested?

  • dashwallet-ios dashwallet scheme builds successfully with the updated package (local SPM reference)
  • dashwallet-ios dashpay scheme builds successfully
  • The consumer side (dashwallet-ios) exercises storeMnemonic during wallet creation/migration/backfill and retrieveMnemonic during backup phrase display. Runtime
    testing deferred to a device that doesn't hit the known [DSChain retrieveWallets] host-app crash on iPhone 17 + iOS 26.3.

Breaking Changes

None. Additive only — three new public methods and one new error case. No existing API signatures changed.

Summary by CodeRabbit

  • New Features

    • Added mnemonic management: secure storage, retrieval, and deletion of wallet mnemonics.
    • Improved validation and user-facing error messages when mnemonic data is missing or invalid.
  • Bug Fixes / Reliability

    • Made keychain mutations and retrievals more robust, reducing failure cases and improving error mapping for missing items.

llbartekll and others added 7 commits April 6, 2026 15:41
Package.swift declares `.iOS(.v17)` as the minimum, but build_ios.sh
hardcoded IPHONEOS/IPHONESIMULATOR_DEPLOYMENT_TARGET=18.0, so the
DashSDKFFI.xcframework it produced was stamped iOS 18.0. Downstream
consumers targeting iOS 17 hit link-time warnings like:

  ld: warning: object file (.../librs_unified_sdk_ffi.a(...)) was
  built for newer iOS Simulator version (18.0) than being linked (17.0)

Replace the hardcoded exports with `${VAR:-17.0}` defaults so the
deployment target matches Package.swift by default while still being
overridable via env var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Storage

dashwallet-ios needs to construct a CoreWalletManager from outside the
SwiftDashSDK module to perform a one-shot wallet key migration from
DashSync's legacy keychain layout. The existing designated init takes
an internal SPVClient, and WalletStorage's synthesized init defaulted
to internal — both unreachable from external callers.

Add a public convenience init on CoreWalletManager that hides SPV
plumbing behind a KeyWalletNetwork parameter (constructs an SPVClient
with no on-disk data dir and an in-process SwiftData ModelContainer),
and add a public init() to WalletStorage so external callers can
construct it directly.

Both changes are additive and minimal — no behavior change, no
breaking change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ger convenience init

The earlier convenience init `CoreWalletManager(keyWalletNetwork:)` added
in 223dda6 had two real problems:

  1. Use-after-free. The convenience init constructed an SPVClient as a
     local variable and chained to the existing internal designated init
     `init(spvClient:modelContainer:)`. That designated init only stores
     `self.sdkWalletManager = try spvClient.getWalletManager()` and
     discards the SPVClient parameter. Because `WalletManager.init(handle:)`
     sets `ownsHandle = false` (the handle is non-owning, owned by the
     SPVClient), dropping the local SPVClient at end-of-init triggered
     `SPVClient.deinit -> destroy() -> dash_spv_ffi_client_destroy`,
     leaving `self.sdkWalletManager.handle` dangling. Build succeeded
     because this is a runtime ARC bug; never observed at runtime due to
     a separate iPhone 17 + iOS 26.3 [DSChain retrieveWallets] crash.

  2. Unverified `dataDir: nil` semantics. The convenience init passed
     `dataDir: nil` to SPVClient, which simply skips
     `dash_spv_ffi_config_set_data_dir`. Every other call site in the
     SDK passes an explicit Documents-based path; the FFI behavior with
     no dataDir is unknown and the SPVClient header comment hints at a
     directory lock that may persist even with nil.

Revert the convenience init. Instead, expose `HDWallet.init` publicly so
external callers can use the lower-level public API surface
(`WalletManager` standalone init + `Mnemonic` + `WalletStorage` + direct
`HDWallet` construction inserted into a fresh `ModelContext`) without
any SPVClient ceremony. This eliminates the lifetime bug, the dataDir
uncertainty, the inner-Task lifetime risk from `CoreWalletManager.init`,
and the @mainactor + async constraints — all in a single visibility
flip.

The `public init() {}` on `WalletStorage` from 223dda6 is still
required and stays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Out-of-module consumers (dashwallet-ios) need to construct SPVClient
directly with their own event handler implementations to drive SPV
chain sync from outside the SDK. Today everything in Core/SPV is
internal — SPVClient class, all 5 event handler protocols, the
SPVEventHandlers bundle, and every method on the client.

Mark them public:

  - SPVClient class + init + getSyncProgress + setMasternodeSyncEnabled
    + clearStorage + destroy + broadcastTransaction + startSync +
    stopSync + getWalletManager.
  - SPVEventHandlers struct + var fields + a new public memberwise init
    (the synthesized init defaults to internal on a public struct).
  - SPVProgressUpdateEventHandler / SPVSyncEventsHandler /
    SPVNetworkEventsHandler / SPVWalletEventsHandler /
    SPVClientErrorEventsHandler protocols.
  - SPVSyncManager enum (referenced in public protocol method
    signatures, must itself be public).

Strictly additive — same risk profile as the prior HDWallet.init /
WalletStorage.init / CoreWalletManager visibility patches in
223dda6 and b3bb5fc. No behavior change. The internal
intoFFIEventCallbacks bridging stays internal.

Verified: dashwallet and dashpay targets in dashwallet-ios both
build clean against the patched SwiftDashSDK via local SPM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to 400b4b2. The 5 SPV event handler protocols are now
public, but their `intoFFI*Callbacks()` default implementations in
the protocol extensions were still internal-scoped. That meant
out-of-module conformers (dashwallet-ios) saw the protocol
requirement but could NOT see the default implementation, forcing
them to either implement the FFI bridge themselves or fail to
compile.

Mark the 5 default implementations public so out-of-module
conformers get the bridge for free:

  - SPVProgressUpdateEventHandler.intoFFIProgressCallback
  - SPVSyncEventsHandler.intoFFISyncEventCallbacks
  - SPVNetworkEventsHandler.intoFFINetworkEventCallbacks
  - SPVClientErrorEventsHandler.intoFFIClientErrorCallback
  - SPVWalletEventsHandler.intoFFIWalletEventCallbacks

The FFI types these methods reference (FFIProgressCallback,
FFISyncEventCallbacks, etc.) are already visible to out-of-module
consumers because SwiftDashSDK.swift uses
`@_exported import DashSDKFFI`. No new surface area is being added
on top of what 400b4b2 already exposed.

Verified: dashwallet-ios still builds clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add storeMnemonic/retrieveMnemonic/deleteMnemonic methods to
WalletStorage for persisting the BIP39 mnemonic phrase in iOS
keychain. Plain keychain with kSecAttrAccessibleWhenUnlocked
ThisDeviceOnly — no PIN-derived encryption (iOS keychain hardware
protection is the security boundary, parity with how DashSync
stores mnemonics).

New keychain account: "wallet.mnemonic" under the existing
"org.dash.wallet" service. Idempotent writes (delete-before-add),
idempotent deletes (accepts errSecItemNotFound).

This enables dashwallet-ios to store and retrieve the human-readable
12-word phrase independently of DashSync, which is needed to migrate
the backup seed phrase display away from DashSync's
seedPhraseIfAuthenticated API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 10, 2026
@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added mnemonic keychain support to WalletStorage (store, retrieve, delete). Also adjusted keychain mutation and retrieval paths to validate deletion results and map keychain errors to explicit WalletStorageError cases.

Changes

Cohort / File(s) Summary
Mnemonic Persistence
packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift
Added storeMnemonic(_:), retrieveMnemonic(), and deleteMnemonic() to persist mnemonic UTF‑8 plaintext in the keychain using kSecAttrAccessibleWhenUnlockedThisDeviceOnly; introduced WalletStorageError.mnemonicNotFound.
Keychain mutation & retrieval adjustments
packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift
Refactored keychain mutation code paths to use explicit deleteQuery variables and validate SecItemDelete results (treating errSecItemNotFound as acceptable). Seed retrieval now explicitly maps errSecItemNotFound to WalletStorageError.seedNotFound; keychain errors are translated to WalletStorageError.keychainError.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble bytes and hide them deep,
A mnemonic snug where secrets sleep.
Store, fetch, erase — a tidy chore,
The rabbit guards the wallet door. 🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The pull request title 'feat(swift-sdk): add mnemonic storage to WalletStorage' directly and clearly summarizes the main change: adding mnemonic storage capability to the WalletStorage class in the Swift SDK.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 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-mnemonic-storage

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 and usage tips.

@thepastaclaw

thepastaclaw commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

Review Gate

Commit: 2e04587d

  • Debounce: 1461m ago (need 30m)

  • CI checks: build failure: PR title

  • CodeRabbit review: comment found

  • Off-peak hours: off-peak (12:42 AM PT Tuesday)

  • Run review now (check to override)

@github-actions

github-actions Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "9a3d3409968528001cd357b0253ff151920b1091a22982a08714cfb27afefaf0"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift`:
- Around line 105-121: In storeMnemonic(), don't reuse the add-query for
SecItemDelete; build a separate deleteQuery containing only identifying
attributes (kSecClass/kSecAttrService/kSecAttrAccount using keychainService and
mnemonicKeychainAccount) and call SecItemDelete with that, verify the result
allows errSecSuccess or errSecItemNotFound before proceeding; then construct the
add-query with kSecValueData and kSecAttrAccessible and call SecItemAdd,
throwing WalletStorageError.keychainError(status) on failure.
- Around line 124-140: The retrieveMnemonic() function currently maps all
SecItemCopyMatching failures to WalletStorageError.mnemonicNotFound; change it
to distinguish not-found vs other Keychain errors by checking the returned
status: if status == errSecSuccess proceed as before, if status ==
errSecItemNotFound throw WalletStorageError.mnemonicNotFound, otherwise throw or
map the status into a Keychain-specific error (e.g.,
WalletStorageError.keychainError(status)) similar to how deleteMnemonic()
handles non-found vs other errors; update references in retrieveMnemonic to use
SecItemCopyMatching, errSecSuccess, errSecItemNotFound and WalletStorageError to
locate and implement the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4c3e4b2-e70c-4e0c-860d-ad332596cb5c

📥 Commits

Reviewing files that changed from the base of the PR and between bb4c91d and a9b0de9.

📒 Files selected for processing (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift

- Separate delete and add queries in all store methods (storeSeed,
  storeMnemonic, storePINHash, enableBiometricProtection) so
  SecItemDelete uses only identifying attributes (class/service/account)
- Verify delete status before proceeding with add
- Distinguish errSecItemNotFound from other keychain errors in
  retrieveSeed and retrieveMnemonic
- Add docstrings to mnemonic and seed storage methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@llbartekll llbartekll changed the title Feat/swift sdk mnemonic storage feat(swift-sdk): add mnemonic storage to WalletStorage Apr 13, 2026
@llbartekll llbartekll changed the title feat(swift-sdk): add mnemonic storage to WalletStorage feat(swift-sdk): add mnemonic storage to WalletStorage Apr 13, 2026
@QuantumExplorer QuantumExplorer merged commit 8121dc2 into v3.1-dev Apr 14, 2026
38 of 42 checks passed
@QuantumExplorer QuantumExplorer deleted the feat/swift-sdk-mnemonic-storage branch April 14, 2026 07:45
QuantumExplorer pushed a commit that referenced this pull request Apr 19, 2026
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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