Skip to content

fix(swift-sdk): drop transitive keypath from PersistentTxo unspent prefetch#3691

Merged
QuantumExplorer merged 1 commit into
v3.1-devfrom
fix/persistor-prefetch-keypath-crash
May 20, 2026
Merged

fix(swift-sdk): drop transitive keypath from PersistentTxo unspent prefetch#3691
QuantumExplorer merged 1 commit into
v3.1-devfrom
fix/persistor-prefetch-keypath-crash

Conversation

@llbartekll
Copy link
Copy Markdown
Contributor

@llbartekll llbartekll commented May 20, 2026

Issue being fixed or feature implemented

PlatformWalletPersistenceHandler.loadWalletList() asserts inside SwiftData internals on the first run where the restorable.isEmpty gate opens — i.e. any wallet registration via the platform-wallet Rust crate after at least one wallet with populated accounts already exists on disk (app restart, second wallet import, key-migration flows, etc.). The full stack ends at the unspent PersistentTxo fetch:

SwiftData.ModelContext.fetch<PersistentTxo>(FetchDescriptor)
  ↑ PlatformWalletPersistenceHandler.loadWalletList() at line 2471
  ↑ FFIPersister::load (Rust)
  ↑ register_wallet (Rust)
  ↑ create_wallet_from_mnemonic / load_persisted_wallet

Reproducible by creating a wallet, force-quitting, relaunching — or by importing a second wallet into an app that already has one. Symptom is a Swift runtime fatal error with "Couldn't find \PersistentTxo.<computed (Optional<PersistentAccount>)>?.<computed (PersistentWallet)>? on PersistentTxo with fields [outpoint, vout, ...]".

What was done?

Dropped the chained second hop from the prefetch hint:

-unspentDescriptor.relationshipKeyPathsForPrefetching = [\.account, \.account?.wallet]
+unspentDescriptor.relationshipKeyPathsForPrefetching = [\.account]

SwiftData's prefetch resolver only supports direct, single-hop relationship keypaths — chained / transitive keypaths like \.account?.wallet trip an internal assertion in the schema walker even when both ends have proper relationship metadata. Adding @Relationship annotations to PersistentTxo.account / PersistentAccount.wallet does not fix it (verified by trying and watching the same crash recur with the annotations present).

The single-hop \.account prefetch is preserved, so the TXO → account bulk join still happens. The account → wallet hop now faults on demand inside the iteration loop — N+1 worst case, but loadWalletList runs at registration/restore time on a bounded set of TXOs and the cost is acceptable. If the chained prefetch is wanted back later it'd take a model-layer rework (e.g. denormalizing the wallet pointer onto TXO directly, or refactoring the loop to pre-fetch wallets via a [walletId: PersistentWallet] dictionary).

How Has This Been Tested?

  • dashwallet-ios, iOS Simulator (iPhone 17 Pro), wallet migrator flow that triggers createOrImportWallet against an existing on-disk wallet — reproduces the crash before the fix, completes cleanly after.
  • Same simulator, fresh wallet creation in SwiftExampleApp — unaffected (early-return gate keeps it off the buggy path either way).

Breaking Changes

None. Behavior change is purely the loss of one prefetch optimization; the relationship traversal at the use site still works, just fault-loaded.

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 added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

Summary by CodeRabbit

  • Refactor
    • Optimized wallet loading performance through improved relationship handling.

Review Change Stack

…efetch

`relationshipKeyPathsForPrefetching = [\.account, \.account?.wallet]`
in `PlatformWalletPersistenceHandler.loadWalletList` asserted inside
SwiftData internals the first time the fetch ran with non-empty
restorable wallets — the chained `account?.wallet` second hop hits a
code path SwiftData's prefetch resolver doesn't support, only direct
single-hop relationships do. Any wallet registration via the
platform-wallet Rust crate after the `restorable.isEmpty` gate opens
(restart with persisted wallet, second wallet import, etc.) tripped
the assertion.

Drop the chained second hop. The `account → wallet` relationship still
resolves on demand at the use site; only the bulk prefetch optimization
for that hop is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c0f9471-b0e5-4420-a29f-420339d9e51c

📥 Commits

Reviewing files that changed from the base of the PR and between e2f6e42 and 431e36c.

📒 Files selected for processing (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift

📝 Walkthrough

Walkthrough

PlatformWalletPersistenceHandler adjusts the SwiftData prefetch configuration in loadWalletList to remove the nested account?.wallet relationship prefetch while keeping the account relationship prefetch for unspent PersistentTxo queries.

Changes

SwiftData Prefetch Optimization

Layer / File(s) Summary
Prefetch relationship configuration
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
SwiftData prefetch parameter for unspent transaction output fetching is simplified to prefetch only the account relationship instead of the nested account?.wallet relationship.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐰 A wallet queries without nested strain,
One prefetch path, not two—so plain!
Account alone, the swift way home,
Data fetches lean, through the code we roam. 🌿

🚥 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 title directly and clearly summarizes the main change: removing a transitive keypath from PersistentTxo unspent prefetch to fix a SwiftData runtime assertion.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/persistor-prefetch-keypath-crash

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.

@github-actions github-actions Bot added this to the v3.1.0 milestone May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

✅ DashSDKFFI.xcframework built for this PR.

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

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

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
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

One-line SwiftData fix is correct and well-scoped. Two non-blocking review items remain: the block comment above the fetch still describes the removed transitive prefetch, and no automated test exercises the wallet-restore path where the original crash occurred.

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

2 additional finding(s)

nitpick: Stale comment still claims the fetch prefetches `account.wallet`

packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift (line 2444)

The block comment says the fetch "Prefetches account.wallet to keep the legacy-walletId routing path … from triggering one SwiftData fault per row when we resolve the parent wallet." After this PR the descriptor only prefetches \.account; the account → wallet hop now faults lazily inside the loop (lines 2485–2492). This is the same justification a future reader will reach for when considering re-adding the chained keypath, so leaving it stale invites the regression to be reintroduced. Update the comment to state that only account is prefetched, that the wallet hop intentionally faults on demand, and that the chained keypath trips a SwiftData internal assertion.

suggestion: Wallet-restore regression has no automated coverage

packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift (line 2462)

This single keypath edit is the entire behavioral fix, but no test exercises loadWalletList() against a populated SwiftData store. A grep confirms loadWalletList is referenced only in PlatformWalletPersistenceHandler.swift, PlatformWalletManager.swift, and WalletManagerStore.swift — never in SwiftExampleAppTests or SwiftDashSDKTests. The original crash only triggers after the restorable.isEmpty gate opens (second wallet import or app restart with persisted wallets) and only surfaces at runtime inside SwiftData, so the inline comment is the only signal preventing a future contributor from re-adding \.account?.wallet or another transitive keypath. Add an integration test that seeds a PersistentWallet with a restorable PersistentAccount and at least one unspent PersistentTxo (covering both the populated-walletId and legacy empty-walletId paths in lines 2483–2495), then invokes the load path, so the assertion is caught in CI rather than on a user's device.

🤖 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.

- [NITPICK] In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:2444-2450: Stale comment still claims the fetch prefetches `account.wallet`
  The block comment says the fetch "Prefetches `account.wallet` to keep the legacy-walletId routing path … from triggering one SwiftData fault per row when we resolve the parent wallet." After this PR the descriptor only prefetches `\.account`; the `account → wallet` hop now faults lazily inside the loop (lines 2485–2492). This is the same justification a future reader will reach for when considering re-adding the chained keypath, so leaving it stale invites the regression to be reintroduced. Update the comment to state that only `account` is prefetched, that the `wallet` hop intentionally faults on demand, and that the chained keypath trips a SwiftData internal assertion.
- [SUGGESTION] In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:2462-2462: Wallet-restore regression has no automated coverage
  This single keypath edit is the entire behavioral fix, but no test exercises `loadWalletList()` against a populated SwiftData store. A grep confirms `loadWalletList` is referenced only in `PlatformWalletPersistenceHandler.swift`, `PlatformWalletManager.swift`, and `WalletManagerStore.swift` — never in `SwiftExampleAppTests` or `SwiftDashSDKTests`. The original crash only triggers after the `restorable.isEmpty` gate opens (second wallet import or app restart with persisted wallets) and only surfaces at runtime inside SwiftData, so the inline comment is the only signal preventing a future contributor from re-adding `\.account?.wallet` or another transitive keypath. Add an integration test that seeds a `PersistentWallet` with a restorable `PersistentAccount` and at least one unspent `PersistentTxo` (covering both the populated-`walletId` and legacy empty-`walletId` paths in lines 2483–2495), then invokes the load path, so the assertion is caught in CI rather than on a user's device.

Inline posting hit GitHub HTTP 422, so I posted the same verified findings as a top-level review body.

@QuantumExplorer QuantumExplorer merged commit 637a8e5 into v3.1-dev May 20, 2026
18 checks passed
@QuantumExplorer QuantumExplorer deleted the fix/persistor-prefetch-keypath-crash branch May 20, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants