Skip to content

feat(platform-wallet-ffi): persister error codes, contact-account fix, and cleanup - #4587

Draft
Claudius-Maginificent wants to merge 4 commits into
feat/platform-wallet-typed-persister-errorsfrom
feat/platform-wallet-ffi-persister-codes-and-fixes
Draft

feat(platform-wallet-ffi): persister error codes, contact-account fix, and cleanup#4587
Claudius-Maginificent wants to merge 4 commits into
feat/platform-wallet-typed-persister-errorsfrom
feat/platform-wallet-ffi-persister-codes-and-fixes

Conversation

@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator

TL;DR: Bundles four small, mostly-independent pieces split out of #3968: new FFI error codes for persister failures, a contact-account address-scanning fix, a provider-account rebuild dedup, and assorted cleanup.

Stacked on #4586 (feat/platform-wallet-typed-persister-errors) — this branch is built on top of it, so the diff shown here includes #4586's commits until #4586 merges. Review this PR's own commits individually, or wait for #4586 to merge first and this diff will shrink to just this PR's content.

⚠️ The last commit on this branch (FFI provider-rebuild dedup) does not compile until #3968 merges — it calls rebuild_provider_key_account, which only exists once #3968's trimmed storage-crate PR lands on v4.2-dev. CI on this draft is expected to be red on that count until then. Do not merge before #3968.

User story

As a platform-wallet FFI host application, I want persistence failures to surface as distinct transient/fatal error codes instead of the generic wallet-operation code, so I can decide whether to retry.

As a DashPay user, I want addresses on a newly accepted contact's payment account to be covered by the wallet's ongoing address scan, so incoming payments to that account aren't missed.

Scenario

Base flow

A host app calls into platform-wallet-ffi and a persistence operation fails; separately, a user accepts a contact request and the wallet derives a payment account for that contact.

Actual behavior

Every persistence failure was flattened to the generic ErrorWalletOperation code, so the host app couldn't tell a transient failure (worth retrying) from a fatal one. And accepting a contact request added its payment account without invalidating the wallet's filter-scan generation, so the account's addresses were never covered by the running blockchain scan.

Expected behavior

Persistence failures now surface as ErrorPersisterFatal (49) or ErrorPersisterTransient (50) so host apps can retry appropriately. Contact-account registration now goes through ManagedAccountOperations::add_managed_account, which bumps the wallet's scan generation so new addresses are covered.

Detailed discussion

What was done

Split out of #3968 as part of a coordinated PR-splitting effort — see that PR's description for the full rationale. Four grouped changes, each a separate commit:

  • FFI persister codes (B): rs-platform-wallet-ffi/src/error.rs gains ErrorPersisterFatal=49 / ErrorPersisterTransient=50 (declared in ascending order — the split review caught and fixed an ordering nit here), From<PersistenceError> mapping, ERROR_CODE_REGISTRY.md rows, Swift mirrors (PlatformWalletResult.swift, ErrorHandlingTests.swift). Depends on fix(platform-wallet): typed persister errors with bounded transient retry #4586's Persister* error variants.
  • Contact-account generation fix (D): contacts.rs / platform_wallet_traits.rs / payments.rs — registers new contact accounts via add_managed_account so filter-scan generation gets bumped; also removes a dead-end synced_height == 0 early return in reconcile_dashpay_rescan that skipped marking contacts covered (and reattaches an orphaned explanatory comment onto the right binding).
  • FFI provider-rebuild dedup (C): rs-platform-wallet-ffi/src/persistence.rs now shares rebuild_provider_key_account with the storage crate instead of duplicating the logic. Depends on feat(platform-wallet-storage): embeddable SQLite persistence backend with seedless rehydration #3968 merging first — see warning above.
  • Cosmetics (F): broadcaster.rs (drop hardcoded 30s SPV timeout, shorter than dash-spv's own budget), SendTransactionView.swift (fix a Platform-Payment account index leaking into the BIP44 Core account namespace on core→core sends), run_tests.sh (tolerate a CI runner with no keychain), util.rs+contact_requests.rs (now_secs() dedup), platform_wallet.rs test re-seed off reserve(), changeset/core_bridge.rs doc, wallet_lifecycle.rs new_watch_onlynew_external_signable doc rename, three Swift doc-only files.

Testing

cargo clippy -p platform-wallet -p platform-wallet-ffi --all-targets clean at the pre-dedup (B/D/F) commit. cargo nextest run -p platform-wallet targeted at touched areas: 95 passed, 0 failed. The final (C) commit does not build standalone until #3968 lands — see warning above. Swift edits applied and reviewed; no xcodebuild run (no macOS toolchain available in this environment).

Breaking changes

None to the public FFI surface's meaning — new error codes are additive.

Checklist

  • I have performed a self-review of my own code
  • I have added or updated relevant unit tests
  • No breaking changes
  • No documentation changes needed

Prior work

Split out of #3968 as part of a coordinated 4-PR split: PR 0 (trimmed #3968), #4586 (typed persister errors, this PR is stacked on it), #4585 (asset-lock size gate). See #3968 for the full rationale.

🤖 Co-authored by Claudius the Magnificent AI Agent

`From<PersistenceError>` flattened every persistence failure into the generic
`ErrorWalletOperation` (6), so hosts lost the transient/fatal classification
the wallet layer now preserves. Adds `ErrorPersisterFatal = 49` and
`ErrorPersisterTransient = 50`, claimed from the registry's allocation
frontier, and de-flattens the conversion: `PersisterLoad` / `PersisterStore`
map on `is_transient()`, `PersisterRestore` unwraps to its typed inner error.

Ships the full three-layer parity the registry mandates — Rust enum with a
discriminant pin test, `ERROR_CODE_REGISTRY.md` rows 47-50 with the frontier
moved to 51, the Swift `PlatformWalletResultCode` / `PlatformWalletError`
mirrors, and Swift raw-value pins.

The variants are declared in ascending discriminant order (49 then 50), the
order both enums otherwise keep; the comment records why 50 is not 48.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
…counts

Contact-account registration went through `accounts.insert_funds_bearing_account`,
which does not bump the wallet's `account_generation`. The running filter scan
therefore never picked up the new account's addresses — a contact's incoming
payments stayed invisible until something else happened to invalidate the
scan. Registration now goes through `ManagedAccountOperations::add_managed_account`,
and `PlatformWalletInfo` forwards `account_generation()` to the core wallet so
the invalidation is observable. Tests assert generation `1` after registering
both a contact and an external account.

`reconcile_dashpay_rescan` no longer bails on `synced_height == 0`. A zero
checkpoint already means "scan from genesis", but bailing left candidates
unmarked, so once that scan advanced the very same contacts triggered a
redundant funding-height rewind. Candidates are now marked as covered and the
height is left alone.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
…ndex

`broadcaster.rs` deleted its hardcoded 30 s `SPV_ACCEPTANCE_TIMEOUT` and now
passes `None` to `broadcast_and_wait`. The local override was shorter than
dash-spv's own budget, so sends were reported `MaybeSent`/uncertain while the
SPV layer was still legitimately waiting.

`SendTransactionView` non-platform -> platform flows use `senderAccountIndex = 0`
instead of "first key-class-0 account with a positive balance": that search
returned a key-class Platform-Payment account index, which was then fed to
`CoreTransactionBuilder.setFunding(accountType: .bip44, ...)` — a different
namespace, so core -> core sends could draw on the wrong funding account.

Also in this batch: `run_tests.sh` tolerates a CI runner with no user default
keychain under `set -euo pipefail`; `now_secs()` moves to `util.rs` as
`pub(crate)`; the shield-input-selection regression test re-seeds off
`reserve()` rather than hardcoded balances so it survives fee-schedule
changes; and doc comments are corrected (`Wallet::new_watch_only` ->
`new_external_signable`, `derive_spent_utxos` defaults, restore-loop skip
behaviour).

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
The FFI restore path carried its own copy of the provider-key account
reconstruction, which had already drifted from the SQLite backend's. Both now
call `platform_wallet::changeset::rebuild_provider_key_account`, so FFI and
SQLite restore provider accounts identically (-54/+30).

Ordering: `rebuild_provider_key_account` ships with the wallet-storage PR's
required bucket (`changeset/changeset.rs` + `changeset/mod.rs`). Until that
lands on the base branch this commit does not compile — the sole error is the
unresolved import. Land the storage PR first.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

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.

2 participants