Skip to content

feat(swift-sdk): add Core wallet balance diagnostics - #4580

Open
llbartekll wants to merge 11 commits into
v4.2-devfrom
codex/cj-balance-diagnostics-sdk
Open

feat(swift-sdk): add Core wallet balance diagnostics#4580
llbartekll wants to merge 11 commits into
v4.2-devfrom
codex/cj-balance-diagnostics-sdk

Conversation

@llbartekll

@llbartekll llbartekll commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Adds read-only diagnostics for a support case where a CoinJoin sweep appeared spent locally but no corresponding transaction or AssetLock was found on-chain. It also adds regression coverage for CoinJoin-funded transactions with owned BIP44 change, the persistence behavior addressed by #4438.

What was done?

  • Added a lightweight core_restore_buffer_snapshot during wallet restore. It reuses rows already fetched for restore and performs no additional SwiftData history scan or Rust FFI query.
  • Added the public nonthrowing emitCoreWalletDiagnostics(for:) API. Full SwiftData, Rust-memory, AssetLock, shielded-store, owned-output, and DB-to-memory diagnostics run only when the host explicitly requests a pre_export snapshot.
  • Added an owned-output audit for CoinJoin-funded transactions with BIP44 change.
  • Added stable hashed fingerprints, reference-only identifiers, redacted errors, and a 25-detail limit per anomaly class.
  • Added a separate diagnostic native-operation counter, taken before the database half and released after the Rust half: diagnostics do not block synchronous wallet operations, while shutdown drains the whole export — for at most the stage in flight, since shutdown() raises a cancellation flag that both the queue-confined database pass (between fetch stages) and the Rust reads check.
  • The database pass runs on a scratch ModelContext on the persistence queue: it sees only committed state (never a changeset's pending rows) and is dropped with the pass, so the up-to-110k objects it registers do not stay resident. If it returns nothing, the Rust half still runs and its diffs are marked database_snapshot_available=false.
  • Added core_store_open_result with SQLite main/WAL/SHM sizes, duration, migration_path, and an accurate store-open outcome without claiming that a migration ran. Events emitted before the file sink exists (the store open runs in the host's init()) are buffered and replayed into swift/run.log on install, so they reach the exported artifact.
  • Added core_rescan_requested logging: what was asked and whether the FFI accepted it (accepted / failed / invalid_wallet_id), with no claim about whether a rewind happened — that would need the filter-scan checkpoint, which is neither the core wallet's synced height nor readable without a blocking Rust-lock call on the main actor.
  • Kept transaction-history decoding and the full PersistentTxo audit manual/export-only, with Rust analysis off MainActor. The export holds the persistence serial queue for its duration (documented on emitCoreWalletDiagnostics(for:)); above CoreDiagnosticRowLimits (100k TXO rows / 10k transaction rows table-wide) it narrows to the wallet's own rows and declines the exact audit with audit_incomplete=true, reason=tables_too_large_for_exact_audit rather than truncating and misclassifying. A paged variant that lifts the ceilings is swift-sdk: paged Core wallet diagnostics export that keeps the exact #4438 classification #4607.
  • Added a synthetic v4.2.0-dev.1 fixture and made DashModelContainer.open(_:) public. One production behaviour change: when the staged DashMigrationPlan rejects a store with Cocoa 134504 ("unknown model version" — what every dev.1 store hits until the remaining V1/V2 shapes are frozen, see DashSchemaFrozenModels.swift), open retries with inferred lightweight migration instead of letting the host crash at launch. The decision is made on the store's metadata, not the error (SwiftData surfaces the checksum failure as an opaque loadIssueModelContainer): classifyStore(at:) returns a verdict, and the fallback runs only for driftedRegisteredVersion. A store from a newer build — an unregistered version identifier, or an entity this schema lacks — is newerThanRegistered and is rethrown, because inferred migration would otherwise trim it silently; so are unreadable stores and stores that match a registered version but failed anyway (a future custom MigrationStage). The verdict is logged as store_verdict. Same entity names with a kept identifier are then compared hash-by-hash against the declared version's model: the fallback runs only if every disagreeing entity carries exactly the hash knownDriftedEntityHashes lists for it — the dev.1 shapes of the two entities changed in place since V1, pinned byte-for-byte to the fixture by a test. A hash is a function of the shape, so a newer build's version of any entity, those two included, is refused as unexpected_entity_drift; there is no same-name-unknown-shape residual. The fallback answers exactly the store the fixture proves. Dev1StoreUpgradeTests pins the fallback on the dev.1 fixture, the refusal of a corrupt store and of a store with an extra @Model, and that the migrated store reopens through the staged path afterwards.
  • DashWallet builds its own ModelContainer and does not call create; it needs to adopt DashModelContainer.open(_:) in a follow-up to receive the store-open telemetry and the fallback. Until then only SwiftExampleApp emits them.

How Has This Been Tested?

  • SwiftDashSDK build-for-testing: passed.
  • Targeted AssetLock privacy, Core analyzer/integration, shutdown, and Dev1 compatibility suites: 27 passed, 0 failed, 0 skipped.
  • Production SwiftDashSDK build with warnings-as-errors: passed.
  • git diff --check: passed.

Breaking Changes

None. Public additions: the nonthrowing read-only API emitCoreWalletDiagnostics(for:) and DashModelContainer.open(_:). The only behaviour change is the narrowly-scoped migration fallback described above, which acts only on a path that previously threw.

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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Added comprehensive wallet diagnostics for persisted data, in-memory state, asset locks, shielded data, and restore status.
    • Added rescan status reporting, including requested heights and failure outcomes.
    • Improved diagnostic event buffering and replay when logging becomes available.
    • Added safer store opening with schema validation and migration handling.
  • Bug Fixes

    • Improved detection and reporting of wallet data inconsistencies and missing account relationships.
    • Redacted sensitive details from persistence failure logs.
    • Prevented shutdown counter errors from disrupting diagnostics cleanup.
  • Tests

    • Added coverage for diagnostics, rescan classification, privacy, shutdown behavior, and migration compatibility.

Loading
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