feat: hide document button - #7
Conversation
WalkthroughThe changes involve a modification to the Changes
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Outside diff range and nitpick comments (1)
src/ui/components/left_panel.rs (1)
78-81: LGTM! Consider a more flexible approach for future enhancements.The implementation effectively hides the document button from the left panel, aligning with the PR objective. The change is minimal and doesn't affect the rendering of other buttons.
For future enhancements, consider a more flexible approach to handle conditional button rendering. For example, you could add a
is_visibleproperty to each button definition in thebuttonsarray. This would allow for easier management of multiple buttons' visibility without modifying the loop structure. Here's a potential refactor:let buttons = [ ( "I", RootScreenType::RootScreenIdentities, "icons/identity.png", true, ), // ... other buttons ... ( "Q", RootScreenType::RootScreenDocumentQuery, "icons/doc.png", false, // Set to false to hide the button ), // ... remaining buttons ... ]; // In the rendering loop for (label, screen_type, icon_path, is_visible) in buttons.iter() { if !is_visible { continue; } // ... rest of the button rendering logic ... }This approach would make it easier to manage the visibility of multiple buttons in the future without adding multiple conditional statements.
- Replace assume_checked() with require_network() for address validation (CodeRabbit #2) - Use styled Frame-with-dismiss error display matching Send dialog pattern (CodeRabbit #3) - Don't open dialog when no wallet selected; show MessageBanner instead (CodeRabbit #4) - Extract shared load_bip44_external_addresses() helper to eliminate near-duplicate code between mine and receive dialogs (CodeRabbit #5) - Add backend-side network guard (Regtest/Devnet) for defense-in-depth (CodeRabbit #6) - Rename shadowed ctx binding to refresh_ctx for clarity (CodeRabbit #7) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… mode (#638) * feat(wallet): add Mine Blocks dialog for Regtest/Devnet dev mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add manual test scenarios for mine blocks dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wallet): close Mine Blocks dialog after Cancel/Mine click The dialog stayed open (in a broken state) after clicking Mine or Cancel because the local `open` variable was written back to `is_open` after the dialog state had already been reset. Pass `is_open` directly to egui's `.open()` and use a separate `close` flag for button-triggered dismissal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wallet): address audit findings for Mine Blocks dialog - Wrap `generate_to_address` in `spawn_blocking` to avoid blocking the async runtime thread (HIGH) - Replace `.expect()` on core client lock with `.map_err()?` for graceful error handling instead of panic (HIGH) - Cap block count at 1000 to prevent resource exhaustion on the Core node (HIGH) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wallet): filter non-numeric input in Mine Blocks block count field Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wallet): address review comments on Mine Blocks dialog - Replace assume_checked() with require_network() for address validation (CodeRabbit #2) - Use styled Frame-with-dismiss error display matching Send dialog pattern (CodeRabbit #3) - Don't open dialog when no wallet selected; show MessageBanner instead (CodeRabbit #4) - Extract shared load_bip44_external_addresses() helper to eliminate near-duplicate code between mine and receive dialogs (CodeRabbit #5) - Add backend-side network guard (Regtest/Devnet) for defense-in-depth (CodeRabbit #6) - Rename shadowed ctx binding to refresh_ctx for clarity (CodeRabbit #7) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wallet): address remaining review comments on Mine Blocks dialog - Change MineBlocksSuccess(usize) to MineBlocksSuccess(u64) for type consistency with block_count parameter (Claudius #5) - Align dialog close pattern with Send/Receive: use local `open` variable for egui X button, reset state inside closure for Cancel/Mine buttons (Claudius #6) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove dead `_is_sync_operation` parameter from `Database::set_platform_address_info` and all call sites (Finding #6) - Add doc comment on `set_platform_sync_info` explaining column name drift: `last_platform_sync_checkpoint` now stores SDK sync height (Finding #4) - Remove trivial `set_platform_address_info_from_sync` delegate and update callers to use `set_platform_address_info` directly (Finding #7) - Combine unnecessary `let provider` + `let mut provider = provider` rebinding into single `let mut` block (Finding #10) - Document UTXO selection race window on `broadcast_and_commit_asset_lock`: std::sync::RwLock guard is !Send so it cannot span async broadcast Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rmSyncMode (#635) * refactor(wallet): simplify platform sync by removing PlatformSyncMode Remove the PlatformSyncMode enum (Auto/ForceFull/TerminalOnly) and terminal sync logic (apply_recent_balance_changes, last_terminal_block, last_full_sync_balance). The SDK now handles incremental sync internally via AddressProvider::current_balances() and last_sync_height(). Key changes: - Remove PlatformSyncMode enum from backend_task::wallet - Simplify fetch_platform_address_balances to use new SDK API with stored state (with_stored_state, current_balances, last_sync_height) - Change CoreTask::RefreshWalletInfo to use bool instead of Option<PlatformSyncMode> - Remove last_full_sync_balance from PlatformAddressInfo - Simplify database sync info to 2-tuple (timestamp, height) - Remove set_last_terminal_block from database - Simplify RefreshMode enum (remove PlatformFull, PlatformTerminal, CoreAndPlatformFull, CoreAndPlatformTerminal variants) Note: requires updated dash-sdk with new sync_address_balances API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: update platform SDK to rev 0fa82e6652 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add manual test scenarios for platform sync simplification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wallet): address PR #635 audit findings and extract broadcast helper - DB schema v28: drop obsolete columns (last_terminal_block, last_full_sync_balance), rename last_platform_sync_checkpoint → last_platform_sync_height, with SQLite ≥3.35 runtime check - Store asset lock TX before broadcast to prevent SPV InstantSend race - Defer UTXO removal until after successful broadcast - Replace .unwrap() on RwLock with .map_err() to avoid panics - Remove unused _is_sync_operation param and set_platform_address_info_from_sync wrapper - Fix redundant let-mut rebinding in fetch_platform_address_balances - Extract broadcast_and_commit_asset_lock() on AppContext to consolidate the store→broadcast→cleanup→UTXO-removal pattern from 5 code paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(db): schema v28 — drop obsolete sync columns and clean up DB interface (#653) - Bump DEFAULT_DB_VERSION 27 → 28 - Drop last_terminal_block from wallet table (unused after sync simplification) - Drop last_full_sync_balance from platform_address_balances table - Rename last_platform_sync_checkpoint → last_platform_sync_height - Add runtime SQLite ≥3.35 check (required for DROP COLUMN) - Idempotent migration: checks column existence before each ALTER - Remove unused _is_sync_operation param from set_platform_address_info() - Remove set_platform_address_info_from_sync() wrapper - Fix redundant let-mut rebinding in fetch_platform_address_balances Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * revert(db): remove schema v28 migration from this branch The v28 schema changes (drop obsolete sync columns, rename last_platform_sync_checkpoint → last_platform_sync_height) will be applied separately and should not ship on this branch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review comments — remove dead code and document risks - Remove dead `_is_sync_operation` parameter from `Database::set_platform_address_info` and all call sites (Finding #6) - Add doc comment on `set_platform_sync_info` explaining column name drift: `last_platform_sync_checkpoint` now stores SDK sync height (Finding #4) - Remove trivial `set_platform_address_info_from_sync` delegate and update callers to use `set_platform_address_info` directly (Finding #7) - Combine unnecessary `let provider` + `let mut provider = provider` rebinding into single `let mut` block (Finding #10) - Document UTXO selection race window on `broadcast_and_commit_asset_lock`: std::sync::RwLock guard is !Send so it cannot span async broadcast Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: apply nightly fmt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
HIGH #6: Add tracing::warn to 7 silent identity_id decode continues. All `Err(_) => continue` in load() now log before skipping. Matches the existing pattern for account_type/txid decode failures. HIGH #7: payment_type/status catch-all masks corruption. Unknown payment_type no longer defaults to Received — skips row with tracing::warn. Unknown status no longer defaults to Pending — same treatment. Explicitly matches "pending"/"sent"/"received"/ "confirmed"/"failed". HIGH #8: created_at type mismatch (write i64, load u64). Load now reads as Option<i64> (matching the write cast), clamps negative values to 0 via .max(0), converts explicitly to u64. HIGH #9: Proof decode failure silently degrades asset lock status. When proof_data was present but decode failed AND no legacy fallback columns (islock_data, chain_height) exist, skip the entire row with tracing::error instead of loading a degraded entry with Broadcast status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…01 hardening (#867) * docs(secret-seam): Phase-1 design artifacts (UX disclosure + test case spec) UX disclosure spec by Diziet; 30-case TDD test spec by Marvin. Design reference for the secret-storage raw-SecretBytes seam re-architecture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat(wallet-backend): add raw-SecretBytes secret seam + typed errors (T2,T4) Crikey, here's the one socket every wallet secret will squeeze through. T2 — new wallet_backend/secret_seam.rs: SecretSeam over raw SecretBytes with put_secret/get_secret/delete_secret, a no-encryption pass-through to the upstream vault TODAY. Every put/get body carries the greppable `TODO(per-secret-encryption):` tag so wiring real per-secret encryption later is a localized change. Prompt-free — the passphrase requirement lives only in the retained legacy readers, never here. No-serialization guard mechanism: compile_fail doctests (no new deps — static_assertions/trybuild stay out of Cargo.toml). One asserts a newtype cannot derive Serialize over a SecretBytes; one asserts serde_json::to_string on a SecretBytes is rejected. If upstream ever adds Serialize to SecretBytes these start compiling and the canary fires (TS-INV-01). TS-INV-02 round-trips a SecretBytes through the real signatures (compiler is the assertion). T4 — TaskError variants (no String fields, typed #[source]): SecretSeam, SecretSeamMissing (loud funds-safety miss), IdentityKeyVault, IdentityKeyMissing. Promote the private assert_no_leak (hex + decimal-array) into a shared wallet_backend/leak_test_support.rs so the seam/sidecar/QI/Debug leak cases reuse one impl instead of copy-pasting. TS-NOLEAK-01: the on-disk vault file holds no raw secret in either form. Tests: 6 seam unit + 2 compile-fail doctests, all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * fix(model): redacting Debug for ClosedSingleKey (T9, 6a2818cd) ClosedSingleKey derived Debug and its encrypted_private_key holds the raw 32 key bytes in the no-password / pre-migration shape — a derived Debug dumped them as a decimal byte array straight into logs. Hand-write a redacting Debug mirroring ClosedKeyItem / SingleKeyEntry: key_hash + lengths, never the bytes. Parents SingleKeyData / SingleKeyWallet are safe by delegation. TS-DBG-01 asserts via the shared assert_no_leak_bytes (hex AND decimal-array — the decimal form is the one the pre-fix Debug leaked) at all three levels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat(model): PrivateKeyData::InVault placeholder + migration probes (T1) Identity private keys get a non-resident home. New PrivateKeyData::InVault appended at bincode index 4 — discriminants 0-3 (AlwaysClear/Clear/Encrypted/ AtWalletDerivationPath) are untouched, so blobs written before it still decode (TS-RESID-02 round-trips all four pre-existing variants + InVault). Redacting Debug/Display arms (carries no bytes — trivially clean). KeyStorage probes: - is_in_vault / public_key_for — a vault placeholder reports true yet still surfaces its public key for display + signing-key selection. - take_plaintext_for_vault — rewrites every Clear/AlwaysClear to InVault and returns the raw bytes (Zeroizing) the migration must store in the vault FIRST (vault-before-blob order). Wallet-derived + encrypted keys untouched — they were never plaintext-at-rest. get/get_resolve_local gain an InVault arm (resolve through the vault, not locally). key_info_screen gains degraded InVault arms (securely-stored notice; full JIT view/sign via dedicated identity-key WalletTasks is the T8 follow-up). Promote the private assert_no_leak + distinctive_secret to the shared leak_test_support helper (no fork). TS-RESID-01 / TS-NOLEAK-03: post-migration KeyStorage has only InVault, and the re-encoded blob leaks neither secret in hex nor decimal-array form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat(model,wallet-backend): WalletMeta+ImportedKey sidecar fields, schema-gated (T5) Non-secret metadata moves out of the per-wallet seed envelope into the sidecar. WalletMeta gains uses_password + password_hint. Because WalletMeta is positional bincode behind the DetKv envelope, #[serde(default)] alone is NOT forward-compatible (R-SCHEMA) — so a real version gate: WALLET_META_VERSION (v2) framed as [version | bincode] at the WalletMetaView boundary, plus a retained decode-only WalletMetaV1. decode_versioned detects v2 / v1-framed / bare-legacy and migrates a v1 blob into v2 (defaults uses_password=false), never positionally misparsing it. The global DetKv SCHEMA_VERSION is deliberately untouched (it governs every payload, not just WalletMeta). TS-META-01 covers all three shapes. ImportedKey gains public_key_bytes (the compressed SEC1 PUBLIC key) so the locked-render cold-boot path can rebuild a protected key's display wallet without the secret — moved out of the SingleKeyEntry vault blob ahead of the raw-seam migration. NON-secret; #[serde(default)] for old entries. write_wallet_meta now carries uses_password/password_hint from the open Wallet; the legacy-table drain (finish_unwire) defaults them (the authoritative flag is read from the envelope at the migrating unlock). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * chore(wallet-backend): satisfy fmt + clippy for the secret-seam batch - leak_test_support: drop redundant inner #![cfg(test)] (mod.rs already gates it). - encrypted_key_storage: factor take_plaintext_for_vault's return into the VaultBoundKey type alias (clippy::type_complexity). - wallet_hydration bench: carry the new WalletMeta password fields. - nightly-fmt whitespace. Gate: cargo +nightly fmt --all clean; cargo clippy --all-features --all-targets -D warnings clean; cargo test --all-features --workspace = 944 lib + 146 + 10 + 3 + 2 pass, 0 fail; 2 compile_fail doctests pass; det-cli standalone smoke (network-info / tools / core-wallets-list) all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat(wallet-backend): SecretScope::IdentityKey + seam-first SecretAccess (T3) The chokepoint learns identity keys and goes seam-first for everyone. - SecretScope::IdentityKey { identity_id:[u8;32], target, key_id } (DET-opaque; KeyID is just u32, PrivateKeyTarget is a DET model enum). identity_key_label() builds identity_key_priv.<m|v|o>.<key_id> — a stable one-char target tag keeps the label inside the upstream allowlist. - SecretPlaintext::IdentityKey + expose_identity_key; Plaintext::IdentityKey. Borrowed-only, zeroizing, never resident — same hygiene as the other kinds. - decrypt_jit is now SEAM-FIRST for all three classes: the raw label wins; the retained legacy reader (decrypt_hd_seed / SingleKeyEntry::decrypt) is the migration fallback for HD seeds and single keys. IdentityKey reads raw via the seam → loud IdentityKeyMissing if absent (never silent). - scope_has_passphrase: a migrated raw secret reports false (the password no longer gates it); only a not-yet-migrated legacy entry can still be protected; IdentityKey is always false → prompt-free fast-path → headless/MCP signing works. - DetSigner treats an IdentityKey plaintext as a raw single key (same secp256k1 shape, no derivation tree). Tests: TS-FAST-01 (identity key resolves prompt-free, ask_count 0, can_resolve_without_prompt true), IdentityKeyMissing is loud, TS-LEGACY-01 (legacy envelope served when raw absent), raw-wins-over-legacy precedence. The pre-existing protected-HD/single-key tests now exercise the legacy fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat(wallet-backend): identity_key_store + seed/single-key seam-raw writes (T6) Secrets start landing raw. No DET envelope for the new write paths. - New wallet_backend/identity_key_store.rs: IdentityKeyView with store/get/delete + store_all/delete_all over raw 32 bytes via SecretSeam (scope = identity_id, label identity_key_priv.<m|v|o>.<key_id>). NO StoredIdentityKey envelope — the InVault marker in the QI blob is the only on-disk trace. store_all is the migration's vault-first writer (call before the blob rewrite); delete_all backs purge_identity_scope. - WalletSeedView gains set_raw/get_raw/delete_raw (raw 64-byte seed under seed.raw.v1 via the seam) + legacy_envelope_get (retained decode-only reader). - write_seed_envelope now branches: a no-password wallet writes the RAW seed (encrypted_seed_slice() is verbatim the seed); a password wallet keeps the legacy AES-GCM envelope at creation and migrates lazily at unlock (T7). - import_wif_with_passphrase: unprotected import writes RAW 32 bytes under the existing single_key_priv.<addr> label (no SingleKeyEntry framing); protected import keeps the legacy SingleKeyEntry (lazy-migrates at unlock). The locked-render pubkey rides in the ImportedKey sidecar (the T5 field). SingleKeyEntry::decode treats a bare 32-byte blob as unprotected, so a raw-written key still rebuilds + opens at cold boot. Tests: identity_key_store round-trip / scope+target isolation / store_all+ delete_all; seed raw round-trip independent of the legacy label; single-key unprotected import is exactly 32 raw bytes (no framing) and signs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat: crash-safe dual-format migration + InVault resolver + vault delete (T7) This is the part that actually moves secrets. Funds-safety ordering throughout. Resolver (mod.rs): resolve_private_key_bytes gains the InVault route — keyed by is_in_vault/public_key_for, it fetches the raw bytes per-use via with_secret(IdentityKey{...}) (prompt-free). No chokepoint wired ⇒ fail closed (WalletLocked); bytes never resident. EAGER migration on load (dialog-free): - Identity keys (identity_db::migrate_identity_keys_to_vault, run per identity in load_identities_filtered): take_plaintext_for_vault → IdentityKeyView store_all (vault FIRST) → rewrite the QI blob with InVault. Vault-write failure restores the resident plaintext for this session and defers; a blob-rewrite failure is re-detected and retried next load. Idempotent. - No-password HD seeds (hydration::reconstruct_wallet): raw seam wins (precedence raw > legacy); a no-password legacy envelope is re-stored raw (set_raw, vault FIRST) then deleted. reconstruct_from_envelope extracted so the raw and legacy paths share the xpub-decode + build tail. LAZY migration on unlock (one prompt, the unlock the user already does): promote_and_maybe_migrate_hd_seed re-stores the just-decrypted legacy seed raw (set_raw before delete) inside the borrowed Zeroizing scope and reports migrated=true; handle_wallet_unlocked then flips WalletMeta.uses_password=false and shows the one-time disclosure (T8 Copy A/D). Delete: forget_wallet_local_state now deletes BOTH the raw seed and the legacy envelope (a wallet may be in either form) — closes a wipe gap where a migrated no-password seed would survive removal. identity_db.clear_identity_vault_keys drains an identity's raw vault keys on single-delete + devnet sweep. Loud, never silent: a seed in neither form ⇒ TaskError::SecretSeamMissing (was WalletNotFound) on both scope_has_passphrase and decrypt_jit. Tests: TS-EAGER-01/04 (no-pw seed migrates + idempotent), TS-CRASH-01 read (raw wins, legacy cleaned), TS-MISS-01 (SecretSeamMissing loud). Updated 5 wallet_lifecycle removal/clear tests to assert the raw seed (the new at-rest form) in BOTH precondition and post-delete. wallet_lifecycle 38, hydration 10, identity_db 16, encrypted_key_storage 4 — all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * feat: key_info_screen JIT identity signing + single-key Copy B disclosure (T8) Real JIT for vault-backed identity keys, and the per-key migration notice. Two new WalletTasks + handlers, opening with_secret(IdentityKey{...}): - DeriveIdentityKeyForDisplay → derive_identity_key_for_display: fetches the raw key JIT, returns only the WIF (Secret). - SignMessageWithIdentityKey → sign_message_with_identity_key: signs in the backend, returns only the public Base64 envelope. New result variants IdentityKeyForDisplay / IdentityMessageSigned (identity- flavored — carry identity_id/target/key_id, not a meaningless seed_hash). key_info_screen: the InVault arms are now real — "View Private Key" queues DeriveIdentityKeyForDisplay and renders the returned WIF/hex via the existing render_decrypted_key_grid; "Sign" queues SignMessageWithIdentityKey. The degraded placeholders are gone. display_task_result handles both new results. Single-key protected lazy migration + Copy B: verify_passphrase now re-stores the just-decrypted protected entry raw under the same label (upsert replaces the AES-GCM framing) and clears the persistent has_passphrase flag, returning a migrated bool. verify_single_key_passphrase surfaces the one-time per-key disclosure (Copy B — text DISTINCT from the wallet Copy A so set_global's dedup keeps both) on migration. decrypt_jit's sign path also lazy-migrates (migrate_single_key_to_raw + in-memory flag flip) — idempotent defense-in-depth. SingleKeyView::clear_passphrase_flag persists the flip to the sidecar. Tests: TS-LAZY-03 — protected single key migrates via the chokepoint, the vault holds raw 32 bytes after, and a second resolve under a never-prompt host is prompt-free with the WIF-plaintext bytes. secret_access 24 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * chore: fmt + clippy for the T3-T8 integration batch - secret_access: drop explicit_auto_deref on set_raw(seed_hash, seed) — a &Zeroizing<[u8;64]> auto-derefs to &[u8;64]. - nightly-fmt whitespace across the touched files. Gate: cargo +nightly fmt --all clean; cargo clippy --all-features --all-targets -D warnings clean; cargo test --all-features --workspace = 957 lib + 146 + 10 + 3 + 2 pass, 0 fail, 1 ignored (funded-testnet TS-SIGN-E2E-01); 2 compile_fail doctests pass; det-cli standalone smoke (network-info / core-wallets-list / tools) all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * fix(wallet-backend): dual-format read for WalletMeta + ImportedKey sidecars The real defect QA caught (PROJ-001/002/003 + SEC-003): appending fields to a positional-bincode DetKv value is format-breaking, and my T5 framing made it WORSE — WalletMeta writes went through kv.put::<Vec<u8>>(versioned-frame) and reads through kv.get::<Vec<u8>>, which type-confuses an OLD kv.put::<WalletMeta> blob (decodes the alias's UTF-8 bytes AS the Vec) → alias/is_main silently lost. ImportedKey appended public_key_bytes with no legacy reader → old keys vanish from the picker. Fix (one policy for both sibling sidecars): drop the hand-rolled version byte (SEC-003: it could collide with a bincode length varint — a 1/2-char alias). Instead lean on the DetKv schema envelope + try-decode-both: - write the current shape directly (kv.put::<WalletMeta> / ::<ImportedKey>); - on read, try the current shape; on a bincode Decode error (an old blob runs out of bytes for the appended fields) fall back to the legacy shape (WalletMetaV1 / ImportedKeyV1, decode-only) and RE-STORE in the new shape. Order is load-bearing and tested: the 6-field struct CANNOT decode a 4-field blob (runs past end), so "new first, then V1" never mis-promotes. A DetKv schema-version mismatch stays a hard error; only Decode triggers the fallback. Removes the now-dead encode_versioned/decode_versioned/WALLET_META_VERSION (PROJ-002 — the unreachable legacy branch + its overclaiming test are gone; the legacy path is now live via the view and tested end-to-end). Tests: model leg (ts_meta_01) asserts the order-sensitivity + the SEC-003 1/2-char-alias collision case; view legs (old_wallet_meta_blob_*, old_imported_key_blob_*) write an OLD blob exactly as the base branch did, read it back through the view preserving every field, and confirm re-store in the new shape. wallet::meta 3, wallet_meta 13, single_key all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * test(identity-db): identity-key migration, deletion, write-fault no-loss (QA-002/003/005) Refactor the eager identity-key migration core out of AppContext into a free fn migrate_keystore_to_vault(secret_store, id, qi, persist) returning a KeystoreMigration outcome, so the funds-safety logic is unit-testable with a bare SecretStore + a controllable persist closure (no full AppContext). QA-002 — migration is vault-FIRST: the persist closure asserts the raw keys are already in the vault and the blob being persisted is InVault-only; the AtWalletDerivationPath key is untouched; zero plaintext remains; idempotent (second run = Nothing). QA-005 — write-fault no-loss (the write half CRASH-01's read half misses): with the vault parent dir chmod'd read-only so store_all fails, the migration restores the resident plaintext keystore byte-for-byte, does NOT call persist, and reports VaultWriteFailed — keys never lost on a mid-write fault. (#[cfg(unix)].) QA-003 — identity-key deletion is scoped + isolated: delete_all over the victim's (target,key_id) set removes its vault keys while a second identity's key under the same (target,key_id) is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * test(wallet-lifecycle): assert lazy-migration secret post-conditions (QA-004) The protected-wallet-unlock test asserted only upstream registration. Add the secret post-conditions the lazy migration is actually for: after handle_wallet_unlocked the raw seed is written and equals the true 64-byte seed, the legacy envelope.v1 is deleted, WalletMeta.uses_password flipped false, and a SECOND resolve through a never-prompt chokepoint over the now-raw vault returns the seed with zero prompts (the migrated wallet is permanently prompt-free). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * test(backend-e2e): TS-SIGN-E2E-01 InVault identity signs + broadcasts (QA-001) New #[ignore] backend-e2e test: migrate the shared identity's plaintext signing keys to the vault (PrivateKeyData::InVault, exactly as the eager load-path migration does), assert residency (zero Clear/AlwaysClear remain), wire the chokepoint, then build + sign + broadcast an IdentityUpdateTransition. Signing runs through the async QualifiedIdentity Signer → resolve_private_key_bytes → with_secret(IdentityKey{..}) — the JIT free-rider path. A successful broadcast + the new key appearing on Platform proves the InVault MASTER key signed live without ever being resident. Requires E2E_WALLET_MNEMONIC + live DAPI/SPV; run command + RUST_MIN_STACK in the header. Compiles + registered in main.rs; left #[ignore] for a manual/live run during QA. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * refactor(wallet-backend): zeroize migration source, flavor identity-key errors, lift signed-message helper PROJ-004 (security): take_plaintext_for_vault now zeroizes the resident Clear/AlwaysClear array BEFORE the InVault overwrite drops it — de-residenting the key is the function's whole purpose, so it must wipe the source, not just the moved-out copy. PROJ-005: IdentityKeyView::store/get/delete now map the generic seam error to the identity-flavored TaskError::IdentityKeyVault (previously a producerless variant), so an identity-key vault failure surfaces with identity-specific banner copy. Wrong-length stays SecretDecryptFailed. QA-DEDUP-01: lift dash_signed_message (the recoverable-envelope builder) from sign_message_with_key.rs to backend_task/wallet/mod.rs as pub(crate); both the wallet-key and identity-key signers now call it instead of two drifting copies. The recovery-header round-trip tests move alongside the shared helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * test(secret-seam): TS-INV-03 audit guard + TS-NOLEAK-02 sidecar no-leak (SEC-001/002) SEC-001 (TS-INV-03): source-text audit over the changed secret-path modules — no Serialize/Encode struct may name a plaintext-key field (SecretBytes, Zeroizing<[u8, [u8;32], [u8;64]). Catches the bare-Vec/array plaintext bypass the compile_fail doctests can't (they only catch an embedded SecretBytes). The module list mirrors the blast-radius table; ciphertext fields are deliberately not flagged. Passes — the invariant holds today and now has a regression guard. SEC-002 (TS-NOLEAK-02): assert the encoded WalletMeta + ImportedKey sidecar blobs contain neither secret (hex AND decimal-array via the shared assert_no_leak_bytes), and that the ImportedKey's PUBLIC key IS present (locked render needs it). Canary coverage — the sidecars structurally hold no secret. Plus a clarifying "// no secret to (de)crypt" note at delete_secret instead of an encryption TODO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * test(kittest): disclosure-banner copy coverage (QA-007/Diziet) Extract the interim at-rest disclosure copy into pure pub fns (wallet_migration_notice / single_key_migration_notice) + pub INTERIM_AT_REST_DETAILS, re-exported from context, so the exact copy is testable without an AppState and i18n-extractable. Both callsites now use them. New tests/kittest/disclosure_banner.rs (QA-007): Copy A and Copy B each render as Warning banners naming the wallet/key, the ⚠ icon shows (not color-only), the two copies are DISTINCT (so set_global's text-dedup keeps both when a wallet and a key migrate in one session), and all copy (A/B/D) is jargon-free (no AES/vault/seam/encryption/0600). 4 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * docs: comment hygiene + CLAUDE.md seam pointer + user-story softening (QA-DOC/DOC) QA-DOC-01: strip ephemeral review IDs from comments I authored in the secret-seam surface — "Smythe must-fix #3/#4/#5", "Q-HEADLESS", "(F-2)", "6a2818cd" — keeping the rationale prose. (Pre-existing PROJ-010/TC-W-*/F43/F63 in code outside this PR's diff are left untouched to avoid scope creep.) QA-DOC-02: drop the "Promoted from…" history line in leak_test_support.rs (belongs in git, not the module header). QA-DOC-03: secret_access module-header resolution order now lists the unprotected fast-path as an explicit step 2 (cache → unprotected → prompt), matching the three-branch body. DOC-001: CLAUDE.md wallet_backend bullet now points at secret_seam.rs as the single secret chokepoint + the TODO(per-secret-encryption): grep convention + the design dir. DOC-002: user-stories WAL-006 gains the post-migration no-password-prompt note; WAL-025 "modern encrypted vault" → "on-device secret vault" (no longer asserts encryption that is presently absent — the accepted interim regression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * chore: nightly fmt for the QA-findings batch Whitespace-only reformat (cargo +nightly fmt --all) of the files touched while closing the QA findings. No behavioral change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * test(backend-e2e): seed Clear key so TS-SIGN-E2E-01 exercises the InVault JIT path The shared_identity() fixture registers a wallet-derived identity, so its keys are PrivateKeyData::AtWalletDerivationPath and take_plaintext_for_vault() (which migrates only Clear/AlwaysClear) correctly found nothing — the test panicked in setup before reaching the path under test. Add materialize_master_key_as_clear(): derive the master key's raw bytes from the HD seed through the real with_secret(SecretScope::HdSeed) chokepoint (identity index 0, key 0) and insert_non_encrypted() them as Clear, so the migration carries a genuine plaintext key into the vault as InVault and the JIT signing path produces a signature whose bytes match the on-chain master key. The !taken.is_empty() assertion is unweakened; no signer stub, no mocked broadcast. Stays #[ignore]: the live broadcast additionally needs a funding wallet that derives within its rehydrated window (the e2e funding step hit the known core-wallet gap-window/rehydration limitation, unrelated to the InVault path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cMrX7YiMeFXUjswbM5jo6 * chore(deps): repin platform deps to feat/platform-wallet-secret-protection (fb7953ea) Moves the 4 dashpay/platform branch deps (dash-sdk, rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage) — and their 23 transitive platform crates, 27 total — from fix/wallet-core-derived-rehydration@ea0082e6 to feat/platform-wallet-secret-protection@fb7953ea (PR #3953), establishing the green baseline for the secret-handling-hardening work. Done on top of the merge of origin/docs/platform-wallet-migration-design (ac0c3d98), which brought in #864 (headless masternode/evonode withdrawals) and #866 (DPNS blocking overlay). The merged DET tree compiles cleanly against the secret-protection branch — no API breakage. Verified green: cargo build --all-features cargo clippy --all-features --all-targets -- -D warnings cargo +nightly fmt --all -- --check Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(secret): open the vault keyless (file_unprotected) for the Tier-1 baseline PR #3953 ("platform-wallet-secret-protection") hardened upstream `SecretStore::file(path, passphrase)` to reject a blank passphrase (`SecretStoreError::BlankPassphrase`). DET's `open_secret_store` opened the vault with `SecretString::new("")`, so after the repin every AppContext init failed at the secret-store open and 7 secret_seam/secret_access tests broke. Switch to the explicit keyless door `SecretStore::file_unprotected(path)`, which upstream documents for exactly this model: the vault file itself is keyless (at-rest floor = owner-only perms) and per-secret confidentiality comes from Tier-2 object passwords on the individual secrets. Behavior for the Tier-1 baseline is unchanged from the old empty-passphrase open. Restores the green baseline at the fb7953ea pin: build/clippy/fmt clean, the 8 secret_seam/secret_access vault tests pass again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(secret): add Tier-2 seam capability (protected set/get + scheme probe) Adds the upstream Tier-2 object-password path to the secret seam, the single coherent encrypt/decrypt chokepoint: - `put_secret_protected` / `get_secret_protected` seal/unseal a secret under its OWN object password via upstream `SecretStore::set_secret/get_secret` (Argon2id + XChaCha20-Poly1305). Per-secret, never a shared/per-wallet pw. - `scheme()` reports the at-rest tier (Absent / Unprotected / Protected) of a stored secret WITHOUT the password, via a `get(None)` probe that reads the upstream `NeedsPassword` signal. - The plain `*_secret` methods stay Tier-1 (unprotected) and are documented as such; the 3 `TODO(per-secret-encryption)` markers are resolved — the per- secret encryption IS the upstream envelope selected by the password arg. Additive and behavior-preserving: existing Tier-1 callers are unchanged; the read/migration wiring in SecretAccess lands next. Build/check + the 8 secret_seam/secret_access tests stay green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(secret): adopt Tier-2 per-secret passwords for HD seeds Routes HD-seed at-rest crypto through the upstream Tier-2 object-password envelope instead of DET AES-GCM, KEEPING protection rather than downgrading a password-protected seed to a raw, password-free secret on first unlock. - `WalletSeedView` gains `scheme()` / `set_protected()` / `get_protected()`: a protected seed lives at the `seed.raw.v1` label as a Tier-2 envelope (Argon2id + XChaCha20-Poly1305) sealed under that seed's OWN object password; an unprotected seed stays Tier-1 raw. - `scope_has_passphrase` + `decrypt_jit` are now scheme-driven (via the seam `get(None)` `NeedsPassword` probe): Unprotected → raw, no prompt; Protected → unseal with the JIT-prompted per-seed password; Absent → decode the legacy AES-GCM envelope (decode-only reader) and LAZY re-wrap to Tier-2 (protected) or raw (unprotected), then drop the legacy envelope. Crash-safe: re-store upserts before the legacy delete; the scheme probe prefers the new label. - `promote_and_maybe_migrate_hd_seed` no longer downgrades; it reports "no downgrade" so the unlock callsite's `uses_password=false` finalizer never fires — protection is kept and the metadata stays accurate, with no change to `wallet_lifecycle.rs`. - `is_wrong_passphrase` now also catches the upstream `WrongPassword` so a Tier-2 unseal with a bad object password re-prompts instead of aborting. Per-SECRET model: the session cache is plaintext keyed by `SecretScope`, so remembering seed A never satisfies seed B — each prompts and decrypts only with its own password. Tests: lazy re-wrap keeps protection (legacy gone, raw read of a protected seed fails), Tier-2 wrong-password re-ask, and the A/B different-password isolation. 72 secret tests pass; clippy/fmt green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(secret): clean keep-protection replacement of the downgrade subsystem (HD seed) Supersedes the transitional "inert return" approach with a clean excision of #865's downgrade-to-raw machinery, now that wallet_lifecycle.rs is editable (user WIP stashed). Protected HD seeds STAY protected (Tier-2 object password); nothing downgrades them to a raw, password-free secret. - `wallet_lifecycle.rs`: remove `finish_lazy_seed_migration` (the `uses_password=false` downgrade flip + the "protection removed" notice) and collapse the two `promote_*` methods into one `promote_hd_seed_with_passphrase` (decrypt + cache) — the lazy re-wrap lives in `decrypt_jit`. The unlock callsite no longer finalizes a downgrade. - `finish_unwire::migrate_wallet_meta`: carry the legacy `wallet.uses_password` / `password_hint` into `WalletMeta` (it was defaulting `false`). The persisted flag is now accurate from cold-start (`true` for a protected wallet) and always agrees with the at-rest scheme — no stale/drift-prone metadata. - `protected_wallet_registers_..._on_unlock` acceptance test rewritten to the keep-protection end-state: after the migrating unlock the seed is Tier-2 (scheme=Protected), a raw read fails, `WalletMeta.uses_password` stays true, and a second resolve prompts for the object password. 1009 lib tests pass; clippy -D warnings + fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(secret): adopt Tier-2 keep-protection for imported single keys Extends the Tier-2 keep-protection model from HD seeds to imported single keys, replacing their downgrade-to-raw migration. A protected imported key STAYS protected under its own object password instead of being re-stored raw. - `decrypt_jit` / `scope_has_passphrase` (SingleKey) are scheme-driven (seam `get(None)` → `NeedsPassword` probe): Protected → unseal with the JIT-prompted per-key password; Unprotected → a migrated raw-32 key wins prompt-free, else the not-yet-migrated legacy `SingleKeyEntry` blob's `has_passphrase` decides; the in-band length-32 check disambiguates raw vs legacy-framed. - `migrate_single_key_to_raw` → `migrate_single_key_to_tier2`: lazy re-wrap the just-decrypted protected key to a Tier-2 envelope under the same password (upsert replaces the AES-GCM framing). `has_passphrase` is NOT flipped — protection is kept and the index/persisted flag stay accurate. - `single_key::verify_passphrase` (the unlock-gesture path): re-wraps to Tier-2 instead of downgrading to raw; returns `()` (no migration bool). The `clear_passphrase_flag` finalizer is removed. Downgrade-disclosure machinery retired (Tier-2 keeps protection, nothing to disclose): removed `show_single_key_migration_notice` + the `wallet_migration_notice` / `single_key_migration_notice` / `INTERIM_AT_REST_DETAILS` copy + their re-exports, and the obsolete `tests/kittest/disclosure_banner.rs`. Tests: `ts_lazy_03` rewritten to the keep-protection end-state (vault holds a Tier-2 envelope, password-free read fails, second resolve prompts). 1009 lib tests pass; clippy -D warnings + fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(secret): address Smythe Tier-2 review findings (SEC-001/002/004/005) Smythe verdict on the Tier-2 adoption: SOUND, 0 Critical/High (it closes a prior HIGH-grade protected-seed downgrade-to-obfuscation). Folds in the carry-forward findings (SEC-003 — excise the inert downgrade — already landed in 6dafbdab): - SEC-001 (LOW): GC an orphaned legacy `envelope.v1`. The seed Protected read branch (`decrypt_jit`) now best-effort `view.delete(seed_hash)` so an `envelope.v1` left behind by a crash/delete-failure during the re-wrap (which still decrypts under the seed's OLD password) cannot survive forever — the Absent branch, the only other deleter, is never re-entered once Protected. The single-key path migrates in-band (same-label upsert) and has no such orphan. - SEC-004 (LOW): assert the NEGATIVE crypto property. `ts_t2_03` (seed) and the new `ts_t2_sk_iso` (single key) now prove A's object password is REJECTED by B's envelope (`WrongPassword`) — the upstream per-object-salt + AAD binding — not merely that the DET cache is scope-keyed. - SEC-002 (MEDIUM, doc): record loudly that the keyless `file_unprotected` vault is "obfuscation, not confidentiality" for Tier-1 secrets (no-password seeds, raw single keys, identity keys rest on file perms ALONE; only Tier-2 object passwords give real at-rest confidentiality). Documented at `open_secret_store`, reworded `ts_noleak_01` (proves non-literal-plaintext, NOT confidentiality), and in the design note's threat-model residual. - SEC-005 (info): one-line note in `seed_envelope.rs` — the legacy reader is decode-only / local owner-only vault, uses bincode 2.x; the RUSTSEC-2025-0141 bincode 1.3.3 is a transitive dep. No code change. 1010 lib tests pass; clippy -D warnings + fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(migration): note the wallet.uses_password/password_hint schema invariant Smythe's schema-robustness query on `migrate_wallet_meta`'s new SELECT (it reads `uses_password`/`password_hint` unprobed, unlike the probed optional `core_wallet_name`). Verified + documented the invariant rather than adding a needless probe: the wallet-seed migration (`migrate_wallet_seeds_rows_from_conn`) already SELECTs both columns unconditionally and runs FIRST over the same `wallet` table at the same cold-start, so any schema lacking them fails there before the meta pass. The unprobed read here is therefore exactly as robust as the shipped seed migration; `core_wallet_name` stays probed because it is the one droppable column. Comment-only — 1010 lib tests pass, clippy -D + fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(test): eliminate register_wallet_from_seed race in cold-boot test The `ensure_identity_funding_accounts_succeeds_on_cold_booted_watch_only_wallet` test failed in CI (1000+ parallel tests) with: WalletBackend { source: WalletNotFound("70dba4c1d8c5c3854aa02c8f15e0fcd66df6661841d7ae822891fa21aaef48d2") } Root cause: the test wired the backend BEFORE calling register_wallet, which caused register_wallet_upstream to spawn a background subtask that called create_wallet_from_seed_bytes concurrently with the test's own explicit register_wallet_from_seed call. The upstream register_wallet (inside create_wallet_from_seed_bytes) inserts into wallet_manager (step A) and into self.wallets (step B) with async work in between (persister.store + load_persisted + initialize). A concurrent caller that lands between A and B sees WalletAlreadyExists from step A, then get_wallet returns None (step B not yet complete) → resolve_registered_wallet returns WalletNotFound. Under CI load this window is reliably hit. Fix: register the wallet BEFORE wiring the backend. register_wallet_upstream finds no backend and returns early without spawning the subtask. The backend is then wired, and the explicit register_wallet_from_seed call runs race-free (no concurrent subtask competing for the same wallet slot). <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * fix(wallet-backend): keep Tier-2 protected wallets visible at cold boot and stop plaintext key writes Addresses PR #865 review findings on the secret-storage seam. A (BLOCKER): identity write paths no longer serialize plaintext keys. insert/update_local_qualified_identity (and the alias re-encode) now route through encode_identity_blob_vault_first — the write-path twin of the load migration: plaintext keys go into the vault FIRST, the persisted blob carries only InVault placeholders, and a vault-write failure aborts the write (never lands Clear/AlwaysClear bytes in det-app.sqlite). B (HIGH) / C (BLOCKER): cold-boot hydration no longer drops Tier-2-protected wallets. reconstruct_wallet (HD seed) and rebuild_wallet (imported single key) branch on the at-rest SecretScheme before reading the secret. A Protected secret rehydrates CLOSED from the public sidecar (xpub / public_key_bytes) instead of propagating NeedsPassword as fatal, so a keep-protection-migrated wallet stays in the picker across launches. D: the HD Absent-branch legacy-envelope delete is now best-effort (log, don't propagate), matching the Protected branch — a transient delete failure no longer fails an otherwise-successful unlock. E: the eager no-password seed migration wraps the extracted 64-byte seed in Zeroizing so the stack copy wipes on drop. F: resolve_registered_wallet tolerates the registration TOCTOU window with a bounded re-poll before declaring a wallet missing; the fund-routing xpub gate is unchanged. G: present-but-malformed identity-key bytes map to SecretDecryptFailed (with a warn) in both the display and sign tasks, distinct from genuinely-absent IdentityKeyMissing. I/J: refreshed stale doc-comments (single-key has_passphrase, WalletMeta uses_password, wallet_seed_store header) to describe the Tier-2 keep-protection shape, and stripped ephemeral review-finding IDs from secret-path comments. Regression tests cover A, B, and C. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * fix(wallet-backend): seal fresh protected single-key imports Tier-2, typed malformed-identity-key error, skip needless keystore clone Follow-up to PR #865 review on the secret-storage seam. Fresh protected single-key imports now seal Tier-2 at import time instead of writing the legacy DET AES-GCM SingleKeyEntry envelope and migrating lazily on first unlock. import_wif_with_passphrase routes the protected branch through the seam's put_secret_protected, so the storage chokepoint is a single shape from import onward. raw_key_bytes and verify_passphrase branch on the at-rest SecretScheme: a Tier-2 key surfaces SingleKeyPassphraseRequired on a direct read and is verified by unsealing (wrong password -> SingleKeyPassphraseIncorrect, no oracle), while the legacy decode + lazy re-wrap path is retained for pre-existing installs. The legacy AES-GCM SingleKeyEntry remains a decode-only reader. sec_002_import_with_passphrase_encrypts_payload tightens to assert SecretScheme::Protected at import; ts_lazy_03 now starts from a directly-written legacy entry so the legacy->Tier-2 migration stays covered. Present-but-malformed identity-key bytes map to a new typed TaskError::IdentityKeyMalformed (jargon-free "stored but unreadable / re-import to refresh") in both the display and sign tasks, replacing the off-domain SecretDecryptFailed ("recovery phrase") message and staying distinct from the genuinely-absent IdentityKeyMissing. migrate_keystore_to_vault and encode_identity_blob_vault_first skip the KeyStorage clone in the steady-state (already-InVault) case via a new KeyStorage::has_plaintext_for_vault probe, so cold-boot load and identity re-saves no longer clone per identity for no benefit. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * docs(secret-seam): correct drifted docs to Tier-2 keep-protection reality - 01-ux-disclosure.md: full rewrite — the previous doc described the retired drop-protection design (password downgraded to file-permission only, one-time disclosure notices). Replaced with the Tier-2 keep-protection reality: protected secrets re-wrap under the same password, uses_password/has_passphrase stay true, migration is silent, no disclosure notices. Removed candy tally and agent byline. - 02-test-spec.md: update TS-LAZY-01/02/03 expected outcomes to Tier-2: scheme stays Protected, uses_password/has_passphrase stay true, second unlock still prompts (ask_count == 1). Added source-test names (ts_t2_01_*, ts_lazy_03_*). Removed machine-local plan paths, Marvin's note, and future-tense TDD framing. Added section-5 note that raw seam applies only to unprotected secrets. - user-stories.md WAL-006: replace false bullet ("no longer prompts, one-time notice") with the truth: Tier-2 re-seal, wallet keeps prompting, migration is silent. - CLAUDE.md wallet_backend/ bullet: remove dead TODO(per-secret-encryption) grep pointer (zero hits); describe present state — put_secret_protected/ get_secret_protected implemented; keyless-vault residual is deferred tier. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * feat(wallet-backend): optional per-identity at-rest encryption for identity keys (SEC-001) Identity keys default to keyless (Tier-1 raw, prompt-free) so headless/MCP signing of a non-opted-in identity is unchanged byte-for-byte. A user may opt in per identity to seal that identity's keys Tier-2 over the existing seam (Argon2id + XChaCha20-Poly1305) — no new crypto. The at-rest vault scheme is the single source of truth: scope_has_passphrase probes SecretSeam::scheme for the identity-key label (Protected -> prompt, Unprotected -> prompt-free, Absent -> IdentityKeyMissing), and decrypt_jit gains a symmetric Tier-2 arm. A protection-aware IdentityKeyView::store refuses a keyless write over a Protected label (IdentityKeyProtectionDowngrade), with store_unprotected as the deliberate opt-out downgrade. New crash-safe, idempotent migrations IdentityTask::Protect/UnprotectIdentityKeys re-seal an identity's keys keyless<->Tier-2 under one per-identity password. A display-only IdentityMeta sidecar carries the password hint + prompt copy (never the gate), seeded into the chokepoint's identity prompt index at identity load. UI: a collapsible 'Key Protection' section on the Key Info screen (default closed) with danger-gated opt-in (new password + confirm + strength + hint) and opt-out (verify) flows; PassphraseModalConfig gains remember_label so the sign-time prompt says 'key', not 'wallet'. Opted-in signing prompts just-in-time; headless yields SecretPromptUnavailable. Per-identity password isolation (TS-T2-IK-ISO twins TS-T2-SK-ISO). <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * fix(wallet-backend): seal new keys on a protected identity Tier-2, never keyless (SEC-001) Smythe MUST-FIX: a key added to a password-protected identity slipped through the per-label downgrade guard (a new key_id is scheme Absent), so AddKeyToIdentity -> insert_non_encrypted(Clear) -> encode_identity_blob_vault_first -> store_all wrote it Tier-1 keyless — a fully-capable signing key in plaintext on an identity the user believed protected. Two layers close it: (1) an identity-level fail-closed guard in encode_identity_blob_vault_first / migrate_keystore_to_vault refuses to move resident plaintext into the vault when the identity already has any Tier-2 key (IdentityKeyProtectionDowngrade / new KeystoreMigration::ProtectedSkipped), so a keyless write is impossible. (2) add_key_to_identity now seals the new key Tier-2 via SecretAccess::seal_new_identity_key, which prompts once, verifies the password against an existing protected key (so the identity stays under one password, with the standard wrong-pass re-ask), seals the new key, and marks it InVault before the save — headless yields SecretPromptUnavailable (fail closed; signing also fails closed earlier). KeyStorage::mark_in_vault performs the post-seal transition. SEC-002 (SHOULD-FIX): protect_identity_keys now re-enforces the password policy in the backend (validate_protection_password) so a non-UI caller cannot seal under a too-short password. SEC-003/SEC-004 tracked as code comments (store-guard TOCTOU bounded by the single-writer lock + UI in-flight gate; pre-opt-in plaintext may persist in freed filesystem blocks until reused). Tests: secret_access seal-new-key (seals Tier-2 under verified password / headless fails closed with no write / wrong-pass re-asks); identity_db encode+migrate refuse keyless on a protected identity; protect_identity_keys rejects a weak password. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * fix(identity): fail closed before broadcast when adding a key to a protected identity (SEC-001 O-2) Adding a key to a password-protected identity used to seal the new key Tier-2 (or fail closed) only during LOCAL persist, which runs AFTER the on-chain AddKeys broadcast. A headless add therefore broadcast the state transition on-chain and only then failed closed locally (no password) — leaving the key on-chain but never persisted by DET: an on-chain/local divergence. Move the protected-identity precondition BEFORE any on-chain side effect. `add_key_to_identity` now determines up front whether the identity is protected (`protected_identity_verify_scope`) and, if so, prompts for and VERIFIES its object password before building or broadcasting the state transition. Headless (`NullSecretPrompt` → `SecretPromptUnavailable`) or a wrong password returns the typed error before the broadcast, so no state transition is ever sent. The seal then runs after the broadcast with the already-verified password — a single prompt, split across the broadcast. `SecretAccess::seal_new_identity_key` is split into `verify_identity_object_password` (prompt + verify, returns an opaque `VerifiedIdentityPassword` that zeroizes on drop) and `seal_new_identity_key_with_password` (no prompt); the original composes the two and keeps its tests. The d965ca50 encode fail-closed guard (`IdentityKeyProtectionDowngrade`) stays as the defense-in-depth backstop. Also: O-1 — `mark_in_vault`'s bool return is now checked and warns on an unexpected miss (the encode guard still backstops it). O-3 — document that a Mixed identity fails closed on a plain re-save until "Finish protecting" reseals the remaining keys (intended secure behavior). <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * fix(identity): harden SEC-001 identity-key paths (r2 review) Address four thepastaclaw findings on the SEC-001 identity-key code at fcf6da15: - BLOCKING: `seal_identity_keys` now verifies the supplied password opens every already-`Protected` key BEFORE sealing any keyless one. A Mixed-state "Finish protecting" re-run with a different password is rejected up front with `IdentityKeyPassphraseIncorrect` and zero state changes, so an identity can never be split across two passwords. - `get_identity_by_id` now mirrors the bulk-load vault migration, so the single-get read path (and the SEC-001 protect/unprotect tasks that use it) migrates legacy resident `Clear`/`AlwaysClear` keys to the vault on read instead of returning and re-persisting plaintext. - A post-broadcast seal failure in `add_key_to_identity` now surfaces the typed, actionable `IdentityKeyAddedButNotSaved` (key is on-chain; retry after freeing disk space), preserving the upstream cause in the source chain — never a silent loss and never a keyless-write fallback. - The three prompt-meta setters recover a poisoned lock (`unwrap_or_else(|p| p.into_inner())`), matching `forget`/`forget_all`, so prompt-copy metadata can self-heal after a panicked reader instead of silently freezing. Adds regression tests for each (the blocker's split-prevention, read-path migration via an offline AppContext, and the typed orphan-error mapping). <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> * docs(single-key): correct has_passphrase on-disk-shape doc to Tier-2-direct The has_passphrase field doc claimed fresh protected imports use a legacy AES-GCM envelope migrated on first unlock; imports seal Tier-2 directly at import time. Align the field doc with the function docstring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(dashpay-e2e): use real curve points in tc_045 fixture (QA-008) The bumped secp256k1 now validates curve membership on `PublicKey::from_slice`, and `[0x02; 33]` / `[0x03; 33]` are not points on the curve, so tc_045 paniced with `Secp256k1(InvalidPublicKey)` before it could test anything. Swap the hand-written bytes for two deterministic pubkeys derived from fixed secret keys — stable across runs, valid on the curve, and matching the file's existing secret-key→pubkey idiom. Pure fixture fix; no product behavior involved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(wallet-backend): return WalletNotFound for an unknown seed hash (QA-002) `GenerateReceiveAddress` for a seed hash that matches no wallet returned the transient `WalletNotLoaded` ("still loading, wait and retry") instead of `WalletNotFound`. The two mean very different things to a user: one is a permanent "this wallet does not exist", the other a momentary boot state. `resolve_wallet` cannot tell them apart on its own — a missing `id_map` entry covers both — and ~24 callers rely on its `WalletNotLoaded` for the genuine cold-boot case, so it must stay. Instead, resolve the existence question one layer up in `generate_receive_address`, where the DET-side wallet store (`self.wallets`) is the source of truth: unknown wallet -> `WalletNotFound`; known-but-not-yet-loaded -> `WalletNotLoaded`. This mirrors the sibling `generate_platform_receive_address`, which already does exactly this. Confirmed against design spec TC-019. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(core-e2e): expect SingleKeyWalletsUnsupported in tc_009 (QA-001) test_tc009 asserted `RefreshSingleKeyWalletInfo` returns `OperationRequiresDashCore` in SPV mode — but single-key wallets are intentionally unsupported this release (PROJ-007 / single-key-mock.md Decision #7: "Every operation returns `Err(TaskError::SingleKeyWalletsUnsupported)`", and refresh is one of those operations). The product correctly returns `SingleKeyWalletsUnsupported`, and the sibling TC-003 already asserts that — so test_tc009 was simply stale and contradicted both. Align its expectation (and its comments) with the by-design behavior. Also corrected TC-003's own header comment, which still described the superseded `OperationRequiresDashCore` outcome while its assertion already checked the right variant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(identity): compute a meaningful top-up fee after a backend reload (QA-006) A wallet-funded identity top-up reported `actual_fee == 0` after a backend reload. The fee was derived inline as `amount*1000 - (new_balance - balance_before)`, where `balance_before` came from the passed-in (post-reload, stale) `QualifiedIdentity`. When that cached balance lags the real platform balance, the apparent increase exceeds the minted credits and `saturating_sub` collapses the fee to zero — physically impossible, since a top-up can never grow the balance by more than the asset lock mints. Move the computation into `model/fee_estimation.rs` (DET policy: no inline fee math) as `resolve_identity_topup_actual_fee`, and have it fall back to the deterministic estimate whenever the balance delta yields a zero fee — the reliable signal that `balance_before` was stale. The happy path is unchanged (a consistent delta still reports the real processing fee). Adds unit tests for both the consistent-delta and stale-balance branches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(spv-e2e): assert restart-in-place reconnect contract (QA-003) The B-reconnect test asserted `wallet_backend().is_err()` after `stop_spv()`, a leftover from the superseded drop-and-reopen design. The current lifecycle is restart-in-place by intent: `stop_spv` calls `stop_in_place()` and KEEPS the backend (and its `Arc<SqlitePersister>`) wired, so the next Connect fast-paths on the populated slot and restarts the SAME instance — the persister DB is never closed/reopened, making `AlreadyOpen` impossible by construction. This is exactly what the offline unit tests `stop_spv_in_place_keeps_backend_and_disconnects_indicator` and `reconnect_restart_in_place_reuses_backend` lock in, and the latter even names this e2e test as its live-network counterpart. Update the test to assert the real contract over a live network: backend stays wired and unstarted after `stop_spv`, and the reconnect reuses the same instance (`Arc::as_ptr` equality) with sync restarted. Header comment and the reconnect failure message rewritten to describe restart-in-place. Product code is correct as-is; the assertion was stale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(wallet): gate sends on spendable balance, not confirmed (QA-010) Upstream classifies a UTXO as `confirmed` only once it is in a block, chain- locked, or flagged instant-locked locally; until then — including the window after an IS-lock but before the local flag is applied — it sits in `unconfirmed`. Coin selection draws from `spendable()` (confirmed + unconfirmed), and the "Max" button already reserves against `spendable()`, but several send paths still gated/validated on `confirmed`. The result: "Max" could exceed the validation, and sends coin selection would happily fund were rejected as "Insufficient confirmed balance" while funds showed as pending. Align the UI with the coin selector: - `send_screen::get_core_balance` -> `spendable()` (4 amount validations + the source-selector display). - wallets-screen send dialog validation -> `spendable()` (and drop the now-misleading "confirmed" from the message). - dashpay send_payment balance display + Max -> `spendable()`. No change to actually-correct sites: `snapshot_has_balance` already counts confirmed||unconfirmed, the MCP balances tool exposes all three buckets distinctly, and `.total` displays are intentional. Harness: `wait_for_spendable_balance` polled `.confirmed`, contradicting its own "spendable" contract, so it timed out whenever funding landed as IS-locked / unconfirmed. Poll `.spendable()` (the coin-selector set) and report it in the timeout diagnostic. Audit note: at the pinned platform-wallet rev (fb7953e / key-wallet 981e97f) IS-locked-FLAGGED UTXOs are classified `confirmed`, not `unconfirmed` — the balance has no separate IS-locked bucket. So `spendable()` (= confirmed + unconfirmed) is the correct, safe gate, not an over-count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(identity-e2e): poll for key visibility after broadcast (QA-004) `identity_in_vault_sign` and `z_broadcast_st_tasks::tc_066` slept a fixed ~1s after broadcasting an IdentityUpdate, then re-fetched once and asserted the new key was visible. That single delay races DAPI propagation — the node serving the re-fetch may not have processed the block yet — so the tests failed spuriously even though the broadcast (and SEC-001 signing) succeeded. Replace the fixed sleep with a bounded poll: re-fetch the identity until the new key appears or a ~10s deadline passes, then assert. Test robustness only; no product change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(harness): retry transient wallet registration with backoff (QA-013) The framework-wallet register and `create_funded_test_wallet` both called `register_wallet` exactly once and panicked on any error. Under the shared- runtime backend-e2e harness the fail-closed sidecar writes (`WalletSeedStorage` / `WalletMetaStorage`) can briefly lose a SQLite race, and registration can surface the typed transient `WalletBackend` ("retry in a moment") signal — a single attempt then aborts init and masks the test under exercise (identity_create / identity_cold_boot). Add `register_wallet_with_retry`: bounded ~30s retry with backoff on the transient variants only (`WalletBackend`, `WalletBackendNotYetWired`, `WalletSeedStorage`, `WalletMetaStorage`); permanent errors surface immediately, and `WalletAlreadyImported` is returned as-is so the framework path keeps its idempotent-reuse branch. Wired into both registration sites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(wallet-e2e): mark tc_012 address-advance assertion PENDING (QA-005) QA-005 disposition is DEFER: "same address on consecutive GenerateReceiveAddress calls" is correct, funds-safe BIP-44 keypool behavior (upstream `next_unused` returns the lowest UNUSED address until it is used on-chain). The fresh-each-call UX needs a reserve-on-hand-out API that does not exist in the pinned upstream. - Annotate tc_012's `assert_ne!(address1, address2)` as PENDING (commented out with a soft observation log) so the test passes on the current funds-safe behavior. tc_012b's gap-window funds-safety assertion stays active. - Enhance the existing `TODO(PROJ-015)` in `wallet_backend/mod.rs` to cite the fix's 3-layer propagation: dashpay/rust-dashcore#818 (`next_unused_and_reserve`, ready-for-review) → platform surface (`CoreWallet::next_receive_address_and_reserve_for_account`) → DET dep bump + switch `next_receive_address` to the reserving variant. Re-enable the `assert_ne!` once that lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(wallet-lifecycle): correct stop_spv rustdoc to restart-in-place (QA-015) The `stop_spv` rustdoc still described the superseded drop-and-reopen design ("drop the wired wallet backend", "WalletBackend::shutdown", "Unwire the backend"), none of which the implementation does. It calls `stop_in_place()` and KEEPS the backend (and its `Arc<SqlitePersister>`) wired, re-arming the start latch and coordinator gate so the next same-network Connect restarts the SAME instance — which is exactly why a reconnect cannot hit `WalletStorageError::AlreadyOpen` (the persister is never closed/reopened). Rewrite the doc to describe the actual restart-in-place semantics and note that full teardown (`WalletBackend::shutdown`, dropping the backend + releasing the persister) happens only on the network-switch and app-close paths, never here. Companion to the QA-003 test/e2e-header fixes. Doc-only; no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(identity-e2e): widen cold-boot funding to clear top-up minimum (QA-016) `cd_cold_boot_identity_register_and_topup` funded 30M duffs, which after scenario C's asset lock + registration fees left 4,999,703 duffs — 297 below the 5M scenario-D top-up minimum, so scenario D failed on a buffer shortfall (the watch-only-no-private-key bug is already fixed; scenario C passes). Bump the funding to 35M so both transactions clear their network fees. Test-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(dashpay-e2e): defer dashpay backend-e2e module pending upstream (platform#3841) The dashpay backend-e2e tests fail because upstream `platform-wallet` dashpay support is incomplete. The completion lands in dashpay/platform#3841 ("fix(platform-wallet)!: complete dashpay", shumkov, branch feat/dashpay-m1-sync-correctness); we retest once it merges and the DET platform-wallet dep is bumped. - Comment out `mod dashpay_tasks;` in main.rs with a TODO(dashpay-e2e) citing #3841 and the affected tests (tc_032/033/036/037/041/043/044/045/046). - Add a matching deferral note to the dashpay_tasks.rs module doc. This removes 9 dashpay tests AND their SharedDashPayPair registration burst from the run. The QA-008 tc_045 fixture fix stays in the file, dormant until re-enabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(harness): widen funded-wallet SPV-pickup budget to 120s (QA-017) QA-013 was verified INNOCENT against the re-run log: the "retrying after backoff" warning logged 0 times, so `register_wallet_with_retry` never fired — all 17 timeouts were in `wait_for_wallet_in_spv` (the 30s SPV-pickup wait), downstream of the retry wrapper. Root cause is throughput saturation: the other fixes (and, before deferral, the dashpay tests) unmasked more funded-wallet registrations, and the suite runs serially (`--test-threads=1`), so as wallets accumulate in the upstream manager each later pickup round (bloom-filter rebuild + re-sync) exceeds the tight 30s budget. Give `create_funded_test_wallet`'s `wait_for_wallet_in_spv` the same 120s headroom the framework wallet already uses, via a named `FUNDED_WALLET_REGISTRATION_TIMEOUT`. Concurrency throttling is unnecessary — the run is already serial. Test-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(identity): fail closed when opt-in protection leaves resident plaintext keys protect_identity_keys could emit IdentityKeysProtected{count:0} when the silent get_identity_by_id vault migration failed (VaultWriteFailed), leaving Clear keys with Absent vault labels that seal_identity_keys skips. Guard the protect boundary with a typed error so the user retries instead of believing the identity is sealed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(identity): prove the protect fail-closed guard is wired into the task (QA-001) The guard's wiring was unverified: deleting the call passed every test because the only fail-closed test invoked the helper directly and the end-to-end test was the happy path. Extract the post-load protect logic into protect_loaded_identity_keys (called by protect_identity_keys after get_identity_by_id) and add a test that drives it on a qi carrying resident plaintext, asserting IdentityKeyProtectionIncomplete. Deleting the guard line now turns that test red (it returns IdentityKeysProtected{count:0}). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(fee-estimation): fall back to estimate when balance_before is stale-HIGH (RUST-001) The real-fee branch was gated only on `delta_fee == 0` (stale-LOW). When `balance_before` is stale-HIGH (`balance_after <= balance_before`), `balance_increase` saturates to 0 and `delta_fee` equals the full minted amount, producing a wildly wrong "fee" (e.g. 5 M duffs → ~5 B-credit fee). Gate the real-fee branch on `0 < delta_fee < expected_credits` so both extremes fall back to the deterministic estimate. Add a unit test for the stale-HIGH case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(identity-db): zeroize rollback clone after successful vault migration (SEC-002) `before = qi.private_keys.clone()` holds raw identity private-key bytes (Clear/AlwaysClear) as a rollback guard. On the success path it was dropped UN-zeroized, leaving plaintext on the freed heap. Call `before.take_plaintext_for_vault()` immediately after the vault write succeeds — the method already zeroizes each `[u8; 32]` in-…
* refactor(ui): trim dev-history comments and dedupe identity home helpers
Rewrite sprint/wave/task-numbering comments in home.rs and hub_screen.rs as
present-tense invariants (git blame covers history); PR #842 kept as the one
citable regression reference. Move the duplicated network_label helper from
home.rs/import_single_key.rs into ui/theme.rs next to network_label_color.
Rename home.rs's format_credits_as_dash to format_credits_short to stop
colliding in name (but not behavior) with model::fee_estimation's version.
Drop the #[cfg(test)]-only forwarding shim and the dead-code import-pinning
stub in home.rs.
* refactor(wallet_backend): delete dead one-impl PersistedWalletLoader seam
The `loader: Arc<dyn PersistedWalletLoader>` field injected the backend
into itself through a trait object: the sole production impl,
`UpstreamFromPersisted`, was a unit struct whose whole body was
`backend.load_from_persistor_seedless(ctx).await`. No test substituted a
loader double — the cold-boot tests drive `load_from_persistor_seedless`
directly — so the seam bought nothing.
Remove the `PersistedWalletLoader` trait, the `UpstreamFromPersisted`
unit struct, the `loader` field and its constructor parameter, and the
two object-safety/`Default` compile-check tests. `register_persisted_wallets`
now calls `self.load_from_persistor_seedless(ctx)` directly. The
DET-opaque `LoadedWallets` / `PersistedLoadSkip` outcome types stay in
`loader.rs`. Stale doc/comment references to the removed type point at
`load_from_persistor_seedless` instead. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(wallet_backend): dedupe InMemoryKv test fake across 14 files
Consolidate 13 byte-for-byte (or near-identical) copies of the
in-memory KvStore test fake into one canonical `kv_test_support`
module, following the existing `leak_test_support` shared-fixture
pattern. The shared fixture's `list_keys` always sorts — matching
kv.rs's own doc comment (which the duplicated unsorted copies
silently violated) and the 5 copies that already sorted explicitly;
no caller depended on unsorted (insertion) order.
`single_key.rs`'s fixture is a structurally different fake (global-only
BTreeMap<String, Vec<u8>> with scope assertions) and is left as-is.
`FailingKv` in kv.rs is not duplicated elsewhere and is left in place.
Registering the shared module requires one additive line in
wallet_backend/mod.rs (mirroring the leak_test_support declaration) so
context/ and backend_task/migration/ can reach it too.
* refactor(wallet_backend): split god-impl into shielded/identity_ops/payments
The single `impl WalletBackend` block in `mod.rs` spanned ~3,100 lines
across many domains. Following the pattern `hydration.rs` / `dashpay.rs`
already established (each an additional `impl WalletBackend` block in its
own sibling file), relocate three domain groups verbatim — no visibility,
ownership, or behavior change:
- `shielded.rs` — the 14 Orchard shielded-pool methods (already fenced).
- `identity_ops.rs` — register/top-up identity, ensure-managed, and the
platform-address funding methods (incl. the private
`provision_identity_funding_account` helper).
- `payments.rs` — send_payment / create_asset_lock_proof /
broadcast_transaction / assert_can_sign and the
`derive_private_key_from_held` helper (the funds-signing path).
Domain-exclusive private helpers stay in `mod.rs` (`map_shielded_op_error`,
`map_identity_*`, `map_platform_address_fund_error`, `DEFAULT_BIP44_ACCOUNT`)
and are reached from the sibling modules via `use super::` — child modules
see ancestor privates, exactly as `dashpay.rs` reaches `self.inner.*`.
Shared methods (`hd_scope`, `resolve_wallet`) remain in `mod.rs`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(wallets): remove redundant balance-consistency health check
The end-of-SPV-sync health check compared each wallet's authoritative
Core/Platform totals against a second, independently derived per-category
total and warned the user when they disagreed. A runtime detector that
tells the user the app's own bookkeeping disagrees with itself is a
symptom, not a fix — the follow-up commit makes the per-account breakdown
single-sourced, so the two figures can no longer diverge by construction.
Removes:
- AppState::run_wallet_balance_health_check + the dedupe signature/banner
fields and BALANCE_HEALTH_WARNING copy
- collect_wallet_balance_mismatches / balance_health_signature and their tests
- BackendTaskSuccessResult::WalletBalanceHealthCheckRequested and the
EventBridge SyncComplete emitter that produced it
- the model::wallet::balance_consistency module (no other callers)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(wallets): single-source the per-account balance breakdown
The wallets screen derived its per-account breakdown a second time from
the legacy in-memory `Wallet` model (watched_addresses + per-address
platform_address_info), in parallel with the backend's authoritative
totals. The two derivations had diverged in shipped history, which is
what motivated the (now removed) health check.
Source the breakdown entirely from the backend:
- collect_account_summaries now takes only the display snapshot's
address_balances + address_paths (no `&Wallet`) and computes the Core
per-category totals from that single dataset. Platform credits leave
the breakdown entirely: the Platform tab reads the one authoritative
figure, AppContext::platform_balance_duffs — the exact value the wallet
header shows — so the tab and header can no longer disagree.
- The Platform tab is shown from the coordinator snapshot signal
(a positive balance or a completed platform-address sync), preserving
the empty/receive tab without consulting the legacy model.
Behavior is preserved for the numbers users see; the change is the data
source. The legacy `Wallet` model's balance/UTXO fields are no longer read
by the breakdown (see report for its remaining uses).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(context): correct platform_sync_info doc to "sync completed", not "funds found"
The cursor advances on every successful platform-address sync pass regardless
of whether it found funded addresses (see event_bridge summary_ok_sync_cursors),
so `Some` means "a sync pass completed," not "a funded address was reported."
The old wording misleads anyone reasoning about the Platform-tab visibility gate
that reads this accessor (QA-005).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(wallets): read header total and per-account breakdown from one snapshot generation
QA-001: the snapshot's headline balance and its UTXO-derived per-address
breakdown were two independently-refreshed fields. `recompute` always read the
lock-free `wallet.balance()` atomics for `.balance`, but only refreshed
`.address_balances` when `try_state()` won the lock — on contention it carried a
stale breakdown forward beside a fresh total. The wallet-header total
(`core_balance_duffs` -> `.balance.total`) and the Core tab sum
(`collect_account_summaries` over `.address_balances`) could therefore disagree
for the duration of any lock-contention window. The atomics are also maintained
by a separate event handler that can lag the wallet state under its own map
contention, so even off-contention the two sources could skew.
Fix: derive the headline balance from `state.balance()` (the stored
`WalletCoreBalance`, refreshed alongside every UTXO mutation under the same
wallet-manager write lock) read from the SAME `try_state()` guard as
`state.utxos()`. Balance and breakdown now reflect one generation of wallet
state. On contention, carry the ENTIRE prior (already-consistent) snapshot
forward — balance included — instead of splicing a fresh total onto a stale
breakdown. The non-blocking property is preserved: `try_state()` still yields
`None` under contention and never blocks the event callback.
QA-003: replace the tautological pinned test. The old
`two_funded_bip44_accounts_keep_distinct_per_account_totals` summed hand-fed
literals back to themselves and asserted zero Platform facts. Add
`header_total_reconciles_with_core_tab_breakdown_through_real_accessors`, which
publishes a realistic snapshot through the real `publish` seam — including a
funded address outside the generated-path window — and asserts the Core-tab sum
from `collect_account_summaries` equals the exact `.balance.total` the header
renders, proving no funded address is dropped. The old test's misleading
header-agreement comment is corrected to describe what it actually pins.
Adds `contention_carries_whole_prior_snapshot_...` and a store round-trip test
covering the carry-forward invariant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(wallets): reconcile default-mode tabs and show Platform tab on load
Extracts the tab-visibility logic into a pure, unit-testable `plan_account_tabs`
so these rules have real coverage (the existing kittest suite is generic
frame-stability smoke with no wallet loaded).
QA-004: the Platform tab was gated on `platform_balance_duffs > 0 ||
platform_sync_info(..).is_some()`, so a freshly created/imported wallet showed
no Platform tab — not even empty, no receive address — until the first async
platform-address sync pass completed. If sync was slow, failed, or the network
was unreachable, the user's only in-app route to their Platform receive address
stayed hidden indefinitely. Every HD wallet unconditionally bootstraps a
platform-payment address at load (`Wallet::bootstrap_known_addresses`), so the
tab is now shown immediately (empty until funded).
QA-002: any balance in a non-visible ("system") category — `Other(Unknown)`,
CoinJoin, etc. — was only reachable via the developer-mode System tab. A
default-mode user saw a header total that included those funds with no visible
tab summing to them, exactly the drift class the deleted health check warned
about. Default mode now surfaces a consolidated "Other" tab whenever a
non-visible category holds funds, so the visible tabs always reconcile the
header total. The System-tab content gains a short explanation and lists only
funded categories in default mode.
QA-006: add a de-dup guard so the dedicated Platform push never produces a
second Platform tab if a future upstream bump folds the platform-payment pool
into `all_accounts()`. Pinned by `plan_account_tabs` tests plus an upstream
tripwire test (in snapshot.rs) asserting today's exclusion still holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(wallets): note the accepted first-contended-recompute zero-snapshot edge case
When the very first recompute for a wallet loses the try_state() race there is no
prior snapshot to carry forward, so the all-zero default is published and the
wallet is marked as having a snapshot. A wallet with genuine prior funds can then
render "0 DASH, synced" for one event cycle before the next event wins the lock
and publishes the real balance. Documented as a known, accepted, self-healing
edge case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(identity): show spendable balance, not total, on funding screens
show_wallet_balance() on both the Create-Identity and Top-Up
"use wallet balance" screens read DetWalletBalance.total, while the
insufficient-funds banner, gate, and picker on the same screens all
read .spendable(). With immature or CoinJoin-locked funds present,
the user saw a positive "Wallet Balance" directly contradicted by a
"not enough Dash" banner one line below. Switch both to .spendable()
so all four surfaces agree.
Raised on PR #869 review (thepastaclaw); verified still live post-merge.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs(wallets): document deferred non-remember-unlock registration gap
Full-repo audit CODE-005 (untriaged) found that handle_wallet_unlocked's
None-passphrase early return skips drive_unlock_registration, so a
non-remember unlock never re-registers the wallet with the upstream SPV
backend until next launch. User decision 2026-07-08: defer rather than
fix now, to keep the adjacent CODE-024 cleanup (Wave 11) unconstrained.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallets): tidy wallet-backend caches and trim dead surface
Wave 9 of the wallet-backend architecture audit — caches & nits.
- Avatar cache eviction now orders entries by a sibling `det:avatar_ts:`
timestamp index (small i64 reads) instead of deserializing every cached
image's full bytes on each put. put/invalidate/clear/evict keep the index
in lock-step with the byte entries.
- Trim test-only / over-public wallet-backend surface: gate `wallet_count`
and `AuthPubkeyCacheView::delete` behind `#[cfg(test)]`; make the
`AVATAR_TTL_MS`, `MAX_AVATAR_ENTRIES`, and `DASHPAY_REQUEST_EXPIRY_DAYS`
constants private; delete the unused `shielded_activity`/`shielded_notes`
stubs; correct the stale `ensure_wallets_registered` migration-engine doc.
- Name the Devnet/Regtest SPV P2P ports and collapse `spv_primary_peer_socket`
into one match with an early `_ => None`.
- Unify the two SPV error-snippet truncations under one
`SPV_ERROR_SNIPPET_MAX` constant and drop the shadowing `use` lines in
`on_platform_address_sync_completed`.
- Drop the dead `TokenBalanceSnapshot` re-export.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(context): consolidate identity_db kv access, errors, and vote helpers
Wave 6 of the architecture audit — context/identity_db.rs plus the shared
kv-access surface it touches.
- CODE-001: replace the `format!("{:?}", identity_type)` Debug-repr
discriminator (four writers) and the hardcoded string filters with a
stable `IdentityType::as_tag()` / `from_tag()` mapping. Writer and filter
now share one source of truth, immune to a variant rename. Adds a
round-trip test.
- CODE-007: extract `hydrate_stored_identity()`, shared by the bulk-load
and single-get paths so both reconstruct an identity identically.
- CODE-014: introduce per-domain `err`-style free fns (`identity_err`,
`scheduled_vote_err`, `top_up_err`, `contract_err`, `token_err`,
`contest_err`) replacing the verbose `.map_err(|source| …)` closures and
the fully-qualified error paths; collapse the three `*_kv()` accessors
into one `AppContext::det_kv()`.
- CODE-035: add `scheduled_vote_keys()` and `remove_vote_voter_from_index()`
helpers; the three hand-rolled list/delete/prune loops shrink to a few
lines.
- CODE-041: delete the dead `load_local_qualified_identities_in_wallets`
and its speculative `#[allow(dead_code)]`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): delete dead UI surface and dedup icon loaders
Wave 20 dead-UI-surface cleanup:
- Delete the unused `left_wallet_panel.rs` (no opener anywhere) and its
module declaration.
- Consolidate the three copied `Assets`/`load_icon`/`load_svg_icon`
RustEmbed icon loaders into one shared `components/icons.rs`; drop the
dead copy in `top_panel.rs` and the duplicate in `left_panel.rs`.
- Remove dead styled widgets `GlassCard`, `HeroSection`, `AnimatedIcon`,
`AnimatedGradientCard` and `styled_text_edit_multiline`, plus their
catalog rows in `components/README.md`.
- Collapse `StyledButton` to its single reachable configuration (drop the
never-constructed `ButtonVariant`/`ButtonSize` enums and arms); remove
the commented-out `StyledCard` builders and dead `title` field/branch;
remove the inert `GradientButton::glow` field/builder and its no-op
call site in `left_panel.rs`.
- Drop the six blanket `#[allow(dead_code)]` blocks in `theme.rs`. This is
a library crate, so `dead_code` never fires on `pub` items; clippy
`--all-features --all-targets -D warnings` is clean without them, so no
per-item `#[expect(dead_code)]` is warranted (it would be unfulfilled).
- Delete five zero-caller `helpers.rs` items (`BUTTON_ADJUSTMENT_PADDING_TOP`,
`render_key_selector`, `is_platform_address`, `PLATFORM_ADDRESS_HINT`,
`PLATFORM_ADDRESS_EXAMPLES`); the `is_platform_address_string` re-export
stays (four live callers).
- Delete `OptionBannerExt::replace` (unused alias + its essay); drop the
duplicate inherent `AmountInputResponse::{is_valid,has_changed}` (the
`ComponentResponse` impl provides them); `add_connection_indicator`
returns `()`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallet_backend): dedup funds paths and harden error typing (Wave 2)
Wave 2 of the architecture audit — wallet-backend funds paths and error
integrity. Behavior-preserving throughout; funds/secret-adjacent code kept
conservative.
- CODE-004: extract `WalletBackend::seed_wallet` for the repeated
`from_seed_bytes` construction; replace fabricated
`PlatformWalletError::{WalletCreation,AssetLockTransaction,InvalidIdentityData}`
string wraps with typed `SeedWalletBuildFailed` /
`IdentityFundingAccountProvisionFailed` variants carrying the real
`#[source]` (`key_wallet::Error`).
- CODE-011: replace `unreachable!("handled by pre-flight")` in
`map_shielded_op_error` with a defensive `WalletBackend` fallthrough so an
ambiguous shielded-spend result can never panic a funds path.
- CODE-012: rewrite the DashPay adapter module doc in present tense and fix
the inverted sidecar-scope comment (2 Global / 4 Identity families).
- CODE-015: delete dead `flush_persister` (+ orphaned
`WalletPersistenceFlushFailed`) and `broadcast_transaction`; keep the
documented single-key signing chokepoint `sign_single_key` (intentional,
test-exercised infra awaiting single-key send) but fix its
source-discarding `map_err` via a typed
`SingleKeySignFailed { #[source] DetSignerError }`.
- CODE-025: parse identity funding intent once into a local `Funding` enum —
the repeated `AccountType` matches, `unreachable!` arms and now-impossible
`UnsupportedIdentityFundingAccount` disappear; fix the semantically-wrong
`WalletRegistrationXpubMismatch` reuse in `upstream_identity_from_seed`
(-> `WalletStateInconsistent`).
- CODE-029: collapse `record_sent`/`record_incoming_contact_request` into one
`record_contact_request(..., direction)` over a 2-variant enum; the public
names stay thin wrappers.
- CODE-034: extract a `with_managed` closure helper for the 4x DashPay view
find-wallet -> kv -> state -> managed_identity preamble.
- CODE-046: collapse the byte-identical `contact_sidecar_key` into
`sidecar_key`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallet): scope test-only single-key signing helpers to tests
`SingleKeyView::sign_with`, `raw_key_bytes`, and the `sign_message_with_raw_key`
free fn were production-visible but reachable only from unit tests — production
JIT single-key signing goes through `SecretAccess` + `DetSigner`, which signs
inline. Move all three behind `#[cfg(test)]` (next to the tests module) and gate
their now-test-only `Message`/`Signature` imports. Delete the dead
`has_passphrase` method (zero callers) and its stale doc.
Audit R2 CODE-010.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallet): dedup secret decrypt, bincode framing, prompt meta & key mapping
Wave 1 of the R2 architecture audit — secret access & signing surface dedup.
No behavior change: error mappings and length validation are preserved exactly.
- CODE-003: extract one `decrypt_message` + two-variant `DecryptError` in
model/wallet/encryption.rs; route all three AES-GCM legacy readers
(decrypt_hd_seed, SingleKeyEntry::decrypt, ClosedKeyItem::decrypt_seed)
through it. Those readers are retained deliberately — they decode secrets
already written to users' disks pending the lazy Tier-2 re-wrap, so this is
the safe local dedup, NOT the upstream per-secret migration. See
docs/ai-design/2026-07-08-secret-decrypt-dedup. Wave-16 CODE-087 still applies
unchanged (encryption.rs is kept).
- CODE-031: make `identity_key_from_bytes` and `identity_flavored` pub(crate);
route IdentityKeyView::get/get_protected through the shared length check
(standardizing get's mapping onto IdentityKeyMalformed); drop the hand-rolled
SecretSeam->IdentityKeyVault map in seal_new_identity_key_with_password.
- CODE-038: extract `versioned_bincode::{encode_tagged, decode_tagged_or}`;
wire wallet_seed_store and SingleKeyEntry encode/decode through it, per-format
fallback staying a closure.
- CODE-039: maybe_remember takes the plaintext by value and moves it into the
cache box (copied once, not twice); the duplicated per-variant boxing match
is gone.
- CODE-050: merge the identical WalletPromptMeta/IdentityPromptMeta into one
PromptMeta.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallet-backend): dedup KV/sidecar layer
Consolidate the machinery behind the network-scoped KV sidecars and
offline caches so five near-identical patterns live in one place.
- Generic `SidecarView<V>` (new `sidecar.rs`): the wallet-meta,
identity-meta, and auth-pubkey-cache views become thin typed wrappers
parameterised by infix, value type, scope, and error mapper. Domains
needing a legacy-format fallback override `SidecarValue::read`
(WalletMeta's dual-format migration + one-shot re-store).
- One `network_prefix` in `kv.rs`, imported at 7 sites (SPV storage
paths, migration sentinels, the four sidecar keys). The MCP
`network_display_name` keeps its distinct Regtest->"local" mapping.
- `kv_get_logged` / `kv_get_or_default` in `kv.rs` replace seven
read-then-default/None call sites (selected wallet/identity, DashPay
timestamps, avatar + contact-profile caches, auth-pubkey cache via the
generic view).
- One `map_kv_storage_error` funnel behind all five sidecar error
mappers; adds `TaskError::ContactProfileCacheStorage` so contact-profile
failures stop misreporting as `AvatarCacheStorage`.
- One `bip44_account0_xpub` helper replaces four copies of the
fund-routing gate's BIP44 account-0 predicate (2 production + 2 test).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(dashpay): typed errors + move derivation to model/ (Wave 13)
Mechanical typed-error and placement pass across the DashPay backend
subdomain, plus supporting cleanups.
- CODE-068 + CODE-051: move the pure DIP-14/DIP-15 derivation files to
`model/dashpay_derivation/{dip14,hd}.rs` and introduce a typed
`DerivationError` (`#[from]` bip32/secp256k1), replacing ~stringly
`Result<_, String>` derivation signatures. Wire it into `DashPayError`.
- CODE-051: replace the four `DashPayError::Internal { message }` boundary
wraps with typed `TaskError` at their source functions
(`payments::load_payment_history`, `register_dashpay_addresses_for_identity`,
`generate_auto_accept_proof`); add a typed `KeyVerificationError` for the
three identity voting/owner/payout key checks; carry the per-vote
`DPNSVoteResults` error as `Arc<TaskError>` instead of a pre-formatted
String.
- CODE-053: carry DashPay payment amounts as `u64` duffs end-to-end
(task field, backend fn, `DashPayPaymentSent` success variant); the UI
resolves duffs at its edge — no f64 crosses the backend boundary.
- CODE-055: drop the redundant `encryption_tests.rs` (its three checks are
already covered by superior `#[cfg(test)]` tests in `encryption.rs`);
delete the module decl and the dead task builders.
- CODE-059: migrate the two UI callers to `Display`; delete `user_message()`.
- CODE-060: delete `ckd_pub_256`, the legacy `send_payment_to_contact`
wrapper (rename `_impl`), the dead `profile::send_payment` /
`identity_to_child_number`, `From<String>`, the `ToDashPayError` trait,
the `DashPayResult` alias, the unused error-factory helpers, and the
now-unconstructed `Internal` + dead taxonomy variants, with their
speculative allows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallets): single-source fee/amount/dpns and drop dead send surface
Wave 4 architecture-audit cleanup. Consolidates duplicated fee, amount, and
DPNS logic onto single sources of truth and removes a dead send dialog.
- CODE-090: replace the 9-arg estimate_contract_create_detailed with a
ContractComponents struct; delete the argument-ignoring from_platform_version
(no callers); /1000 -> /CREDITS_PER_DUFF; derive CREDITS_PER_DASH from
DASH_DECIMAL_PLACES.
- CODE-095: move estimate_platform_fee, both transition-based estimators,
AddressAllocationResult and allocate_platform_addresses[_with_fee] out of
send_screen.rs into model/fee_estimation.rs; hoist ESTIMATED_BYTES_PER_INPUT
to one module constant. Add unit tests for the convergence, shortfall and
fee-payer/destination paths.
- CODE-093: consolidate all credits/duffs->DASH display formatting on the model
formatters (add format_duffs_as_dash); delete the private format_dash/
format_credits copies, the local CREDITS_PER_DUFF re-declaration and the
hand-rolled float conversions; one trimmed-Amount precision policy.
- CODE-077: Amount::partial_cmp returns None unless is_same_token, keeping
ordering consistent with equality.
- CODE-092: strip_dash_suffix branches on has_dash_suffix and slices once;
validate_dpns_input returns a fieldless NonDashDomainError.
- CODE-103: move account_summary into ui/state (non-widget view state).
- CODE-099: delete the unreachable SendDialogState, render_send_dialog,
prepare_send_action, the field/init/render call and orphaned imports.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(dashpay): unify avatar pipeline, profile validation, delete dead editor
Wave 17 — DashPay UI cleanup (three findings).
PROJ-003 + CODE-097 — single avatar pipeline. Add a rendering `Avatar`
component (`ui/components/avatar.rs`) backed by an `AvatarCache` fetch state
(`ui/state/avatar_cache.rs`) that dispatches through the App Task System
(`DashPayTask::FetchAvatar` → `BackendTaskSuccessResult::DashPayAvatar`).
Disk-cache consult/populate now happens once, in the backend task
(`avatar_processing::fetch_avatar_cached`). The three screens
(contact_profile_viewer, contacts_list, profile_screen) delegate to the
component instead of each running a raw `tokio::spawn` with its own texture
map and decode/stash copies. No orphaned frame-loop spawns remain.
CODE-094 — one profile-field validator. Add `validate_profile_fields` plus
`MAX_*_CHARS` constants and `ProfileFieldError` to `model/dashpay.rs`
(char-count, matching the protocol). The backend size check and both editors
(profile_screen, identity settings) delegate to it, collapsing four divergent
copies — including the avatar-URL cap, unified on the DIP-0015 value of 2048.
Empty display name is decided legal (matches the backend and DIP-0015).
CODE-100 — delete the dead `contact_info_editor.rs` (no opener anywhere) and
its `ui/mod.rs` wiring. Extract the shared nickname/note/hidden save into
`persist_contact_private_info`, used by the three live inline editors
(contact_profile_viewer, contact_details, contacts_list).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(tokens): honest fees, shared token executor, grouped contract params
Structural fee honesty and token-op deduplication across the token backend:
- FeeResult.actual_fee is now Option<u64> with a FeeResult::estimated_only
constructor; the 22 sites that faked an actual fee by passing the estimate
twice now report an estimate only, so the UI/MCP no longer claim a settled
fee the platform never returned.
- Extract AppContext::execute_token_op: a single executor owning the SDK-call
error mapping, post-broadcast side effects, and the fee tail. All 11 token
state-transition ops shrink to their builder setup plus a delegation.
- Introduce TokenContractParams, replacing the 26-field RegisterTokenContract
variant and the 26-argument build_data_contract_v1_with_one_token with one
grouped struct; delete the always-NotTradeable marketplace_trade_mode field
and its dead selector (its match arms were identical).
- run_token_task now matches the owned task, moving fields into handlers
instead of cloning, aligning with document.rs/dashpay.rs.
Consumers (send screen, MCP identity/masternode outputs, e2e assertions)
updated for the Option-typed actual fee.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(backend): typed contract recovery + document mutation helper
Remove fragile string parsing and duplication from the contract/document/
contested-names recovery paths:
- Delete extract_contract_id_from_error (parsed the contract id out of an
error message string). register/update now use data_contract.id(), already
in scope before broadcast, and share one AppContext::recover_contract_after_
proof_error helper built on log_drive_proof_error with named refetch delays.
- Add a shared log_contested_proof_error for the GroveDB proof-failure shape
the three contested-resource queries surface, collapsing three identical
logging blocks; run_contested_resource_task now matches the owned task.
- Extract DocumentTask::fetch_document_for_mutation (fetch-by-id + bump
revision) used by transfer/purchase/set-price, and convert the six
multi-field DocumentTask tuple variants to named-field struct variants for
readable construction at the UI call sites.
- Document/contract fee tails switch to FeeResult::estimated_only.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(model): enforce model-layer purity and tidy MCP tool boundary
Wave 15 of the architecture audit — strip upward edges out of `model/`,
type the MCP tool errors, and consolidate the tool helpers.
- PROJ-005: move `RootScreenType`/`ThemeMode` into `model::settings`
(re-exported from `ui`); invert the `model → backend_task` error edges in
`passphrase.rs` and `wallet/mod.rs` via model-local error enums wired into
`TaskError` with manual `From` impls. `qualified_identity` keeps `TaskError`
because the `SecretAccess::with_secret` chokepoint contract requires it
(documented).
- CODE-070: relocate `FeatureGate` + predicate to `context/` (dead
`FeatureGateUiExt` deleted); move token `From` conversions next to their UI
types; `IdentityStatus → Color32` mapping to `ui::theme`; model-local
`MasternodeInputError` converted to `McpToolError` at the tool boundary.
- CODE-072: move GroveSTARK proof generation/verification into
`backend_task::grovestark` (data types + serialization stay in `model`),
drop 59 info-level hex dumps, type `GroveSTARKError` with `#[source]` fields.
- CODE-073: `qualified_identity_public_key` extracts `find_wallet_path` and
warn-skips malformed network-supplied key data instead of panicking.
- CODE-080/081/082/083: delete dead items (gate test-only ones with
`#[cfg(test)]`), collapse the auth-key accessors behind
`authentication_keys_matching`, drop dead `RequestType` u8 conversions and
rename the module to `request_type`, delegate `borrow_decode` to `decode`.
- CODE-079: `tool_ctx()` returns `McpToolError` natively — kills the lossy
`McpError → String` round-trip at all 26 tool invocations.
- CODE-085: fold the blank-`network` check into `resolve::require_network`;
merge `validate_amount`/`validate_credits` into `validate_positive_amount`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(app): collapse boot/reconcilers, fold context_provider, drop identity-hub flag
Wave 12 of the full-repo architecture audit — app.rs boot path, feature
flags, and the context provider.
PROJ-001: collapse the four duplicated eager wallet-backend init shapes
(boot, network switch, post-onboarding auto-start, manual Connect) into one
`AppState::spawn_backend_init` parameterized by a `BackendInitReason`. Extract
the per-frame reconcilers accreted on `AppState` into small `update()`-style
structs under `src/app/reconcilers.rs` — `AccessibilityActivator`,
`SpvBlockReconciler`, `ConnectionBanner`, and `MigrationReconciler` (the latter
two return a `BackendTask` for `AppState` to dispatch, keeping the channel
chokepoint in one place). Reduces `AppState` from ~13 flat reconciler fields to
four cohesive members; no behavior change.
PROJ-004: move scheduled-vote casting off the UI thread into a new
`ContestedResourceTask::CastDueScheduledVotes` backend task. The 60s tick only
dispatches; the DB query, identity load, and casting run in the task; the DPNS
Scheduled Votes screen learns progress via `display_task_result`
(`ScheduledVotesInProgress` then per-vote `CastScheduledVote`). Removes the two
frame-aborting `return`s and names the 2-minute lateness window
(`SCHEDULED_VOTE_MAX_LATENESS_MS`).
PROJ-006: delete the `identity-hub` and `identity-hub-activity-feed` Cargo
features and every `cfg` fallback branch — the hub is now always built and
registered; the Activity tab renders its gated "coming soon" message
unconditionally.
PROJ-008: fold `context_provider_spv.rs` into `context_provider.rs`. Drop the
unused `db` field and `_db` params, make `SpvProvider::new` infallible, and
replace the `Mutex` with an `Arc<ArcSwapOption>` so the manual `Clone` impl and
lock-poison plumbing disappear. Keeps the `SYSTEM_CONTRACT_COUNT` compile check.
PROJ-009: give dir/env/logger boot setup a single owner,
`boot::prepare_environment()`, called from both `main` and
`AppState::boot_inputs`; the logger's `Once` guard keeps the second call a
no-op. Preserves the `testing`-cfg `boot_inputs` split.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(backend_task): update stale proof_log_item import after Wave 15 rename
Missed in the Wave 15 merge commit -- contested_names/mod.rs's
log_contested_proof_error (added by Wave 3) imported RequestType from
the old model::proof_log_item path, which Wave 15 renamed to
model::request_type. Same collision class as the two request_type
import conflicts resolved in that merge, just not flagged by git
since the lines didn't overlap.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(context): tidy contract_token_db — typed encode error, dedup, param & fn moves
Wave 7 of the round-2 architecture audit on context/contract_token_db.rs:
- CODE-018: propagate TokenConfiguration encode failures through a new typed
TaskError::TokenConfigSerialization variant instead of silently no-oping;
drop the pre-C7 tombstone comment.
- CODE-019: extract load_sorted_tokens(); both token listers map its output,
killing the warn-vs-silent drift on unparseable token keys.
- CODE-030: drop three dead parameters — get_contracts pagination
(limit/offset, all callers passed None,None), ConnectionStatus::tooltip_text
app_context, and request_to_det_contact's unused ContactRequest.
- CODE-033: move remove_wallet to context/wallet_lifecycle.rs, next to
register_wallet and its existing tests.
- CODE-044: iterate the system contracts for the five get_contracts insertion
blocks; fold the five system-contract load blocks in context/mod.rs into one
local closure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(backend_task): consolidate error.rs and replace message-as-protocol with typed signals
Wave 14 — error.rs consolidation + typed messages.
CODE-061: add `consensus_cause(&SdkError) -> Option<&ConsensusError>` used by all
three extraction sites; replace the intermediate `ConsensusKind` enum in
`From<SdkError>` with per-arm constructor closures so each of the 13
consensus→TaskError mappings is written exactly once.
CODE-066: replace `TaskError::MustRetry(String)` with the typed
`CoreWalletAutoDetected { wallet_name }` — the sentence lives in `#[error(...)]`,
no user-facing String field. Update the app.rs consumer and the unit test.
CODE-062: delete the `NO_IDENTITIES_FOUND` string constant and the
message-text comparison in tokens_screen; route the no-identities case through
the existing typed `TaskError::NoIdentitiesFound` via a new `display_task_error`
hook (also fixes a latent refresh-spinner hang).
CODE-065: add typed success variants `AssetLockBroadcast { txid }`,
`DashPayAddressesRegistered { addresses, contacts, errors }`, and
`IdentitiesLoaded { count }`; backends stop composing conditional grammar
fragments and sentence assembly moves to the UI/app layer as complete
i18n-clean strings. create_asset_lock_screen reads `txid` from the variant
instead of parsing it out of the message string.
CODE-069: delete the empty `impl BackendTaskSuccessResult {}` and collapse the
identity `match` in `run_backend_tasks_sequential` to a direct push.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(context): consolidate wallet_lifecycle orchestration (Wave 11)
Wave 11 placement/dedup pass over context/wallet_lifecycle.rs, no behavior
change to production flows.
- Fold the start_spv -> spawn_backend_start -> run_backend_start chain into
the async ensure_wallet_backend_and_start_spv chokepoint (the only
production start path); retarget the two orphaned tests onto the
wallet_backend() wiring gate and the wiring-does-not-start invariant.
- handle_wallet_unlocked now takes `&str` instead of `Option<&str>`, making
the inert no-passphrase case unrepresentable: delete the two provably
no-op `(wallet, None)` callers (cold-boot bootstrap loop and
try_open_wallet_no_password) and gate the unlock popup's call to the
keep-unlocked branch only. The deferred non-remember-registration note
moves verbatim to the popup's non-remember branch, where that decision
now lives. try_open_wallet_no_password keeps a dead `_app_context` param
for now (TODO(cleanup) logged) to avoid a ~40-callsite UI sweep mid-batch.
- Hoist one shared `copy_dir_recursive` test helper (two nested copies) and
route the two inline `INSERT INTO wallet` blocks through the existing
seed_legacy_unprotected_hd_wallet_row helper.
- Strip the issue7 test's eprintln scaffolding, rewrite its assertions in
present-tense guard voice (a pass is now the invariant, not a "reproduced"
bug), and rename migration_status' failed_state test to state what it
asserts.
- Add a module-doc note marking wallet_lifecycle.rs as the intentional thin
AppContext-delegation layer, distinct from wallet_backend's upstream
orchestration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(identities): consolidate funding-method chooser + preserve explicit picks
Wave 5 — identity funding screens + PR-869 fold-ins.
CODE-096: move `FundingMethod` (enum, labels, `default_funding_state`) into
`funding_common.rs` so the create-identity and top-up screens share one type;
extract a `wallet_selection_combo` picker (parameterised label + enabled) used
by all three screens and an `actionable_asset_locks` gate shared by both
unused-funding pickers; fix the ungrammatical wallet-selection heading to an
i18n-clean sentence.
F2: sweep the residual RwLock-poisoning `.read()/.write().unwrap()` pattern in
the funding-screen cluster to graceful forms — `TopUpIdentityScreen` gains
`current_step`/`set_step`/`current_funding_method` helpers; the balance and
asset-lock sub-screens and both `by_platform_address` step sites degrade
instead of panicking; `update_wallet`'s `.expect("wallet lock poisoned")`
becomes `is_ok_and`.
F3: track whether the user has explicitly chosen a funding method. On a wallet
switch the create screen recomputes its default pre-selection only while no
explicit choice has been made; once a method is picked, a switch preserves it
untouched (new pure `funding_method_after_switch`, unit-tested for both cases).
Removes the anchoring `TODO(bilby)`.
F4: the single-wallet Top-Up default now uses
`spendable_covers_minimum(spendable, estimate_identity_topup())` instead of
`snapshot_has_balance`, so a dust/locked balance is never pre-selected then
immediately blocked.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(context): remove write-only Core-RPC health plumbing (CODE-016)
The rpc_online / rpc_last_error fields, their getters/setters,
update_from_chainlocks, the handle_task_result ChainLocks arm, the
set_rpc_last_error(None) writer in core, and the dead reset_timer had
zero readers anywhere — connection health is sourced entirely from SPV
and DAPI state. Delete them; the surviving ChainLock arm just refreshes
overall state.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(context): make settings updaters atomic, drop dead shims (CODE-027, CODE-042)
CODE-027: the update_* methods did a read-modify-write across two
separate lock acquisitions (get_app_settings then set_app_settings),
so concurrent updates could lose writes. Introduce update_app_settings,
which runs the whole read → mutate → persist cycle under one held
cached_settings write guard (the same SettingsCacheGuard scheme), and
route every updater through it. Factor the uncached load out of
get_app_settings so both paths share it. Add a sequential RMW test and
a concurrent-flip test proving no field update is lost.
CODE-042: delete the zero-caller get_settings legacy shim,
update_user_mode, and update_show_evonode_tools.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(context): extract shared platform-address seeding loop (CODE-023)
apply_platform_address_push and warm_start_platform_addresses carried an
identical per-address inner loop (hash → canonical address → info-write →
signer registration), differing only in set- vs seed-if-absent semantics.
Extract seed_platform_address_entries taking the info-write as a closure;
both entry points become thin batch adapters.
Also collapses default_platform_version's four identical match arms to the
constant (CODE-045).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(context): name DPNS contest-duration constants (CODE-040)
Replace the inline 14-day / 90-minute literals in
contest_duration_for_network with named MAINNET_CONTEST_DURATION and
NON_MAINNET_CONTEST_DURATION constants carrying a spec reference, and
note that the joinable window is the first half of the contest.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: small context/kv one-liner cleanups (CODE-045)
- Inline ConnectionStatus::spv_connected (status.is_active()) at its sole
caller and delete the wrapper; drop the now-unused import.
- delete_scheduled_vote / insert_scheduled_votes take &str / &[..] instead
of &String / &Vec, dropping the clippy::ptr_arg allow.
- Delete the map_kv_error identity wrapper; call sites use
KvAdapterError::Store directly, with its rationale folded into the
variant doc.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): Wave 19 — dedup UI component surface
CODE-107: Extract one generic add_subscreen_chooser_panel + nav_button
(subscreen_chooser_panel.rs); the four dashpay/dpns/tokens/tools panels
now build item lists and delegate. Move ToolsSubscreen next to the tools
screens (ui/tools/mod.rs), matching its siblings.
CODE-109: Extract modal_chrome (backdrop + centered bordered Window) from
passphrase_modal; rebase passphrase_modal and the confirmation/selection/
info dialogs onto it. Dedupe the NOTHING sentinel into modal_chrome. Drop
ConfirmationDialog's write-only `status` field (current_value never read it).
CODE-110: Rewrite ScreenType::eq as explicit payload arms + discriminant
equality (no `_ => false`). Collapse the seven ScreenLike delegation methods
onto one exhaustive delegate_to_screen! macro — a missing variant is now a
compile error. (Macro, not a dyn accessor, to preserve inherent-method
resolution for screens with an inherent refresh.)
CODE-113: Extract key_eligibility, render_key_combo, render_no_eligible_key_group
and render_info_section in ui/helpers.rs; the two key choosers and two success
screens delegate. Unify the divergent no-eligible-key wording.
CODE-115: Wire the contract-chooser right-click via Response::context_menu on
the header row (Copy Hex / Copy JSON), removing the unreachable manual Window
and its three dead state fields; derive Default; flatten the font-size ladder.
CODE-117: Extract stage_progress + window_fraction in network_chooser_screen;
the five progress calculators shrink to thin wrappers.
Also folds in a stray cargo-fmt import-order fix in
backend_task/contested_names/mod.rs (Wave 15 rename residue).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallet): Wave 16 — wallet model internals
Model-layer cleanups across the wallet crate. All error paths in
model/wallet now carry typed WalletError up to the TaskError boundary;
the platform-payment registration, coin-type mapping, and AES-GCM
envelope shapes are single-sourced.
- CODE-071: model/wallet/mod.rs derivation/registration functions return
Result<_, WalletError> instead of stringly errors (~17 KeyDerivation
erasures + 5 bare erasures gone). New typed WalletError variants:
PublicKeyParse, AccountDerivationPath, PlatformAddressConversion,
AddressNetworkMismatch. TaskError::WalletAddressProviderSetupFailed
now holds a typed #[source] instead of a String. database/utxo.rs
get_utxos_by_address returns rusqlite::Result.
- CODE-076: delete Wallet::coin_type + four inline network matches; call
the canonical coin_type_for_network everywhere.
- CODE-078: extract Wallet::register_platform_payment_entry; the five
duplicated known/watched insert blocks collapse to one call. The
vestigial post-T-W-01 `register` param on
generate_platform_receive_address_with_seed is dropped.
- CODE-087: replace the (Vec<u8>,Vec<u8>,Vec<u8>) crypto triple (and its
type_complexity allows) with a named EncryptedEnvelope struct.
- CODE-088: one summary log per platform-address sync; the per-key dumps
in WalletAddressProvider::on_address_found and QualifiedIdentity::sign
move behind their failure paths, off the hot loop.
- CODE-089: delete the unused DASH_SECRET_MESSAGE constant.
- CODE-091: cross-link + status-mark the three single-key modules (LIVE
imported-key sidecar vs the LEGACY Decision-#7 runtime/DB pair).
cargo clippy --all-features --all-targets -D warnings clean; full lib
suite (1364 tests) green.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(wallet): fold shielded screens into unified send screen (CODE-098)
The three standalone shielded screens (shield / shielded-send / unshield)
duplicated flow logic the unified WalletSendScreen already owns end to end
(dispatch + result handling for every shield/unshield/private-send path).
Consolidate them into routes on the one canonical send screen.
- Add `SendFlow` preset {General, Shield, ShieldedSend, Unshield}. A preset
locks the source — and, for Shield, auto-targets the wallet's own pool with
a sentinel shielded destination (the shield dispatch ignores the destination
address) — so the screen shows only the controls that flow needs while
reusing the unified validation, fee/amount limits, and dispatch.
- Extract shared shielded-recipient parsing into
`model::address::parse_shielded_recipient` (Bech32m or 43-byte hex) so the
send dispatch and any validation path cannot diverge.
- Route the Shielded tab's Shield / Send (Private) / Unshield buttons to open
the unified send screen pre-configured for the flow.
- `ScreenType::WalletSendScreen` now carries the `SendFlow`; remove the three
`ScreenType`/`Screen` shielded variants and delete the standalone screen
files.
Behavioral parity preserved for all five shielded dispatches. One delta:
raw-hex shielded recipient entry is dropped from the UI (AddressInput accepts
canonical Bech32m only); the parser still accepts hex at dispatch.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(tokens): Wave 18 — unify token action screens + dedup rules renderers
Collapse the seven near-identical single-shot token action screens (pause,
resume, freeze, unfreeze, destroy-frozen-funds, burn, mint) onto one
parameterised scaffold, and merge the two duplicated control-rule renderers.
- Add `TokenActionScreen<A: TokenAction>` (ui/tokens/token_action_screen.rs):
the shared shell — authorization resolution, wallet unlock, advanced key
chooser, per-action form slot, public note, fee estimate, confirmation
dialog, success screen and status. Each action supplies only its diffs
(labels, rules accessor, form, task builder, success variant) via the
`TokenAction` trait; the seven screens become thin `type` aliases + a small
action struct. Net ~3,900 lines removed.
- Fold the token authorization check into the shared scaffold's one call site
via the previously zero-caller `check_token_authorization`; also route
`update_token_config`'s inline resolver through it.
- Merge `render_mint_control_change_rules_ui` into
`render_control_change_rules_ui` via an optional `MintRecipientSection`,
deleting the ~180-line duplicate; update all call sites.
- `change_context` now rebinds token-screen context through `.common` via a
`set_app_context` setter and a `common_set` macro arm.
Deferred (TODO markers in-file): set_token_price migration onto the scaffold
(large pricing form; verb-based auth message template does not fit "set price"
grammatically) and the TokensScreen god-struct per-subscreen split.
cargo fmt + clippy (all-features, all-targets) clean; lib unit tests and the
kittest suite pass.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(wallet): address QA findings on shielded send-flow fold
- QA-002: Shield-from-Platform "Max" now reserves the two-action shielded-fee
headroom (>50M credits) instead of the plain platform-transfer estimate
(~8M). ShieldFromBalance pays the shield fee from the same balance as the
amount, so the old reserve under-shot ~6x and a Max attempt was rejected
upstream. New model helper `shield_from_balance_fee_headroom` keeps the fee
math in model/fee_estimation.rs. Shield-from-Core parity was already correct.
- QA-001: restore raw-hex shielded-recipient entry. `AddressKind::detect` and
`AddressInput::validate_shielded` now accept the 43-byte (86-hex-char) form
via the shared `parse_shielded_recipient`, matching what the old private-send
screen advertised.
- QA-003: on a network switch, `WalletSendScreen` now drops the wallet seed
hash, source, destination, and amount (`reset_for_network_switch`) so a
preset flow can no longer resurrect stale cross-network state / balance.
Tests: model coverage for the fee headroom (asserts the shielded-fee reserve,
not the transfer estimate) and for hex shielded detection at both the model
and AddressInput layers.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs(comments): Wave 21 — comment/narration hygiene sweep
Strip development-history narration and ephemeral review IDs from comments;
keep present-state invariants only. No executable logic changed.
- CODE-021: rewrite archaeological rationale (det_platform_signer, snapshot,
wallet_backend/mod, secret_seam, wallet_seed_store, wallet_lifecycle
stop_spv); dedup the AlreadyOpen restart-in-place rationale to one copy.
- CODE-067: present-state the Phase B/D/E + Post-D4c narration in
backend_task/shielded/mod.rs and dashpay.rs; stale "until Phase-E lands"
reworded now the push writer exists.
- CODE-084: drop RUST-001 / 6a2818cd IDs from fee_estimation.rs and
wallet/single_key.rs comments; keep the durable TS-DBG-01 test-spec ID.
- CODE-102: delete the shielded_tab tombstone (keep "Fund-moving results
only."); present-state the "replaces the dropped/legacy…" docs in
wallets_screen, contacts_list, contact_details.
- CODE-106: drop INTENTIONAL(CMT-010/RUST-003/CODE-003) prefixes and FIX N
markers in theme, message_banner, address_input, add_new_identity_screen,
key_info_screen.
- PROJ-007: remove the removed-subsystem ZMQ sentence and the "RPC, ZMQ"
ConnectionStatus residue from CLAUDE.md; protoc v25.2+ verified against CI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs(changelog): add round-2 audit user-facing changes
Adds Changed/Fixed entries for the genuinely user-observable outcomes
of the round-2 architecture audit (21 waves + CODE-098): the shielded
screens' fold into the unified Send screen, DashPay's now-optional
display name, two identity-funding UX fixes, the My Tokens
loading-spinner hang, and a settings-save race. Internal refactors,
dedup, and error-typing from the same round are intentionally omitted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(identity): confine hero card gradient to card bounds
The Identity Hub hero card painted its 14%-opacity gradient band using
`ui.max_rect()` — the available space, not the card's content bounds — so
on the Home tab the band bled downward through every sibling widget below
the card (quick actions, onboarding checklist, recent activity).
Reserve a shape slot before laying out content, then fill it afterward
from `ui.min_rect()`, confining the band to the card's actual bounds. Add
unit tests asserting the strips stay within the given rect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(errors): replace stringly-typed error variants with typed sources
Convert the largest cluster of `detail: String` TaskError variants to typed
`#[source]`/`#[from]` variants and delete the dead ones, tightening the
Display/Debug separation and reducing `result_large_err` pressure.
- platform_info: introduce a `WithdrawalParseError` source enum (platform-value
field errors, missing/invalid timestamps, unrecognized status, boxed
ProtocolError for the daily limit). `WithdrawalDocumentParsingError` now wraps
it via `#[from]`; the two duplicated per-document formatting blocks are
extracted into `format_withdrawal_line` / `format_completed_withdrawal_line`.
`ShieldedSyncFailed` now carries `Box<SdkError>`.
- key input: move `verify_key_input` out of the backend into `model/key_input.rs`
as the stateless single source of truth, returning a typed `KeyInputError`
(NotHex / BadWif / UnsupportedLength) with complete, i18n-ready sentences.
`KeyInputValidationFailed` becomes a transparent `#[from]` wrap, removing the
double-naming and fragment concatenation; callers use `?`. Unit-tested.
- dashpay: delete the unconstructed `reason: String` variants BroadcastFailed,
QueryFailed, PlatformError, RateLimited and their dead retry-classification
clone arms; make the sole remaining recoverable variant `NetworkError`
fieldless.
- delete unconstructed TaskError variants (WalletUtxoReloadFailed,
WalletBalanceRecalculationFailed, WalletPaymentFailed, UtxoUpdateFailed,
SerializationError, RpcProviderCreationFailed, AssetLockTransactionBuildFailed,
ShieldedTreeUpdateFailed, ShieldedNullifierSyncFailed,
ShieldedMerkleWitnessUnavailable); make InvalidPrivateKey and
NetworkContextCreationFailed fieldless (their strings were hardcoded, not
upstream errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(contested-names): point contract-not-found TODOs at issue #875
The 'contract not found when querying from value with contract info'
substring match in the three contested-name query retry loops is a
deliberate workaround: the condition originates server-side as
QuerySyntaxError::DataContractNotFound and reaches the client only as a
gRPC Internal status with message text, so no client-side structural SDK
variant exists to match on yet. Reference the tracking issue at all three
sites.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(wallet): unify lock-poison recovery, route single-key I/O through the secret seam, and type encryption key hygiene
Three wallet-backend hardening changes.
- Lock-poison policy: add `wallet_backend/poison.rs` with `read_recover` /
`write_recover`, which recover a poisoned `RwLock` guard instead of erroring.
The single-key in-memory index and the secret session cache guard derived,
rebuildable state, so recovery is correct and self-healing. Route the
single-key index (imports, alias, forget, rehydrate, list) and the
`SecretAccess` session cache (lookup, eviction, remember) through it, deleting
the two lying poison mappings — `ImportedKeyNotFound` (told the user to
re-import) and `SecretDecryptFailed` (claimed a decrypt failure).
- Secret seam: route every raw vault access in `single_key.rs` through
`SecretSeam::{put_secret, get_secret, put_secret_protected, delete_secret}`
instead of hand-rolled `SecretStore` calls, so imported keys honor the same
chokepoint and failure variant (`TaskError::SecretSeam`) as their siblings.
The verify-passphrase wrong-password signal is preserved by matching the
seam's typed `SecretSeam` source structurally.
- Encryption hygiene: `derive_password_key` returns `Zeroizing<Vec<u8>>` and
`ClosedKeyItem::decrypt_seed` returns `Zeroizing<[u8; 64]>` (copied straight
into the zeroizing buffer, no bare stack copy), so the derived AES key and the
decrypted seed wipe on drop. Introduce a typed `EncryptionError`
(WrongPassword / Malformed / KeyDerivation / Encryption, Everyday-User
messages) and return it from the encryption primitives and the two
`ClosedSingleKey` crypto methods, replacing their `Result<_, String>`. The
broad `SingleKeyData::open` / `WalletSeed::open` / `SingleKeyWallet::new`
String APIs (pre-existing model/wallet debt that mixes crypto with key-parse
errors) render the typed error through `Display` at the boundary; a full
type-through of those APIs is deferred.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(address): centralize network-prefix validation in model/ and gate the credits-to-address MCP destination
Two coupled changes around address↔network validation.
- Single source of truth: add `validate_platform_address_for_network` and its
Orchard twin `validate_orchard_address_for_network` to `model/address.rs`,
returning a typed `AddressNetworkMismatch` (Everyday-User message). They parse
the bech32m HRP (`dash1…` mainnet, `tdash1…` testnet, case-insensitive) and
reject a cross-network address. `address_input.rs` delegates its hand-rolled
`validate_platform` / `validate_shielded` prefix checks to these, keeping the
same banner copy; the GUI's format/length/case checks stay put.
- Close the MCP network gap (funds-adjacent): `identity_credits_to_address`
decoded the destination with `PlatformAddress::from_bech32m_string` and no
network check, so a mainnet `dash1…` address could be paid on testnet (or
vice-versa). Gate the parsed destination against `ctx.network()` via the new
model validator, returning `InvalidParam` on mismatch — mirroring the withdraw
tool's Core `require_network` guard. The required `network` param only pins the
active network; it never validated the destination, which was the gap.
The `resolve::validate_address` first-Base58-char Core heuristic is left as-is:
it is a network-agnostic format sanity check with no network parameter, and Core
network validation is already performed via `Address::require_network` at each
tool. Adding a model Core validator would duplicate that with no clean
delegation target, so it is deferred rather than widened here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(wallet): add AppContext::wallet_arc helper; log skipped corrupt address rows; drop per-item logging loops
Three wallet-layer cleanups from PR review.
- Dedup wallet lookup: add `AppContext::wallet_arc(&self, &WalletSeedHash) ->
Result<Arc<RwLock<Wallet>>, TaskError>` as the single source of truth for the
"look up a wallet arc or `WalletNotFound`" pattern, replacing 15 copy-paste
blocks across backend_task/{wallet,identity,shielded} and context. The helper
recovers a poisoned lock via `wallet_backend::poison::read_recover` rather than
erroring: the in-memory wallet map is rebuildable, so recovery matches the
Wave-2 poison discipline for rebuildable state. This is a behavior change on
the rare poison path — the replaced sites previously surfaced `LockPoisoned`
via `.read()?`; they now self-heal, consistent with `mcp::resolve::wallet_arc`
(which now delegates to this helper and re-wraps the id-bearing MCP error).
- Loud failure on corrupt rows: `database/wallet.rs` `get_wallets` skipped
unparseable address rows silently. Silently dropping an address could hide a
missing key and lead to lost funds, so log a `warn!` (row index + error) and
continue, matching the wallet_backend hydration skip-logging discipline.
- Drop per-item logging loops: `fetch_platform_address_balances` logged every
address/balance/nonce and `transfer_platform_credits` logged every input
address/amount at `info!`. Both keep their aggregate summary line; the
per-address financial detail is removed — it violates the no-loop logging rule
and should not sit in plaintext logs at the default level.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: replace panicking unwrap/expect in production paths with error propagation and graceful UI degradation
Eliminate every bare `.unwrap()` on the non-test production paths (~150 sites)
so a broken invariant degrades instead of aborting the process. Applied by
category:
- Lock poison recovery: extend `wallet_backend/poison.rs` with a `Mutex`
`lock_recover` free fn plus ergonomic `RwLockRecover` (`read_recover` /
`write_recover`) and `MutexRecover` (`lock_recover`) extension traits. Route
the wallet/step/settings `RwLock` and the DPNS/identity/token-search `Mutex`
screen-state locks — all rebuildable in-memory state — through them, so a
thread panicking mid-update never wedges the UI. Unify the coordinator gate's
action mutex on the same recovery policy (its `should_fire` already tolerated
poison via `is_ok_and`, while `arm`/`try_fire`/`reset` panicked). The
`Mutex<Connection>` gains a `Database::locked_conn()` helper that recovers the
guard: a `rusqlite::Connection` carries no invariant a panic can break, so
recovering avoids cascading one unrelated panic into every later DB call.
- Graceful UI fallback: egui frame paths return `AppAction`/`BackendTask` and
cannot `?`. The `Option::unwrap()` selection reads now guard with `let-else` /
`if let` and degrade — an actionable `MessageBanner`, an early return, or a
logged skip — instead of panicking. The document-action builders share a new
`require_selections()` helper that returns `BackendTask::None` with a banner
when a selection is missing.
- Invariant expects: unwraps that are provably infallible after a preceding
guard, on constants, or on a construction invariant are upgraded to
`.expect("invariant: …")` so a future regression fails loudly with context
rather than a bare panic. BIP32 child indices are documented as `< 2^31`
(hardened constants, small coin type, and hash indices masked with
`& 0x7FFFFFFF`).
The `dashpay_increment_send_index` mutex keeps its documented fail-loud
poison contract: a panic mid-increment can leave the address-index counter
inconsistent, and surfacing that is safer than handing out a duplicate index.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: remove ephemeral review-IDs, tombstones, speculative dead_code, and a redundant predicate
Four small cleanups; every deletion grep-verified against both the lib and the
integration-test crates first.
- Ephemeral review-IDs: strip the per-run tags from committed source — the
`RUST-002` tag on the app.rs banner-flash TODO (the substance and the #660
issue link stay) and the `CODE-027` prefixes on two settings_db doc comments
(replaced with a plain statement of the read-modify-write guarantee). The
standards-body `RUSTSEC-2025-0141` reference is deliberately kept.
- Tombstones: drop the two deleted-code tombstones in `database/wallet.rs`
`get_wallets` (git is the record) and renumber the trailing "step 8" to
"step 4" so the step comments read coherently.
- Dead code: delete genuinely-unused speculative items and the code they guard
— three unconstructed `BackendTaskSuccessResult` variants, the `CoreTask::
GetBestChainLock` variant plus its handler / PartialEq arm / classification
test (and the now-orphaned `AppContext::rpc_error_with_url` its handler was
the sole caller of), a duplicate `decrypt_private_data`, an unused avatar
`to_grayscale`, three unused `KeyStorage` accessors, an unused token-info
constructor, a write-only `core_address` field, and a dead visualizer search
field. Items that turned out to be live are handled correctly instead of
deleted: `AppAction::Refresh`, `IdentityTask::SearchIdentityFromWallet`,
`KeyStorage::keys_set`, and `send_screen`'s `selected_wallet_seed_hash` had
stale `#[allow(dead_code)]` (the attribute is simply removed); `CORE_APPLICATION`
is non-Linux-only, so it becomes `#[cfg(not(target_os = "linux"))]` and is only
compiled where used; and `MessageBanner::{has_global, set_auto_dismiss}` plus
`build_identity_registration` are exercised by the kittest / backend-e2e
integration-test crates (a separate compilation the lib does not see), so they
keep `#[allow(dead_code)]` with a reason comment rather than `#[expect]`, which
would be unfulfilled under the `--all-targets` gate.
- Redundant predicate: inline `is_distinct_change_candidate` (a one-line
…
The purpose of this PR is to hide the document page on the left panel. We do this because the document screen isn't ready yet.
Summary by CodeRabbit
New Features
Bug Fixes