Skip to content

fix(platform-wallet): register contact accounts via add_managed_account, dedup provider-key rebuild, and cleanup - #4587

Draft
Claudius-Maginificent wants to merge 8 commits into
v4.2-devfrom
feat/platform-wallet-ffi-persister-codes-and-fixes
Draft

fix(platform-wallet): register contact accounts via add_managed_account, dedup provider-key rebuild, and cleanup#4587
Claudius-Maginificent wants to merge 8 commits into
v4.2-devfrom
feat/platform-wallet-ffi-persister-codes-and-fixes

Conversation

@Claudius-Maginificent

@Claudius-Maginificent Claudius-Maginificent commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

TL;DR: Registers new contact-account payment accounts through add_managed_account so they're covered by the wallet's address scan, deduplicates the provider-key-account rebuild logic between platform-wallet-storage and platform-wallet-ffi, and carries a few small cleanups.

Updated 2026-09-10 after merging v4.2-dev: this PR originally also proposed new FFI persister error codes (49/50) and an FFI provider-rebuild import that depended on an unmerged shape of #3968. Both landed differently upstream in the meantime — see below.

User story

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.

As a platform-wallet maintainer, I want provider-key-account rebuild logic to exist in exactly one place, so storage and FFI can't drift out of sync.

Scenario

Base flow

A user accepts a contact request and the wallet derives a payment account for that contact; separately, both platform-wallet-storage and platform-wallet-ffi independently reconstruct provider (BLS/EdDSA) key accounts when rehydrating wallet state.

Actual behavior (before this PR)

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. Provider-key-account rebuild logic was duplicated between the storage crate and the FFI crate.

Expected behavior

Contact-account registration now goes through ManagedAccountOperations::add_managed_account, which bumps the wallet's scan generation so new addresses are covered — note this means accepting a contact now triggers a full-history rescan instead of a funding-height rewind; please sign off on this scan-cost trade-off explicitly. Provider-key-account rebuild now lives once, in platform_wallet::changeset::provider_key_account, called by both platform-wallet-storage and platform-wallet-ffi.

Detailed discussion

What was done

  • Contact-account generation fix: contacts.rs / platform_wallet_traits.rs / payments.rs — registers new contact accounts via add_managed_account; removes a dead-end synced_height == 0 early return in reconcile_dashpay_rescan that skipped marking contacts covered.
  • Provider-key-account rebuild dedup: new packages/rs-platform-wallet/src/changeset/provider_key_account.rs exposes rebuild_provider_key_account + ProviderAccountRebuildError, called from both rs-platform-wallet-storage/src/sqlite/provider_accounts.rs (rehydrate path) and rs-platform-wallet-ffi/src/persistence.rs (restore path). No behavior change — characterization tests written before the refactor confirm identical outcomes (error strings, error kind, and success paths) on both callers.
  • Cleanup: broadcaster.rs (drop hardcoded 30s SPV timeout), 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 shield-regression fixture, changeset/core_bridge.rs doc, wallet_lifecycle.rs new_watch_onlynew_external_signable doc rename, Swift doc-only files.

What changed after merging origin/v4.2-dev

Testing

  • cargo nextest run -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi: 2390 passed, 0 failed, 7 ignored — includes contact-account tests (register_contact_account_persists_account_registration, register_external_with_precomputed_shared_key_builds_account, rescan_is_a_noop_when_synced_height_is_zero, rescan_uses_min_funding_across_contacts_and_drip_feed_settles) and the new provider-key-account characterization/rebuild tests.
  • cargo clippy -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi --all-targets: clean.
  • cargo fmt --check: clean.
  • Swift: applied and reviewed, not built (no macOS toolchain in this environment) — needs xcodebuild / run_tests.sh in CI.

Breaking changes

None to the public FFI ABI's meaning — provider-rebuild dedup is behavior-preserving; contact-account registration change is a behavior improvement (fixes missed scan coverage) with a scan-cost trade-off flagged above for reviewer sign-off.

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

Known unrelated pre-existing issue

packages/rs-drive/src/query/mod.rs:1 imports DocumentPropertyType unconditionally, but its only use is behind #[cfg(feature = "server")]; any -D warnings build of drive without server fails on unused_imports (introduced by d6ab675124). Not touched by this PR; worth a separate small fix.

Prior work

Originally split out of #3968 as part of a coordinated 4-PR split alongside #4586 (typed persister errors, now merged) and #4585 (asset-lock size gate, merged). See #3968 for the full rationale.

🤖 Generated with Claude Code

…etry

Persistence failures on the wallet rehydration and registration paths were
flattened into `PlatformWalletError::WalletCreation(String)`, destroying the
transient/fatal classification callers need and severing the `#[source]`
chain. Adds typed `PersisterLoad` / `PersisterStore` / `PersisterRestore`
variants carrying the `PersistenceError` (boxed for the recursive restore
case) and routes every persister boundary through them.

On top of that, `retry_transient` (4 attempts, 20 -> 200 ms doubling backoff)
now wraps persister `store` / `flush` / `load` on the registration, startup
and identity-discovery paths, so a transient `SQLITE_BUSY` no longer aborts
wallet registration outright or costs the identity-scan verdict its
durability (#4365). Fatal errors still fail fast. The retry re-drives a
failed `store` via a bare `flush`, which `PlatformWalletPersistence::store`
now documents as a backend contract.

Also fixes the persister leak behind #4133: a failed `load_from_persistor`
left the wallet-event adapter holding an `Arc<P>` clone, so re-opening the
same path returned a spurious `AlreadyOpen` masking the real error.
`load_from_persistor` now shuts the manager down on both failure paths, with
a `Drop` backstop cancelling and aborting the adapter task.

`record_or_persister_or_log` and `reconcile_sent_payments` stop swallowing
permanent read failures as "not found": transient errors still defer to the
next sweep, permanent ones propagate as `PersisterLoad` instead of stalling
an unbounded poll loop with no explanation.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
`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.

@lklimek
lklimek force-pushed the feat/platform-wallet-typed-persister-errors branch from acaf86b to 1f5b3d2 Compare September 3, 2026 11:51
Base automatically changed from feat/platform-wallet-typed-persister-errors to v4.2-dev September 9, 2026 13:17
@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 9, 2026
lklimek and others added 3 commits September 10, 2026 07:24
…let-ffi-persister-codes-and-fixes

Base carries the squashed forms of this branch's prerequisites (#4586
typed persister errors, #3968 storage backend, #4481 SPV timeout), so the
pre-squash copies on this branch resolve to the merged versions:

- 16afd79 (pre-squash #4586): error.rs, manager/{load,mod,startup,
  wallet_lifecycle}.rs, asset_lock/sync/proof.rs, identity/network/
  {discovery,payments}.rs and changeset/traits.rs take the base. The
  branch's "store Transient MUST buffer" doc contradicted the base's
  store_transient_is_reissuable contract and is dropped.
- f93aa3f (codes 49/50): superseded by the base's six-code 49-54
  persister block (49 = LoadTransient, not Fatal). FFI error.rs, the
  registry and Swift PlatformWalletResult take the base; the stale Swift
  49/50 test is removed, the code-26 raw-value pin is kept.
- 40e04c2 (FFI provider-rebuild dedup): rebuild_provider_key_account
  landed as pub(super) in platform-wallet-storage, not in
  platform_wallet::changeset, and the FFI crate does not depend on the
  storage crate, so the import cannot resolve. FFI persistence.rs keeps
  the base's inline rebuild; dedup needs a follow-up relocation.

Kept from this branch: contact-account add_managed_account generation
fix and reconcile_dashpay_rescan zero-height change, reserve-derived
shield regression fixture, now_secs dedup, doc and Swift cleanups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e merge

The v4.2-dev merge took the base's permanent-read-failure sweep in
reconcile_sent_payments, whose catch-all `Err(e)` records the failure and
continues. The pre-squash arm that returned `PersisterLoad` immediately
sat outside the conflict block and survived as dead code (unreachable
pattern warning). No behavior change: the arm could never match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… storage and ffi

Provider key-material accounts (BLS ProviderOperatorKeys / EdDSA
ProviderPlatformKeys) were rebuilt by two copies of the same logic: a
pub(super) helper private to platform-wallet-storage's sqlite module and
an inline copy in platform-wallet-ffi's build_wallet_start_state. The FFI
crate does not depend on the storage crate, so the canonical helper now
lives in platform-wallet (a dependency of both) as
platform_wallet::changeset::provider_key_account::{
rebuild_provider_key_account, ProviderAccountRebuildError}, gated on the
bls/eddsa features that make its variants exist.

No behavior change:
- storage keeps its Invalid -> AccountRecordInvalid and
  Rejected -> ProviderKeyAccountEntryMismatch mapping;
- ffi keeps its bincode decode (and the unmaintained-bincode-decoder
  note), and maps helper errors to byte-identical Fatal
  PersistenceError::backend messages.

Characterization tests pin both call sites before the move:
build_wallet restoring/rejecting provider manifest entries (storage) and
build_wallet_start_state restoring both provider accounts from
bincode-encoded specs (ffi). The helper's own unit tests move with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Claudius-Maginificent Claudius-Maginificent changed the title feat(platform-wallet-ffi): persister error codes, contact-account fix, and cleanup fix(platform-wallet): register contact accounts via add_managed_account, dedup provider-key rebuild, and cleanup Sep 10, 2026
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