Skip to content

fix(sdk): wallet-flow network fixes for SwiftExampleApp#3772

Open
QuantumExplorer wants to merge 11 commits into
v3.1-devfrom
fix/swift-wallet-flow-network-fixes
Open

fix(sdk): wallet-flow network fixes for SwiftExampleApp#3772
QuantumExplorer wants to merge 11 commits into
v3.1-devfrom
fix/swift-wallet-flow-network-fixes

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 31, 2026

Issue being fixed or feature implemented

A set of wallet-flow fixes in the iOS SwiftExampleApp / swift-sdk for operating across multiple networks. The same mnemonic can now live on several networks, and the create / rescan / add-to-network flows behave correctly when the target network isn't the currently-active one.

Bugs fixed:

  1. Mainnet/testnet wallet creation silently failed. The devnet-only platformQuorumURL override was applied to every network, leaking a non-https http://… quorum URL into mainnet/testnet SDK construction. Rust's trusted context provider rejects a non-https custom quorum URL on those networks, so the build failed and no wallet was created.
  2. Create dialog ignored the ticked network. Ticking e.g. "Mainnet" while on devnet created a devnet wallet instead — only the first/active network was used.
  3. No feedback on create failure. The error alert is bound to CreateWalletView, but the pushed SeedBackupView stayed on top with its submit button stuck disabled — the user was stranded with no visible error.
  4. Per-network "+" add-to-network did nothing. The button in Wallet Info didn't actually add an existing wallet to another network.
  5. Re-scan for Identities showed wallets from all networks. The picker used an unfiltered @Query, listing every wallet across every network instead of just the active-network ones.

What was done?

Per-network persistence infrastructure

  • PersistentWallet.swift — make rows unique per (walletId, network) and add predicate(walletId:) / predicate(walletId:network:) helpers, so the same mnemonic can coexist across networks without colliding.
  • PlatformWalletPersistenceHandler.swift / PlatformWalletManager.swift — scope persister wallet lookups to the manager's network, so each per-network manager restores only its own rows.

Create flow

  • CreateWalletView.swift — create the wallet in every ticked network via WalletManagerStore.backgroundManager(for:) (each network has its own network-locked Rust manager); walletId is network-independent, so the Keychain mnemonic + metadata are written once and isImported is stamped on every per-network row. On failure, pop the pushed backup screen so the error alert becomes visible.
  • SDK.swift — gate the platformQuorumURL (and DAPI address) override behind useOverrideAddresses (regtest/devnet or opt-in docker). Mainnet/testnet now use the SDK's automatic canonical quorum endpoints.

Add-to-network / identities / rescan

  • WalletDetailView.swift — implement the per-network "+" add-to-network action.
  • IdentitiesContentView.swift — take an explicit network and scope the identities @Query to it; IdentitiesView.swift passes the active network; ContentView.swift threads the active network through.
  • SearchWalletsForIdentitiesView.swift — add platformState, query all wallets, and filter to the active network via a computed property (mirrors WalletsContentView@Query can't reference a runtime env-object value at property-init time). The rescan sheet re-injects platformState.

All changes are Swift-side only (persist / load / bridge) — no Rust/FFI changes, no protocol changes.

How Has This Been Tested?

Verified on the booted iOS simulator (iPhone 17 Pro) against the live SwiftData store:

  • Create routes to ticked network: a testnet-only create while on devnet produced exactly one networkRaw=1 row and no devnet fallback row.
  • Add-to-network: the per-network "+" produced a new per-network row + UI checkmark for the target network.
  • Rescan network filter: with currentNetwork = mainnet and 9 persisted wallet rows across 4 networks (mainnet ×1, testnet ×3, devnet ×2, regtest ×3), the Re-scan for Identities picker now lists only the single mainnet wallet — count dropped 9 → 1, matching the active-network set exactly.
  • Quorum override: confirmed via boot console that mainnet SDK construction logs no DAPI addresses provided, using defaults for network / Mainnet (no leaked devnet http://…:8080 quorum URL), while the devnet manager still uses its caller-provided quorum URL.
  • Create-failure UX: verified the backup screen pops on failure so the error alert is reachable.

Note: this PR does not fix a separate, pre-existing crash in the external dash-spv dependency (SegmentedStorage::get_items() panics on segments with a non-zero first_valid_offset), which causes the Core Sync "Start" button to appear to do nothing on mainnet. Filed upstream at dashpay/rust-dashcore#792.

Breaking Changes

None. Swift SDK / example-app behavior only; no consensus or protocol changes.

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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Multi-network wallet creation persists wallets per network and saves mnemonics per network.
    • UI respects selected network across Identities, search, and Wallet detail.
    • Wallet detail can enable a wallet on additional networks.
  • Bug Fixes

    • Wallet IDs can repeat across networks (now treated as network-scoped).
    • Safer deletion: keychain mnemonic/metadata and cached data are removed only when the last network instance is deleted.
    • Avoids leaking dev/test quorum endpoints into production.

Three independent wallet-flow bugs surfaced while testing on
non-active networks:

- SDK.swift: the devnet-only `platformQuorumURL` override was applied
  to every network, leaking a non-https http quorum URL into
  mainnet/testnet SDK builds. Rust's trusted context provider rejects
  that, so mainnet/testnet wallet creation silently failed. Gate the
  override behind `useOverrideAddresses` so mainnet/testnet use the
  SDK's automatic (canonical) quorum endpoints.

- CreateWalletView.swift: on create failure the error alert is bound to
  CreateWalletView, but the pushed SeedBackupView sat on top of it with
  its submit button stuck disabled — the user got no feedback. Pop the
  backup screen in the catch block so the alert is visible.

- SearchWalletsForIdentitiesView.swift / IdentitiesContentView.swift:
  the Re-scan for Identities picker used an unfiltered @query and listed
  wallets from every network. Filter to the active network (query-all +
  computed-filter, matching WalletsContentView) and re-inject
  platformState into the sheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer requested a review from shumkov as a code owner May 31, 2026 22:07
@github-actions github-actions Bot added this to the v3.1.0 milestone May 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR refactors multi-network wallet support across the Swift SDK and example app. The data model shifts PersistentWallet uniqueness from per-walletId to composite (walletId, networkRaw). Persistence handlers implement network-scoped lookups via walletRecordPredicate. Deletion logic checks cross-network row counts to preserve mnemonics safely. SDK initialization gates quorum endpoint forwarding to prevent devnet leaks. SwiftUI views add network routing, filtered queries, and multi-network wallet creation and enablement flows.

Changes

Multi-network wallet architecture and UI

Layer / File(s) Summary
PersistentWallet schema and network-scoped predicates
packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentWallet.swift
PersistentWallet removes per-walletId unique attribute and declares composite uniqueness on (walletId, networkRaw). New predicate(walletId:network:) overload filters both fields for network-scoped queries.
Persistence handler network-aware wallet lookups
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
Introduces shared walletRecordPredicate(walletId:) that matches (walletId, networkRaw) when network is set, otherwise walletId-only. Updates ensureWalletRecord, findWalletRecord, fetchWalletForLink to use network-scoped lookups. Adds public walletRowCountAcrossNetworks(walletId:) to count rows across all networks, and aligns identityIdsForWallet and deleteWalletData with network scoping.
Safe multi-network wallet deletion
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
deleteWallet(walletId:) and deleteWalletData check remaining row count across networks. Mnemonic and metadata are purged from Keychain and shared cached-state tables only when the wallet is the last remaining row across networks; otherwise those entries are preserved.
SDK initialization quorum endpoint gating
packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift
init(network:platformVersion:) now conditionally forwards platformQuorumURL to FFI config only when useOverrideAddresses is true, preventing devnet non-HTTPS quorum endpoints from leaking into mainnet/testnet builds.
Network-aware identity and wallet query infrastructure
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/IdentitiesContentView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/SearchWalletsForIdentitiesView.swift
ContentView passes currentNetwork to IdentitiesTabView. IdentitiesTabView stores network and forwards to IdentitiesContentView. IdentitiesContentView takes init(network:) and configures @Query via PersistentIdentity.predicate(network:). SearchWalletsForIdentitiesView filters wallets by currentNetwork.rawValue from an allWallets query.
Multi-network wallet creation flow
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
Injects walletManagerStore and refactors creation from single-network to multi-network: collecting selectedNetworks, iterating each to create via backgroundManager(for:), tolerating per-network failures, storing mnemonic once, stamping isImported across all wallet rows, and writing Keychain metadata per created wallet/network. Success/error UI handling dismisses backup screen on error to show alerts.
Wallet multi-network management in detail view
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
Injects WalletManagerStore. loadNetworkStates() fetches all PersistentWallet rows for the wallet and derives enabled-network flags from their network set. enableNetwork(_:) retrieves mnemonic from WalletStorage, creates wallet on target network via backgroundManager, handles missing mnemonic as user error, tolerates "already exists" failures, and refreshes network states.
Identity display badges and private key indicators
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift
IdentityRow adds computed hasAnyPrivateKey checking identity-level keys and KeychainManager for private key presence. Badge rendering conditionally shows identity-type and "No Keys"/"No Wallet" badges. New IdentityBadge component encapsulates badge styling.
Search wallet sourcing for identity linking
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/SearchWalletsForIdentitiesView.swift
Search view now queries all wallets and filters by platformState.currentNetwork.rawValue instead of relying on a network-scoped @Query.
Rust wallet-id tests and Cargo updates
packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs, Cargo.toml
Document wallet-id network-scoping and add unit tests asserting distinct/stable wallet IDs across networks. Update workspace dependency rev for dashcore-family crates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dashpay/platform#3755: Both PRs touch SwiftDashSDK/SDK.swift’s trusted-setup/quorum URL wiring, changing when/what quorum_url is forwarded into the Rust/FFI SDK configuration.
  • dashpay/platform#3653: Related work on multi-network wallet deletion and deleteWalletData wiping behavior.
  • dashpay/platform#3765: Overlapping changes to wallet deletion/persistence flows.

Suggested labels

ready for final review

Suggested reviewers

  • shumkov
  • thepastaclaw

🐰 Across the nets I hop with glee,
Each wallet finds its place to be.
Mnemonics kept when siblings stay,
Queries scoped, no keys astray.
A hopping cheer for safer play.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(sdk): wallet-flow network fixes for SwiftExampleApp' directly corresponds to the primary objective: enabling single mnemonics to coexist across multiple networks and ensuring wallet-flow operations respect the selected target network.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/swift-wallet-flow-network-fixes

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.

Supporting per-network wallet infrastructure that the create-wallet and
rescan fixes build on:

- PersistentWallet: make rows unique per (walletId, network) and add
  `predicate(walletId:)` / `predicate(walletId:network:)` helpers so
  the same mnemonic can live on multiple networks without colliding.
- PlatformWalletPersistenceHandler / PlatformWalletManager: scope
  persister wallet lookups to the manager's network so a per-network
  manager only restores its own rows.
- WalletDetailView: implement the per-network "+" add-to-network action
  so an existing wallet can be added to another network.
- IdentitiesContentView: take an explicit `network` and scope its
  identities @query to it; IdentitiesView passes the active network.
- ContentView: thread the active network through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 31, 2026

✅ Review complete (commit 92b6b28)

Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

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

Self Reviewed

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 31, 2026

✅ 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: "f5e7320fb4595c1d59824f08b63b277ae6ec80885693f579cc900d060bc2bbcf"
)

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift (1)

746-752: ⚡ Quick win

Silent error handling may hide real failures from the user.

The catch block logs all errors but shows no UI feedback. While "already exists" is a valid no-op, other failures (network issues, Rust panics, etc.) will silently fail, leaving the user confused why the "+" button didn't enable the network.

Consider distinguishing "already exists" from other errors to surface actionable feedback:

Proposed improvement
         } catch {
-            // An "already exists" throw means the wallet is already on
-            // this network — treat as a no-op and just refresh below.
-            SDKLogger.error(
-                "enableNetwork(\(network.displayName)) create returned: \(error.localizedDescription)"
-            )
+            let message = error.localizedDescription
+            // "already exists" is a no-op; other errors should surface
+            let isAlreadyExists = message.lowercased().contains("already exists")
+            if !isAlreadyExists {
+                SDKLogger.error(
+                    "enableNetwork(\(network.displayName)) failed: \(message)"
+                )
+                errorMessage = "Failed to enable \(network.displayName): \(message)"
+                showError = true
+                return
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift`
around lines 746 - 752, The catch in WalletDetailView's enableNetwork call
currently logs every error and treats all failures as no-ops; update the catch
in the enableNetwork (or the closure where SDKLogger.error is called) to inspect
the thrown error (e.g., error.localizedDescription or error code) and only
suppress/log silently when it indicates "already exists" (or equivalent
SDK-specific error type/enum); for any other error, propagate user-facing
feedback by triggering the view's error UI (e.g., set a `@State` showAlert /
errorMessage, call presentError, or dispatch to main to update UI) so the user
sees a clear failure message while still refreshing on the benign "already
exists" case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 2643-2645: The deleteWalletData path only scopes the
PersistentWallet lookup via walletRecordPredicate(walletId:) but still deletes
child rows by walletId alone; update deleteWalletData so all deletions
(PersistentTxo, PersistentPendingInput, PersistentAssetLock) include the network
scope as well (e.g., add networkId/network predicate) similar to the wallet
lookup. Locate deleteWalletData and change the predicates used for
FetchDescriptor/PersistentTxo, PersistentPendingInput, and PersistentAssetLock
to combine walletId AND network (or reuse an updated walletRecordPredicate that
accepts network), ensuring child-table deletions are network-scoped and do not
wipe sibling-network state.
- Around line 423-437: walletRecordPredicate currently scopes by network when
self.network is set but walletNetwork(walletId:) still fetches by walletId-only;
update walletNetwork(walletId:) to use walletRecordPredicate(walletId:) for its
fetch so it selects the network-scoped PersistentWallet row when self.network is
non-nil, and audit other fetches used by persistSyncState, identity/token
writes, and deriveAndStoreIdentityKey to replace any walletId-only queries with
walletRecordPredicate to avoid cross-network row leakage.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift`:
- Around line 26-31: hasAnyPrivateKey currently checks only that identifiers
(identity.votingPrivateKeyIdentifier, identity.ownerPrivateKeyIdentifier,
identity.payoutPrivateKeyIdentifier) are non-nil, which can incorrectly report
keys present even if the keychain item is missing; update hasAnyPrivateKey to
verify actual key presence in the keychain for each non-nil identifier (e.g.,
call your Keychain helper or use SecItemCopyMatching for each identifier) and
return true only if at least one identifier both exists and has a corresponding
key entry; alternatively remove this shortcut and call the existing concrete
key-existence check used elsewhere so the view reflects real keychain state.

---

Nitpick comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift`:
- Around line 746-752: The catch in WalletDetailView's enableNetwork call
currently logs every error and treats all failures as no-ops; update the catch
in the enableNetwork (or the closure where SDKLogger.error is called) to inspect
the thrown error (e.g., error.localizedDescription or error code) and only
suppress/log silently when it indicates "already exists" (or equivalent
SDK-specific error type/enum); for any other error, propagate user-facing
feedback by triggering the view's error UI (e.g., set a `@State` showAlert /
errorMessage, call presentError, or dispatch to main to update UI) so the user
sees a clear failure message while still refreshing on the benign "already
exists" case.
🪄 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: 62019d46-f017-434b-846d-1acbfb8ac91c

📥 Commits

Reviewing files that changed from the base of the PR and between 745eb5a and ee59225.

📒 Files selected for processing (10)
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/SDK.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/IdentitiesContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/SearchWalletsForIdentitiesView.swift

Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift Outdated
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

PR introduces multi-network wallet support by making PersistentWallet unique on (walletId, networkRaw) and giving each persistence handler a bound network. The wallet-row scoping is correct, but several downstream paths weren't updated to match the new invariant: deriveAndStoreIdentityKey still uses the network-agnostic PersistentWallet.predicate(walletId:) (line 1815) and can derive keys on the wrong network; and persister caches/cleanups keyed only by walletId (PersistentTxo, PersistentAssetLock, PersistentPlatformAddress, PersistentShieldedSyncState) now leak across networks because the same walletId can have rows on more than one. The new multi-network create + add-to-network UX also masks every per-network failure as a benign 'already exists' case, producing silent partial creates and writing keychain metadata claiming networks were enabled when they weren't.

🔴 4 blocking | 🟡 1 suggestion(s) | 💬 1 nitpick(s)

3 additional finding(s) omitted (not in diff).

🤖 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/PlatformWalletPersistenceHandler.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:1815-1824: deriveAndStoreIdentityKey can pick the wrong network row after the (walletId, networkRaw) split
  After this PR, `PersistentWallet` is unique on `(walletId, networkRaw)` and every other lookup on this handler routes through `walletRecordPredicate(walletId:)` so it scopes to `self.network` when set. This call site still uses the bare `PersistentWallet.predicate(walletId:)` and takes `.first`, so a per-network handler whose wallet also exists on another network can resolve to the other network's row, read `persistentWallet.network` from it, and feed the wrong `coin_type` into `KeyDerivation.getIdentityAuthenticationPath`. The resulting identity authentication path is built for the wrong chain and the private-key bytes written to Keychain are unusable for the on-chain identity. This is exactly the breakage `walletRecordPredicate` was introduced to prevent — apply it here too.
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:2722-2748: Per-walletId caches and cleanups leak across networks now that walletId is no longer unique
  `PersistentWallet` was made unique on `(walletId, networkRaw)`, but `PersistentTxo`, `PersistentPendingInput`, `PersistentAssetLock`, `PersistentPlatformAddress`, and `PersistentShieldedSyncState` have no network column and are still fetched/deleted by `walletId` alone. With the new ability to materialize the same mnemonic on multiple networks (`CreateWalletView` loop, `WalletDetailView.enableNetwork`), this contaminates several flows that share one SwiftData store across per-network handlers:

  - `deleteWalletData` (this hunk) deletes every `PersistentTxo` / `PersistentPendingInput` / `PersistentAssetLock` with the matching `walletId` — wiping another network's UTXOs, pending inputs, and asset-lock state when the user removes the wallet from one network.
  - `loadCachedBalancesOnQueue` (lines 241-258) and `loadCachedAssetLocksOnQueue` (lines 192-210) feed Rust restore state from rows that may belong to a different network's wallet of the same id.
  - The unspent-TXO bucket map in `loadWalletList` (lines 2941-2980) buckets by raw `walletId`, so restore can re-seed Rust with the wrong network's UTXOs.
  - `PersistentShieldedSyncState` is `#Unique([\.walletId, \.accountIndex])` (no network), so adding the wallet to a second network silently shares the shielded watermark.

  Either thread `networkRaw` onto these models and scope every lookup/delete by `(walletId, networkRaw)`, or scope deletion/load by the handler's `self.network` when it is set.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:343-417: Multi-network create reports success when only some selected networks succeeded
  The per-network loop catches *every* error from `createWallet` and treats it as a benign 'already exists'. Real failures (`backgroundManager(for:)` throwing, Rust manager construction failures, persistence errors, endpoint misconfiguration, etc.) on some networks are masked: as long as one network succeeds, `createdWalletId` is non-nil and the flow continues straight into `storage.setMetadata(...)`, writing keychain metadata that claims `networks: selectedNetworks.map { $0.networkName }` were all enabled. The dialog then dismisses as if the operation fully succeeded. The user has no UI signal that some of the ticked networks were skipped, and orphan-recovery later will believe the wallet was added everywhere it was selected. Inspect the error and only swallow a real 'already-exists' variant (preferred), or accumulate the per-network failures, report them in the UI, and only stamp `networks:` with the networks that actually got a row.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:739-755: enableNetwork swallows every create error as 'already exists' with no UI feedback
  The catch block treats any throw from `mgr.createWallet(...)` as the benign 'already on this network' case and only logs through `SDKLogger.error`. Genuine failures — Rust manager construction failing, persistence errors, network/quorum config rejected by the trusted-context provider — are silently swallowed: `errorMessage` / `showError` are never set on the create path, `loadNetworkStates()` simply observes the row is missing, and the toggle quietly reverts. The user gets zero signal that ticking the network did nothing. This is also asymmetric with the mnemonic-retrieval branch above (which does surface through `errorMessage`/`showError`). Inspect the error and only treat the actual 'already exists' variant as a no-op; surface everything else through the existing error UI.
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:723-752: Add-to-network pulls the mnemonic across the FFI boundary — anti-precedent per swift-sdk/CLAUDE.md
  `enableNetwork` retrieves the mnemonic via `WalletStorage().retrieveMnemonic(for:)` and hands it back to `mgr.createWallet(mnemonic:network:name:)` to materialize the wallet on another network. `packages/swift-sdk/CLAUDE.md` calls out this exact pipeline as the anti-precedent: 'Don't fetch the mnemonic from Keychain, hand it back to Rust, wait for derived bytes, and write those to Keychain — that's the same pipeline orchestrated on the wrong side. If you need this pipeline, add a single FFI entry point that does the whole thing.' The `CreateWalletView` loop has the same shape (Swift-side iteration deciding which networks to materialize and reusing `createWallet(mnemonic:)` N times). The original TODO acknowledged that the proper fix is a Rust-side `platform_wallet_add_to_network` (or `platform_wallet_create_on_networks([networks])`) FFI; the stop-gap re-exposes the seed phrase across the boundary on every add and cements the wrong shape. Push this down to `platform-wallet` so Swift only persists what Rust returns.

QuantumExplorer and others added 2 commits June 1, 2026 00:33
…l key check

CodeRabbit review on #3772:

- PlatformWalletPersistenceHandler.walletNetwork(walletId:): use
  walletRecordPredicate so the network resolved for sync-state /
  identity / token writes and key derivation is THIS manager's network,
  not an arbitrary sibling row when the same seed lives on two networks.

- deleteWalletData: the txo / pending-input / asset-lock child tables
  are keyed by the network-independent walletId and carry no network
  column, so they're shared across networks. Only wipe them when the
  row being deleted is the wallet's LAST remaining per-network row;
  otherwise deleting one network erased a sibling network's cached
  state. The wallet row itself stays network-scoped.

- WalletDetailView.enableNetwork: only swallow an "already exists" throw
  (a genuine no-op); surface any other failure via the error alert
  instead of silently doing nothing.

- IdentitiesView.hasAnyPrivateKey: a stored *PrivateKeyIdentifier only
  means an identifier string was persisted — the Keychain item can be
  missing. Confirm actual presence via hasSpecialKey(identifier:) before
  counting it, so the "No Keys" badge isn't wrongly hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per CodeRabbit's alternative suggestion on #3772: a stored
*PrivateKeyIdentifier only means an identifier string was persisted —
the backing Keychain item can be missing. Remove the non-nil-identifier
shortcut and rely solely on the existing concrete key-presence checks
(hasPrivateKey / hasIdentityPrivateKey) so the 'No Keys' badge reflects
actual Keychain state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Reconciling the six prior PastaClaw findings at HEAD 207b4f8: #4 is fully fixed (enableNetwork now distinguishes 'already exists' from real errors). #1, #3, #5, and #6 are carried forward unchanged. #2 is partially fixed on the delete path (per-walletId wipes now gated on 'last network row'), but the symmetric read paths (loadCachedAssetLocksOnQueue, loadCachedBalancesOnQueue, loadShieldedSyncStates / loadShieldedNotes, the loadWalletList TXO bucketing) still bucket by network-independent walletId, so a wallet enabled on multiple networks still cross-feeds cached state across managers. No new defects were introduced by the latest delta. Three blocking issues remain in scope for this PR (which is what makes multi-network walletIds load-bearing).

🔴 3 blocking | 💬 1 nitpick(s)

4 additional finding(s) omitted (not in diff).

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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/PlatformWalletPersistenceHandler.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:1815-1824: deriveAndStoreIdentityKey still resolves the wallet row by bare walletId — can derive identity keys under the wrong network's coin_type
  Carried forward from prior finding #1 — STILL VALID. After this PR made `PersistentWallet` unique on `(walletId, networkRaw)`, every other lookup in this handler was routed through `walletRecordPredicate(walletId:)` so it intersects with `self.network` (most recently the explicit fix to `walletNetwork(walletId:)` at line 4083, with the same rationale). This call site is the last holdout: it uses `PersistentWallet.predicate(walletId: walletId)` and then `.first`, which arbitrarily picks any row matching the id. When the same mnemonic is enabled on more than one network — exactly what this PR enables — the per-network handler's persister callback can resolve to a sibling-network row, read `persistentWallet.network` from it, and feed the wrong `Network` into `KeyDerivation.getIdentityAuthenticationPath` at line 1852. That changes the DIP-9 `coin_type` segment, so `key_wallet_derive_private_key_from_seed` returns a 32-byte scalar that does not correspond to the public key registered for the identity on this manager's network. The derived bytes are then stored in Keychain under the identity id with no public/private match check, so the corruption is silent until a signature later fails. The fix is one token — match every other call site in the file.
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:192-260: Read-side caches (asset locks, platform addresses, shielded sync state, restored UTXOs) still bucket by network-independent walletId — cross-network state leaks back across the FFI on load
  Carried forward from prior finding #2 — write/delete paths were narrowed by this delta (per-walletId wipes are now gated on `isLastNetworkRow` at lines 2733-2767, good), but the symmetric read paths still match by `walletId` alone. With this PR's invariant that the same walletId can legitimately live on multiple networks, those reads cross-feed state between per-network managers:
    • `loadCachedAssetLocksOnQueue` (line 192-211) uses `PersistentAssetLock.predicate(walletId:)`; `unused_asset_locks` for one network can rehydrate into the other.
    • `loadCachedBalancesOnQueue` (line 241-260) uses `PersistentPlatformAddress.predicate(walletId:)`; platform-address balances from a foreign network restore into this network's provider.
    • `loadWalletList` TXO bucketing (line 2960-3000) groups all unspent `PersistentTxo` by `row.walletId` only, so the per-wallet UTXO buffer Rust receives can include rows that belong to a different network's wallet sharing the id.
    • `loadShieldedSyncStates` / `loadShieldedNotes` (lines 2417, 2525) DO call `inNetworkWalletIds()` — but because both networks' `PersistentWallet` rows share the same walletId, both managers' `inNetworkWalletIds()` sets include that id and the filter no longer separates them. Same-mnemonic-on-multiple-networks watermarks/notes are still shared. The fix is structural: add `networkRaw` to `PersistentTxo`, `PersistentPendingInput`, `PersistentAssetLock`, `PersistentPlatformAddress`, and `PersistentShieldedSyncState` (matching what `PersistentWallet` just got), backfill from the owning wallet, and switch these lookups to network-scoped predicates. This is the same hazard the delete-path fix was protecting against, just on the read side — and the per-network-wallets feature is what makes it exploitable.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:343-417: Multi-network create still reports success when only some selected networks succeeded; keychain metadata records networks that were never created
  Carried forward from prior finding #3 — STILL VALID. The companion fix you just landed in `WalletDetailView.enableNetwork` (lines 746-763) already does the right thing: branch on whether the error description contains 'already exists' and surface anything else through the alert. This call site was not migrated. The loop here catches every `createWallet` error and logs it identically to the benign 'already exists' case; as long as any single ticked network succeeds, the flow dismisses the sheet, writes `WalletKeychainMetadata.networks = selectedNetworks.map { $0.networkName }` (line 409), and reports success. So the very class of bug this PR was originally written to fix (devnet quorum URL leaking into mainnet/testnet manager construction) can silently leave the user with no row on the affected network while orphan-recovery metadata claims one exists. Mirror the WalletDetailView idiom: distinguish 'already exists' from real failures, only stamp `networks` for those that actually succeeded, and surface a partial-failure error to the user.

thepastaclaw review on #3772: deriveAndStoreIdentityKey resolved the
wallet's network with the bare PersistentWallet.predicate(walletId:),
which can pick a sibling network's row now that the same walletId can
have one row per network — feeding the wrong coin_type into
KeyDerivation.getIdentityAuthenticationPath and writing key bytes
unusable for the on-chain identity. Use walletRecordPredicate so the
lookup is scoped to this handler's network, matching every other
PersistentWallet fetch on the handler (the lone remaining bare lookup,
walletRowCountAcrossNetworks, is deliberately cross-network).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

QuantumExplorer commented May 31, 2026

Thanks for the re-review. Responses to the new findings:

Fixed (076c0a0):

  • 🔴 deriveAndStoreIdentityKey wrong-network lookup — confirmed valid. It used the bare PersistentWallet.predicate(walletId:) and read persistentWallet.network to choose the coin_type, so a per-network handler could resolve a sibling network's row and derive the key on the wrong chain. Now uses walletRecordPredicate(walletId:), matching every other PersistentWallet fetch on the handler. (The lone remaining bare lookup, walletRowCountAcrossNetworks, is deliberately cross-network.)

Already addressed in prior commits (threads resolved):

  • 🔴 enableNetwork swallowing errors → 207b4f8 surfaces non-"already exists" errors via errorMessage/showError.
  • 💬 hasAnyPrivateKey65807e7 dropped the identifier shortcut; relies only on concrete Keychain checks.

Acknowledged, proposing follow-up rather than expanding this PR:

  • 🔴 Per-walletId child-table leak across networks (PersistentTxo / PersistentPendingInput / PersistentAssetLock / PersistentPlatformAddress / PersistentShieldedSyncState). Valid: these models have no network column, so the same mnemonic on two networks shares their rows. deleteWalletData is already guarded (child rows only wiped on the last network row), but the load/restore paths can still conflate. A correct fix means adding networkRaw to those 5 models and updating the Rust persister write contract (rs-platform-wallet-ffi) that populates them — a cross-cutting schema + FFI change that belongs in its own PR with the swift-rust-ffi-engineer, not this Swift-only bugfix.
  • 🟡 Add-to-network pulls the mnemonic across the FFI boundary (CLAUDE.md anti-precedent). Agreed in principle. enableNetwork / the CreateWalletView loop reuse the existing createWallet(mnemonic:) entry point (the same sanctioned path initial creation uses), but the proper shape is a single Rust-side platform_wallet_create_on_networks([...]) / platform_wallet_add_to_network FFI so Swift only persists what Rust returns. Tracking as the follow-up the original TODO called for.

Branch tip 076c0a0, build green.

…iew)

thepastaclaw review on #3772: the multi-network create loop caught every
createWallet error and treated it like the benign "already exists" case,
so a real failure on one network (e.g. the devnet quorum URL leaking
into mainnet/testnet manager construction — the very bug this PR fixes)
was masked: as long as one network succeeded the sheet dismissed as
success and keychain metadata claimed every ticked network was enabled.

Mirror the WalletDetailView.enableNetwork idiom:
- only "already exists" counts a network as present (benign no-op);
- genuine failures are collected and surfaced via the error alert;
- keychain metadata records only the networks the wallet actually has a
  row on (presentNetworks), not every ticked network;
- a partial create (wallet exists but not on all ticked networks) throws
  a descriptive error instead of silently dismissing as success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 1814-1820: The current method performs mnemonic→seed→DIP-9
path→private-key derivation in Swift (around the wallet lookup using
walletRecordPredicate and the FetchDescriptor<PersistentWallet> /
walletDescriptor), which violates the rule to keep derivation inside
platform-wallet; refactor by removing all mnemonic/seed/path/key derivation from
PlatformWalletPersistenceHandler and instead call a new or existing
platform-wallet FFI entry (exposed from Rust) that accepts the walletId (and
network scope) and returns the derived key material or a handle; update the code
that currently reads from Keychain and derives keys to call that FFI function
and persist only the returned key material, leaving walletRecordPredicate and
FetchDescriptor logic intact but delegating all derivation to the Rust
platform-wallet API.
- Around line 2737-2743: The current logic computes isLastNetworkRow solely from
the siblingDescriptor count which can be true even when walletRow is nil; update
the check so you only treat it as the last network row when the handler actually
owns a row. Concretely, ensure walletRow is non-nil before using the
cross-network count (e.g. compute isLastNetworkRow as walletRow != nil && ((try?
backgroundContext.fetchCount(siblingDescriptor)) ?? 0) <= 1) and use that
updated isLastNetworkRow where you decide to wipe shared
TXO/pending-input/asset-lock tables.
🪄 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: 6bc471fe-33d7-472f-862a-f1e7c5725b41

📥 Commits

Reviewing files that changed from the base of the PR and between ee59225 and 076c0a0.

📒 Files selected for processing (3)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift

Comment on lines +1814 to +1820
// testnet). Scope to THIS handler's network via
// `walletRecordPredicate` — the same `walletId` can now have
// a row per network, and a bare walletId-only fetch could
// resolve to a sibling network's row and derive the key on
// the wrong chain (unusable on-chain).
let walletDescriptor = FetchDescriptor<PersistentWallet>(
predicate: PersistentWallet.predicate(walletId: walletId)
predicate: walletRecordPredicate(walletId: walletId)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 31, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move identity-key derivation behind a Rust/platform-wallet FFI entry point.

This method still pulls the mnemonic from Keychain, derives the seed, builds the DIP-9 path, and derives the private key in Swift. The new network-scoped lookup helps correctness, but the full mnemonic → seed → path → key pipeline is still implemented on the Swift side instead of persisting only Rust-returned key material.

As per coding guidelines, "Do not build derivation paths in Swift", "Do not orchestrate multi-step derivation pipelines in Swift (mnemonic → seed → path → key → store)", and "Identity key derivation and any action using a derived key must route through platform-wallet, not be re-implemented in Swift".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`
around lines 1814 - 1820, The current method performs mnemonic→seed→DIP-9
path→private-key derivation in Swift (around the wallet lookup using
walletRecordPredicate and the FetchDescriptor<PersistentWallet> /
walletDescriptor), which violates the rule to keep derivation inside
platform-wallet; refactor by removing all mnemonic/seed/path/key derivation from
PlatformWalletPersistenceHandler and instead call a new or existing
platform-wallet FFI entry (exposed from Rust) that accepts the walletId (and
network scope) and returns the derived key material or a handle; update the code
that currently reads from Keychain and derives keys to call that FFI function
and persist only the returned key material, leaving walletRecordPredicate and
FetchDescriptor logic intact but delegating all derivation to the Rust
platform-wallet API.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged, not addressed in this PR. You're right that the mnemonic→seed→DIP-9 path→key pipeline belongs in platform-wallet per the swift-sdk CLAUDE.md rules. This code predates this PR (the PR only changed the wallet-row lookup from a bare predicate to walletRecordPredicate for network-correctness); pushing the whole derivation behind a Rust FFI entry point (platform_wallet_derive_identity_key or similar) is a cross-language refactor that belongs in its own PR with the swift-rust-ffi-engineer, not this Swift-only bugfix. Tracking as a follow-up.

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.

@QuantumExplorer, understood — the scope of this PR is well-defined (network-scoped wallet-row lookup) and the derivation-pipeline refactor is legitimately a separate cross-language effort. Would you like me to open a GitHub issue to track the follow-up (platform_wallet_derive_identity_key FFI entry + removing the Swift-side mnemonic→seed→path→key pipeline from PlatformWalletPersistenceHandler)?


✏️ Learnings added
Learnt from: QuantumExplorer
Repo: dashpay/platform PR: 3772
File: packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:1814-1820
Timestamp: 2026-06-01T07:59:33.680Z
Learning: In dashpay/platform, the Swift-side `deriveAndStoreIdentityKey` method in `PlatformWalletPersistenceHandler.swift` still implements the full mnemonic→seed→DIP-9 path→private-key derivation pipeline in Swift instead of delegating to a Rust FFI entry point. This is known technical debt that predates PR `#3772`; the planned fix is a new `platform_wallet_derive_identity_key` (or similar) FFI entry exposed from `rs-platform-wallet-ffi` that accepts walletId + network scope and returns the derived key material, removing the mnemonic/seed/path/key logic from Swift. This cross-language refactor is tracked as a follow-up PR requiring coordination with the swift-rust-ffi engineer and should not be flagged again on unrelated Swift-only PRs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: QuantumExplorer
Repo: dashpay/platform PR: 3581
File: packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift:30-30
Timestamp: 2026-05-04T18:54:22.991Z
Learning: In the dashpay/platform repo, SwiftData schema changes in the Swift SDK are intentionally not migrated. When reviewing code in packages/swift-sdk/**/*.swift, do not flag schema updates (e.g., adding new Model types or changing relationships) for missing MigrationStage entries or missing VersionedSchema bumps—this repo expects dev stores to be rebuilt from scratch on schema changes, and SwiftData migrations are not required.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Prior finding reconciliation for 076c0a0: carried forward STILL VALID prior findings: #2:network-independent-wallet-caches-read-side-leak; fixed/outdated prior findings: #1:derive-identity-key-bare-walletid-network-row, #3:create-wallet-partial-network-success-metadata, #4:enable-network-mnemonic-crosses-ffi-boundary, #5:identity-row-private-key-probes-sync-hot-path; intentionally deferred prior findings: none. Latest-delta findings are listed separately when present.

Latest delta (commits 65807e7 and 076c0a0) fixed the prior derive-identity-key cross-network lookup, but three prior blockers persist (read-side caches scoped only by walletId, multi-network create swallows per-network failures and writes optimistic keychain metadata, enableNetwork still round-trips the mnemonic through Swift), and the over-aggressive simplification of hasAnyPrivateKey introduced a new UI regression that mislabels masternode/evonode identities loaded via LoadIdentityView as having no keys.

🔴 1 blocking

1 additional finding(s) omitted (not in diff).

🤖 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/PlatformWalletPersistenceHandler.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:187-269: Read-side caches (asset locks, platform addresses, shielded sync state, restored UTXOs) still bucket by network-independent walletId — cross-network state leaks back across the FFI on load
  Carried forward from prior finding #2. The latest delta narrowed write/delete behavior, but the symmetric read paths still match by `walletId` alone while this PR makes the same walletId valid on multiple networks. `loadCachedAssetLocksOnQueue` uses `PersistentAssetLock.predicate(walletId:)`, `loadCachedBalancesOnQueue` uses `PersistentPlatformAddress.predicate(walletId:)`, `loadWalletList` groups unspent `PersistentTxo` by raw walletId, and shielded sync-state/note reads still cannot separate two network rows with the same walletId because the child models do not carry networkRaw. Add networkRaw/network-scoped predicates to these child cache models before feeding cached state back across the FFI.

  Current-head validation: STILL VALID. Current child cache models still lack a networkRaw/network discriminator (PersistentTxo, PersistentAssetLock), and read paths still key cache state by walletId.

@thepastaclaw
Copy link
Copy Markdown
Collaborator

Correction to my review at #3772 (review): the submitted top-level body undercounted the carried-forward findings while reconciling the prior round. The REQUEST_CHANGES result still stands for 076c0a0a.

Current-head reconciliation:

I am not opening duplicate inline threads for findings already raised on older review rounds; this correction records the verifier-kept set for the exact reviewed SHA.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Latest delta in CreateWalletView correctly surfaces partial multi-network creates and only stamps keychain metadata for networks where the row landed. However, the prior blocking finding about child-cache network scoping is only partially addressed: shielded loaders now filter via inNetworkWalletIds(), but asset-lock, platform-address, and unspent-UTXO read paths used during loadWalletList still match by raw walletId, and PersistentPlatformAddress is globally unique on address/addressHash — so once one mnemonic is added to two networks, write-side uniqueness collisions and cross-network read leaks corrupt the rehydrated FFI snapshot. Two smaller suggestions on the new CreateWalletView create flow.

🔴 1 blocking | 🟡 2 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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/PlatformWalletPersistenceHandler.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:186-260: Carried-forward: asset-lock, platform-address, and UTXO caches still bucket by network-independent walletId
  Re-validated at cee4b2cb against the prior finding from 076c0a0a — partially fixed (shielded note/sync-state loaders now filter via `inNetworkWalletIds()`), but the remaining read paths used during multi-network restore are still keyed by raw `walletId`:

  - `loadCachedAssetLocksOnQueue(walletId:)` at PlatformWalletPersistenceHandler.swift:192 uses `PersistentAssetLock.predicate(walletId:)`. `PersistentAssetLock` has no `networkRaw` column, so when the same `walletId` has rows persisted from a mainnet handler and a testnet handler, the mainnet loader rehydrates testnet's tracked asset locks. `loadWalletList` invokes this per-restored wallet at line 3243 and feeds the result into `entry.tracked_asset_locks` — Rust then revives the foreign-network locks into `unused_asset_locks` for the wrong manager.
  - `loadCachedBalancesOnQueue(walletId:)` at line 241 uses `PersistentPlatformAddress.predicate(walletId:)`. The bucketed call at 3086 mixes cached balances from another network into `entry.platform_address_balances`.
  - The unspent UTXO fetch at 2964–3004 grabs every `isSpent == false` `PersistentTxo` row and groups by `row.walletId` (or `row.account.wallet.walletId` for legacy rows) with no network constraint, so a wallet that exists on two networks gets the other network's UTXOs routed into its restore buffer at line 3213.

  Compounding this, `PersistentPlatformAddress` has `@Attribute(.unique) public var addressHash: Data` (PersistentPlatformAddress.swift:40). `addressHash` is the raw 21-byte payload, which is network-independent — the same derived key collides across networks regardless of the differing bech32m HRP on the `address` string. Once one mnemonic exists on multiple networks the second handler's write either conflicts or overwrites the first handler's row, so `loadCachedBalancesOnQueue(walletId:)` can only ever restore whichever network wrote last.

  The write side is otherwise network-scoped (each `PlatformWalletManager` owns a per-network handler), so the corruption only manifests when the same walletId has been persisted on more than one network — which is exactly the scenario this PR introduces. Apply the same `inNetworkWalletIds()` filter the shielded loaders use AND add a `networkRaw` discriminator to `PersistentAssetLock`, `PersistentPlatformAddress`, and `PersistentTxo` so their uniqueness/predicates can scope to `(walletId, networkRaw)` the way `walletRecordPredicate` already does for `PersistentWallet`.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:385-396: All-networks-already-exist path throws misleading "could not be created" error
  When every selected network reports `"already exists"`, `presentNetworks` ends up populated but `createdWalletId` stays `nil` because it's only assigned on the success branch at line 363. The guard at 385 then throws `AllNetworksFailed` with an empty `detail` (since `failures` is empty), surfacing "Wallet could not be created on any selected network." even though the wallet actually exists on every ticked network — directly contradicting the inline comment at 367 ("An 'already exists' throw means the wallet is already on this network — benign, the row is present, so count it").

  This is reachable any time a user re-imports an already-present mnemonic with the same network selections. Either resolve the walletId from `WalletStorage` / a SwiftData lookup when `createdWalletId` is `nil` but `presentNetworks` is non-empty (treat as a no-op success and proceed to metadata refresh + dismiss), or short-circuit before throwing when there are no real failures.
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:366-382: Brittle substring match on "already exists" to classify a benign error
  The branch classifying the per-network failure as benign relies on a case-insensitive substring search for `"already exists"` against `error.localizedDescription`. This matches the current `PlatformWalletError::WalletAlreadyExists` Display string, but it's an implicit coupling: a tweak to the Rust error message, a localization layer, or any wrapping that prepends context can silently break the classification — partial creates would then be reported as full failures (or vice versa). The same pattern is repeated in `WalletDetailView.swift:752`. Prefer exposing a typed FFI error code / dedicated Swift error case (e.g. `PlatformWalletError.walletAlreadyExists`) and switching on it, with the string match kept only as a last-resort fallback.

Comment on lines +366 to +382
let message = error.localizedDescription
// An "already exists" throw means the wallet
// is already on this network — benign, the
// row is present, so count it. Any other
// error is a genuine failure to record.
if message.range(of: "already exists", options: .caseInsensitive) != nil {
presentNetworks.append(net)
SDKLogger.error(
"Wallet already present on \(net.displayName); continuing"
)
} else {
failures.append((net, message))
SDKLogger.error(
"Wallet creation failed for \(net.displayName): \(message)"
)
}
}
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.

🟡 Suggestion: Brittle substring match on "already exists" to classify a benign error

The branch classifying the per-network failure as benign relies on a case-insensitive substring search for "already exists" against error.localizedDescription. This matches the current PlatformWalletError::WalletAlreadyExists Display string, but it's an implicit coupling: a tweak to the Rust error message, a localization layer, or any wrapping that prepends context can silently break the classification — partial creates would then be reported as full failures (or vice versa). The same pattern is repeated in WalletDetailView.swift:752. Prefer exposing a typed FFI error code / dedicated Swift error case (e.g. PlatformWalletError.walletAlreadyExists) and switching on it, with the string match kept only as a last-resort fallback.

source: ['claude']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed it's brittle. Acknowledged but not changing in this PR — the proper fix is a typed FFI error code / dedicated PlatformWalletError.walletAlreadyExists case to switch on, which is an FFI surface change (rs-platform-wallet-ffi) beyond this Swift-only bugfix. The substring match (here and WalletDetailView) is the current best available signal; tracking the typed-error work as a follow-up.

Two defects in the prior review-round fixes, both caught by CodeRabbit /
thepastaclaw on #3772:

- deleteWalletData: `isLastNetworkRow` was computed from the sibling
  count alone, so a handler asked to delete a wallet it doesn't own
  (walletRow == nil) but whose sibling network still has a row (count
  == 1) would read as "last row" and wipe the shared txo / pending-input
  / asset-lock tables out from under the other network. Gate the
  last-row check on `walletRow != nil`.

- CreateWalletView multi-network create: when every selected network
  reported "already exists", `createdWalletId` stayed nil (only set on
  the fresh-create branch) while `presentNetworks` was populated, so the
  guard threw "Wallet could not be created on any selected network" with
  an empty detail — contradicting the benign "already exists" handling.
  Re-importing an already-present mnemonic is now a no-op dismiss; the
  failure path only fires when there's a genuine (non-"already exists")
  failure and nothing succeeded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Re-validated cee4b2c's three prior findings against head 815751a: #1 (cross-network read-path leak via walletId-only cache keys) STILL VALID and confirmed in source at PlatformWalletPersistenceHandler.swift L192-211/L241-260 (asset-lock + balance loaders) and L2976-3016 + L3098/L3225/L3255 (load-wallet-list path). #2 (all-networks-already-exist throwing AllNetworksFailed) FIXED at CreateWalletView.swift L387-396 (now dismisses cleanly when failures.isEmpty). #3 (substring 'already exists' classifier) STILL VALID at CreateWalletView L371 and now duplicated into WalletDetailView L752 by this PR's new per-network enable path. No new latest-delta-only findings beyond the carried-forward ones. CodeRabbit returned no inline findings to react to.

🔴 1 blocking | 🟡 1 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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/PlatformWalletPersistenceHandler.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:186-260: Multi-network restore still cross-feeds asset locks, platform balances, and UTXOs across networks for same-mnemonic wallets
  Carried-forward from cee4b2cb — re-validated at 815751a9 and STILL VALID. The new `isLastNetworkRow` guard added to `deleteWallet` (L2737-2783) explicitly documents that `PersistentAssetLock`, `PersistentTxo`, and `PersistentPendingInput` are keyed by network-independent `walletId` and shared by every per-network `PersistentWallet` row for the same mnemonic. That sharing is now fenced on the delete side, but the read paths used by multi-network restore still bucket by raw `walletId`:

  - `loadCachedAssetLocksOnQueue(walletId:)` (L192-211) fetches via `PersistentAssetLock.predicate(walletId:)` with no network filter. The wallet-load callback rehydrates these into the Rust `unused_asset_locks` map at L3255-3261.
  - `loadCachedBalancesOnQueue(walletId:)` (L241-260) fetches via `PersistentPlatformAddress.predicate(walletId:)` with no network filter. L3098 reseeds the network-bound provider with the union of every sibling network's cached balances/nonces.
  - The unspent-UTXO bucketing pass at L2976-3016 buckets every `PersistentTxo` where `isSpent == false` by raw `walletId`, then `unspentBuckets[w.walletId]` at L3225 feeds `buildUtxoRestoreBuffer`.

  Net effect: this PR's create flow (CreateWalletView writes the same `walletId` into per-network `PersistentWallet` rows for each selected network) will, on the next per-network manager load, hand each network's Rust manager the union of all networks' UTXOs, address balances, and tracked asset locks — exactly the corruption the new delete-side `isLastNetworkRow` guard was added to prevent on the write side. Add a network discriminator column to `PersistentTxo` / `PersistentPlatformAddress` / `PersistentAssetLock`, expose `predicate(walletId:network:)` variants, and scope every loader in this file to `self.network` when the handler is network-bound.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:365-382: Benign 'wallet already exists' classification still relies on a substring match against a Rust display string, now duplicated into WalletDetailView
  Carried-forward from cee4b2cb — re-validated and STILL VALID. The per-network create loop classifies a benign duplicate by `error.localizedDescription.range(of: "already exists", options: .caseInsensitive)` (L371) instead of a typed error. This branch controls whether `presentNetworks` includes the network and whether the all-failed `AllNetworksFailed` path fires — i.e. whether this PR's freshly-fixed 'no wallet could be created' regression stays fixed. Any rewording on the Rust side (`PlatformWalletError::WalletAlreadyExists` display impl), localization wrapper, or future error chaining silently flips a benign case into the `failures` bucket and re-surfaces the misleading 'Wallet could not be created on any selected network' alert.

  This PR also propagates the same heuristic to `WalletDetailView.swift:752` in the new `enableNetwork(_:)` button — the per-network `+` action — so a single wording change now breaks two user-visible flows. Replace with a typed FFI error code / dedicated Swift error case for `WalletAlreadyExists` (e.g. matching on `PlatformWalletError.walletAlreadyExists` rather than the string) and keep the substring fallback only as a defense-in-depth check.

Comment on lines +365 to +382
} catch {
let message = error.localizedDescription
// An "already exists" throw means the wallet
// is already on this network — benign, the
// row is present, so count it. Any other
// error is a genuine failure to record.
if message.range(of: "already exists", options: .caseInsensitive) != nil {
presentNetworks.append(net)
SDKLogger.error(
"Wallet already present on \(net.displayName); continuing"
)
} else {
failures.append((net, message))
SDKLogger.error(
"Wallet creation failed for \(net.displayName): \(message)"
)
}
}
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.

🟡 Suggestion: Benign 'wallet already exists' classification still relies on a substring match against a Rust display string, now duplicated into WalletDetailView

Carried-forward from cee4b2c — re-validated and STILL VALID. The per-network create loop classifies a benign duplicate by error.localizedDescription.range(of: "already exists", options: .caseInsensitive) (L371) instead of a typed error. This branch controls whether presentNetworks includes the network and whether the all-failed AllNetworksFailed path fires — i.e. whether this PR's freshly-fixed 'no wallet could be created' regression stays fixed. Any rewording on the Rust side (PlatformWalletError::WalletAlreadyExists display impl), localization wrapper, or future error chaining silently flips a benign case into the failures bucket and re-surfaces the misleading 'Wallet could not be created on any selected network' alert.

This PR also propagates the same heuristic to WalletDetailView.swift:752 in the new enableNetwork(_:) button — the per-network + action — so a single wording change now breaks two user-visible flows. Replace with a typed FFI error code / dedicated Swift error case for WalletAlreadyExists (e.g. matching on PlatformWalletError.walletAlreadyExists rather than the string) and keep the substring fallback only as a defense-in-depth check.

source: ['claude', 'codex']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same finding as the existing thread on this file (#discussion_r3331111688) — acknowledged and deferred. The robust fix is a typed PlatformWalletError.walletAlreadyExists case to switch on (an rs-platform-wallet-ffi error-surface change), with the substring match kept only as a defense-in-depth fallback. Tracking as a follow-up rather than expanding this Swift-only bugfix PR; the substring check is the current best available signal in both CreateWalletView and WalletDetailView.

Adopts the network-scoped wallet id from rust-dashcore #793 (merged to
dev as 3d0d5dcd) to structurally fix the carried-forward blocking
finding #2: child caches keyed by the network-INDEPENDENT walletId
(PersistentTxo / PersistentPendingInput / PersistentAssetLock /
PersistentPlatformAddress / PersistentShieldedSyncState) cross-fed
state between a single mnemonic's per-network wallets.

With #793 the wallet id is network-scoped BY DEFAULT at construction
(`Wallet::from_mnemonic` / `from_seed` fold a domain-tagged, wire-stable
network byte into the digest), so the same mnemonic now yields a
DISTINCT id per network. Every walletId-keyed structure becomes
network-correct by construction — no networkRaw columns needed on the
child models, and the leak is eliminated at the source.

- Cargo.toml / Cargo.lock: pin all 8 rust-dashcore crates to 3d0d5dcd
  (the squash-merge of #793 on dev).
- rs-platform-wallet: no behavior change needed — construction already
  stamps the scoped id. `register_wallet` carries an explanatory
  comment. Added two unit tests proving same-mnemonic → 4 pairwise-
  distinct ids across networks, and stable per-(seed,network).
- CreateWalletView: the multi-network create now persists the mnemonic
  + metadata + `isImported` per FRESHLY-CREATED network's scoped
  walletId (each is independently recoverable), instead of once under a
  single shared id; metadata records just that network. All-already-
  exist is a benign dismiss.
- WalletDetailView.enableNetwork: stores the mnemonic under the
  newly-enabled network's scoped walletId so that wallet's own keychain
  lookups resolve.
- PlatformWalletManager.deleteWallet: comment updated for the scoped-id
  model (the walletRowCountAcrossNetworks guard stays correct).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

Finding #2 (cross-network cache leak) — closed via network-scoped walletId (14d353e)

Rather than add a networkRaw column to the five child models, I closed the blocker at its source: the wallet id is now network-scoped, so the same mnemonic produces a distinct walletId per network. Every walletId-keyed structure (PersistentTxo / PersistentPendingInput / PersistentAssetLock / PersistentPlatformAddress / PersistentShieldedSyncState) is therefore network-correct by construction — there's no longer a shared id for two networks' state to collide or cross-feed on, on either the read or write side. This also resolves the PersistentPlatformAddress.addressHash unique-constraint concern, since the rows can no longer belong to two networks at once.

This builds on dashpay/rust-dashcore#793 (now merged to dev as 3d0d5dcd), which makes the scoped id the construction default. Changes here:

  • Cargo.toml/lock: pin all 8 rust-dashcore crates to the merged 3d0d5dcd.
  • rs-platform-wallet: no behavior change needed — Wallet::from_mnemonic/from_seed already stamp the scoped id. Added two unit tests through the real construction path proving same mnemonic → 4 pairwise-distinct ids (mainnet/testnet/devnet/regtest) and stable per-(seed,network) (the watch-only restore path reuses the persisted id, so stability matters). Both pass.
  • CreateWalletView: multi-network create now persists the mnemonic + metadata + isImported under each freshly-created network's scoped walletId (each independently recoverable), not once under a shared id.
  • WalletDetailView.enableNetwork: stores the mnemonic under the newly-enabled network's scoped walletId so that wallet's own keychain lookups resolve.

Verification: Rust + FFI build clean on the merged rev; the iOS app builds and launches clean. End-to-end multi-network create couldn't be shown in a freshly-wiped simulator (standing up a non-active network's manager needs per-network infra the clean sim lacks — mainnet connectivity prefetch, devnet quorum URL, regtest docker), which is orthogonal to the id scoping — hence the deterministic Rust-level proof.

Still open (deliberately deferred, separate follow-ups — not closed by this commit):

  • 3331066833 — move the Swift identity-key derivation pipeline behind a platform-wallet FFI entry. Cross-language refactor.
  • 3331111688 / 3332647368 — replace the substring "already exists" classifier with a typed PlatformWalletError.walletAlreadyExists. FFI error-surface change.

Branch tip 14d353eb67.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.18%. Comparing base (73eb0ae) to head (92b6b28).
⚠️ Report is 7 commits behind head on v3.1-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3772      +/-   ##
============================================
- Coverage     87.19%   87.18%   -0.01%     
============================================
  Files          2611     2624      +13     
  Lines        320018   321014     +996     
============================================
+ Hits         279034   279892     +858     
- Misses        40984    41122     +138     
Components Coverage Δ
dpp 87.73% <ø> (-0.01%) ⬇️
drive 86.05% <ø> (+0.05%) ⬆️
drive-abci 89.54% <ø> (-0.06%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 47.85% <ø> (-0.96%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs (1)

503-516: ⚡ Quick win

Expand stability assertions to all supported networks.

The stability contract is defined for any (mnemonic, network) pair, but this test currently checks only Mainnet/Testnet. Add Devnet/Regtest assertions to fully cover the intended invariant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs` around lines 503
- 516, The test same_mnemonic_same_network_is_stable currently only asserts
stability for Network::Testnet and Network::Mainnet; extend it to include
Network::Devnet and Network::Regtest by adding assertions that
wallet_id_for(Network::Devnet) equals itself and wallet_id_for(Network::Regtest)
equals itself so the stability invariant is covered for all supported Network
variants; update the same_mnemonic_same_network_is_stable test to call
wallet_id_for with Devnet and Regtest and assert equality just like the existing
Testnet/Mainnet checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- Around line 503-516: The test same_mnemonic_same_network_is_stable currently
only asserts stability for Network::Testnet and Network::Mainnet; extend it to
include Network::Devnet and Network::Regtest by adding assertions that
wallet_id_for(Network::Devnet) equals itself and wallet_id_for(Network::Regtest)
equals itself so the stability invariant is covered for all supported Network
variants; update the same_mnemonic_same_network_is_stable test to call
wallet_id_for with Devnet and Regtest and assert equality just like the existing
Testnet/Mainnet checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9cbda7b-1d14-4f77-9863-f14af89324ff

📥 Commits

Reviewing files that changed from the base of the PR and between 815751a and 14d353e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • Cargo.toml
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift

CodeRabbit nitpick on #3772: same_mnemonic_same_network_is_stable only
asserted Mainnet/Testnet. Loop over all four Network variants (adds
Devnet/Regtest) so the per-(seed, network) stability invariant — which
the watch-only restore path depends on — is covered for every supported
network.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

The shift to network-scoped walletIds at this head structurally fixes the prior cross-network asset-lock/UTXO/balance leak — same-mnemonic wallets now get distinct walletIds per network, so every walletId-keyed table is network-correct by construction. However, the Swift side was not fully updated to that new id model: WalletDetailView.loadNetworkStates still assumes one walletId shared across networks (the inline comment even states this), so the network-state UI can only ever see the row it was opened from; and WalletDetailView.enableNetwork writes the mnemonic for the freshly created scoped walletId but never mirrors the per-wallet keychain metadata that orphan-recovery and bootstrap pre-warming rely on. The two carried-forward suggestions about classifying duplicates via Rust display-string substring ("already exists") remain valid in both CreateWalletView and WalletDetailView.

🔴 2 blocking | 🟡 2 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/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:631-646: loadNetworkStates can't discover sibling-network wallets after switch to network-scoped walletIds
  The inline comment claims that a wallet now has one `PersistentWallet` row per network with the *same* `walletId` and distinct `networkRaw`, and the fetch keys on `PersistentWallet.predicate(walletId:)` (walletId-only predicate at PersistentWallet.swift:145-147). That assumption was invalidated by the same push: `register_wallet` in `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs:131-143` documents that the walletId is now network-scoped — the same mnemonic produces a DISTINCT id per network — which is the structural fix relied on elsewhere in this PR. As a result, this fetch returns at most the current network's row, so `mainnetEnabled` / `testnetEnabled` / `regtestEnabled` / `devnetEnabled` only ever reflect the network the row was opened on. Wallet Info therefore shows a wallet that exists on both testnet and mainnet as if it lived on only one, and `loadAccountCounts()` stamps the current row's account count onto every "enabled" network label. That directly breaks the PR's stated multi-network UX. The lookup needs to span sibling network-scoped ids (e.g. via shared mnemonic / metadata, or by recomputing the scoped id per network and probing each).
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:743-778: enableNetwork persists the mnemonic for the new scoped walletId but not its WalletKeychainMetadata
  `enableNetwork` correctly creates a freshly scoped wallet on the target network and stores the mnemonic under the new `created.walletId` (lines 743-760), but it never writes a `WalletKeychainMetadata` blob for that new id. The create-wallet flow in `CreateWalletView` (lines 458-470) sets metadata per network — `networks: [created.network.networkName]`, name, birthHeight — and the rest of the app relies on it: `ContentView.recoverWallet` chooses the restore network via `entry.network ?? metadata?.resolvedNetworks.first ?? platformState.currentNetwork` (ContentView.swift:457-458), and the post-launch warmup is keyed off the same metadata. A wallet enabled via the `+` button therefore has a mnemonic in the keychain but no metadata, so after a wipe its orphan entry falls back to whichever network is currently active and can recreate the wallet on the wrong chain. Mirror the same `WalletKeychainMetadata(name:walletDescription:networks:birthHeight:)` write here using `created.walletId` and the target network, in the same `do/try? storage.setMetadata` shape used by CreateWalletView.
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:761-778: Same substring-match fragility duplicated in enableNetwork
  Carried forward from prior reviews and still valid. `enableNetwork`'s catch arm now uses the same `description.range(of: "already exists", options: .caseInsensitive)` heuristic to decide between a benign already-enabled state and a real failure. The fragility is identical to the create flow: a change to the Rust Display string makes legitimate already-present cases reach the user as "Failed to add <Network>: …", and an unrelated error containing those words gets swallowed. Same remediation — branch on a typed error case once the FFI exposes one — and ideally route both call sites through the shared helper.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:365-386: Duplicate-wallet classification still relies on substring-matching a Rust display string
  Carried forward from prior reviews (cee4b2cb, 815751a9) and still valid at 14d353eb. The per-network create loop classifies a benign duplicate by `error.localizedDescription.range(of: "already exists", options: .caseInsensitive)` rather than a typed error. That branch decides whether an already-present wallet is recorded as a no-op or surfaced via the `failures` array as a user-visible "Wallet could not be created". Any wording, capitalisation, localisation, or FFI-wrapper text change to `PlatformWalletError`'s Display impl breaks this classification silently. Expose a typed Rust variant (e.g. `PlatformWalletError::WalletAlreadyRegistered`) across the FFI so Swift can match by case instead of substring.

Comment on lines +365 to +386
let message = error.localizedDescription
// An "already exists" throw means the wallet
// is already on this network — benign. We do
// NOT resolve the existing scoped walletId to
// re-store the mnemonic: a wallet that already
// exists on this network had its mnemonic +
// metadata stored under that scoped id at its
// original creation, so there is nothing to
// write. It is also not counted as a freshly-
// created wallet. Any other error is a genuine
// failure.
if message.range(of: "already exists", options: .caseInsensitive) != nil {
SDKLogger.error(
"Wallet already present on \(net.displayName); continuing"
)
} else {
failures.append((net, message))
SDKLogger.error(
"Wallet creation failed for \(net.displayName): \(message)"
)
}
}
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.

🟡 Suggestion: Duplicate-wallet classification still relies on substring-matching a Rust display string

Carried forward from prior reviews (cee4b2c, 815751a) and still valid at 14d353e. The per-network create loop classifies a benign duplicate by error.localizedDescription.range(of: "already exists", options: .caseInsensitive) rather than a typed error. That branch decides whether an already-present wallet is recorded as a no-op or surfaced via the failures array as a user-visible "Wallet could not be created". Any wording, capitalisation, localisation, or FFI-wrapper text change to PlatformWalletError's Display impl breaks this classification silently. Expose a typed Rust variant (e.g. PlatformWalletError::WalletAlreadyRegistered) across the FFI so Swift can match by case instead of substring.

source: ['claude', 'codex']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged, deferring (same as the existing thread on this file). The robust fix is a typed PlatformWalletError.walletAlreadyExists to switch on, which is an rs-platform-wallet-ffi error-surface change beyond this Swift-side bugfix. Tracking as a follow-up; the substring match is the current best available signal.

Comment on lines +761 to 778
} catch {
let description = error.localizedDescription
// An "already exists" throw means the wallet is already on
// this network — a genuine no-op, so fall through to refresh.
// Any other failure (SDK build error, Rust-side error, etc.)
// must surface to the user instead of silently doing nothing.
if description.range(of: "already exists", options: .caseInsensitive) == nil {
SDKLogger.error(
"enableNetwork(\(network.displayName)) failed: \(description)"
)
errorMessage = "Failed to add \(network.displayName): \(description)"
showError = true
return
}
SDKLogger.error(
"enableNetwork(\(network.displayName)) create returned: \(description)"
)
}
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.

🟡 Suggestion: Same substring-match fragility duplicated in enableNetwork

Carried forward from prior reviews and still valid. enableNetwork's catch arm now uses the same description.range(of: "already exists", options: .caseInsensitive) heuristic to decide between a benign already-enabled state and a real failure. The fragility is identical to the create flow: a change to the Rust Display string makes legitimate already-present cases reach the user as "Failed to add : …", and an unrelated error containing those words gets swallowed. Same remediation — branch on a typed error case once the FFI exposes one — and ideally route both call sites through the shared helper.

source: ['claude', 'codex']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged, deferring — same typed-error follow-up as the CreateWalletView thread. Once the FFI exposes a typed walletAlreadyExists case I'll route both this and CreateWalletView through it (ideally a shared helper). Substring match stays as the interim signal.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Latest push only expands the Rust scoped-walletId stability test to cover all four Network variants; no production code changed since 14d353e. Re-validated all four prior findings against head 5e76921 — every one remains valid against the current Swift sources. Both blocking findings (loadNetworkStates' walletId-only fetch and enableNetwork's missing keychain metadata write) directly break the PR's stated multi-network UX; the two substring-match suggestions are unchanged carry-forwards. CodeRabbit produced no actionable inline findings.

🔴 2 blocking | 🟡 2 suggestion(s)

No new inline comments were opened in this review round because all four current findings are carried-forward issues with existing open review threads. They are listed here so this exact head SHA still has an explicit cumulative review.

Prior finding reconciliation

  1. [blocking] loadNetworkStates cannot discover sibling-network wallets after the switch to network-scoped walletIds (carried forward from 14d353e)
    packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:631-646
    Status: STILL VALID on 5e76921e. This is carried forward from the 14d353e review; the existing inline thread is still open, so I am not duplicating the inline comment here.
  2. [blocking] enableNetwork persists the mnemonic for the new scoped walletId but not its WalletKeychainMetadata (carried forward from 14d353e)
    packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:743-778
    Status: STILL VALID on 5e76921e. This is carried forward from the 14d353e review; the existing inline thread is still open, so I am not duplicating the inline comment here.
  3. [suggestion] Duplicate-wallet classification still relies on substring-matching a Rust display string (carried forward)
    packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift:365-386
    Status: STILL VALID on 5e76921e. This is carried forward from the 14d353e review; the existing inline thread is still open, so I am not duplicating the inline comment here.
  4. [suggestion] Same substring-match fragility duplicated in enableNetwork (carried forward)
    packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:761-778
    Status: STILL VALID on 5e76921e. This is carried forward from the 14d353e review; the existing inline thread is still open, so I am not duplicating the inline comment here.

CodeRabbit produced no actionable inline findings for this push; no CodeRabbit reactions were posted.

Two blocking regressions from the network-scoped-walletId change
(thepastaclaw review on #3772):

1. WalletDetailView.loadNetworkStates discovered a wallet's sibling-
   network rows by matching `walletId`. Under scoped ids the same
   mnemonic has a DIFFERENT id per network, so the fetch only ever
   returned the row the view was opened on — the "Networks" section
   showed a multi-network wallet as living on one network.

   Fix: introduce `walletGroupId`, the network-INDEPENDENT id (the
   `None`-scoped digest = SHA256(rootPubKey || chainCode), i.e. what
   walletId was before scoping). It's computed in Rust at
   register_wallet and threaded through the wallet-metadata changeset →
   FFI callback → Swift persister → a new PersistentWallet.walletGroupId
   column. loadNetworkStates now groups sibling rows by it. Every
   network's wallet for one seed shares the same group id, while their
   scoped walletIds differ.

2. WalletDetailView.enableNetwork stored the mnemonic under the new
   scoped walletId but never wrote its WalletKeychainMetadata, so an
   orphan-recovered wallet could fall back to the active network and
   recreate on the wrong chain. Now writes the same metadata blob
   CreateWalletView does, keyed by the new id + target network.

Layers touched:
- changeset.rs: WalletMetadataEntry gains wallet_group_id: [u8; 32].
- wallet_lifecycle.rs: register_wallet computes it via
  compute_wallet_id_from_root_extended_pub_key(root, None) (falls back
  to the scoped id for keyless watch-only/external wallets). Added a
  unit test proving the group id is network-independent and differs
  from the scoped id.
- rs-platform-wallet-ffi/persistence.rs: on_persist_wallet_metadata_fn
  gains a wallet_group_id pointer param.
- PersistentWallet.swift: walletGroupId column + index + predicate.
- PlatformWalletPersistenceHandler.swift: marshal + persist it.
- WalletDetailView.swift: group by walletGroupId; write metadata in
  enableNetwork.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@QuantumExplorer
Copy link
Copy Markdown
Member Author

This review round reconciled against 5e76921e, but both blocking findings were fixed in the next push, 7f142107e6 (after this round started):

  • chore: synchronize packages dependency versions #1 loadNetworkStates — fixed via a network-independent walletGroupId (the None-scoped digest) threaded Rust→FFI→Swift; loadNetworkStates now groups sibling-network rows by it. Thread resolved (reply on 3334667286).
  • feat: enable mainnet for dashmate #2 enableNetwork metadata — now writes the WalletKeychainMetadata blob under the new scoped walletId. Thread resolved (reply on 3334667291).

A unit test proves the group id is identical across all four networks while the scoped walletId differs; platform-wallet + FFI build clean and the iOS framework builds.

The two 🟡 suggestions (substring "already exists" → typed PlatformWalletError.walletAlreadyExists) remain open as a deliberate cross-language FFI-surface follow-up, replied-to on their threads. A re-review at 7f142107e6 should clear the two blockers.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Verified at head 7f14210. Prior blocking findings #1 (loadNetworkStates sibling discovery) and #2 (enableNetwork keychain metadata) are FIXED in this commit — loadNetworkStates now groups by walletGroupId with a legacy single-row fallback, and enableNetwork writes the WalletKeychainMetadata blob under the new scoped walletId alongside the mnemonic. Prior suggestion findings #3 and #4 (substring-matching the Rust 'already exists' display string in CreateWalletView and WalletDetailView.enableNetwork) remain STILL VALID at this head; both reviewers (Claude and Codex) independently re-flag them and confirmed the FFI layer still collapses WalletAlreadyExists into a generic display-string error. No new latest-delta findings.

🟡 1 suggestion(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:792-809: enableNetwork repeats the same substring-match heuristic on the FFI error description
  `enableNetwork`'s catch arm uses an identical `description.range(of: "already exists", options: .caseInsensitive) == nil` test on `error.localizedDescription` to distinguish a benign already-on-this-network result from a real failure. The fragility is the same as `CreateWalletView`'s site — Rust's typed `WalletAlreadyExists` is flattened into a localized free-form string before Swift inspects it, and this code path decides whether to fall through to a UI refresh (implying the network was enabled) or surface an alert. A phrasing or localization drift on the Rust side will either hide real failures or spuriously alarm the user. Should be fixed in lockstep with the CreateWalletView site by introducing a structured `walletAlreadyExists` case on the wallet-FFI error surface and matching on that case from both Swift call sites. Carried forward from 14d353eb / 5e76921e — STILL VALID at 7f142107.

Comment on lines +792 to 809
} catch {
let description = error.localizedDescription
// An "already exists" throw means the wallet is already on
// this network — a genuine no-op, so fall through to refresh.
// Any other failure (SDK build error, Rust-side error, etc.)
// must surface to the user instead of silently doing nothing.
if description.range(of: "already exists", options: .caseInsensitive) == nil {
SDKLogger.error(
"enableNetwork(\(network.displayName)) failed: \(description)"
)
errorMessage = "Failed to add \(network.displayName): \(description)"
showError = true
return
}
SDKLogger.error(
"enableNetwork(\(network.displayName)) create returned: \(description)"
)
}
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.

🟡 Suggestion: enableNetwork repeats the same substring-match heuristic on the FFI error description

enableNetwork's catch arm uses an identical description.range(of: "already exists", options: .caseInsensitive) == nil test on error.localizedDescription to distinguish a benign already-on-this-network result from a real failure. The fragility is the same as CreateWalletView's site — Rust's typed WalletAlreadyExists is flattened into a localized free-form string before Swift inspects it, and this code path decides whether to fall through to a UI refresh (implying the network was enabled) or surface an alert. A phrasing or localization drift on the Rust side will either hide real failures or spuriously alarm the user. Should be fixed in lockstep with the CreateWalletView site by introducing a structured walletAlreadyExists case on the wallet-FFI error surface and matching on that case from both Swift call sites. Carried forward from 14d353e / 5e76921 — STILL VALID at 7f14210.

source: ['claude', 'codex']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged, deferring — same typed-error follow-up as the CreateWalletView site (threads 3331111688 / 3334667296). The robust fix is a structured PlatformWalletError.walletAlreadyExists case exposed across rs-platform-wallet-ffi, matched from both Swift call sites (ideally via a shared helper). That's an FFI error-surface change out of scope for this Swift-side bugfix PR; tracking as a follow-up. Leaving open (not resolving) so it stays visible until that work lands.

With network-scoped walletIds the network is folded into the id digest,
so `walletId` is globally unique on its own — a given id maps to exactly
one (seed, network) pair. The `(walletId, networkRaw)` composite was a
leftover from the pre-scoping model, where one seed shared a single id
across networks and `networkRaw` was the only distinguishing column.
Drop it to `#Unique([\.walletId])` so the constraint states the actual
invariant. Pre-release: dev stores rebuild, no migration. Comment
updated to explain the history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

The latest delta (92b6b28) tightens PersistentWallet's SwiftData uniqueness from the composite (walletId, networkRaw) to walletId alone, with refreshed doc comments. Given the upstream change that folds a domain-tagged network byte into the walletId digest, this is consistent and correct — no new defects in the latest delta itself. Both prior findings from 7f14210 (substring-matching the Rust FFI error display string "already exists" to drive Swift control flow in CreateWalletView and WalletDetailView) are carried forward STILL VALID — the flagged lines are unchanged at this head. One additional low-confidence nitpick on a now-over-specified predicate helper is included.

🟡 1 suggestion(s) | 💬 1 nitpick(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift:792-809: Carried forward — enableNetwork repeats the same FFI-display-string heuristic
  Re-verified at 92b6b28; lines unchanged from the 7f142107 review. `enableNetwork(_:)`'s catch arm uses `description.range(of: "already exists", options: .caseInsensitive) == nil` against `error.localizedDescription` to distinguish a benign already-on-this-network outcome (fall through to `loadNetworkStates()` / `loadAccountCounts()`) from a real failure (surface to the user). The fragility is symmetric to the `CreateWalletView` site, but the asymmetric failure mode here is more dangerous: a real Rust-side failure whose display text happens to contain "already exists" (the `IdentityAlreadyExists` / DashPay variants in `rs-platform-wallet/src/error.rs`, or any wrapped error that mentions "already exists" in context) is silently treated as a successful add-to-network — the function falls through, the user sees the network as added, but no per-network wallet row, keychain mnemonic, or metadata was actually written. Same root cause and same remediation as the CreateWalletView finding: propagate a typed `WalletAlreadyExists` discriminator across the FFI and switch on the type rather than the display string. Tracking both sites together is reasonable.

Comment on lines +792 to 809
} catch {
let description = error.localizedDescription
// An "already exists" throw means the wallet is already on
// this network — a genuine no-op, so fall through to refresh.
// Any other failure (SDK build error, Rust-side error, etc.)
// must surface to the user instead of silently doing nothing.
if description.range(of: "already exists", options: .caseInsensitive) == nil {
SDKLogger.error(
"enableNetwork(\(network.displayName)) failed: \(description)"
)
errorMessage = "Failed to add \(network.displayName): \(description)"
showError = true
return
}
SDKLogger.error(
"enableNetwork(\(network.displayName)) create returned: \(description)"
)
}
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.

🟡 Suggestion: Carried forward — enableNetwork repeats the same FFI-display-string heuristic

Re-verified at 92b6b28; lines unchanged from the 7f14210 review. enableNetwork(_:)'s catch arm uses description.range(of: "already exists", options: .caseInsensitive) == nil against error.localizedDescription to distinguish a benign already-on-this-network outcome (fall through to loadNetworkStates() / loadAccountCounts()) from a real failure (surface to the user). The fragility is symmetric to the CreateWalletView site, but the asymmetric failure mode here is more dangerous: a real Rust-side failure whose display text happens to contain "already exists" (the IdentityAlreadyExists / DashPay variants in rs-platform-wallet/src/error.rs, or any wrapped error that mentions "already exists" in context) is silently treated as a successful add-to-network — the function falls through, the user sees the network as added, but no per-network wallet row, keychain mnemonic, or metadata was actually written. Same root cause and same remediation as the CreateWalletView finding: propagate a typed WalletAlreadyExists discriminator across the FFI and switch on the type rather than the display string. Tracking both sites together is reasonable.

source: ['claude']

Comment on lines +177 to +185
public static func predicate(
walletId: Data,
network: Network
) -> Predicate<PersistentWallet> {
let networkRaw = network.rawValue
return #Predicate<PersistentWallet> {
$0.walletId == walletId && $0.networkRaw == networkRaw
}
}
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.

💬 Nitpick: predicate(walletId:network:) is over-specified now that walletId is globally unique

With the latest delta's invariant that walletId is network-scoped and globally unique on its own (the network byte is folded into the digest, and the #Unique constraint on the model is now [\.walletId]), the composite predicate $0.walletId == walletId && $0.networkRaw == networkRaw can never narrow the result set further than the walletId clause alone, but it CAN spuriously return empty for rows whose networkRaw is nil — the property's own doc-comment (lines 47–49) explicitly permits pre-persistWalletMetadata rows to have networkRaw == nil. This is a low-impact mismatch with the model rather than a correctness break, and is worth flagging only because the just-rewritten walletId doc-comment advertises that the network is already baked into walletId — keeping a sibling lookup helper that still requires a networkRaw match invites confusion at future call sites. Consider either dropping the network clause from this predicate or removing the helper in favor of predicate(walletId:).

source: ['claude']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants