Skip to content

feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692

Draft
Claudius-Maginificent wants to merge 18 commits into
feat/platform-wallet-storage-secretsfrom
feat/platform-wallet-rehydration
Draft

feat(platform-wallet): watch-only rehydration from persistor (seedless load)#3692
Claudius-Maginificent wants to merge 18 commits into
feat/platform-wallet-storage-secretsfrom
feat/platform-wallet-rehydration

Conversation

@Claudius-Maginificent
Copy link
Copy Markdown
Collaborator

@Claudius-Maginificent Claudius-Maginificent commented May 20, 2026


STACKED PR — review diff against feat/platform-wallet-storage-secrets (PR #3672), not v3.1-dev.

Merge order: #3625 (feat/platform-wallet-sqlite-persistor) → #3672 (feat/platform-wallet-storage-secrets) → this PR.

The wallet_id sign-gate that this PR's earlier iterations bundled has been extracted to PR #3735 (security patch targeting v3.1-dev directly). Land #3735 first; the standard merge-up cycle will pull the gate into this PR's lineage naturally.


Issue being fixed or feature implemented

After the SQLite persister landed (#3625), restarting the wallet app required a full re-scan from birth height — the DB held all the data but nothing reconstituted live wallets from it. This PR closes that gap.

The user story matches how the real iOS host works. The app launches with the Keychain locked. There is no seed in memory. The wallet UI needs to come back instantly with all balances, UTXOs, identities, and asset-lock state — without prompting the user to unlock — so they can see their funds, scroll their history, and decide whether to act. Only when they do act (sign a transaction, register an identity key) does the Keychain unlock and the seed arrive, gated to that one operation. This was validated against dashwallet-ios (swift-sdk-integration branch): loadFromPersistor() is zero-arg, called at app launch with locked Keychain; signing flows take the MnemonicResolverHandle vtable on demand.

The implementation reflects that: load is seedless and watch-only. Every persisted wallet comes back as Wallet::new_watch_only(...) — no key material derived, no signing capability, no seed touched. Wrong-seed detection moves to the sign path — covered by the companion security PR #3735 against v3.1-dev.

What was done?

Seedless watch-only load (rs-platform-wallet)

PlatformWalletManager::load_from_persistor() reconstructs each persisted wallet from the keyless ClientWalletStartState:

pub async fn load_from_persistor(&self) -> Result<LoadOutcome, PlatformWalletError>

For each wallet in the persisted wallets map, the manager:

  • Builds an AccountCollection from the account_manifest: one Account::from_xpub(parent_wallet_id, account_type, account_xpub, network) per AccountRegistrationEntry.
  • Constructs Wallet::new_watch_only(network, wallet_id, accounts)key_wallet::WalletType::WatchOnly variant, no Mnemonic/Seed variant, no key bytes anywhere.
  • Routes the keyless CoreChangeSet (UTXOs, tx records, IS-locks, sync watermarks) into the wallet via the existing apply_persisted_core_state(...) path, which correctly handles non-BIP44 topologies (CoinJoin-only / DashPay) via all_funding_accounts_mut() — the F2 silent-zero balance fix carries through.

A wallet whose persisted rows fail to decode is skipped, not silently mis-loaded. LoadOutcome.skipped carries (WalletId, SkipReason::CorruptPersistedRow { kind: CorruptKind }) where CorruptKind is MissingManifest | MalformedXpub | DecodeError(String). A PlatformEvent::WalletSkippedOnLoad { wallet_id, reason } fires per skip. One corrupt row never aborts the rest. The caller receives Ok(LoadOutcome) (non-empty skipped is success, not an error).

New schema readers

Item Reader Notes
A1 schema::accounts::load_state Reads account_registrations + pools; decodes AccountRegistrationEntry; no Wallet built
B schema::core_state::load_state Bulk reconstructs ManagedWalletInfo — UTXOs, tx records, IS-locks, derived-address flags, sync watermarks, last_applied_chain_lock; routes UTXOs to the first funds-bearing account of any topology (no BIP44 assumption); no silent zero balance
A2 schema::asset_locks::load_unconsumed Status-predicate reader excluding terminal Consumed rows at SQL level (WHERE status NOT IN ('consumed'))

FFI

// before — earlier iteration of this PR (now removed)
int32_t platform_wallet_manager_load_from_persistor(
    const PlatformWalletManagerHandle* manager,
    const PlatformWalletPersisterHandle* persister,
    const ResolverSeedProvider* resolver,
    LoadOutcomeFFI* out_outcome);

// after
int32_t platform_wallet_manager_load_from_persistor(
    const PlatformWalletManagerHandle* manager,
    const PlatformWalletPersisterHandle* persister,
    LoadOutcomeFFI* out_outcome);

The resolver arg is gone — load is purely watch-only. LoadOutcomeFFI surfaces loaded_count / skipped_count / skipped[] so the host can retry skipped wallets after a corruption-fix flow.

Swift wrapper

PlatformWalletManager.swift::loadFromPersistor() aligns to the new 2-arg + outparam C signature (passes nil for the outcome ptr — the iOS host doesn't surface skip reasons to the UI today).

No V002 migration

Every column required for this phase is in V001. No SQL migration is added.

Not in this PR

How Has This Been Tested?

cargo fmt --all --check
cargo clippy -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi --all-targets -- -D warnings
cargo check --workspace
cargo test -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi
cargo test --doc -p platform-wallet -p platform-wallet-storage

Result: 410 tests passed, 0 failed, 8 ignored. Doctests: 3 passed, 0 failed, 1 ignored.

Targeted suite (packages/rs-platform-wallet/tests/rehydration_load.rs):

  • RT-WO — persist N wallets, drop, reopen, load_from_persistor(); assert every wallet comes back as Wallet::WatchOnly with correct wallet_id, accounts, balances. No seed ever touched.
  • RT-Corrupt — feed a corrupt manifest blob for one wallet; assert that wallet appears in LoadOutcome.skipped with CorruptPersistedRow, the other wallets load cleanly, exactly one PlatformEvent::WalletSkippedOnLoad fires.
  • RT-Z — assert LoadOutcome, SkipReason, WalletSkippedOnLoad payloads carry no key material in Display or Debug.

Persister-side readers:

cargo test -p platform-wallet-storage --test sqlite_accounts_reader \
                                       --test sqlite_core_state_reader \
                                       --test sqlite_asset_locks_filter \
                                       --test sqlite_load_wiring \
                                       --test sqlite_load_reconstruction

13/13 tc_p4_* passes including corruption-is-hard-error variants.

Breaking Changes

This PR rewrites a load path that was added in earlier commits of this same PR (and has never shipped). There are no breaking changes against v3.1-dev. For reviewers tracking the in-PR evolution:

  • PlatformWalletManager::load_from_persistor() no longer takes a &dyn SeedProvider (the trait itself was deleted — MnemonicResolverHandle is the on-demand contract).
  • ClientWalletStartState no longer carries a Wallet field (assembled in the manager via Wallet::new_watch_only).
  • FFI dropped the 3rd resolver arg from platform_wallet_manager_load_from_persistor.

No ! in the title because this is additive capability on an unreleased API — v3.1-dev carries none of the previous PR-internal shapes.

AR-7 hygiene

Load path eliminates AR-7 entirely — the manager never constructs WalletType::Mnemonic|Seed, only WalletType::WatchOnly (no key material). AR-7's residual Debug concern was about derived Wallet values on the load path; that path no longer derives.

Sign path keeps AR-7 discipline (Zeroizing + non_secure_erase()); the sign-time wallet_id gate that enforces it ships in PR #3735.

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

Note on the checklist item above: no ! in the title because no public API on v3.1-dev changes. The FFI signature change is internal to this PR branch (never released).

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent


Rebuild note (2026-05-25): History rewritten to remove the sign-gate code that was extracted to PR #3735. The 5-commit minimal rework on top of the original PR-1 rehydration work yields a focused diff: watch-only load via Wallet::new_watch_only, FFI resolver-arg drop, Swift wrapper align. The sign-time wallet_id gate ships via #3735 against v3.1-dev.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2aaac47a-114c-46ea-b240-d7d4d5943728

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/platform-wallet-rehydration

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.

lklimek and others added 11 commits May 20, 2026 17:10
schema::accounts::load_state reads account_registrations rows back into
a deterministic Vec<AccountRegistrationEntry> manifest — the account-set
oracle and per-account xpub cross-check source for rehydration. Mints no
Wallet, fail-hard on a corrupt blob. RT: sqlite_accounts_reader (3 tests).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…wrong-seed gate (S)

- platform-wallet: storage-agnostic SeedProvider trait with zeroizing,
  Debug-redacted SecretPhrase/SecretSeed newtypes (M-DONT-LEAK-TYPES);
  SeedUnavailable/SecretStoreErrorKind structural projections.
- manager::rehydrate::rehydrate_wallet: fail-closed, constant-time
  wrong-seed gate (compute_wallet_id recompute + per-account xpub
  cross-check via subtle) yielding typed WrongSeedForDatabase that
  carries only the two 32-byte ids. AR-7 noted at the call site.
- manager::rehydrate::apply_persisted_core_state: keyless CoreChangeSet
  → ManagedWalletInfo apply (balance no-silent-zero contract).
- load_from_persistor signature → (&dyn SeedProvider) -> LoadOutcome;
  seed-unavailable ⇒ skip (continue before insert, LoadOutcome.skipped,
  PlatformEvent::WalletSkippedOnLoad); wrong seed ⇒ hard-fail.
- ClientWalletStartState made keyless by type (no Wallet/seed field).
- platform-wallet-storage: secrets-gated CredentialStoreSeedProvider
  adapter over `keyring_core::api::CredentialStoreApi` (mnemonic→seed
  label order, no secret in logs/errors). File-backend WrongPassphrase
  is recovered via `downcast_failure` on the cross-SPI marker so the
  operator-actionable case survives the seam.

RT: seed_provider (4) + rehydrate (3) unit tests, secrets_seed_provider
_adapter (10). secrets_scan/secrets_guard still green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
core_state::load_state rebuilds the keyless CoreChangeSet projection
(unspent UTXOs with address recovered from script+network, tx records,
IS-locks, sync watermarks) for one wallet — the safety-critical balance
source. spent rows excluded; fail-hard on a corrupt blob. Documents the
reconstructed-vs-deferred split: last_applied_chain_lock /
per-account-attribution / coinbase flags re-warm on first post-load
sync (the no-V001-column deviation from dev-plan §5 is recorded inline).

RT-2 (sqlite_core_state_reader): a non-zero balance survives
store→drop→reopen→load→apply, reconstructed exact in the confirmed
bucket — the no-silent-zero contract proven end-to-end. 4 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…on reader (A2)

asset_locks::load_unconsumed excludes terminal 'consumed' rows at the
SQL level so a spent one-shot lock never resurrects as actionable on
rehydration (A04/A08); historical rows stay on disk via load_state.
Corrects the factually-wrong list_active doc-comment (consumed locks do
NOT leave via AssetLockChangeSet::removed — they upsert and persist).

RT-4 (sqlite_asset_locks_filter): mix incl. terminal Consumed — row
still on disk, absent from filtered feed, non-terminal survive. 2 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…d (C)

SqlitePersister::load() now populates ClientStartState.wallets with the
keyless per-wallet payload (network, birth_height, account_manifest,
core_state, identity_manager, Consumed-filtered unused_asset_locks) via
the A1/B/A2 readers + identities::load_state. Return type carries no
Wallet/seed by construction. Real wallets_rehydrated tracing count;
LOAD_UNIMPLEMENTED shrunk to the genuinely-deferred set
(contacts/identity_keys/last_applied_chain_lock); load() rustdoc
corrected.

RT (sqlite_load_wiring): keyless payload round-trips, empty DB stays
empty, metadata-only wallet still present. 3 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
rehydration_load: load_from_persistor through a real
PlatformWalletManager (mock SDK, in-memory keyless persister, test
SeedProvider) —
- seed round-trip: wallet registered + signing-capable by construction;
- RT-W: present-but-wrong seed ⇒ WrongSeedForDatabase, NOT in skipped,
  NO WalletSkippedOnLoad event, wallet absent;
- RT-S: seed absent ⇒ skip (other wallets load, skipped wallet ABSENT
  from manager, LoadOutcome.skipped + exactly one WalletSkippedOnLoad
  event, Ok), then recoverable on a fresh targeted re-load;
- RT-S(ii): KeyringLocked ⇒ StoreUnavailable skip;
- RT-Z: no seed byte leaks into LoadOutcome / SkipReason /
  WrongSeedForDatabase Display+Debug.
5 tests.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…tion (F)

sqlite_load_reconstruction: header rewritten (no longer 'blocked on
upstream Wallet::from_persisted'); tc_p4_006/tc_p4_007 now assert
wallets_rehydrated=N / pending=0 and a populated wallets payload;
tc_p4_012 asserts O(1)-per-wallet + small constant shared overhead
(no brittle magic-number pin) instead of the old fixed-2. All 13 green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
… SELECTs (F)

The full-rehydration readers (accounts/core_state load_state) use
prepare() for one-shot SELECTs by design; add them to
READ_ONLY_PREPARE_ALLOWED so tc_p1_003 (writers must use
prepare_cached) stays green without weakening the writer-side rule.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
… doc (F2,F3)

F2 (MEDIUM): apply_persisted_core_state previously routed persisted
UTXOs only into the first BIP44 account, silently dropping ALL UTXOs
(→ Ok + balance 0) for CoinJoin-only / non-BIP44 topologies. Now route
into the wallet's first funds-bearing account of ANY topology (BIP44/
BIP32/CoinJoin/DashPay) via all_funding_accounts_mut(); the wallet
total stays exact (it is a sum). A wallet with persisted UTXOs but no
funds account at all fails closed with the new typed
PlatformWalletError::RehydrationTopologyUnsupported (wallet_id +
utxo_count, no key material) instead of a silent zero. Signature is
now Result<(), PlatformWalletError>.

F3 (LOW): moved the last_applied_chain_lock bullet from the
'Reconstructed' to the 'Deferred' rustdoc section (it is always None
from disk — no V001 column).

RT: f2_no_bip44_wallet_nonzero_balance_survives_reopen (CoinJoin-only,
9_000_000 duffs) fail→pass; RT-2 + B-2/B-3/B-4 still green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
F4 (LOW): the plain '!=' wallet_id re-check after insert_wallet was
shadowed-dead — the constant-time rehydrate_wallet gate already proves
compute_wallet_id() == expected_wallet_id pre-insert and a mismatch is
the typed fail-closed WrongSeedForDatabase. The legacy check only
emitted a weaker untyped WalletCreation error and confused readers;
removed. Also wires the F2 apply_persisted_core_state Result into the
hard-fail/rollback path. RT-W still passes (typed WrongSeedForDatabase
from the real gate unaffected).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…PI (F1)

F1 (HIGH): workspace no longer compiled against the new
load_from_persistor signature / keyless ClientWalletStartState.

- New ResolverSeedProvider wraps the existing Swift MnemonicResolver-
  Handle vtable (same mechanism as sign_with_mnemonic_resolver) as a
  SeedProvider — minimal correct seed source, no second secret path,
  no mnemonic round-tripping. Chosen over SecretStoreSeedProvider
  because the iOS host already owns the resolver, not a SecretStore.
- build_wallet_start_state now projects its reconstructed wallet +
  wallet_info into the keyless ClientWalletStartState shape
  (account_manifest from accounts, core_state CoreChangeSet from the
  restored UTXO set + sync watermarks); the local Wallet is dropped
  (manager re-derives from the resolver seed + runs the wrong-seed
  gate).
- platform_wallet_manager_load_from_persistor gains a resolver param
  and an optional *mut LoadOutcomeFFI out-param: the LoadOutcome is no
  longer silently discarded — every skipped (wallet_id, reason) is
  logged AND surfaced (loaded_count/skipped_count/skipped[]) so the
  host can retry the skipped set. New platform_wallet_load_outcome_free
  releases the heap array.

Acceptance: cargo check --workspace AND --all-features both exit 0.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…m-wallet-rehydration

# Conflicts:
#	packages/rs-platform-wallet-ffi/src/manager.rs
@github-actions
Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-05-22T10:49:18.857Z

@Claudius-Maginificent
Copy link
Copy Markdown
Collaborator Author

🔁 Seedless-load rework landed — 34532e57d5..6f22c0e9f5

Per design validation against dashwallet-ios swift-sdk-integration: real iOS host loads watch-only at app launch (Keychain locked) and signs on-demand via the existing MnemonicResolverHandle vtable. The original seeded-load model (load takes &dyn SeedProvider, derives Wallet per persisted id, runs constant-time wrong-seed gate at load) did not match the real consumer. Pushed 7 commits flipping the model. Existing PR description above is stale — supersedes it for the deltas below.

What's new

  • load_from_persistor() is now seedlesspub async fn load_from_persistor(&self) -> Result<LoadOutcome, PlatformWalletError>. Manager reconstructs each persisted wallet via Wallet::new_watch_only(network, wallet_id, accounts), with accounts assembled from the keyless account_manifest (one Account::from_xpub(...) per registration entry). No seed touched on the load path.
  • Wrong-seed gate moved to the sign pathcrate::sign_gate::verify_seed_matches_wallet_id(root_pub, expected_wallet_id) -> bool (constant-time via subtle::ConstantTimeEq). Wired into all 4 resolver-fed FFI sign entrypoints:
    • sign_with_mnemonic_resolver.rs::dash_sdk_sign_with_mnemonic_resolver_and_path
    • identity_derive_and_persist.rs::dash_sdk_derive_and_persist_identity_keys
    • derive_identity_key_at_slot.rs::dash_sdk_derive_identity_key_at_slot_with_resolver
    • shielded_sync.rs::platform_wallet_manager_bind_shielded
  • SeedProvider trait + adapter + FFI shim deletedMnemonicResolverHandle (which already existed) is the sole on-demand signing contract. Files removed: rs-platform-wallet/src/seed_provider.rs, rs-platform-wallet-ffi/src/rehydration_seed_provider.rs, rs-platform-wallet-storage/src/secrets/seed_provider_adapter.rs (+ its test).
  • FFI signature: platform_wallet_manager_load_from_persistor(manager, persister, *mut LoadOutcomeFFI) — dropped the 3rd resolver arg (reverts b7508a0d47's 3-arg shape).
  • Swift PlatformWalletManager.swift aligns to the 2-arg + outparam C signature (passes nil for the outcome ptr).

ABI deltas (new in this PR, no v3.1-dev impact)

  • New result code ErrorWrongSeedForWallet = 13 in PlatformWalletFFIResultCode.
  • SkippedWalletFFI::reason_code family remapped to 100/101/102 for the new CorruptKind variants (MissingManifest, MalformedXpub, DecodeError). The old 0/1/2 (seed-availability) are gone — those skip-triggers don't exist anymore.

AR-7 hygiene

  • Load path eliminates AR-7 entirely — manager never constructs WalletType::Mnemonic|Seed; only WalletType::WatchOnly (no key material). AR-7's residual Debug concern was about derived Wallet values on the load path; that path no longer derives.
  • Sign path keeps AR-7 disciplineZeroizing + non_secure_erase() on the gate's throwaway master key on the mismatch path, before returning the error tag. No {:?} over any secret type.

Test reshape

  • tests/rehydration_load.rs — RT-WO, RT-Corrupt, RT-Z (watch-only construction, corrupt-row hard-fail, secret-hygiene). Wrong-seed scenarios removed from this file (no seed at load anymore).
  • Co-located gate tests in each of the 4 FFI sign entrypoints:
    • sign_with_mnemonic_resolver: happy + wrong + full-buffer-zero assertion
    • identity_derive_and_persist: wrong + persister-callback-never-fires assertion
    • derive_identity_key_at_slot: happy + wrong (asserts populated out_row on happy, zero/null fields on mismatch)
    • shielded_sync: wrong + null-resolver-handle-rejected (happy path requires full SDK + coordinator + commitment-tree — covered structurally by gate-fires-before-storage-touch assertion)
  • New sign_gate::tests unit module exercises CT helper directly.

What survived (unchanged by this rework)

  • Keyless PersistedWalletData / ClientWalletStartState (commit b9af9935)
  • A1/B/A2 schema readers (accounts::load_state, core_state::load_state, asset_locks::load_unconsumed)
  • F2 no-BIP44 silent-zero balance fix (commit 96a9aa90)
  • F4 dead post-insert wallet_id re-check removal (commit 62bd4754)
  • WrongSeedForDatabase typed error (re-homed to sign path)

Verification

  • cargo fmt --all --check: pass
  • cargo clippy -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi --all-targets -- -D warnings: pass
  • cargo check --workspace: pass
  • cargo test -p platform-wallet -p platform-wallet-storage -p platform-wallet-ffi: 414 passed, 0 failed, 7 ignored
  • cargo test --doc -p platform-wallet -p platform-wallet-storage: 3 passed, 1 ignored
  • cargo check --workspace --all-features: fails on pre-existing ShieldedChangeSet: Serialize/Deserialize gap at changeset.rs:914, verified reproducible on origin/feat/platform-wallet-rehydration@34532e57 — out of scope for this rework

🤖 Co-authored by Claudius the Magnificent AI Agent

@Claudius-Maginificent Claudius-Maginificent changed the title feat(platform-wallet): add full wallet rehydration from persistor + seed (skip-and-report) feat(platform-wallet): watch-only rehydration from persistor + wallet_id sign-gate (seedless load) May 25, 2026
lklimek and others added 5 commits May 25, 2026 13:10
…atch_only

load_from_persistor now rebuilds every persisted wallet watch-only from
its keyless AccountRegistrationEntry manifest (Wallet::new_watch_only)
and applies the keyless core-state projection on top. No seed material
is touched on the load path: signing keys are derived on demand later
through the MnemonicResolverHandle sign entrypoints, which carry the
fail-closed wrong-seed gate themselves.

Drops the SeedProvider port + WalletSecret/SecretPhrase/SecretSeed
payloads (and the storage CredentialStoreSeedProvider adapter that fed
them) — load no longer needs the abstraction. WrongSeedForDatabase
stays on PlatformWalletError for the sign-path gate. RT suite reshapes
to RT-WO (watch-only round-trip) + RT-Corrupt (per-row decode skip with
SkipReason::CorruptPersistedRow{kind: CorruptKind::MissingManifest}) +
RT-Z (no key material in any LoadOutcome / SkipReason surface).

apply_persisted_core_state and its F2/F3/F4 fixes are unchanged.

AR-7 residual risk on the load path is eliminated (no Wallet of a
signing type is constructed during load, so its Debug-leak surface is
gone from this path).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…stor

platform_wallet_manager_load_from_persistor is now a 2-arg call
(manager_handle, out_outcome). The Swift host never passed a real
resolver at load time anyway — load is watch-only, signing keys are
derived later on demand through the same MnemonicResolverHandle vtable
the per-call sign entrypoints already use (next commit lands the
wallet_id gate there).

Drops the MnemonicResolverHandle → platform_wallet::SeedProvider
adapter (rehydration_seed_provider.rs); no consumer left.

LoadOutcomeFFI.SkippedWalletFFI.reason_code reshapes to the new
CorruptKind family (100/101/102) — ABI-bump for #3692 since the seed-
availability codes (0/1/2) it previously carried are gone with the
seedless load path.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…edless FFI

platform_wallet_manager_load_from_persistor is now (handle,
out_outcome) — the resolver argument is gone with the seedless-load
rework. Pass nil for out_outcome (Swift doesn't surface skipped
wallets yet; corrupt rows are logged on the Rust side).

Doc string refreshed to reflect Wallet::new_watch_only as the
underlying load primitive and the on-demand-signing + wrong-seed-gate
contract on the resolver-fed sign entrypoints.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
… sign-gate split

The wrong-seed detection moves off the load path and onto the resolver-fed
FFI sign entrypoints. That gate + its coverage now ships in PR #3735
(security patch against v3.1-dev), not here. Drop the dangling reference
to the never-existed `sign_wrong_seed_gate.rs` file and point readers at
PR #3735 instead.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
@lklimek lklimek force-pushed the feat/platform-wallet-rehydration branch from 0e92cb4 to f57b117 Compare May 25, 2026 11:21
@Claudius-Maginificent Claudius-Maginificent changed the title feat(platform-wallet): watch-only rehydration from persistor + wallet_id sign-gate (seedless load) feat(platform-wallet): watch-only rehydration from persistor (seedless load) May 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants