Repository navigation
fix(swift-sdk): reconcile SwiftData transaction accounting per wallet - #5417
Claudius-Maginificent wants to merge 8 commits into
Conversation
A stored Core transaction row kept the amount and direction of its last writer, computed without the wallet's own spent inputs: a self-spend with change showed as a receipt, and an asset lock as a zero transfer. The SwiftData host now recomputes amount and direction from its own TXO rows at load and at the end of each persistence round, keeps contact-only outputs out of the sum, and answers per wallet for a transaction that several local wallets share. Schema V4 records an amount that cannot be determined, and the example app's transaction views show wallet-relative amounts and direction. These changes were developed on the branch of #5150 and are carried over unchanged. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
⛔ Final review complete — 1 blocking finding(s) (commit 94f9833) · triage: critical |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
The PR addresses the primary SwiftData accounting scenario and the supplied Swift build, tests, and schema checks pass. Three in-scope correctness gaps remain: contact-only outputs can incorrectly make a known amount unavailable, linked outputs are not scoped to the spending wallet when persisting reconciliation results, and TXO healing records dirty transactions without applying their accounting repairs.
🟡 3 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The diff substantially changes SwiftData persistence and schema migration behavior in PersistentTransaction, DashModelContainer, and PlatformWalletPersistenceHandler, a critical storage-migration surface affecting persisted transaction accounting. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 14% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— general (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— ffi-engineer (completed, effort xhigh); agentphase2-reviewer - Model comparison: every Phase-2 reviewer also ran on
gpt-6-astra; the verifier weighed both sets without knowing which model wrote which
🤖 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/Persistence/Models/PersistentTransaction.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentTransaction.swift:251-261: Ignore contact-only outputs when deciding whether a wallet amount is unavailable
`isWalletOwnedTxo` intentionally excludes DashPay contact/watch-only outputs, but `hasUnownedTxos` treats those outputs as evidence that the wallet's amount cannot be determined. For an incoming transaction with a foreign input, one normal output to this wallet, and one contact output, the normal output still determines the wallet's received amount. However, the contact output makes this check bypass the sole-participant fast path, and the pending foreign input then causes `netAmount(for:)` to return `nil`, displaying “Amount unavailable” instead of the known received amount.
In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:7007-7013: Scope linked outputs to the spending wallets during reconciliation
`ownedVouts` currently includes every locally owned linked output, regardless of its wallet. The address fallback below correctly limits unlinked outputs to `spendingWallets`, so linked and unlinked outputs have inconsistent scope. If wallet A spends 100 and wallet B receives 90 in the same transaction, wallet B's linked output is included in the scalar persisted for the transaction, producing `-10` rather than wallet A's `-100` and potentially classifying the transaction as internal. Filter linked outputs by their resolved wallet in the same way as the address fallback.
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:2932-2935: Apply accounting repairs after out-of-round TXO healing
`drainPendingInputs` adds the parent and spender transactions to `accountingDirty`, but that dictionary is consumed only by `endChangeset`. `reconcileHealMissingTxos` runs outside a changeset and commits through `reconcileSave`, which saves the newly materialized TXO without calling `reconcileTransactionAccounting`. When a healed prevout resolves a previously incomplete spend, the TXO graph is repaired but the transaction's stored `netAmount` and `direction` remain stale until a later load-time reconciliation. Use a common finalization path for `endChangeset` and out-of-round reconciliation saves, or drain and reconcile these dirty transactions before `reconcileSave` commits.
Out-of-scope follow-up suggestions (1)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Centralize accounting semantics in Rust — The Swift model and persistence handler duplicate the wallet-accounting case table already represented by the Rust platform-wallet implementation. This is a concrete long-term drift risk, but the PR explicitly scopes itself to the SwiftData workaround, makes no FFI changes, and identifies #5226 as the follow-up for authoritative per-wallet accounting.
- Follow-up: Implement the planned #5226 Rust/FFI accounting path and have SwiftData persist the authoritative per-wallet result.
…ly output
`netAmount(for:)` treated a DashPay contact's watch-only TXO as a reason
to distrust the sole participant's stored amount, fell through to the
TXO-computed path, and there a pending foreign input made it answer
`nil` ("Amount unavailable") although the wallet's own output gave the
amount.
The stored amount of such a row is still not trusted on its own: rows
with a contact TXO predate the Rust filter that keeps a contact's record
from defining the transaction row. The computed amount now stands
despite a pending input when the wallet is the sole participant, every
unowned TXO is a contact's, and the stored amount agrees with it. A
stored amount the wallet's outputs do not confirm keeps answering `nil`,
since the pending input may then be an unlinked coin of the wallet.
`isContactTxo` names the contact predicate `isWalletOwnedTxo` already
applied; its result is unchanged.
<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…ciliation `reconcileTransactionAccounting` counted every locally owned linked output as received, whichever wallet owned it, while the address fallback below it counted an unlinked output only for the spending wallets. When local wallet A spent 100 and local wallet B received 90 in one transaction, B's credit entered the stored scalar: -10 and "internal" instead of A's -100 and "outgoing". Linked outputs are now scoped by their wallet the same way. The stored scalar of a transfer between two local wallets is therefore the sender's; `testShouldScopeSharedAssetLockDirectionBeforeAndAfterReconciliation` pinned the old "internal" and is updated. Per-wallet answers (`netAmount(for:)`, `direction(for:)`) are unchanged: with two participants they never read the stored scalar. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
`drainPendingInputs` marks the healed coin's parent and its adopted spender in `accountingDirty`, but only `endChangeset` consumed that dictionary. `reconcileHealMissingTxos` runs outside a round and commits through `reconcileSave`, so a healed prevout repaired the TXO graph and left the spender's `netAmount` and `direction` stale until the next load-time pass. The heal step now reconciles the transactions it marked before its save, so the accounting commits with the coin and a failed save rolls both back. The step clears `accountingDirty` when it starts, because entries an earlier out-of-round writer left may point at rolled-back rows, and again once it has consumed them. Accounting is display-only: a failure is logged as `persistence_transaction_accounting_failed` with `phase=txo_reconcile_heal` and does not fail the heal. That catch has no fault-injection test yet (`TODO(test-heal-accounting-failure)`). <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The PR fixes the three previously reported accounting issues, including heal-time reconciliation, wallet-scoped linked outputs, and contact-only pending inputs. One new in-scope SwiftData bug remains: reconciliation counts a wallet-owned prevout for a transaction even after that prevout's spending relationship has moved to a competing transaction, which can overwrite the losing transaction's amount and direction.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large, intricate change to persisted SwiftData storage, including a V4 schema migration and wallet-scoped transaction accounting that directly affects displayed financial history. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 14% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer
🤖 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`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:7004-7007: Do not count a prevout unless it is still linked to this spender
This lookup verifies only that the decoded prevout exists and belongs to a wallet. When a competing transaction displaces the earlier spender, `adoptSpendObservation` moves `row.spendingTransaction` to the competitor while the losing transaction remains persisted and is marked dirty for reconciliation. This loop then still counts the shared prevout as an input of the loser, potentially rewriting its amount and direction as though it still spent the coin. Require the resolved TXO's `spendingTransaction` to match `transaction` before treating the prevout as a complete input; otherwise leave the loser's stored accounting unchanged.
Out-of-scope follow-up suggestions (2)
These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.
- Keep wallet accounting in the Rust accounting owner — The SwiftData model and persistence handler independently infer wallet amounts and direction from TXO relationships, while the canonical accounting logic already exists in Rust. The PR explicitly documents the remaining divergence cases and tracks replacing this Swift-side derivation with Rust-derived per-wallet accounting in #5226, so that broader ownership change is outside this PR.
- Follow-up: Implement the planned #5226 FFI and persistence boundary for Rust-derived per-wallet accounting.
- Account detail views still use unscoped transaction accounting — The account-detail surfaces still render the shared stored amount and direction without passing the owning wallet identifier. This is a concrete display limitation, but those views are unchanged by this PR and the limitation is explicitly disclosed in the PR description.
- Follow-up: Update account-detail transaction views to pass the owning wallet identifier in a separate UI-focused change.
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review after discussion — commit 70a1b30
Standing review was COMMENTED; this re-review is APPROVE: no blocking findings remain.
Approved: the discussion withdrew or deferred every finding the full review of 70a1b30c (run 4931) raised, and the approval rests on that review.
Same commit as the standing review; the inline threads above carry the per-finding outcome. This follow-up exists only to correct the verdict.
Review provenance
Source: conversation lane gpt-6.1-sol (agent: conversation); no reviewer or verifier lanes ran for this follow-up
- Verdict moved because the inline discussion withdrew or deferred findings of the standing review of this commit; the code was not re-reviewed
Combine optional accounting availability with V4 drain operation IDs and retain the frozen V3 step in the accepted V1 migration route. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Freeze the complete base V4 graph and migrate accounting availability through V5. Keep historical routes and the legacy bridge intact. Co-Authored-By: OpenAI Codex <noreply@openai.com>
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Merge the reviewed wallet-base fix that withholds unsupported drain-operation capability after conflict cleanup, including its cleanup/reopen drain-refusal regression and temporary accounting-sanitization issue reference. The merge has no conflicts or implementation adaptations. Scoped storage formatting and all three cleanup/capability tests pass; lint is inherited from the reviewed base fix and the previous scoped branch validation. Swift accounting repair and frozen persistence generations are unchanged. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude GPT-6 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The SQLite capability fix is correctly propagated, and independent source inspection confirms three prior findings are fixed and the prevout-link finding should remain withdrawn. However, the complete PR introduces transaction-accounting policy in Swift, violating the documented Rust ownership boundary; the reviewers' incremental-only scope does not justify deferring this newly introduced logic. Current listed CI checks pass, but none establishes Rust-wallet or Swift SDK build/test success at this head; validation here was static only.
🔴 1 blocking
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: platform-versioning); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The change combines intricate wallet-relative accounting with a persisted-storage migration in DashModelContainer.swift and a frozen V4 schema, meeting the size/complexity and storage-migration criteria. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(lane failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— platform-versioning (completed, effort xhigh); agentphase2-reviewer - Model comparison: every Phase-2 reviewer also ran on
gpt-6-astra; the verifier weighed both sets without knowing which model wrote which
🤖 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/Persistence/Models/PersistentTransaction.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentTransaction.swift:367-376: Keep authoritative transaction accounting in Rust
This PR adds a second implementation of wallet accounting in Swift: this helper determines net amount and direction, while PlatformWalletPersistenceHandler.reconcileTransactionAccounting reconstructs ownership from decoded transactions and TXO rows and overwrites persisted accounting. Rust already owns these rules in rs-platform-wallet/src/changeset/wallet_accounting.rs. Both book/src/contributing/coding-conventions.md ("FFI and mobile SDKs hold no business logic") and packages/swift-sdk/CLAUDE.md require Swift to persist Rust-emitted state rather than independently decide wallet accounting. Comparing the actual PR base dae60456ec08feb6baa52f4155670988961b271b with this head confirms these Swift implementations are additions of this PR, not unrelated pre-existing code. Their being unchanged in the latest five-file merge therefore does not make the ownership issue out of scope. Issue dashpay/platform#5226 tracks the correct destination, but a follow-up cannot defer new wrong-layer logic introduced here. Implement the ownership/accounting fold and repair in rs-platform-wallet, emit authoritative wallet-scoped amounts, directions and unavailable state through persistence callbacks, and leave Swift to store and display those results.
| // Same rule as Rust (`platform_wallet::changeset::wallet_direction`): | ||
| // internal only when nothing leaves the wallet and something stays in | ||
| // it, or an asset lock burns into Platform. Both sides test one table. | ||
| let direction: UInt32 | ||
| if previousDirection == CoreDirectionCode.coinJoin { direction = CoreDirectionCode.coinJoin } | ||
| else if inputs.isEmpty { direction = CoreDirectionCode.incoming } | ||
| else if allOutputsOwned && (!ownedOutputAmounts.isEmpty || isAssetLock) { | ||
| direction = CoreDirectionCode.internalTransfer | ||
| } else { direction = CoreDirectionCode.outgoing } | ||
| return (received - spent, direction) |
There was a problem hiding this comment.
🔴 Blocking: Keep authoritative transaction accounting in Rust
This PR adds a second implementation of wallet accounting in Swift: this helper determines net amount and direction, while PlatformWalletPersistenceHandler.reconcileTransactionAccounting reconstructs ownership from decoded transactions and TXO rows and overwrites persisted accounting. Rust already owns these rules in rs-platform-wallet/src/changeset/wallet_accounting.rs. Both book/src/contributing/coding-conventions.md ("FFI and mobile SDKs hold no business logic") and packages/swift-sdk/CLAUDE.md require Swift to persist Rust-emitted state rather than independently decide wallet accounting. Comparing the actual PR base dae6045 with this head confirms these Swift implementations are additions of this PR, not unrelated pre-existing code. Their being unchanged in the latest five-file merge therefore does not make the ownership issue out of scope. Issue #5226 tracks the correct destination, but a follow-up cannot defer new wrong-layer logic introduced here. Implement the ownership/accounting fold and repair in rs-platform-wallet, emit authoritative wallet-scoped amounts, directions and unavailable state through persistence callbacks, and leave Swift to store and display those results.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, platform-versioning); gpt-6-astra (phase2-reviewer: architecture-layering, platform-versioning)
TL;DR
Transaction history in the Swift SDK's local store showed wrong amounts and directions for transactions that spent the wallet's own coins. This PR makes the store recompute them from the coins it already tracks.
User story
As a wallet user on iOS, I want each entry in my transaction history to show what that transaction really took from or gave to my wallet, so that the history adds up to my balance.
Scenario
Actual behavior
A wallet sends a payment and gets change back. The history lists it as money received, for the amount of the change. An asset lock that used the whole balance is listed as a transfer of zero. The balance itself is right, but the history shows large receipts that never happened. When two wallets on the same device take part in one transaction, both show the same amount.
Expected behavior
The payment is listed as sent, for the amount that left the wallet. The asset lock shows what it locked. When two local wallets share a transaction, each shows its own side of it. When the store cannot work an amount out, it says the amount is unavailable instead of showing a wrong number.
Detailed discussion
Issue being fixed or feature implemented
Fixes #5126 for the SwiftData store, together with #5150, which repairs the Rust accounting and the SQLite store.
Stacked on #5150 (
fix/pr-5126). These changes were developed on that branch and were moved here unchanged so that #5150 is aboutplatform-wallet-storageonly. Nothing in this diff depends on the Rust changes of #5150 and no FFI surface changes; the stacking keeps the tested tree the same. Merge #5150 first, then retarget this PR.What was done?
PlatformWalletPersistenceHandlerrecomputes amount and direction of stored transactions from their linked TXO rows. A failure of the round-end pass is logged under its own event and does not fail the round. Funded asset-lock amounts are protected from empty updates.PersistentTransaction.netAmount(for:)anddirection(for:)answer for one wallet when several local wallets take part in a transaction;preserveAccounting(removingWallet:)keeps the survivor's values when a participant is deleted.netAmountUnavailable, a lightweight migration from the exact frozen V4 in the merged base, so an amount that cannot be determined survives a restart and a wallet deletion.TransactionDirectionStyle).Known limitations
direction(for:)reports a self-transfer as outgoing when a second wallet only recorded the transaction, andpreserveAccountingkeeps that after the second wallet is deleted.TODO(test-round-accounting-failure),TODO(test-heal-accounting-failure)).How Has This Been Tested?
Frozen-schema generation (148 files) and historical-fixture checks (36 files) passed. Python helper tests passed after updating the generated-file count: 98 tests passed in the initial 99-test run, and the corrected 28-test schema suite passed. SwiftData runtime and Swift tests require macOS CI and were not executed on this Linux host.
The Swift SDK build-and-test job (warnings as errors) and the frozen schema check pass in CI on
70a1b30c3a. Nothing was built or run locally: no Swift toolchain was available.TransactionAccountingTests.swift(new) covers the reconcile, contact outputs, wallet-scoped amounts and direction, foreign inputs, pending inputs, a contact output next to a received amount, a transfer between two local wallets and participant deletion;CoreTxoReconcileTestscovers the accounting repaired by a heal;DashReleasedSchemaTests,DashModelMigrationTestsandDashLegacySchemaMigrationTestscover V5, the migration from frozen V4 and the earlier supported migration routes.Breaking Changes
None for the FFI or for consensus. SwiftData gains schema V5 with a lightweight migration from frozen V4. The accepted V1 route still skips historical V2 and migrates through frozen V3 and V4.
Checklist
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull request🤖 Co-authored by Claudius the Magnificent AI Agent
🤖 Generated with Claude Code
https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4