Skip to content

feat: platform-wallet backend rewrite (spec + implementation)#860

Open
lklimek wants to merge 640 commits into
v1.0-devfrom
docs/platform-wallet-migration-design
Open

feat: platform-wallet backend rewrite (spec + implementation)#860
lklimek wants to merge 640 commits into
v1.0-devfrom
docs/platform-wallet-migration-design

Conversation

@lklimek

@lklimek lklimek commented May 18, 2026

Copy link
Copy Markdown
Contributor

Goal

Dash Evo Tool carried its own large SPV/wallet stack that duplicated logic now maintained upstream. This PR rewrites DET's wallet backend on top of the upstream platform-wallet crate (dashpay/platform), which owns SPV chain sync, address derivation, identity/asset-lock handling, and the shielded coordinator.

DET is now a thin adapter over that crate: its bespoke SPV stack (src/spv/) and the legacy RPC wallet mode are gone, chain sync and derivation come from upstream, and the shielded subsystem routes through the upstream coordinator. The BackendTask action/channel contract is preserved, so the UI layer is largely unchanged. Existing wallets are migrated onto the new model on first launch (marker-gated, with a *.premigration backup). Net effect: far less wallet code to maintain in DET, a single source of truth shared with the rest of the platform, and upstream fixes inherited automatically.

Bugs fixed

Funds-safety

  • Funds sent to addresses beyond the SPV gap window — via Receive → New Address, the asset-lock deposit address, or platform top-up change — were never watched and never appeared in the balance. All address derivation now comes from the SPV-watched pool.
  • The asset-lock deposit QR flow could hang at "Waiting for funds…" forever (the event that advances it had no producer).
  • Incoming DashPay contact payments were never detected or credited (the detection chain had no callers).
  • Creating an identity or funding from wallet balance on a reloaded (watch-only) wallet no longer fails with a missing-private-key error.
  • Withdrawing from a Platform address to a Core address could fail with an internal "address not found in wallet" signer error even though the address's balance was visible in the withdrawal picker — a Platform address discovered by the background coordinator sync was not always registered for signing (watched_addresses could desync from platform_address_info). The wallet now reconciles this automatically via seedless reverse-derivation from the account xpub, so a visible balance can always be withdrawn. No funds were ever at risk: the withdrawal failed safely before broadcast.
  • send_payment now builds and signs through upstream's native TransactionBuilder (upstream removed the previous CoreWallet::send_to_addresses API) and broadcasts via broadcast_transaction_releasing_reservation, so a rejected broadcast can't leak a UTXO reservation. Error messages are now variant-specific — a "too many inputs" failure no longer tells the user to check their balance.

Balance display

  • The Wallets selector's platform balance no longer diverges from the per-address total (it previously over-counted non-owned "orphan" addresses, reading up to ~227× too high). The selector total and the per-address tab now derive from the same coordinator-pushed data and are equal by construction.
  • The wallet screen's balance breakdown could show more Dash than its Core or Platform account tabs added up to, on wallets that had handed out many addresses within a session (e.g. 75.62 vs 2.16 DASH on Platform). Root cause: DET was discarding derivation-index metadata upstream already provides, and the per-category summary only ever looked at DET's own address list, not the backend's authoritative one.
  • The header-vs-tab divergence is now closed at the source, not policed after the fact. The per-account breakdown (Core tabs, Platform tab) and the wallet header total are read from one snapshot generation — including under lock contention, where the entire prior snapshot (not just part of it) is carried forward, so the two figures are never spliced from different moments. Platform's tab reads the exact same accessor as the header (platform_balance_duffs), so it can't lag behind. A stopgap runtime health-check banner that used to detect this class of drift after the fact has been removed — it's structurally unreachable now. Default-mode users also get a reconciling tab for any balance in a normally-hidden category (e.g. CoinJoin, or a funded address the managed-wallet pools don't recognize), so visible tabs always sum to the header total, in every mode. The Platform tab is shown immediately on wallet load (matching pre-migration behavior) rather than waiting on a completed sync pass. This fix went through two independent adversarial QA passes; the first caught that the initial version's "cannot diverge" claim was true for Platform but not yet for Core (a deeper root cause — an upstream balance-update path that could silently drop updates under its own lock contention), the second independently re-verified the fix against the actual upstream source and confirmed it closed.

Shielded confirmation-safety

  • Shield-from-asset-lock and the four sibling shielded spends reported success / marked notes spent even when the transaction might never have confirmed. They now surface typed *ConfirmationUnknown errors instead of a false success.

Secret hygiene

  • The passphrase was prompted at startup for wallets the user hadn't chosen to unlock; it is now requested just-in-time per operation and dropped immediately after use.
  • A single-key private key was kept in plaintext for the whole session — removed.
  • DashPay ECDH and HD encryption keys are now zeroized on drop.

Data-safety

  • Password-protected single-key wallets that silently vanished after upgrade are preserved.
  • Wallets left unloaded after the cold-start migration (which required a manual restart) are now rehydrated automatically.

Lifecycle / sync

  • User-set identity aliases are preserved during auto-discovery (previously silently overwritten).
  • Platform/identity sync no longer starts before chain quorum is ready (which previously caused a DAPI ban storm).
  • Disconnect → Connect no longer fails to reopen the wallet database (the SPV persister is now released on shutdown).
  • The wallet could wait forever with no feedback if its storage backend was ever unusually slow to finish preparing (e.g. after a network switch or cold start). The app now shows an actionable message after 30 seconds instead of leaving the wallet silently invisible. (Also confirmed, with a new regression test, that a previously-suspected "double-open blocks chain sync" failure mode is not reproducible in the current code.)

Housekeeping

  • Retired Core-RPC backend plumbing removed. The old RPC/SPV backend-mode selector (core_backend_mode settings field + DB column, FeatureGate::RpcBackend) was already fully inert — chain sync has been SPV-only since this migration started — but the dead weight itself was left behind pending a dedicated schema migration. It's gone now: settings field removed (with a reserved wire byte to keep the existing bincode-encoded det:settings:v1 blob layout intact — no user data at risk), DB column dropped via a new backward-compatible v38 migration (existence-guarded, idempotent, no drop-and-resync needed), and the always-false feature gate removed. Verified: RPC config fields, chain-lock RPC error plumbing, and send_screen's Core/Platform source picker are genuinely still live and were left untouched.
  • debug_assert! invariant guards replaced with real runtime checks. Two sites relied on debug_assert!, which compiles out in release builds — silently disabling the check for every real user. The SPV phase-count drift guard now logs an error instead of vanishing; the tracked-lock funding empty-recipients precondition is now a typed TaskError returned in every build.
  • RUST_MIN_STACK raised to 8 MiB project-wide via .cargo/config.toml, independent of the backend-e2e suite's own dedicated 32 MiB test runtime.
  • Platform pin advanced twice: e6b508f1c2135800 (108 upstream commits; API drift notes below), then c213580044c20e3 to pick up the TransactionBuilder API after upstream removed send_to_addresses (see Bugs fixed → Funds-safety). First bump's adaptations: ManagedIdentity DashPay state sealed behind a dashpay() accessor; ContactXpubData split into { xpub, compact } with the DIP-15 account_reference now HMAC'd over the 69-byte compact xpub (an upstream fix matching the iOS/Android reference clients — DET's own pinned test vector was re-verified against upstream's own known-answer test and re-pinned to the new interop-correct value); IdentityWallet::dashpay_sync() replaced by explicit sync_contact_requests + sync_profiles calls; PlatformWalletError gained 5 variants / dropped 3; a handful of signature changes. Full gate re-verified green after each bump.
  • Simplification audit (34 findings fixed, all independently triaged and reviewed). A dedicated audit of this PR's diff for over-engineering flagged 35 issues; 31 were fixed, 2 deferred with self-contained TODOs, 1 accepted as a documented false-positive. Highlights: the 81-method WalletBackend god-impl split into shielded.rs / identity_ops.rs / payments.rs sibling modules (mirroring the existing hydration.rs/dashpay.rs pattern, zero behavior change, funds-signing path moved verbatim); a 14-copy duplicated InMemoryKv test fake collapsed into one shared kv_test_support module; a dead one-impl PersistedWalletLoader trait seam deleted (the G2 mock-boundary swap it existed for already shipped); several near-twin TaskError variants merged; a stringly-typed warning channel in the wallet-refresh banner replaced with a typed error; stale/self-contradicting migration-module comments (some literally asserting things the code no longer does) rewritten to describe present state only; ~150 ephemeral review-round ID references (SEC-NNN/QA-NNN/etc.) stripped from committed comments per the project's own convention against citing IDs that get reassigned on every consolidation run; two unreferenced UI widgets (ContactRow/ActivityRow, ~750 lines) removed; kittest/e2e test-harness boilerplate deduplicated across 19 files. Full details in the audit's own report artifacts (not part of this PR's diff).

Breaking changes

  • Wallet database must be dropped and re-synced. The upstream platform-wallet pin includes a schema change that rewrites migration V001 in place — it drops the core_derived_addresses and account_address_pools tables and hardcodes core UTXO account_index = 0. Existing spv//platform-wallet.sqlite databases are not forward-compatible and must be deleted and re-synced.

Known gaps

  • Dependency pin — merge-blocker (PROJ-005). The upstream platform-wallet / dash-sdk crates are pinned to an unreleased branch (dash-evo-tool), not a released tag. Must be re-pinned to a released version before merge-to-ship. (Still open — this PR's two in-branch bumps, most recently to 44c20e3, pick up upstream fixes within that same unreleased branch; neither resolves the need for an eventual released-tag pin.)
  • Core balance under-counts after restart-in-place. The header / Balance-breakdown "Core" shows the full persisted balance while the "Dash Core" tab and per-address view under-count to the gap window (e.g. 432.41 vs 7.34 DASH). Watch-only rehydration rebuilds accounts from xpubs and derives only the default gap window (indices 0..=29) without restoring the persisted derivation depth — a regression of bug: Imported wallet shows zero balance — historical UTXOs invisible even after restart #829 Bug 2. The fix belongs upstream in rs-platform-wallet (extend each account's AddressPool to the highest restored index on rehydration); tracked. (Distinct from the header-vs-tab single-sourcing fixed above under Bugs fixed → Balance display — that fix closes a DET-side bookkeeping/consistency gap and does not touch rehydration's gap-window derivation, so this item stays open.)
  • Non-wallet data not migrated (PROJ-034). App settings (network/theme/paths), top-up history, and scheduled DPNS votes reset on upgrade; scheduled votes carry a vote-window deadline risk. Deferred to a follow-up.
  • Single-key wallets partial (PROJ-007). Balance refresh and SPV send for single-key wallets still return SingleKeyWalletsUnsupported, pending upstream watch-only support. Import / data-loss guard / password-restore are done.
  • Minor / deferred: token "stop tracking" is undone by a refresh (PROJ-041); no in-app notice for the removed QR-direct-fund path (DOC-003); external user-docs update pending (PROJ-018); ~2,700 lines of committed AI-review-round design/QA artifacts under docs/ai-design/ are historical record, not user-facing docs — left as-is, flagged for a possible future trim. Merge-time action: fill the ADR floor SHA placeholder (PROJ-019).

Testing

  • cargo build / cargo build --features headless clean; cargo clippy --all-features --all-targets -- -D warnings clean; cargo +nightly fmt --all -- --check clean.
  • Library + UI (kittest) + e2e suites green.
  • det-cli standalone smoke (network-info, tools, tool-describe, core-wallets-list) passes.
  • Funds-safety / security review on the migration, shielded retirement, and address-derivation changes; per-workstream design + QA reports under docs/ai-design/.
  • Simplification audit fixes: each of the 34 items built and verified independently in an isolated worktree, then merged and re-verified together on the integrated branch.
  • Balance single-sourcing fix: two independent adversarial QA passes (one that caught a real gap in the first version, one that re-verified the fix against actual upstream source after the fix). Both passes ran their own build/clippy/full test suite rather than trusting the implementer's report.
  • Final state, full workspace suite: cargo test --all-features --workspace — 1327 lib tests passed (0 failed, 1 ignored), 187 kittest passed, 10 e2e passed, 3 legacy-table-surface passed, all doc-tests passed. cargo clippy --all-features --all-targets -- -D warnings clean.

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 526 files, which is 376 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6aee4814-613a-49e8-b25e-0b9fd9ca0afd

📥 Commits

Reviewing files that changed from the base of the PR and between 87ba5b7 and ee1ce81.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (526)
  • .cargo/config.toml
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • benches/wallet_hydration.rs
  • docs/CLI.md
  • docs/MCP.md
  • docs/MCP_TOOL_DEVELOPMENT.md
  • docs/ai-design/2026-04-22-identity-dashpay-redesign/README.md
  • docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md
  • docs/ai-design/2026-04-22-identity-dashpay-redesign/wireframe.html
  • docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md
  • docs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.md
  • docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md
  • docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/README.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/backend-architecture.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/backendtask-contract.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/data-model-and-migration.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/dip14-migration-hardstop.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/feature-coverage.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/g2-mock-boundary.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/open-questions.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/phasing.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/removal-inventory.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/single-key-mock.md
  • docs/ai-design/2026-05-18-platform-wallet-migration/upstream-reality.md
  • docs/ai-design/2026-05-28-migration-tool/notes.md
  • docs/ai-design/2026-05-29-finish-unwire/notes.md
  • docs/ai-design/2026-06-01-pr860-gap-audit/gaps-report.json
  • docs/ai-design/2026-06-01-pr860-gap-audit/gaps.md
  • docs/ai-design/2026-06-02-jit-secret-access/d4-1112-identity-key-redesign.md
  • docs/ai-design/2026-06-02-jit-secret-access/d4b-authxpub-persistence.md
  • docs/ai-design/2026-06-02-jit-secret-access/design.md
  • docs/ai-design/2026-06-02-jit-secret-access/r3-completion-scope.md
  • docs/ai-design/2026-06-02-rehydration-rewire/design.md
  • docs/ai-design/2026-06-02-signtime-unlock-ux/dev-plan.md
  • docs/ai-design/2026-06-02-signtime-unlock-ux/requirements-and-ux.md
  • docs/ai-design/2026-06-02-signtime-unlock-ux/test-cases.md
  • docs/ai-design/2026-06-03-pr860-doc-followups/external-docs-draft.md
  • docs/ai-design/2026-06-08-wallet-reregistration-fix/design.md
  • docs/ai-design/2026-06-09-platform-wallet-nonce-ownership/upstream-issue.md
  • docs/ai-design/2026-06-09-pr860-full-review/findings.md
  • docs/ai-design/2026-06-16-pr860-grumpy-review/report.json
  • docs/ai-design/2026-06-16-pr860-grumpy-review/report.md
  • docs/ai-design/2026-06-17-blocking-progress-overlay/01-requirements-ux.md
  • docs/ai-design/2026-06-17-blocking-progress-overlay/02-test-spec.md
  • docs/ai-design/2026-06-17-blocking-progress-overlay/03-dev-plan.md
  • docs/ai-design/2026-06-17-blocking-progress-overlay/04-design-addendum.md
  • docs/ai-design/2026-06-17-identity-autodiscovery-gap-limit/design.md
  • docs/ai-design/2026-06-17-identity-autodiscovery-gap-limit/qa-report.md
  • docs/ai-design/2026-06-18-masternode-cli-withdraw/01-requirements-ux.md
  • docs/ai-design/2026-06-18-masternode-cli-withdraw/02-test-spec.md
  • docs/ai-design/2026-06-18-masternode-cli-withdraw/03-dev-plan.md
  • docs/ai-design/2026-06-19-secret-storage-seam/01-ux-disclosure.md
  • docs/ai-design/2026-06-19-secret-storage-seam/02-test-spec.md
  • docs/ai-design/2026-06-30-app-scoped-selection-migration/01-migration-plan.md
  • docs/ai-design/2026-07-06-platform-address-signer-reconciliation/01-design.md
  • docs/ai-design/2026-07-07-asset-lock-finality-retest/retest-findings.md
  • docs/ai-design/2026-07-08-secret-decrypt-dedup/README.md
  • docs/ai-design/2026-07-09-masternode-page-design/01-requirements.md
  • docs/ai-design/2026-07-09-masternode-page-design/02-ux-spec.md
  • docs/ai-design/2026-07-09-masternode-page-design/03-test-case-spec.md
  • docs/ai-design/2026-07-09-masternode-page-design/04-dev-plan.md
  • docs/kv-keys.md
  • docs/personas/platform-developer.md
  • docs/user-stories.md
  • src/app.rs
  • src/app/reconcilers.rs
  • src/app_dir.rs
  • src/backend_task/contested_names/mod.rs
  • src/backend_task/contested_names/query_dpns_contested_resources.rs
  • src/backend_task/contested_names/query_dpns_vote_contenders.rs
  • src/backend_task/contested_names/query_ending_times.rs
  • src/backend_task/contested_names/vote_on_dpns_name.rs
  • src/backend_task/contract.rs
  • src/backend_task/core/create_asset_lock.rs
  • src/backend_task/core/mod.rs
  • src/backend_task/core/recover_asset_locks.rs
  • src/backend_task/core/refresh_single_key_wallet_info.rs
  • src/backend_task/core/refresh_wallet_info.rs
  • src/backend_task/core/send_single_key_wallet_payment.rs
  • src/backend_task/core/start_dash_qt.rs
  • src/backend_task/dapi_discovery.rs
  • src/backend_task/dashpay.rs
  • src/backend_task/dashpay/auto_accept_handler.rs
  • src/backend_task/dashpay/auto_accept_proof.rs
  • src/backend_task/dashpay/avatar_processing.rs
  • src/backend_task/dashpay/contact_info.rs
  • src/backend_task/dashpay/contact_requests.rs
  • src/backend_task/dashpay/contacts.rs
  • src/backend_task/dashpay/encryption.rs
  • src/backend_task/dashpay/encryption_tests.rs
  • src/backend_task/dashpay/errors.rs
  • src/backend_task/dashpay/incoming_payments.rs
  • src/backend_task/dashpay/payments.rs
  • src/backend_task/dashpay/profile.rs
  • src/backend_task/dashpay/validation.rs
  • src/backend_task/document.rs
  • src/backend_task/error.rs
  • src/backend_task/grovestark.rs
  • src/backend_task/identity/add_key_to_identity.rs
  • src/backend_task/identity/auth_pubkey_resolve.rs
  • src/backend_task/identity/discover_identities.rs
  • src/backend_task/identity/load_identity.rs
  • src/backend_task/identity/load_identity_by_dpns_name.rs
  • src/backend_task/identity/load_identity_from_wallet.rs
  • src/backend_task/identity/mod.rs
  • src/backend_task/identity/protect_identity_keys.rs
  • src/backend_task/identity/refresh_loaded_identities_dpns_names.rs
  • src/backend_task/identity/register_dpns_name.rs
  • src/backend_task/identity/register_identity.rs
  • src/backend_task/identity/top_up_identity.rs
  • src/backend_task/identity/transfer.rs
  • src/backend_task/identity/withdraw_from_identity.rs
  • src/backend_task/migration/finish_unwire.rs
  • src/backend_task/migration/mod.rs
  • src/backend_task/migration/single_key_restore.rs
  • src/backend_task/mnlist.rs
  • src/backend_task/mod.rs
  • src/backend_task/platform_info.rs
  • src/backend_task/register_contract.rs
  • src/backend_task/shielded/bundle.rs
  • src/backend_task/shielded/mod.rs
  • src/backend_task/shielded/nullifiers.rs
  • src/backend_task/shielded/sync.rs
  • src/backend_task/system_task/mod.rs
  • src/backend_task/tokens/burn_tokens.rs
  • src/backend_task/tokens/claim_tokens.rs
  • src/backend_task/tokens/destroy_frozen_funds.rs
  • src/backend_task/tokens/freeze_tokens.rs
  • src/backend_task/tokens/mint_tokens.rs
  • src/backend_task/tokens/mod.rs
  • src/backend_task/tokens/pause_tokens.rs
  • src/backend_task/tokens/purchase_tokens.rs
  • src/backend_task/tokens/query_my_token_balances.rs
  • src/backend_task/tokens/query_token_non_claimed_perpetual_distribution_rewards.rs
  • src/backend_task/tokens/resume_tokens.rs
  • src/backend_task/tokens/set_token_price.rs
  • src/backend_task/tokens/transfer_tokens.rs
  • src/backend_task/tokens/unfreeze_tokens.rs
  • src/backend_task/tokens/update_token_config.rs
  • src/backend_task/update_data_contract.rs
  • src/backend_task/wallet/derive_identity_key_for_display.rs
  • src/backend_task/wallet/derive_key_for_display.rs
  • src/backend_task/wallet/fetch_platform_address_balances.rs
  • src/backend_task/wallet/fund_platform_address_from_asset_lock.rs
  • src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs
  • src/backend_task/wallet/generate_platform_receive_address.rs
  • src/backend_task/wallet/generate_receive_address.rs
  • src/backend_task/wallet/mod.rs
  • src/backend_task/wallet/sign_message_with_identity_key.rs
  • src/backend_task/wallet/sign_message_with_key.rs
  • src/backend_task/wallet/transfer_platform_credits.rs
  • src/backend_task/wallet/warm_identity_auth_pubkeys.rs
  • src/backend_task/wallet/withdraw_from_platform_address.rs
  • src/bin/det_cli/connect.rs
  • src/bin/det_cli/headless.rs
  • src/bin/det_cli/main.rs
  • src/boot.rs
  • src/components/core_p2p_handler.rs
  • src/components/core_zmq_listener.rs
  • src/components/mod.rs
  • src/context/connection_status.rs
  • src/context/contested_names_db.rs
  • src/context/contract_token_db.rs
  • src/context/feature_gate.rs
  • src/context/identity_db.rs
  • src/context/migration_status.rs
  • src/context/mod.rs
  • src/context/settings_db.rs
  • src/context/shielded.rs
  • src/context/transaction_processing.rs
  • src/context/wallet_lifecycle.rs
  • src/context/wallet_lifecycle/bootstrap.rs
  • src/context/wallet_lifecycle/mod.rs
  • src/context/wallet_lifecycle/registration.rs
  • src/context/wallet_lifecycle/removal.rs
  • src/context/wallet_lifecycle/spv.rs
  • src/context/wallet_lifecycle/tests.rs
  • src/context/wallet_lifecycle/unlock.rs
  • src/context_provider.rs
  • src/context_provider_spv.rs
  • src/database/asset_lock_transaction.rs
  • src/database/contacts.rs
  • src/database/contested_names.rs
  • src/database/contracts.rs
  • src/database/dashpay.rs
  • src/database/identities.rs
  • src/database/initialization.rs
  • src/database/mod.rs
  • src/database/proof_log.rs
  • src/database/scheduled_votes.rs
  • src/database/settings.rs
  • src/database/shielded.rs
  • src/database/single_key_wallet.rs
  • src/database/test_helpers.rs
  • src/database/tokens.rs
  • src/database/top_ups.rs
  • src/database/utxo.rs
  • src/database/wallet.rs
  • src/lib.rs
  • src/logging.rs
  • src/main.rs
  • src/mcp/error.rs
  • src/mcp/mod.rs
  • src/mcp/resolve.rs
  • src/mcp/server.rs
  • src/mcp/tests.rs
  • src/mcp/tools/identity.rs
  • src/mcp/tools/masternode.rs
  • src/mcp/tools/meta.rs
  • src/mcp/tools/mod.rs
  • src/mcp/tools/network.rs
  • src/mcp/tools/platform.rs
  • src/mcp/tools/shielded.rs
  • src/mcp/tools/wallet.rs
  • src/model/address.rs
  • src/model/amount.rs
  • src/model/contested_name.rs
  • src/model/dashpay.rs
  • src/model/dashpay_derivation/dip14.rs
  • src/model/dashpay_derivation/hd.rs
  • src/model/dashpay_derivation/mod.rs
  • src/model/dpns.rs
  • src/model/fee_estimation.rs
  • src/model/grovestark_prover.rs
  • src/model/identity_discovery.rs
  • src/model/identity_key_protection.rs
  • src/model/key_input.rs
  • src/model/masternode_input.rs
  • src/model/mod.rs
  • src/model/password_info.rs
  • src/model/proof_log_item.rs
  • src/model/qualified_contract.rs
  • src/model/qualified_identity/encrypted_key_storage.rs
  • src/model/qualified_identity/identity_meta.rs
  • src/model/qualified_identity/mod.rs
  • src/model/qualified_identity/qualified_identity_public_key.rs
  • src/model/request_type.rs
  • src/model/secret.rs
  • src/model/selected_identity.rs
  • src/model/selected_wallet.rs
  • src/model/settings.rs
  • src/model/single_key.rs
  • src/model/spv_status.rs
  • src/model/wallet/asset_lock_transaction.rs
  • src/model/wallet/auth_pubkey_cache.rs
  • src/model/wallet/birth_height.rs
  • src/model/wallet/encryption.rs
  • src/model/wallet/meta.rs
  • src/model/wallet/mod.rs
  • src/model/wallet/passphrase.rs
  • src/model/wallet/seed_envelope.rs
  • src/model/wallet/shielded.rs
  • src/model/wallet/single_key.rs
  • src/model/wallet/utxos.rs
  • src/spv/error.rs
  • src/spv/manager.rs
  • src/spv/mod.rs
  • src/spv/tests.rs
  • src/ui/components/README.md
  • src/ui/components/address_input.rs
  • src/ui/components/amount_input.rs
  • src/ui/components/avatar.rs
  • src/ui/components/breadcrumb_pill.rs
  • src/ui/components/confirmation_dialog.rs
  • src/ui/components/contract_chooser_panel.rs
  • src/ui/components/dashpay_subscreen_chooser_panel.rs
  • src/ui/components/dpns_subscreen_chooser_panel.rs
  • src/ui/components/entropy_grid.rs
  • src/ui/components/global_nav_switcher.rs
  • src/ui/components/icons.rs
  • src/ui/components/identity_selector.rs
  • src/ui/components/info_popup.rs
  • src/ui/components/left_panel.rs
  • src/ui/components/left_wallet_panel.rs
  • src/ui/components/message_banner.rs
  • src/ui/components/mod.rs
  • src/ui/components/modal_chrome.rs
  • src/ui/components/passphrase_modal.rs
  • src/ui/components/password_input.rs
  • src/ui/components/progress_overlay.rs
  • src/ui/components/secret_prompt_host.rs
  • src/ui/components/selection_dialog.rs
  • src/ui/components/styled.rs
  • src/ui/components/subscreen_chooser_panel.rs
  • src/ui/components/tokens_subscreen_chooser_panel.rs
  • src/ui/components/tools_subscreen_chooser_panel.rs
  • src/ui/components/top_panel.rs
  • src/ui/components/wallet_unlock.rs
  • src/ui/components/wallet_unlock_popup.rs
  • src/ui/contracts_documents/add_contracts_screen.rs
  • src/ui/contracts_documents/contracts_documents_screen.rs
  • src/ui/contracts_documents/document_action_screen.rs
  • src/ui/contracts_documents/group_actions_screen.rs
  • src/ui/contracts_documents/register_contract_screen.rs
  • src/ui/contracts_documents/update_contract_screen.rs
  • src/ui/dashpay/add_contact_screen.rs
  • src/ui/dashpay/contact_details.rs
  • src/ui/dashpay/contact_info_editor.rs
  • src/ui/dashpay/contact_profile_viewer.rs
  • src/ui/dashpay/contact_requests.rs
  • src/ui/dashpay/contacts_list.rs
  • src/ui/dashpay/dashpay_screen.rs
  • src/ui/dashpay/mod.rs
  • src/ui/dashpay/profile_screen.rs
  • src/ui/dashpay/profile_search.rs
  • src/ui/dashpay/qr_code_generator.rs
  • src/ui/dashpay/qr_scanner.rs
  • src/ui/dashpay/send_payment.rs
  • src/ui/dpns/dpns_contested_names_screen.rs
  • src/ui/helpers.rs
  • src/ui/identities/add_existing_identity_screen.rs
  • src/ui/identities/add_new_identity_screen/by_platform_address.rs
  • src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs
  • src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs
  • src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs
  • src/ui/identities/add_new_identity_screen/mod.rs
  • src/ui/identities/funding_common.rs
  • src/ui/identities/identities_screen.rs
  • src/ui/identities/keys/add_key_screen.rs
  • src/ui/identities/keys/key_info_screen.rs
  • src/ui/identities/keys/keys_screen.rs
  • src/ui/identities/register_dpns_name_screen.rs
  • src/ui/identities/top_up_identity_screen/by_platform_address.rs
  • src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs
  • src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs
  • src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs
  • src/ui/identities/top_up_identity_screen/mod.rs
  • src/ui/identities/transfer_screen.rs
  • src/ui/identities/withdraw_screen.rs
  • src/ui/identity/README.md
  • src/ui/identity/activity.rs
  • src/ui/identity/avatar.rs
  • src/ui/identity/breadcrumb_switcher.rs
  • src/ui/identity/contacts.rs
  • src/ui/identity/home.rs
  • src/ui/identity/hub_screen.rs
  • src/ui/identity/identity_hero_card.rs
  • src/ui/identity/identity_hub_tab_bar.rs
  • src/ui/identity/identity_picker_add_card.rs
  • src/ui/identity/identity_picker_card.rs
  • src/ui/identity/identity_pill.rs
  • src/ui/identity/landing.rs
  • src/ui/identity/mod.rs
  • src/ui/identity/onboarding.rs
  • src/ui/identity/onboarding_checklist.rs
  • src/ui/identity/picker.rs
  • src/ui/identity/profile_cache.rs
  • src/ui/identity/request_card.rs
  • src/ui/identity/settings.rs
  • src/ui/identity/social_profile_gate_card.rs
  • src/ui/identity/tabs.rs
  • src/ui/masternodes/card.rs
  • src/ui/masternodes/detail_screen.rs
  • src/ui/masternodes/list_screen.rs
  • src/ui/masternodes/load_form.rs
  • src/ui/masternodes/mod.rs
  • src/ui/masternodes/testnet_fixture.rs
  • src/ui/mod.rs
  • src/ui/network_chooser_screen.rs
  • src/ui/state/account_summary.rs
  • src/ui/state/avatar_cache.rs
  • src/ui/state/global_nav.rs
  • src/ui/state/hub_selection.rs
  • src/ui/state/masternodes_view.rs
  • src/ui/state/mod.rs
  • src/ui/state/tracked_asset_lock_cache.rs
  • src/ui/theme.rs
  • src/ui/tokens/add_token_by_id_screen.rs
  • src/ui/tokens/burn_tokens_screen.rs
  • src/ui/tokens/claim_tokens_screen.rs
  • src/ui/tokens/destroy_frozen_funds_screen.rs
  • src/ui/tokens/direct_token_purchase_screen.rs
  • src/ui/tokens/freeze_tokens_screen.rs
  • src/ui/tokens/mint_tokens_screen.rs
  • src/ui/tokens/mod.rs
  • src/ui/tokens/pause_tokens_screen.rs
  • src/ui/tokens/resume_tokens_screen.rs
  • src/ui/tokens/set_token_price_screen.rs
  • src/ui/tokens/token_action_screen.rs
  • src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs
  • src/ui/tokens/tokens_screen/distributions.rs
  • src/ui/tokens/tokens_screen/keyword_search.rs
  • src/ui/tokens/tokens_screen/mod.rs
  • src/ui/tokens/tokens_screen/my_tokens.rs
  • src/ui/tokens/tokens_screen/structs.rs
  • src/ui/tokens/tokens_screen/token_creator.rs
  • src/ui/tokens/transfer_tokens_screen.rs
  • src/ui/tokens/unfreeze_tokens_screen.rs
  • src/ui/tokens/update_token_config.rs
  • src/ui/tokens/view_token_claims_screen.rs
  • src/ui/tools/address_balance_screen.rs
  • src/ui/tools/contract_visualizer_screen.rs
  • src/ui/tools/document_visualizer_screen.rs
  • src/ui/tools/grovestark_screen.rs
  • src/ui/tools/masternode_list_diff_screen.rs
  • src/ui/tools/mod.rs
  • src/ui/tools/platform_info_screen.rs
  • src/ui/tools/proof_log_screen.rs
  • src/ui/tools/proof_visualizer_screen.rs
  • src/ui/tools/transition_visualizer_screen.rs
  • src/ui/wallets/add_new_wallet_screen.rs
  • src/ui/wallets/asset_lock_detail_screen.rs
  • src/ui/wallets/create_asset_lock_screen.rs
  • src/ui/wallets/import_mnemonic_screen.rs
  • src/ui/wallets/import_single_key.rs
  • src/ui/wallets/mod.rs
  • src/ui/wallets/restore_single_key.rs
  • src/ui/wallets/send_screen.rs
  • src/ui/wallets/shield_screen.rs
  • src/ui/wallets/shielded_send_screen.rs
  • src/ui/wallets/shielded_tab.rs
  • src/ui/wallets/single_key_send_screen.rs
  • src/ui/wallets/unshield_credits_screen.rs
  • src/ui/wallets/wallets_screen/address_table.rs
  • src/ui/wallets/wallets_screen/asset_locks.rs
  • src/ui/wallets/wallets_screen/dialogs.rs
  • src/ui/wallets/wallets_screen/mod.rs
  • src/ui/wallets/wallets_screen/single_key_view.rs
  • src/ui/welcome_screen.rs
  • src/utils/mod.rs
  • src/utils/path.rs
  • src/wallet_backend/auth_pubkey_cache.rs
  • src/wallet_backend/avatar_cache.rs
  • src/wallet_backend/contact_profile_cache.rs
  • src/wallet_backend/coordinator_gate.rs
  • src/wallet_backend/dashpay.rs
  • src/wallet_backend/det_platform_signer.rs
  • src/wallet_backend/det_signer.rs
  • src/wallet_backend/event_bridge.rs
  • src/wallet_backend/hydration.rs
  • src/wallet_backend/identity_key_store.rs
  • src/wallet_backend/identity_meta.rs
  • src/wallet_backend/identity_ops.rs
  • src/wallet_backend/kv.rs
  • src/wallet_backend/kv_test_support.rs
  • src/wallet_backend/leak_test_support.rs
  • src/wallet_backend/loader.rs
  • src/wallet_backend/mod.rs
  • src/wallet_backend/payments.rs
  • src/wallet_backend/poison.rs
  • src/wallet_backend/secret_access.rs
  • src/wallet_backend/secret_prompt.rs
  • src/wallet_backend/secret_seam.rs
  • src/wallet_backend/shielded.rs
  • src/wallet_backend/sidecar.rs
  • src/wallet_backend/single_key.rs
  • src/wallet_backend/single_key_entry.rs
  • src/wallet_backend/snapshot.rs
  • src/wallet_backend/token_balance.rs
  • src/wallet_backend/versioned_bincode.rs
  • src/wallet_backend/wallet_meta.rs
  • src/wallet_backend/wallet_seed_store.rs
  • tests/backend-e2e/README.md
  • tests/backend-e2e/core_tasks.rs
  • tests/backend-e2e/dashpay_tasks.rs
  • tests/backend-e2e/event_bridge_live.rs
  • tests/backend-e2e/framework/cleanup.rs
  • tests/backend-e2e/framework/dashpay_helpers.rs
  • tests/backend-e2e/framework/fixtures.rs
  • tests/backend-e2e/framework/funding.rs
  • tests/backend-e2e/framework/harness.rs
  • tests/backend-e2e/framework/identity_helpers.rs
  • tests/backend-e2e/framework/mnlist_helpers.rs
  • tests/backend-e2e/framework/mod.rs
  • tests/backend-e2e/framework/shielded_helpers.rs
  • tests/backend-e2e/framework/task_runner.rs
  • tests/backend-e2e/framework/token_helpers.rs
  • tests/backend-e2e/framework/wait.rs
  • tests/backend-e2e/identity_cold_boot.rs
  • tests/backend-e2e/identity_create.rs
  • tests/backend-e2e/identity_in_vault_sign.rs
  • tests/backend-e2e/identity_masternode_withdraw.rs
  • tests/backend-e2e/identity_tasks.rs
  • tests/backend-e2e/identity_withdraw.rs
  • tests/backend-e2e/main.rs
  • tests/backend-e2e/mnlist_tasks.rs
  • tests/backend-e2e/register_dpns.rs
  • tests/backend-e2e/send_funds.rs
  • tests/backend-e2e/shielded_tasks.rs
  • tests/backend-e2e/spv_reconnect.rs
  • tests/backend-e2e/spv_wallet.rs
  • tests/backend-e2e/token_tasks.rs
  • tests/backend-e2e/tx_is_ours.rs
  • tests/backend-e2e/wallet_reregistration.rs
  • tests/backend-e2e/wallet_tasks.rs
  • tests/backend-e2e/z_broadcast_st_tasks.rs
  • tests/common/data_dir.rs
  • tests/e2e/helpers.rs
  • tests/e2e/navigation.rs
  • tests/e2e/wallet_flows.rs
  • tests/kittest/address_input.rs
  • tests/kittest/contract_screen.rs
  • tests/kittest/create_asset_lock_screen.rs
  • tests/kittest/dashpay_screen.rs
  • tests/kittest/global_nav_switcher.rs
  • tests/kittest/identities_screen.rs
  • tests/kittest/identity_hub.rs
  • tests/kittest/identity_hub_activity.rs
  • tests/kittest/identity_hub_contacts.rs
  • tests/kittest/identity_hub_onboarding.rs
  • tests/kittest/identity_hub_settings.rs
  • tests/kittest/identity_hub_switcher.rs
  • tests/kittest/identity_selector.rs
  • tests/kittest/import_single_key.rs
  • tests/kittest/main.rs
  • tests/kittest/masternode_tab.rs
  • tests/kittest/migration_banner.rs
  • tests/kittest/network_chooser.rs
  • tests/kittest/progress_overlay.rs
  • tests/kittest/register_dpns_name_screen.rs
  • tests/kittest/restore_single_key.rs
  • tests/kittest/secret_prompt.rs
  • tests/kittest/skipped_wallets_banner.rs
  • tests/kittest/startup.rs
  • tests/kittest/support.rs
  • tests/kittest/tokens_screen.rs
  • tests/kittest/tools_screen.rs
  • tests/kittest/wallets_screen.rs
  • tests/kittest/withdraw_screen.rs
  • tests/legacy_table_surface.rs
  • tests/mcp_http_auth.rs

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

Use the checkbox below for a quick retry:

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lklimek lklimek changed the title docs: platform-wallet migration design docs: platform-wallet backend — clean-slate rewrite spec May 18, 2026
@lklimek lklimek changed the title docs: platform-wallet backend — clean-slate rewrite spec feat: platform-wallet backend rewrite (spec + implementation) May 19, 2026
lklimek added a commit that referenced this pull request May 29, 2026
refactor: complete data.db unwire — shielded + DashPay (stacked on #860)
@lklimek

lklimek commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Doc sync — 686430a4 (docs/kv-keys.md)

Trillian audited the k/v key reference against current source and corrected three stale entries. All align the doc with code changes already described in this PR:

  • Migration sentinel — was documented as a single global det:migration:finish_unwire:v1 with constant SENTINEL_KEY. Now correctly det:migration:finish_unwire:<network>:v1 (per-network, via sentinel_key_for(network)), consts SENTINEL_KEY_PREFIX + SENTINEL_KEY_VERSION. Tracks SEC-001.
  • Single-key metadata sidecarImportedKey field list extended from 3 → 5 (has_passphrase, passphrase_hint). Tracks SEC-002 Option C.
  • HD seed envelope encoding — value is now [ version byte | bincode::serde(payload) ], not bare bincode::serde. Tracks SEC-005.

All other sections (17 platform-wallet keys, DashPay sidecar, settings, wallet selection, summary counts) verified accurate — no drift. Doc-only; no source touched.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

SPV-start fix (PROJ-001, CRITICAL) + consolidated gap report — 8d35933e

Bug: the Settings "Connect" button did nothing and SPV stayed idle. Root cause: AppContext::start_spv() was an inert Ok(()) compile-floor stub and WalletBackend::start() (the only spawner of the SPV run loop) had zero callers — so SPV / platform-address / identity sync never started in any path (Connect, auto-start, MCP, network switch).

Fix (3 commits):

  • 42388c4b — wire start_spv()WalletBackend::start(); add a per-instance StartLatch so SPV can't be double-driven.
  • 3165f98c — route all callers through one idempotent async chokepoint AppContext::ensure_wallet_backend_and_start_spv() (wire-then-start); fixes the boot auto-start race (sync constructor fired before the backend was wired), MCP ensure_spv_synced, and network-switch restart; hoist SpvStatus::Error above the DAPI gate so chain-sync failures aren't masked as "Disconnected".
  • 36f5a982 — surface start/wiring failures to the user (indicator → Error in the chokepoint; actionable banner at the Connect handler); replaces the dead AppAction::Custom no-op.

QA: two full Marvin rounds (caller-integration focus). All findings resolved; 2 LOW residuals (user-facing wiring-failure feedback + a forward-compat dead branch) closed by 36f5a982. 571 lib tests pass (+8), clippy/fmt clean. 8 new offline tests cover the start-path gating. The one kittest failure (tc_sk_004) is confirmed pre-existing.


Consolidated gap audit — 2313089a (+ status update 8d35933e)

docs/ai-design/2026-06-01-pr860-gap-audit/gaps.md — a whole-PR audit by project-reviewer Adams. 21 confirmed gaps (1 CRITICAL, 4 HIGH, 6 MEDIUM, 7 LOW, 3 INFO), each with file:line. Highlights:

  • PROJ-001 (CRITICAL) — resolved on-branch (above).
  • PROJ-005 (HIGH) — now the sole remaining open merge-blocker: Cargo.toml pins platform to the unreleased #3625 rev (release gate G1).
  • 5 net-new findings, notably PROJ-004 (HIGH) — DashPay outgoing contact-request xpub derivation uses placeholder seed material (let wallet_seed = sender_private_key;), the substrate behind the TC-037/043 association symptom.
  • 4 seeded "gaps" verified already-fixed (eager-init hard-failure → graceful warn, TC-019 precedence, core_backend_mode removal, the 5-manager readiness gate).

Remaining open gaps are catalogued in the report (not filed as separate issues by request).

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

DetKv per-object refactor + platform bump to 35e4a2f129d54d0

Reseats DET's KV layer onto upstream's per-object KvStore and retires two of three duplicate caches. 7 commits:

  • 549ddfa1 — honest error when wallet data is from a newer app version (WalletDataTooNew, replaces the misleading "check disk space" for schema-version skew).
  • dbd94356 — reseat DetKv on the 08b0ed9 per-object KvStore via a DET-side DetScope { Global, Wallet, Identity, Token } (maps to upstream ObjectId only inside kv.rs — no type leak); isolate the platform-address and token-balance caches behind PlatformAddressView/TokenBalanceView seams.
  • 845ff685 — honest error for an incompatible on-disk schema (WalletDataIncompatible, divergent-migration case); finish the token-balance seam.
  • fb50c044 — bump platform to 35e4a2f (meta_* FK relaxed to soft-cascade triggers; public per-(identity,token) balance reader).
  • f6119de7delete the det:token_balance cache; balances now read live from upstream IdentitySyncManager via a lock-free snapshot bridge (pre-sync shows "syncing", not 0).
  • d7ac9a06promote identities + DashPay overlays to Identity scope (identities/top-ups/scheduled-votes → Identity(id); dashpay private/address_indexIdentity(owner)); Global enumeration indices; explicit cleanup (the upstream soft-cascade trigger doesn't fire for DET-only KV removals); hardened private-key Debug redaction.
  • 129d54d0 — docs synced to the per-object model.

Principle applied: DET's KV stores only DET-specific metadata; canonical object state upstream owns (token balances) is read, not duplicated.

QA (Marvin, combined tree): PASS — cargo build --all-features ✓, 598 tests (597 pass, 1 ignored) ✓, clippy -D warnings ✓, fmt ✓. Zero code defects; M-DONT-LEAK-TYPES intact.

Remaining duplicate: the platform-address cache (det:platform_addr/det:platform_sync) stays, staged behind PlatformAddressView — one-line swap once upstream exposes a public per-address nonce reader (the only open platform-side dependency; the FK relaxation and token-balance reader both landed in #3625 @ 35e4a2f).

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Mock/stub findings + SPV progress + DashPay fund-routing — 450214e5 (8 commits)

Post-35e4a2f cleanup from a mock-audit, plus a fund-routing fix the audit surfaced. All on top of the DetKv per-object refactor.

SPV / UX

  • bd0ed0e4SPV sync progress bar restored: two UI sites read an inert SpvStatusSnapshot::default(); EventBridge::on_progress already received full upstream SyncProgress (per-phase heights) but discarded it. Routed it into ConnectionStatus (single source of truth) → determinate per-phase % bar. Also fixed two dead gates (network selector now disables mid-sync; "Clear SPV Data" gated).
  • 7e2553e3 — real per-network platform activation height (was hardcoded 1).

Tokens / MCP

  • 5a047357"Stop Tracking Balance" truly un-watches the (identity, token) pair upstream (update_watched_tokens), not just a local order prune.
  • 5ba4554eplatform_withdrawals_get MCP tool: structured fields + pagination.

DashPay

  • a7327e7c — contact-request expires_at derived from created_at + 7d.
  • 3ac9b3b0update_payment_status now persists transitions (was a logging no-op). check_address_usage documented as blocked-by-design (upstream usage reader is keyed by (wallet, account-type), not arbitrary address; contact-send addresses aren't pool-managed).
  • 6c520a33PROJ-004 (fund-adjacent): contact-request xpub now derived from the real HD seed via upstream derive_contact_xpub (was a placeholder using the ECDH private key → mis-routed contact payments). Seed handling: Zeroizing, confined to the wallet_backend seam, never in the document. Smythe-audited: SAFE.
  • 450214e5SEC-001 (HIGH, fund-routing): receive-side DashPay derivation hardcoded coin-type 5' on all networks while the send side (now correct) uses 1' on testnet → testnet contact payments landed on un-scanned addresses (recoverable; mainnet unaffected). Fixed via a canonical coin_type_for_network helper threaded through all receive paths (incl. the DIP-15 root-encryption + auto-accept keys, same bug). Send==receive xpub now pinned by a cross-implementation equality test on both networks. Smythe-re-audited: SEC-001 RESOLVED.

QA/Security: Marvin (integration) + Smythe (two fund-safety passes) — cargo build --all-features ✓, 611 lib tests ✓, clippy -D warnings ✓, fmt ✓.

Standing gates / follow-ups (non-blocking):

  • Live-network test required before trusting contact payments with funds — mainnet round-trip (real funds) and testnet (now expected to pass post-SEC-001).
  • SEC-003 (G1): fund routing depends on the unreleased platform-wallet @ 35e4a2f — no mainnet release until a published rev (existing G1 gate).
  • SEC-004 (LOW): auto-accept key rooted on a 32-byte private key rather than the HD seed (self-consistent, doesn't route funds) — tracked follow-up.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Platform dep bump → ffdc28b8 + gap-audit refresh

Pushed 2 commits (450214e5..419ef952).

fd209783chore(deps): bump platform deps 35e4a2fffdc28b8 (current head of dashpay/platform#3625, a clean 15-ahead/0-behind fast-forward)

  • All four platform crates re-pinned: dash-sdk, rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage. grovestark untouched.
  • Cargo.lock: 27 platform crates → v3.1.0-dev.8; 13 transitive grovedb crates moved (upstream-pinned, pulled in by the v3.1-dev merge 269e5783). Nothing stray.
  • Zero source fallout — no DET files needed editing. Picks up two fixes that sit under our seam:
    • 1053caa2 platform-wallet-storage single-read KvStore::get (TOCTOU, CMT-001) — public get/put/delete/list_keys signatures unchanged; our DetKv envelope intact, all 9 adapter tests pass.
    • 73eb0ae0 platform-wallet SPV client deadlock-on-stop — behavioral, no API change; WalletBackend::shutdown/stop_spv compile untouched.
  • Build + clippy (-D warnings) + nightly fmt: all green.
  • Caveat: verified API-clean, but the runtime behavior of the TOCTOU/deadlock fixes is upstream's to validate (needs the #[ignore] backend-e2e suite on a funded testnet wallet).

419ef952docs(gap-audit): refresh gaps.md to as-of-450214e5 state

  • 5 PROJ gaps flipped RESOLVED (re-verified against source, not commit messages): PROJ-001 (SPV start wired), PROJ-003 (update_payment_status persists), PROJ-004 (HD-seed contact xpub + SEC-001 per-network coin-type), PROJ-006 (per-network activation heights), PROJ-014 (offline start-path tests) — plus 4 appendix/seed items.
  • 3 new findings: PROJ-022 (LOW — reserved UpstreamPlatformAddresses stub, deferred to upstream nonce reader), PROJ-023 (LOW — pre-existing error-string parsing in add_contact_screen.rs, tracked under refactor: migrate from Result<T, String> to typed errors with TaskError #660), PROJ-024 (BLOCKED-BY-DESIGN — check_address_usage).
  • Merge-blocker verdict: no CRITICAL open. Sole remaining blocker is PROJ-005 (HIGH) — platform pin is an unreleased dev rev (this bump keeps us on ffdc28b8, so G1 release gate stands until #3625 merges and a tag cuts).

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Dead-code cleanup + gap-audit reconciliation

Pushed 2 commits (419ef952..b2febb71).

9529aedarefactor(dashpay): remove dead add_contact/remove_contact stubs + orphaned NotSupported variant (PROJ-002)

  • The two free functions in src/backend_task/dashpay/contacts.rs were orphans from PR feat: HD wallet system, DashPay integration, SPV support, and more #464 (82399a26) — zero call sites, superseded by DashPayTask::SendContactRequest. Removed both, plus the now-constructorless DashPayError::NotSupported variant (errors.rs). 35 deletions.
  • Verified the unrelated profile_search.rs::add_contact UI method is a distinct symbol (left intact). No dead imports left behind; clippy -D warnings clean.

b2febb71docs(gap-audit): resolve PROJ-002, re-file PROJ-012

  • PROJ-002 → RESOLVED (removed).
  • PROJ-012 re-filed from "deferred-by-design (LOW)" to functional gap (MEDIUM — OPEN). It was masking a real wiring bug behind #[allow(dead_code)]: the ZMQ status sender is live (app.rs:819) but rx_zmq_status is never drained and ConnectionStatus::set_zmq_status (connection_status.rs:159) has zero callers — so ZMQ connection-health events flow into a void and the status indicator never reflects ZMQ state. Binary fix: wire the receiver → set_zmq_status, or remove the whole producer→channel→setter chain (the channel pair is built as a unit at context/mod.rs:161, so no piecemeal trim).
  • Also reconciled the executive-summary tally (it didn't sum before): now an honest 23 total / 17 open / 6 resolved.

Net: open-gap count holds at 17, but the audit books are accurate and a previously-hidden wiring defect (PROJ-012) is now surfaced as a real finding.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Seedless wallet rehydration (PROJ-010) + platform → PR #3692

Pushed 5 commits (b2febb71..e6c6c017). Headline is the wallet-load re-wire.

f35ea7b9chore(deps): platform → PR #3692 head ddfa66ed

e6c6c017feat(wallet): seedless wallet load via UpstreamFromPersisted (PROJ-010)

  • Closes the reserved G2 swap-point. PersistedWalletLoader reshaped to a seedless async load() -> LoadedWallets; UpstreamFromPersisted drives upstream PlatformWalletManager::load_from_persistor() (rebuilds wallets watch-only, no seed in memory at load). SeedReregistrationLoader deleted.
  • Two deviations from the design, both forced by upstream crypto reality:
    1. Bridge matches on the BIP44 account xpub, not WalletId — upstream WalletId = SHA256(root_xpub‖chaincode) is depth-0 master, but DET persists the depth-3 account xpub, and you can't derive a parent from a child. The account xpub is the literal scanned==published routing key, so this is a stronger, migration-free guard.
    2. Identity-funding re-provision deferred to the post-unlock asset-lock chokepoint — a watch-only loaded wallet has no private key, so add_account can't run at load.
  • Fund-safety gates: (1) account-xpub-match gate rejects any loaded wallet not matching a DET sidecar (preserves fund routing); (2) seed supplied only at unlock via provide_seed (signing keeps working; load stays seedless).
  • 612 lib tests pass, clippy -D warnings clean. Fund-safety review by Smythe is in flight.
  • Deferred: skipped-wallet MessageBanner (PROJ-010-T6) — skips are logged + returned in LoadedWallets.skipped, no UI surfacing yet (load path has no egui ctx).

Planning docs (rode along): rehydration design (df38b316), and Phase 1 of the sign-time unlock prompt feature — requirements+UX (d6811732) and a 45-case test spec (bf939c69).

Note: PR #3692 is an open, unreleased dev branch — this keeps the G1 release gate (PROJ-005) open by design until it merges and a tag cuts.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Fix: no-password hydrated wallets couldn't sign (Smythe SEC-001/002)

2272bae0 — fixes a HIGH fund-signing bug Smythe found in the seedless re-wire.

Symptom: after the seedless rehydration, a no-password HD wallet hydrated as Open and the UI showed "unlocked," but inner.seeds was never populated — so signer_for returned WalletLocked for send / asset-lock / identity register+top-up. Inconsistent: DashPay/shielded (which read the seed off the Wallet struct) kept working, masking it.

Root cause: seeds reach inner.seeds only via the handle_wallet_unlockedprovide_seed chokepoint, which the no-password unlock paths bypassed; and bootstrap_loaded_wallets ran at AppContext::new (pre-wiring, empty wallet map) — a no-op.

Fix (one chokepoint, every path funnels through it):

  • Re-timed bootstrap_loaded_wallets to the tail of ensure_wallet_backend (after hydration, populated map).
  • Routed both no-password UI unlock paths (try_open_wallet_no_password, should_ask_for_password) through handle_wallet_unlocked.
  • 33 call sites threaded &self.app_context (mechanical).
  • Regression test (no_password_wallet_resignable_via_unlock_chokepoint): reproduces the seedless cold-boot state, asserts WalletLocked before, signing after.

The account-xpub fund-routing gate and seedless load path are untouched. 613 lib tests pass, clippy clean. Smythe re-verify in flight.

Separate pre-existing item (not from this change): the kittest suite has 15 Migration DivergentVersion failures from a stale on-disk migration artifact — verified identical on the clean base. Needs its own test-isolation cleanup.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

JIT secret-access refactor — HD seed no longer parked in memory (R3 complete)

Pushed 24 commits (2272bae0..43f412cf). This replaces the upfront session-long wallet-unlock model with just-in-time secret access: the HD seed is fetched, used, and zeroized within an operation, never held for the session.

Core (ed06c01906a4b92e)

  • SecretAccess chokepoint — async with_secret / with_secret_session, keyed by SecretScope{HdSeed, SingleKey}, behind a UI-agnostic SecretPrompt seam (egui host via mpsc→oneshot drained on the frame loop; NullSecretPrompt → typed error for MCP/CLI).
  • Residency: operation-scoped by default, opt-in "keep unlocked until I close the app" (RememberPolicy, boxed + Zeroizing, cleared on network switch / teardown). Timed-unlock variant modelled for the future (no GUI yet).
  • All signing routed through DetSigner (HD/single-key) and DetPlatformSigner (all 6+ platform-signer SDK sites incl. shielded), each borrowing the seed inside the secret scope — byte-parity proven against an independent reference on Testnet + Mainnet.
  • Identity-auth public keys memoized in AuthPubkeyCache (DET KV sidecar, public keys only — no DB migration / no DivergentVersion risk; the path is hardened to the leaf so xpub-only derivation is impossible).
  • Capstone: WalletSeed::open reshaped to verify-not-park; OpenWalletSeed.seed field dropped; Wallet::seed_bytes() deleted. rg "\.seed_bytes()" src/ → ZERO (compile-gate proof).

QA (three independent passes)

  • Security (Smythe): SHIP — 10/10 end-state gates; the only remaining plaintext-seed residencies are the three deliberate, sound ones (opt-in session cache, JIT single-key borrow, user-initiated DIP-15 QR). block_on on the shielded path confirmed deadlock-safe (off-UI-thread).
  • Tests (Marvin): 676 lib + 83 kittest + 3 doc — green. Added the HD signer parity test (independent reference + pinned vector), reentrancy + open_no_password guards.
  • Consistency (Adams): typed-error conventions, naming, MCP/CLI all clean.

Also (43f412cf) — QA-005: AppState kittests isolated onto a temp data dir (with_isolated_data_dir), eliminating the 14 pre-existing DivergentVersion failures and stopping tests from touching the real user data dir. kittest now 83/0.

CHANGELOG + user-story WAL-006 updated for the new unlock model.

Deferred/non-blocking: single-key send remains an upstream stub (the JIT machinery is wired and waiting). The G1 release gate stands — PR #3692 is still an unreleased dev rev.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

NEW since last push — Settings Disconnect fix + persister-gap marker + nonce-ownership upstream issue (dd5f4e7ea067a747)

9b2aacbf + a067a747 — fix: the Settings/network "Disconnect" button now actually disconnects.
AppContext::stop_spv() was an inert stub — it only nudged a throttle timer via reset_timer(), so ConnectionStatus::overall_state() (the UI's single source of truth) never moved and the button did nothing observable. The click was wired correctly; the method was the half-finished path. Rewrote stop_spv as a real async disconnect mirroring Connect: flip indicator → Stopping, backend.shutdown().await, unwire the backend via new AppContext::take_wallet_backend() (the start latch is one-shot, so the next Connect rebuilds a fresh, restartable backend), then settle Stopped/Disconnected and recompute state. The button now returns AppAction::StopSpv, dispatched off the frame thread symmetric with StartSpv. A single-winner atomic ConnectionStatus::begin_spv_stop() CAS closes a double-click window — the button disables on the click frame and a second dispatch loses the CAS, so no concurrent teardown. No String error fields, no new user-facing strings; ConnectionStatus stays the single source of truth.

QA (Marvin): both specs PASS, all hard regressions clean (no dangling backend Arc, reconnect rebuilds fresh, self-ban path stays closed, network-switch slots intact). Two LOWs he raised are closed in a067a747 (the synchronous double-click guard + a reconnect-rebuild test). 7 targeted tests pass; clippy -D warnings / +nightly fmt --check / build --all-features green.

1e19f46d — docs(wallet-backend): mark the missing core_derived_addresses seeding (utxo_address_not_derived).
Production-log triage surfaced a persister fatal-flush loop (173× across 7 wallets). The upstream rs-platform-wallet sqlite persister keeps two disjoint address tables — account_address_pools (written at registration) and core_derived_addresses (the UTXO writer's lookup, fed only by live addresses_derived events). On a genesis-rescan boot, SPV matches historical UTXOs at registered/watched addresses before their derive-event lands → UtxoAddressNotDerived → classified fatal → the whole buffered changeset is dropped → core_bridge re-emits → loop → receive/change balances under-report. Added an in-code TODO at the post-registration chokepoint; the upstream fix (seed core_derived_addresses from registration pools + narrow the buffer-wipe blast radius) is tracked separately.

dd5f4e7e710df51f — docs(platform-wallet): upstream nonce-ownership issue draft.
Drafted and filed dashpay/platform#3825 (rs-platform-wallet should own and expose the per-address platform nonce: advance-on-submit + local reader), privacy-led, with the DAPI request-count claim corrected to one avoidable request per spend.

Merge-gate update (G1 / PROJ-005): upstream #3625 is now MERGED (6fa4686), and a tagged pre-release v4.0.0-beta.4 contains the persister crate — but re-pinning to it is verified UNSAFE. beta.4 ships only the bare squash-merge whose production load() is the deferred stub (persister.rs returns wallets_rehydrated=0), while the current pin 9e1248c carries the full rehydration stack (un-gate-schema-readers fix, per-account reconstruction, secrets, the wallet_meta → wallets rename). A swap would compile but silently no-op wallet rehydration and shift the on-disk schema. The pin stays at 9e1248c; G1 remains BLOCKED until a tag is cut from a master line that includes the rehydration stack.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

NEW since last push — re-pin to dashpay/platform#3828: persister-loop fix + Orchard security patch + shielded-API port (4247c360, a0d5034a)

Re-pinned all four dashpay/platform git deps from rev 9e1248c to 4f432c9 (the head of dashpay/platform#3828). This single bump lands three things:

1. Fixes the persister fatal-flush loop. #3828 is the upstream fix for the utxo_address_not_derived loop observed in production det.log (173× across 7 wallets): apply_pools now mirrors registration pool addresses into core_derived_addresses, load() additively backfills already-persisted DBs, and the flush blast-radius is contained (a single undeclared UTXO is skipped, not the whole changeset). The DET-side TODO marker added in 1e19f46d is removed — the gap is closed upstream. Verified at the pinned source: load() is the real rehydration path (wallets_rehydrated = len), not the beta.4-tag deferred stub.

2. Patches a live shielded double-spend vulnerability. The bump carries Orchard 0.13.1 → 0.14.0 and halo2_gadgets 0.4 → 0.5.0 — the patched versions for the disclosed Orchard counterfeit/double-spend flaw (the one behind Zcash's recent emergency hard fork). Smythe confirmed the copy_advice base-tying fix is present in the new halo2_gadgets.

3. Hardens the secrets vault at rest. beta.4's persister refuses to open a vault whose parent dir is group/other-writable (InsecureParentDir). DET's open_secret_store now chmod 0700s the secrets dir right after creation — every vault open routes through that one chokepoint, so this is both real at-rest hardening and what keeps the suite green on a default-umask runner (no harness/CI hack).

Shielded/SDK API port (the cost of the bump). 4f432c9 is a newer upstream lineage, so DET's shielded code was ported to the new signatures: shielded builders now return (StateTransition, Credits) and drop the explicit fee arg; the removed sync_nullifiers API → scan-based spend detection (collect scanned-action nullifiers, flip owned notes whose nullifier matches — a line-for-line port of upstream apply_scanned_nullifier_spends); compute_minimum_shielded_fee now returns Result, propagated via a new typed TaskError::ShieldedFeeComputationFailed; shield_from_asset_lock gained surplus_output.

QA — both SHIP. Smythe (funds/privacy): nullifier rewrite correct, no lockout, no under-fee. Marvin (correctness): the 84 lib-test failures under default perms were 100% beta.4 env-hardening (closed by the chmod 0700), the one AlreadyOpen was test-only (the production Disconnect→Connect path correctly drops the backend before reconnect), no port regressions. 779 lib tests pass / 0 fail on a normal runner; clippy -D warnings / fmt clean.

Merge gate (G1 / PROJ-005) — still blocks merge. The pin advanced and is materially better, but 4f432c9 is a PR head, not a tagged release. #860 still must not merge until #3828 merges upstream and a tag is cut from a master line that carries the rehydration stack. Non-blocking follow-ups tracked: a testable seam + unit test for the nullifier spend-match, dual-cursor unification, and the remaining sync.rs chunk-size literal.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

NEW since last push — fix PROJ-028/030 nullifier-cursor regression + v0.10-dev feature-parity audit (39433dac, 96558917)

Fix: shielded nullifier-cursor reset on migration + resync (PROJ-028 HIGH, PROJ-030 MED)

A regression introduced earlier in this branch by the #3828 re-pin (4247c360): the re-pin made nullifier-spend detection scan-based, keyed off a note-tree position cursor (last_nullifier_sync_height) — but pre-#3828 that persisted value was a platform block height, and the finish-unwire migration carried it verbatim. A migrated wallet therefore started its scan past the note-tree tip → spent notes never flipped → balance silently overstated, spends fail. Caught by a v0.10-dev parity pass against migrated (not fresh) state — the isolated #3828 port QA had reviewed fresh state only.

  • PROJ-028 (finish_unwire.rs:730-738): migration now writes the cursor as (0,0) instead of carrying the legacy value, so migrated wallets rescan [0, tip] and correctly re-derive the spent set.
  • PROJ-030 (shielded_tab.rs): "Resync Notes" now also resets the cursor (via delete_shielded_wallet_meta) so a resync truly re-derives spends instead of resurrecting spent notes.
  • SEC-001 (model/wallet/shielded.rs:98): rewrote the stale "Block height" doc comment that caused the confusion — the field is now documented unambiguously as a note-tree position.
  • TC-SH-002 renamed to assert the reset (the old assertion encoded the bug) + 2 new tests (migrated-cursor→scan flips spent note; resync resets). Full suite green (781+).

QA — Smythe SHIP (funds-safe): the from-0 scan accumulates every on-chain note (can't miss a spend) and is set-only (can't re-credit); delete_shielded_wallet_meta is cursor-only (notes dropped + re-fetched separately, transient balance errs understating); the note-sync cursor is tree-derived and carries no block-height hazard.

v0.10-dev feature-parity audit → docs/ai-design/2026-06-01-pr860-gap-audit/

Audited every user-facing feature in the pre-rewrite line v0.10-dev (≈ v1.0-dev) against this PR via four parallel domain passes + consolidation. gaps.md refreshed (now 28 open / 15 resolved; 0 CRITICAL · 3 HIGH · 10 MEDIUM · 15 LOW) and a schema-validated gaps-report.json added (v3.0.0 review-report format, /triage-findings-ready).

The audit surfaced several regressions the prior audit missed. Beyond the now-fixed PROJ-028/030, the open HIGHs are:

  • PROJ-026 — Create-Asset-Lock QR funding flow soft-locks at "Waiting for funds…" forever: the only WaitingOnFunds→FundsReceived transition fires on CoreItem::ReceivedAvailableUTXOTransaction, which has zero producers. User sends DASH, it lands via SPV, the screen never completes.
  • PROJ-027 — incoming DashPay contact payments never detected/credited (all networks): process_incoming_payment/register_dashpay_contact have zero callers; the v0.10 ZMQ detection path was deleted without replacement.

Plus ~10 MEDIUM (Send-Max always errors from a Core wallet; shield-from-Core ignores the chosen source address; settings reset on upgrade; Dash-Qt launcher unreachable; DashPay history/nicknames not migrated) and doc gaps (RPC-removal CHANGELOG sweep; removed Proof-Log screen). All catalogued with file:line in gaps.md and triage-ready in gaps-report.json.

🤖 Co-authored by Claudius the Magnificent AI Agent

@thepastaclaw

Copy link
Copy Markdown
Collaborator

I checked the asset-lock recovery angle from the JS SDK discussion.

The identity paths look correct in this PR: src/backend_task/identity/register_identity.rs and src/backend_task/identity/top_up_identity.rs route wallet-funded and existing-lock flows through upstream IdentityWallet::*_with_funding with AssetLockFunding::FromWalletBalance / FromExistingAssetLock, so upstream owns tracking, resume, IS→CL fallback, submit, and consume cleanup.

But I do not think PR 860 fully handles the same issue for the non-identity asset-lock flows yet:

  • src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs creates a tracked proof via WalletBackend::create_asset_lock_proof(...), then manually calls SDK outputs.top_up(...).
  • src/backend_task/wallet/fund_platform_address_from_asset_lock.rs pulls a tracked proof from list_tracked_asset_locks(...), then manually calls SDK outputs.top_up(...).
  • src/backend_task/shielded/bundle.rs creates a tracked proof via create_asset_lock_proof(...), then manually builds/broadcasts ShieldFromAssetLock; after broadcast it treats wait_for_response failure as only a warning.

Those manual paths bypass the upstream rs-platform-wallet higher-level orchestration that exists for this exact lifecycle (PlatformAddressWallet::fund_from_asset_lock, ShieldedWallet::shielded_fund_from_asset_lock, or equivalent): submit_with_cl_height_retry, IS-proof-invalid → CL-proof upgrade, proof/status persistence before retry, post-success balance/activity bookkeeping, and consume_asset_lock cleanup.

So the fresh asset-lock transaction itself is tracked better than JS SDK, but these DET call sites still have a post-asset-lock / pre-final-accounting gap: Platform submit failures or confirmation ambiguity can leave a valid tracked lock in an intermediate/reusable state, and successful submits are not consistently marked Consumed. That is not the same “lost one-time private key forever” failure as JS SDK, but it is still an incomplete recovery/resume implementation for asset-lock-funded Platform address and shielded flows.

Suggested fix: route these non-identity flows through the upstream wallet orchestrators, or mirror the same orchestration locally: resume/upgrade tracked proof, submit_with_cl_height_retry, persist any CL upgrade before retry, and mark the asset lock Consumed only after Platform acceptance/accounting succeeds.

@lklimek

lklimek commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Verified your analysis against current source — it holds. Here's how PR #860 now addresses it.

Shielded asset-lock flow (shield_from_asset_lock, src/backend_task/shielded/bundle.rs) — fixed. You correctly identified the real fund-safety bug: a wait_for_response failure after a successful broadcast was swallowed (warn! + .ok()) and fell through to Ok(credits) — a false success on a single-use asset lock. It now maps to a dedicated typed TaskError::ShieldedConfirmationUnknown (#[source]-chained, with an actionable, jargon-free user message) and is propagated, so an ambiguous confirmation can never report success. Two new offline tests cover the mapping and the message.

Platform-address flows (fund_platform_address_from_wallet_utxos.rs, fund_platform_address_from_asset_lock.rs) — no false-success; consume/retry orchestration deferred (upstream-gated). We traced both: the manual outputs.top_up(...) calls are ?-propagated through upstream broadcast_and_wait, so a submit/confirmation failure already surfaces as an error — no false success. What's missing is the terminal orchestration you named — submit_with_cl_height_retry, IS→CL proof upgrade, and marking the lock Consumed. Routing through PlatformAddressWallet::fund_from_asset_lock / PlatformWallet::shielded_fund_from_asset_lock is not possible from DET at the pinned rev: PlatformAddressWallet isn't re-exported, and submit_with_cl_height_retry / consume_asset_lock are pub(crate). Residual risk is a tracked asset lock left in a reusable/recoverable state on ambiguous failure — recoverable, not lost funds. Precise in-code TODOs naming the required upstream symbols sit at each choicepoint.

Also surfaced by review: the four sibling shielded spend fns (shield_credits / shielded_transfer / unshield_credits / shielded_withdrawal) swallow the same way and mark notes spent pre-confirmation — but that self-heals on the next nullifier/note resync (check_nullifiers reconciles against chain), so it's a temporary local divergence, not note loss.

All tracked in docs/ai-design/2026-06-01-pr860-gap-audit/ as PROJ-042 (this gap; shield false-success resolved in-PR, platform-address consume orchestration OPEN pending upstream API exposure) and PROJ-043 (the sibling-swallow follow-up). Verified by funds-safety (SHIP) and correctness (PASS) review passes.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier reply. I previously said the platform-address consume/retry orchestration was upstream-gated and not possible from DET. That was wrong — it is reachable through the public PlatformWallet API with no upstream change, and it's now implemented in this PR.

What landed (pushed):

  • WalletBackend::fund_platform_address routes wallet-owned platform-address asset-lock funding through the upstream orchestrator PlatformAddressWallet::fund_from_asset_lock — i.e. resolve → submit_with_cl_height_retry → IS→CL fallback → consume_asset_lock. This closes the recovery gap you flagged for the common (wallet-owned) case.
  • The orchestrator-vs-manual decision queries upstream's own pool membership (contains_platform_address) rather than any DET-side heuristic: a wallet-owned, in-pool destination gets full orchestrated recovery; a foreign/non-pool destination intentionally keeps the manual top_up path (advanced users may knowingly fund any address). Both paths propagate submit failures — no false success.
  • The shielded asset-lock false-success is fixed separately (typed ShieldedConfirmationUnknown, no longer swallowed).

Commits: 37c2889e (shield), 1010828c (orchestrated routing), c45ac35e (upstream pool-membership gate). Verified by funds-safety + correctness review. Remaining residuals are tracked in docs/ai-design/2026-06-01-pr860-gap-audit/ — PROJ-042 resolved; PROJ-043 (a self-healing sibling-swallow in the four shielded spend paths) and a minor fee-from-wallet change-recipient case are being addressed as follow-ups.

Thanks for the catch — your read on the recovery gap was correct, and so was the implication that DET should route through the upstream orchestration.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Two reliability fixes from live testing — crash visibility + DAPI ban storm (3c61a65664632628)

Pushed 5 commits (dd3c390c..64632628). Both surfaced during fresh-boot testing on testnet.

Issue B — silent crashes now leave a trace (3c61a656, 96b61da7). The app could quit with nothing in det.log (a GUI launch loses stderr, and the Rust panic hook only catches catchable panics — native SIGSEGV/SIGABRT/abort/OOM vanish). Added: capture_stderr_to_file() (redirects fd 2 to a sidecar det-stderr.log, rotated, with a guard that no-ops when the logger already fell back to stderr) and install_fatal_signal_handler() (async-signal-safe write of a per-signal marker for SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE, then re-raises so the core dump is preserved). Unix implemented + verified (abort/segv captured); Windows is a documented warn+TODO.

Issue A — Platform/identity sync no longer bans every DAPI node during initial sync (f318c06f, 9d5c865b). Root cause: sync started immediately at boot and issued proof-verifying DAPI calls before SPV had the masternode list → the context provider failed → the SDK's ban_failed_address banned all ~156 nodes (no available addresses). Fix: a CoordinatorGate (one-shot, single-winner latch) defers the platform/identity coordinator starts until the masternode list reaches Synced (which happens right after headers, well before the slow filter/block scan — so Platform still comes up early), driven event-driven from EventBridge; the SPV context provider returns a non-banning "not ready" error pre-quorum; the flag resets on disconnect/network-switch so a new session re-proves readiness. The gate captures Weak handles (a strong capture would pin the persister lock past teardown → AlreadyOpen on reconnect), with a dedicated regression test proven to catch a strong-capture revert.

QA: Smythe SHIP + Marvin PASS on both (gate proven never-strand + exactly-once; signal handler proven async-signal-safe). Consolidated tip: 987 tests pass / 0 fail, clippy + fmt clean. CHANGELOG updated.

Note: the platform pin is unchanged here (still the dev rev 4f432c9); the merge gate (re-pin to a tagged release) remains tracked separately.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Re-pin platform deps to PR dashpay/platform#3828 branch (35f18be9)

Picks up the upstream merge that just landed on #3828 (the wallet-rehydration + core-derived-rehydration / persister fatal-flush-loop fixes). All four platform crates (dash-sdk, rs-sdk-trusted-context-provider, platform-wallet, platform-wallet-storage) re-pinned from rev = "4f432c9" to branch = "fix/wallet-core-derived-rehydration"; Cargo.lock locked to head 925dfcbf. Transitive rust-dashcore eb889af1981e97f1.

Signature-only API port (behavior-preserving): shielded builders gained sender_ovk / dummy_outputs (DET passes None / 0 — byte-equivalent to the prior single-output form; surplus_output pre-existed as None); MasternodeNetInfo.service_address became a Legacy | Extended enum (UI display helpers with a non-routable fallback); two new PlatformWalletError variants (ShieldedBroadcastUnconfirmed / ShieldedSpendUnconfirmed) bucketed Other (do-not-resubmit).

QA — funds-safety SHIP, correctness PASS. Smythe verified the shielded-builder defaults are provably equivalent to prior behavior (recipient funds/spendability unaffected; surplus folds to fee pools, nothing stranded), and the rust-dashcore bump introduced no derivation/signing/quorum drift (bip32 signing + contains_platform_address byte-identical; the breaking network-scoped wallet_id change doesn't affect DET, which routes on the BIP44 account xpub, not wallet_id). Marvin confirmed the port is signature-only with an exhaustive error match, Issue A's masternode-readiness surface unchanged, and 987 tests pass / 0 fail, clippy + fmt clean.

Note: this is a branch = pin (a moving target) to track the in-flight upstream fix; Cargo.lock keeps today's build reproducible at 925dfcbf. The release merge gate still requires an immutable tag/rev before this PR merges.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

NEW since last push — 561bce8d WalletNotLoaded root-cause + upstream BIP32/BIP44 persistor-collision fix

Wallets failed to load on cold boot (WalletNotLoaded for every wallet, even on a fresh DB). Root cause: an upstream platform-wallet-storage primary-key collision. WalletAccountCreationOptions::Default creates both a BIP32 account-0 (m/0', depth-1) and a BIP44 account-0 (m/44'/coin'/0', depth-3), but account_type_db_label collapsed both to the single label "standard", so the two rows shared the account_registrations PK (wallet_id, "standard", 0). On persist, apply_registrations (INSERT…ON CONFLICT DO UPDATE) let the BIP32 depth-1 row overwrite the BIP44 depth-3 row — silently losing the BIP44 xpub. Cold-boot seedless reload then read the wrong depth-1 key and the fund-routing gate rejected every wallet.

Fixed upstream in platform PR #3828: the BIP32/BIP44 standard accounts now use distinct labels ("standard_bip32" / "standard_bip44"), so both rows coexist and BIP32 is preserved (users with legacy m/0' funds keep that account). This branch re-pins the platform crates to #3828 HEAD 925b109d.

  • Re-pin platform crates 925dfcbf → 925b109d (Cargo.lock only; deps are branch-pinned). 561bce8d
  • Regression guard: issue7_fresh_persistor_bip44_xpub_matches_det_bridge flipped from #[ignore] to a live test — it drives the real load_from_persistor_seedless cold-boot and asserts the stored BIP44 xpub is depth-3 and matches DET's bridge, and that the BIP32 row still survives (keep-BIP32 invariant).
  • Terminal startup-failure notice (226db5fc): native startup failures now print a generic, actionable message plus log-file paths to the real terminal (stderr is redirected to a sidecar log), instead of exiting silently.
  • The earlier #251 xpub-drift self-heal (9c45d7f3, b4668129) was investigated and reverted (89378d21) — it treated a symptom; the real cause is the upstream collision above.

Known limitation: there is no heal migration for DBs already corrupted by the old collision — those need a delete + re-import (the upstream PR treats the schema as unshipped). A follow-up protected-wallet-after-unlock registration fix is in progress (locked-at-boot wallets were not re-registered on unlock).

Suite green: 887 lib tests pass; fmt + clippy clean.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up landed — 44caa892 register protected wallet on unlock

The "protected-wallet-after-unlock registration fix in progress" noted above is now in.

A password-protected wallet hydrates Closed (locked) at cold boot, so bootstrap_wallet_addresses_jit skips it on the is_open() gate (to avoid a surprise startup prompt) and it never enters the upstream fund-routing id_map. Previously the unlock gesture only promoted the verified seed into the session cache — it never re-drove registration — so a protected (or freshly migrated) wallet stayed WalletNotLoaded for the session and was skipped again on the next boot.

handle_wallet_unlocked now calls a new drive_unlock_registration, which spawns bootstrap_wallet_addresses_jit on a tracked subtask once the seed is session-cached → the wallet is upstream-registered prompt-free, no restart needed (seed promoted before the spawn ⇒ no second password prompt; the remember=false path is a no-op).

  • Touches only src/context/wallet_lifecycle.rs.
  • Test: protected_wallet_registers_upstream_on_unlock_without_restart — migration-faithful (legacy protected row → migration → hydrated Closed+unregistered → unlock → registered).
  • Suite green (888 lib tests), fmt + clippy -D warnings clean.

This closes the remaining case after the #3828 re-pin (561bce8d): the re-pin fixes the persistor BIP32/BIP44 collision for fresh/unprotected wallets; this fixes the migrated-protected wallet whose upstream registration is empty at boot and can only be registered via the unlock path.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

NEW since last push — shielded backend retired in favour of upstream platform-wallet coordinator (f637365056ac2cfe)

Pushed 16 commits (44caa892..56ac2cfe). DET's home-grown Orchard shielded subsystem is removed and every shielded operation now routes through upstream platform-wallet's shielded coordinator. The bundle.rs:634 TODO(upstream-gated) was stale — the orchestrator is pub; the only real gate was the missing platform-wallet shielded cargo feature.

What landed (net −4,509 lines):

  • Enable + wire the shielded feature; configure_shielded + ShieldedSyncManager lifecycle. (f6373650)
  • WalletBackend op + bind methods wrapping the upstream ops (shielded_fund_from_asset_lock / shielded_shield_from_account / shielded_transfer_to / shielded_unshield_to / shielded_withdraw_to) with an exhaustive map_shielded_op_errorShieldedSpendUnconfirmed can never report success. (caa443af, 18aa6ee2)
  • Push balance snapshot: AppContext::shielded_balances written by EventBridge::on_shielded_sync_completed (maps upstream WalletIdWalletSeedHash), read synchronously in the egui frame loop — no block_in_place. Lazy bind via ensure_shielded_bound on JIT unlock. (7df68d78, 47c6eeba, fa0e46de)
  • Deleted the 6 old shielded modules (context/shielded.rs, wallet_backend/shielded.rs, backend_task/shielded/{bundle,sync,nullifiers}.rs, database/shielded.rs, model/wallet/shielded.rs) + ShieldedTask reduction + a forward DROP TABLE migration + per-network legacy-file cleanup. (479c8c18)
  • 5 new det-cli/MCP self-test tools: core_wallet_import, shielded_init, shielded_sync, shielded_balance_get, shielded_address_get. (63b2e796)

Also in this push (cleanups): module-placement policy added to CLAUDE.md + TrackedAssetLockCacheui/state/; WalletUnlockPopup slimmed to a thin passphrase_modal wrapper; legacy ScreenWithWalletUnlock removed (migrated 3 screens). PROJ-032 closed as N/A (DashPay was never persisted in any released version — verified against v0.9.3); PROJ-034 confirmed real and tracked as a follow-up.

Verification: 874 lib + 88 kittest pass, clippy --all-features --all-targets clean (0 warnings), 3 build profiles green. A full grumpy-review (docs/ai-design/2026-06-16-pr860-grumpy-review/) found 0 critical / 0 high / 4 medium / 21 low; the medium + clear-low findings were fixed (56ac2cfe: mnemonic redact+zeroize in core_wallet_import, doc-accuracy, dead bind-guard, typed errors, stale-balance eviction, awaited coordinator wipe).

Open for reviewers:

  • One MEDIUM left for discussion, not auto-changed: the typed passphrase is parked in a single global egui cache slot keyed by window title (password_input.rs) — possible cross-prompt leak; needs a UX-security call.
  • Live testnet self-test of the shield→sync→transfer→unshield→withdraw loop via det-cli is recommended before merge.
  • Merge remains gated on the upstream fix/wallet-core-derived-rehydration branch being released (the platform deps are pinned to it).

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

NEW since last push — 0484bcb6 post-migration identity auto-discovery (By-Wallet) + rolling gap-limit

Why: After the platform-wallet migration, a wallet's identities were only loaded if the user manually opened Load Identity → By Wallet and searched. This push discovers them automatically once Platform connectivity is ready, with a rolling gap-limit so newly-created identities are always found, and preserves user-set aliases on re-discovery.

What it does:

  • Auto-discovery on Platform-ready. Once the SPV masternode list reaches Synced (the point at which Platform/DAPI is safe to query — querying earlier gets DAPI nodes banned), DET runs By-Wallet identity discovery across all open wallets. Two triggers: a global once-per-session sweep (AtomicBool latch, cleared on SPV stop so it re-arms on reconnect) and a per-wallet, latch-independent trigger fired when a wallet is unlocked.
  • Rolling gap-limit (IDENTITY_GAP_LIMIT = 5). Scans to max(existing identity index) + 5, extending the window on each discovery so 5 empty indices always trail the highest found index (mirrors the BIP-44 address gap-limit). Bounded by a hard cap of 100. Pure decision logic in model/identity_discovery.rs (10 unit tests); one shared scan fn used by both the UI By-Wallet path and the auto-trigger.
  • Alias preservation on re-discovery. Re-discovered identities are updated in place, but DET-only metadata (alias, wallet binding) is carried forward; top-ups are unaffected (stored under a separate key).

Safety items fixed in this push (surfaced by review):

  • (High, pre-existing bug) the single-index By-Wallet search silently wiped a user's alias on every search — fixed (both single-index and gap-limit paths now carry the alias forward).
  • (High, UX hazard) a background sweep over a locked protected wallet would have popped a passphrase modal unprompted (identity-auth keys are hardened-to-leaf, so a cold cache miss triggers a secret prompt); the background path now runs allow_prompt=false and skips locked wallets, and the eventual unlock re-triggers discovery for that wallet.

Verification: headless build clean · clippy -D warnings zero · 889 lib tests pass (4 new offline guards: latch one-shot + stop_spv re-arm, locked-wallet exclusion, alias preservation, search-index cap). Design + QA reports added under docs/ai-design/2026-06-17-identity-autodiscovery-gap-limit/.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Session update — 5 commits pushed (d2f4fd87..1c4f4dcd)

Three fixes landed against the platform-wallet backend, each root-caused from a live testnet symptom:

Reconnect crash (09540cf8)

  • Connect → disconnect → connect failed with WalletStorage(AlreadyOpen { … platform-wallet.sqlite }).
  • Cause: WalletBackend::shutdown() never stopped the SPV run loop, so the persister path stayed registered in the process-global open-set.
  • Fix: shutdown() now calls spv().stop().await before tearing down the wallet manager.

Watch-only asset-lock failure (98bc4913, tests b8517905 + acdd1336)

  • Both "create identity" and "send funds core→identity" failed with AssetLockTransaction("Watch-only wallet has no private key").
  • Cause: wallets reload seedless (watch-only); identity funding-account provisioning called add_account(type, None), which has no key material on a cold-booted watch-only wallet.
  • Fix: provisioning now derives the account xpub from the seed (derive_extended_public_key) inside the with_secret_session scope and calls add_account(type, Some(xpub)). Regression guard uses a two-boot cold-start scenario (write persister from seed → copy data dir → cold-load seedless → assert provisioning succeeds + is idempotent).

Legacy MN-list-diff inspector + Core P2P handler removed (1c4f4dcd, −5,559 LOC)

  • The Masternode-List-Diff inspector was the only RPC-only tool and is dead in SPV-only mode. Removed the screen (~4.5k LOC), core_p2p_handler.rs, MnListTask, TaskError::P2P, all wiring, and the backend-e2e mnlist tests.
  • Verified independently: git grep clean of all removed symbols; surviving chainlock paths use only CoreTask::GetBestChainLock (no dependency on the removed task/handler).

Verification: cargo build (default + --features headless) ✅ · clippy --all-features --all-targets -D warnings ✅ zero · test --all-features --workspace998 passed, 0 failed · +nightly fmt --check ✅ clean.

🤖 Generated with Claude Code

@lklimek

lklimek commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 1c4f4dcd..976ad0d4 (3 commits): backend-e2e coverage (B reconnect + C/D cold-boot), event-driven ensure_spv_synced (②), and the interim persister-release barrier for the reconnect AlreadyOpen regression (B-2).

The B-2 barrier is verified as an interim stopgap (closes the common coordinator-thread drain race). The durable fix — a reusable cancel + await-exit shutdown primitive (StructuredShutdown) covering both DET-owned wallet subtasks and the upstream coordinator threads, with the architecturally clean variant being keep-the-WalletBackend-alive + SPV/coordinator restart-in-place — is designed and will retire the barrier. An upstream rs-platform-wallet issue (make coordinator quiesce() join, not cancel-and-drain) will be filed. See the "Additional fixes folded in (2026-06-18)" section in the description.

🤖 Co-authored by Claudius the Magnificent AI Agent

lklimek and others added 5 commits July 8, 2026 18:29
- 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>
…unified send

Round-2 audit finding CODE-098 ("unified send screen is the way to
go, make it reusable and reuse"): shield_screen.rs, shielded_send_screen.rs,
and unshield_credits_screen.rs deleted; WalletSendScreen gains a
SendFlow preset enum {General, Shield, ShieldedSend, Unshield} +
with_flow() builder, locking source/destination appropriately per
flow (Shield's destination is dispatch-ignored, satisfied by a
discriminant-only sentinel). shielded_tab.rs routes through
open_send_flow() instead of three standalone screens. Shared shielded-
recipient parsing (bech32m + 43-byte hex) extracted to
model::address::parse_shielded_recipient. Net -178 lines from the fold
itself.

Independent QA pass (funds-adjacent surface, given this touches
shield/unshield/shielded-send dispatch) found and this includes fixes
for:
- Shield-from-Platform's Max button under-reserved the shield fee by
  ~6x versus the old screen (new shield_from_balance_fee_headroom()
  in model/fee_estimation.rs restores the correct >50M-credit reserve).
- Raw-hex shielded-recipient entry was dropped from the UI even though
  the model parser still accepted it (AddressInput/AddressKind now
  detect and validate the 43-byte hex form too).
- A network switch didn't clear a preset flow's stale wallet/seed-hash
  state, risking a stale cross-network balance display (send stayed
  blocked, but misleading); WalletSendScreen::reset_for_network_switch()
  fixes it.

Sentinel-destination handling, dispatch routing for both Shield
sources, and the unlock gate were independently verified correct with
no changes needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
…(final)

CODE-021/067/084/102/106, PROJ-007 (round-2 audit, run last since it
touches files across every prior wave): comment-only sweep, zero
executable logic changed. Development-history/archaeology narration
("Phase B/D/E", "Post-D4c", "used to read", ephemeral commit-hash/
finding-ID references) rewritten to present-state descriptions or
concrete TODOs across det_platform_signer.rs, snapshot.rs,
wallet_backend/mod.rs, secret_seam.rs, wallet_seed_store.rs,
wallet_lifecycle.rs, backend_task/shielded/mod.rs, dashpay.rs,
model/fee_estimation.rs, model/wallet/single_key.rs, shielded_tab.rs,
wallets_screen/mod.rs, contacts_list.rs, contact_details.rs, theme.rs,
message_banner.rs, address_input.rs, add_new_identity_screen/mod.rs,
key_info_screen.rs; durable test-spec IDs (TS-DBG-01) kept. CLAUDE.md
corrected: removed the stale per-platform ZMQ mention and narrowed the
ConnectionStatus health list to SPV+DAPI (RPC/ZMQ subsystems were
retired in the platform-wallet migration); protoc v25.2+ claim
verified accurate against CI config.

This is the last of 21 planned waves plus the CODE-098 follow-up --
round 2 of the full-repo architecture audit is now fully merged.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
lklimek and others added 13 commits July 9, 2026 09:57
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>
…urces

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>
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>
… 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>
… 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>
…upt 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>
…ropagation 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>
…, 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
  `candidate != destination` wrapped in a documented fn with two dedicated
  tests) at its single call site and delete the fn and its tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Present-state architectural rationale about a module boundary — Platform
per-address balances and the sync cursor are owned by the upstream coordinator,
so DET keeps no at-rest copy and warm-starts from the coordinator's first push.
This is an external-constraint explanation, not a git-history tombstone, and
was removed in error during the tombstone cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y-scoped submodules

Pure code-move: converts the 4305-line `src/context/wallet_lifecycle.rs` (the
repo's largest file) into a module directory whose `impl AppContext` methods are
grouped by responsibility across submodules — the same multi-impl-of-one-struct
layout `wallet_backend/` already uses. No logic, signature, or behavior change;
the only signature edits are two visibility widenings noted below.

New structure (`src/context/wallet_lifecycle/`):
- `mod.rs` — module docs, shared imports/constants, the three free helpers
  (`spv_storage_dir`, `clear_spv_chain_storage`, `cleanup_legacy_shielded_files`),
  the `AppContext::wallet_arc` lookup, and the submodule + test declarations.
- `spv.rs` — backend wiring and chain-storage lifecycle: `clear_spv_data`,
  `clear_network_database`, `ensure_wallet_backend_and_start_spv`,
  `mark_spv_error`, `stop_spv`.
- `registration.rs` — single-key import and HD-wallet registration/persistence:
  `import_single_key_wif`, `verify_single_key_passphrase`, `register_wallet`,
  `register_wallet_upstream`, `write_seed_envelope`, `write_wallet_meta`,
  `promote_seed_to_session`.
- `removal.rs` — `remove_wallet`.
- `bootstrap.rs` — address derivation and post-unlock warmup: the
  `bootstrap_*`/`wallet_needs_bootstrap` methods, managed-identity reconciliation
  and contact-account/auth-key warming, and the identity-discovery queue.
- `unlock.rs` — lock/unlock handling and the open-wallet snapshot:
  `handle_wallet_unlocked`, `drive_unlock_registration`, `handle_wallet_locked`,
  `open_wallets`, `unregistered_open_wallet_count`.
- `tests.rs` — the `#[cfg(test)]` module, unchanged.

Submodules access `AppContext`'s private fields as descendants of `context`
(no field-visibility change) and share `mod.rs`'s imports via `use super::*`.
The only forced visibility widenings: `open_wallets` and
`reconcile_managed_identities` go from private to `pub(super)` because they are
now called from a sibling submodule (and, for both, from the test module).

Verified move-integrity: the set of `impl AppContext` method names is identical
before/after, the test-function count is unchanged (56), clippy
`--all-features --all-targets -D warnings` is clean, and the full `--lib` suite
passes 1393/1393.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Identity Hub hero card rendered a 14%-opacity blue→purple wash that
read as a heavy blue block rather than the subtle diagonal accent the
wireframe implied. Per the design rule that wireframes govern placement
while colors follow the app theme, drop the gradient entirely: the card
is now a flat `DashColors::surface` fill with the existing border and
elevation.

This supersedes the earlier gradient-containment commit — with no
gradient, the reserved-slot machinery and its geometry tests are removed
along with the now-unused colour-lerp helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sts; recover remaining identities-screen lock

Resolves the QA blockers on the branch.

- Merge gate (workspace test) was red: the Wave-6 god-file split moved the
  wallet_lifecycle test fixtures — which construct legacy-schema `wallet` /
  `single_key_wallet` rows — from the monolithic file into the split module's
  `tests.rs`, but `tests/legacy_table_surface.rs`'s allow-list still pointed at
  the old path, so TC-DEV-001/003 fired on the moved (previously-allowed)
  fixtures. Repoint the single allow-list entry to
  `src/context/wallet_lifecycle/tests.rs`. Verified the guard patterns
  (`FROM wallet`, `INTO single_key_wallet`) appear only in that test file — no
  production submodule contains legacy-table SQL — so the exemption is unchanged,
  only its path.

- The unwrap sweep missed one `self.identities.lock().unwrap()` in
  `identities_screen.rs::ui()` (it was split across lines, so the single-line
  pass skipped it): on a poisoned identities mutex that screen would panic on
  every repaint instead of self-healing. Convert it to `.lock_recover()` like its
  ten siblings. A multi-line re-scan of the sweep-touched files found the same
  line-wrapped gap in four more production wallet/single-key `RwLock` reads
  (add_existing / add_new identity screens, wallets screen); convert those to
  `.read_recover()` too. Remaining line-wrapped matches are all test-only code
  (`#[cfg(test)]` modules, `kv_test_support`) and are left as-is.

- Refresh the `poison.rs` module doc to describe its present usage surface — the
  loaded-wallet map, per-screen view state and settings cache, the Identities /
  DPNS / token-search screen caches, the coordinator-gate slot, and the SQLite
  connection mutex — rather than only the two wallet-backend caches it originally
  named, and note the DashPay address-index mutex's deliberate fail-loud
  exception.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix: round-2 full-repo architecture audit (21 waves + shielded-screen consolidation)
…et rewrite (#876)

* fix(withdraw): pre-select only a locally-signable withdrawal key

The Withdraw screen constructor pre-selected a key via the on-chain
lookup `identity.get_first_public_key_matching(TRANSFER, ...)`, which is
unfiltered by local private-key presence. On loaded masternode/evonode
identities where only the Owner key was supplied, this picked a
"ghost" Transfer key with no local private material, so the withdrawal
failed at signing with a raw, unhelpful protocol error.

- model: add `QualifiedIdentity::default_withdrawal_key()` — sourced from
  `available_withdrawal_keys()` (private-key-backed only), Transfer
  preferred with Owner fallback, `None` when nothing is signable.
- ui: constructor now pre-selects via `default_withdrawal_key()`; the
  developer-mode on-chain escape hatch is preserved. When no usable key
  exists the existing empty-state guides the user to add one.
- error: add `TaskError::NoWithdrawalSigningKey` (typed `#[source]`,
  plain-language actionable Display) mapping the SDK
  `DesiredKeyWithTypePurposeSecurityLevelMissing` protocol error as a
  defense-in-depth backstop instead of leaking a raw string.
- tests: 4 model cases (ghost key rejected, private-backed selected,
  owner fallback, transfer preferred) + 2 error-mapping/Display cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(claude): correct secret-storage note on identity-key encryption

Identity keys (imported/loaded, including masternode voting/owner/payout)
are no longer categorically in the deferred keyless tier: they enter
unprotected at load time but can be sealed to Tier-2 per-identity via
IdentityTask::ProtectIdentityKeys (Key Info screen "Add password
protection"). Clarify that the keyless residual is only no-password
secrets and keys the user has not opted to protect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(masternodes): add page-nav model with two-scope selection (A1)

Introduce ui/state/global_nav.rs: PageNavSpec (page-aware segment-1 +
per-page pill composition) and IdentityPillScope (AppGlobalUser vs
PageScopedObject). The PageScopedObject variant carries its own selection
and never writes AppContext::selected_identity_id — the structural FR-6
boundary the global switcher (A2) and the Masternodes page (B7) build on.

Pure state, renders nothing (module-placement discriminator -> ui/state).

Satisfies TC-NAV-13, TC-NAV-14, TC-NAV-15; foundation for TC-NAV-12,
TC-FR6-07.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): generalize breadcrumb into page-aware global switcher (A2)

Add ui/components/global_nav_switcher.rs: a page-aware switcher driven by
PageNavSpec, rendering segment-1 (page label + link) plus composable
wallet / identity-or-object pills. GlobalNavEffect generalizes the hub's
BreadcrumbEffect and adds SelectPageObject for the page-scoped pill — kept
distinct from SelectIdentity so a page-scoped selection never writes the
app-global identity (FR-6 boundary at the effect level).

Reuses BreadcrumbPill / IdentityPill / BreadcrumbPillMode verbatim (no new
pill widget). breadcrumb_switcher.rs becomes a thin hub-facing shim that
builds the hub spec, delegates to the generalized render, and maps the
effect back (self-nav to the hub root -> OpenPicker), keeping hub behavior
unchanged — verified by the existing identity_hub_switcher kittests.

Satisfies TC-NAV-01, TC-NAV-02, TC-NAV-03, TC-NAV-13, TC-NAV-16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): render global switcher on root screens + shared applier (A3)

Add the shell glue in top_panel.rs: apply_global_nav_effect (the shared
successor to the hub's apply_breadcrumb_effect — silent app-scoped
wallet/identity writes, no forced navigation) and add_top_panel_with_global_nav
(one-call render-plus-apply), with subdued_everyday_spec / subdued_wallet_only_spec
Phase-A rollout helpers.

Wire the switcher onto four non-Hub root screens with Subdued (unwired)
specs + TODO markers: Identities, DPNS, DashPay (everyday: wallet + identity
pills) and Wallets (wallet-only composition, TC-NAV-15). The Hub keeps its
existing interactive pills via the breadcrumb shim (regression — full
kittest suite green).

Document (comment + test, PROJ-010) that set_selected_hd_wallet reconciles
the app-global identity as a side effect on non-Hub pages, and that combined
with B1's resolution-layer filter it must never reconcile onto an MN/Evonode.

Deferred (documented): tokens/tools screens carry in-header sub-navigation
that needs the FR-GLOBAL-NAV-5 content-panel back-row migration before their
plain breadcrumb can be swapped for the global switcher — a follow-up, not a
mechanical swap.

Satisfies TC-NAV-06, TC-NAV-15, TC-NAV-16; enables TC-NAV-12/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): load-time key encryption plumbing (B0)

FR-8: add IdentityInputToLoad.encryption_password: Option<Secret>. When
Some, load_identity validates the password up front (fast fail) then, after
insert migrates the keyless keys into the vault, seals them Tier-2 through
the existing per-identity protect envelope (protect_identity_keys →
put_secret_protected via the secret_seam chokepoint) — no new crypto, no
second persistence path. When None, the keyless Tier-1 path is unchanged.

Relocate validate_protection_password from protect_identity_keys.rs into
model/identity_key_protection.rs (PROJ-006, DET validation-placement rule);
the seal path and load path both call the model validator.

MCP masternode_identity_load passes encryption_password: None (PROJ-007 —
GUI-only this iteration, requirements §2.3) with a TODO for headless
password parity.

Add typed TaskError variants DuplicateProTxHash { identity_id } and
MalformedProTxHash { input } for later duplicate/malformed rejection (B1/B4),
avoiding string parsing.

Tests: model validator (relocated); an offline-wired-AppContext test proving
a load-time password seals a masternode's voting (V-target), owner and
identity (M-target) keys Tier-2 and round-trips under the password — the
exact call load_identity makes (TC-FR8-01/02/10). Load-form UI is B4;
end-to-end load routing is covered by the network backend-e2e suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(masternodes): drop ephemeral review ID from A3 reconciliation comment

Self-review: replace a transient review-finding ID in the apply_global_nav_effect
reconciliation note with the durable FR reference. Comment-only; no behavior
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): FR-6 resolution-layer boundary + masternode accessor (B1)

FR-6 (R1, release-blocking): keep masternode/evonode identities out of every
everyday-user surface by filtering at the resolution layer, not the display
call sites.

- resolve_selected_identity(): candidate set filtered to IdentityType::User
  before resolving, so neither keep-if-loaded nor the first-loaded fallback
  can ever resolve a masternode — even when a masternode is the only/first
  loaded identity (TC-NAV-12b).
- set_selected_hd_wallet(): the wallet-switch identity reconciliation resolves
  only over the wallet's User identities.
- restore_selected_identity_from_kv(): one-time sanitization — a masternode
  persisted as selected_identity_id in a prior session is cleared on load;
  a User selection is kept (TC-NAV-12c). In-memory only (non-destructive).
- Display sources switched to the established User-only accessor
  load_local_user_identities(): the global switcher's identity pill + dropdown
  and the Identity Hub landing/picker now list User identities only, so the
  wallet-less "no wallet on this device" group can no longer surface an
  MN/Evonode (TC-NAV-17). Masternodes stay in the legacy Identities table
  (unfiltered accessor untouched — locked decision #2).

New context accessor load_local_masternode_identities() (hydrated MN/Evonode)
— the Masternodes-page card list + page-scoped pill source (B3/B7).

Tests: offline-wired-AppContext unit tests (accessors, resolution filter incl.
lone-masternode fallback, stale-MN sanitization) + a Hub kittest asserting a
seeded Masternode+Evonode never appear on the hub while remaining in the
masternode accessor. TC-FR6-01…06, TC-NAV-12b/12c, TC-NAV-17.

Deferred to consuming tasks (documented): FR-7 refresh (TC-FR7-02/03) composes
existing RefreshIdentity + contested-names refresh at the card Refresh button
(B3); the per-node open-contest card read accessor lands in B3 where the card
consumes it and it is testable against the rendered status line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): register Expert-gated Masternodes root tab (B2)

Add RootScreenType::RootScreenMasternodes (stable int 28, round-trip test),
ScreenType::Masternodes, Screen::MasternodesScreen, create_screen and all
ScreenLike dispatch arms; register the always-present root screen in app.rs
(gated at runtime by Expert Mode, not a Cargo feature, so the screen exists
to switch into when the gate is on).

Nav: a "Masternodes" left-panel entry gated FeatureGate::DeveloperMode,
positioned directly below the identity cluster (locked decision #3),
independent of the identity-hub feature. Distinct glyph voting.png (TODO:
dedicated node/server icon). The existing per-entry gate skip hides the nav
item and route when Expert Mode is off.

Live de-gating (§10.11): active_root_screen_mut falls the active tab back to
Identities (always registered) if Expert Mode flips off while Masternodes is
selected, so the gated screen is never shown without its gate.

MasternodesScreen is a scaffold (global-nav header + left rail + island
placeholder); the empty state + card grid land in B3, the page-scoped
masternode pill in B7. Network-switch already calls change_context on
main_screens; the sub-screen reset (§10.10) applies once B4/B5 push
sub-screens (noted for B8).

Tests: RootScreenType round-trip (int 28 stable); kittest — nav absent
Expert-off / present Expert-on, and de-gating falls back to Identities.
TC-FR1-01…07, TC-EDGE-05/06.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): empty state + card grid + card body (B3)

Render the Masternodes root screen content on top of the B2 scaffold:

- Empty state (FR-2): canonical §7 copy — heading, body, "Load a
  masternode" primary CTA, and the ProTxHash reassurance line.
- Card grid (FR-3): responsive minmax(260,1fr) grid reusing the identity
  picker's visual language via a new `MasternodeCard` (monogram +
  `draw_type_badge`, both now `pub(crate)`). Body adds masternode rows the
  picker lacks: voter readiness, compact `V O P` key status (glyph, not
  colour-only — NFR-6), DPNS status line, and the IdentityStatus dot+label.
- DPNS status precedence (§10.1): open-contest count first, then a pending
  scheduled vote, then "No open contests", via a display-layer
  `AppContext::masternode_contest_summary` read (no new backend concept).
- Key presence: `QualifiedIdentity::masternode_key_presence` maps
  Voting/Owner/Payout to voter-identity / OWNER / TRANSFER keys.
- Top-right Refresh toolbar button (FR-7) reloads the cached node list.
- Whole card is a single accessible click target (`WidgetInfo::labeled`,
  NFR-6); selection/load intents are captured for B4/B5a/B7 wiring.

Traceability: TC-FR2-01…07, TC-FR3-01…15, TC-FR7-01, TC-NFR6-01/03.
Unit tests cover heading/sub-line, DPNS precedence, key tokens, badge, and
the 8 V/O/P combinations; kittest covers empty-state copy and the grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): dedicated load form + ProTxHash validator (B4)

Add the MN/Evonode-only load flow (FR-4), carved out of the generic
add-existing-identity path:

- `ui/masternodes/load_form.rs`: ProTxHash (required), Masternode/Evonode
  segmented toggle (default Masternode, no User option), optional alias,
  V/O/P key inputs (reused hold-to-reveal `PasswordInput`), optional at-load
  encryption password (drives B0's seal), always-visible Warning-tone
  key-storage note, and a Load button gated on a non-empty ProTxHash with the
  §7 disabled tooltip. Switching node type clears all fields (§10.6). No
  auto-derive affordance — masternode keys are never wallet-derived
  (US-6 retired, §Locked-#4).
- `model/masternode_input.rs`: `is_valid_pro_tx_hash` shape validator (hex or
  Base58) for inline on-blur validation; the backend load task remains the
  authoritative existence/duplicate check.
- Masternodes screen gains a List/Load view enum; the empty-state CTA and a
  `+ Load` toolbar button open the form; submit dispatches
  `IdentityTask::LoadIdentity`; cancel/submit return to the list with a fresh
  form on reopen.
- `add_existing_identity_screen`: remove Masternode/Evonode from the
  Advanced-Options Identity-Type dropdown (User-only remains) — no competing
  entry point (§10.2 / TC-FR4-22, FR-6).

Traceability: TC-FR4-01…22 (render/logic; live-network accept/duplicate/
error-banner paths land in B8), TC-EDGE-01/02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): detail view — header, actions, keys, remove (B5a)

Add the node detail view (FR-5), reusing existing screens rather than
reimplementing:

- Fixed section order Header → Actions → Keys → DPNS → Remove (TC-FR5-01,
  the human-requested Actions-above-Keys correction), pinned by a unit test.
- Header: conditional alias, shortened ProTxHash + copy-full-value, type
  badge (shared `draw_type_badge`), IdentityStatus dot + label.
- Actions row (FR-9): Withdraw / Top up / Transfer push the existing
  WithdrawalScreen / TopUpIdentity / TransferScreen scoped to the node's
  QualifiedIdentity (both node types). Evonode-only `Claim token rewards ›`
  cross-link (FR-11), absent for a plain masternode.
- Keys section (FR-10): V/O/P presence, copyable voter-identity id, at-rest
  protection tier (vault-scheme probe), Add-protection offered only Tier-1,
  `Manage keys ›` into the existing key screen.
- DPNS section: collapsible, open-contest count in the header (voting table
  lands in B5b).
- Remove: danger ConfirmationDialog; deletes the node and its voter identity.
- `‹ All masternodes` back row + detail Refresh (FR-7/TC-FR7-04); card click
  opens the detail via a List/Load/Detail view enum.

Deviations (documented): the Evonode claim cross-link routes to the Tokens
area — precise ClaimTokensScreen token-scoping is deferred to B8 where the
evonode reward-token context is resolvable. Add-protection routes into the
reused key screen (which hosts the password-entry seal flow) rather than
duplicating the form.

Traceability: TC-FR5-01…05/07, TC-FR7-04, TC-FR9-01/02, TC-FR11-01/02.
Live-network credit/claim routing and TC-FR8-07 land in B8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): Testnet Fill-Random on the load form (B6)

Add the FR-12 dev convenience to the masternode load form:

- New `testnet_fixture` module owns the `.testnet_nodes.yml` structs + loader.
  The loader returns None for BOTH a missing and a malformed file — a malformed
  file is logged at debug and treated as absent (TC-FR12-04, a deliberate
  divergence from the legacy screen which banners the parse error).
- Fill-Random button + hint render only when Expert Mode is on, the network is
  Testnet, and the fixture is present — never shown-but-disabled (TC-FR12-01…06).
  The `dev_mode` gate is a defense-in-depth re-check at the call site
  (TC-FR12-09 decision: added on future-proofing grounds — a plaintext-key dev
  tool stays inside the Expert-Mode envelope).
- Button label follows the node-type toggle (TC-FR12-01/02).
- Autofill pulls from the correct list per type (TC-FR12-07/08): Masternode →
  `masternodes` (Voting + Owner only; the fixture has no payout key, PROJ-003),
  Evonode → `hp_masternodes` (all three keys). The node-type toggle still clears
  autofilled fields (§10.6).
- The fixture loads once when the form opens (Testnet only), not per frame.

Traceability: TC-FR12-01…09.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): inline DPNS voting + missing-voter prompt (B5b)

Populate the detail view's collapsible DPNS section (FR-5):

- Collapsed by default; header shows the open-contest count
  (`DPNS name contests to vote on (N)`, TC-DPNS-01/02).
- Voter present + open contests: per-contest Abstain / Lock / Vote-for-candidate
  choices with the candidate list scoped to that contest's contestants; a
  `Cast votes` button dispatches the existing
  `ContestedResourceTask::VoteOnDPNSNames` backend inline (locked decision #1 —
  not a deep-link). TC-DPNS-03/04/05.
- Voter present, zero open contests: exact §7 empty copy (TC-DPNS-08).
- Missing voter identity: the actionable §7 message (never the raw
  NoVotingIdentity error) plus an `Add voting key` action that opens a scoped,
  in-place voter-key prompt with the node context pre-bound — distinct from
  FR-4's load form, no ProTxHash re-entry (TC-DPNS-09/10/11, §10.8). Save
  re-loads this node with just the voting key to update its voter identity.
- Detail Refresh now re-reads both the contest summary and the open-contest
  list.

Active/open contests only — scheduled/past history stays on the DPNS Scheduled
Votes screen (§10.7).

Traceability: TC-DPNS-01…11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): page-scoped nav pill — FR-6 boundary in code (B7)

Wire the Masternodes page into the global-nav switcher with a page-scoped
masternode pill whose selection lives on the page and is NEVER written to
`AppContext::selected_identity_id` — the structural FR-6 boundary in code,
complementing B1's resolution-layer filter.

- New `ui/state/masternodes_view.rs` builds the page's `PageNavSpec`: page-aware
  `Masternodes` segment-1, an interactive wallet pill (funds Top up — FR-9), and
  a page-scoped identity pill via `IdentityPillScope::PageScopedObject`. Empty →
  subdued `(no masternode yet)` placeholder; ≥1 node → interactive dropdown with
  `Choose a masternode` placeholder; the pill reflects the node in detail and
  resets to the placeholder on `‹ All masternodes` (§10.4).
- New `top_panel::add_top_panel_with_global_nav_capturing` surfaces the
  `SelectPageObject` pick to the caller (applying all other effects as usual)
  without ever routing it into the app-global identity selection.
- The Masternodes screen builds the spec each frame from its node list + current
  view and opens the picked node's detail — two-way with the card grid.

TC-NAV-12 / TC-FR6-07 (release-blocking): a masternode selected on the page
never becomes, or resolves as, the app-global identity — verified across
Identities and the Identity Hub with no User identity loaded.

Traceability: TC-NAV-01/03/04/05/07, TC-NAV-12, TC-FR5-06, TC-FR6-07.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(masternodes): cross-cutting integration coverage (B8)

Add the Remove-flow integration kittest (TC-US4-01/02/06/07): the detail danger
button opens a confirmation carrying the `Remove masternode` verb, and
confirming deletes only the target node — its card disappears while other nodes
survive (isolation). Also sets the confirmation's confirm verb to
`Remove masternode` (§7 / TC-US4-02), the one small production touch the test
surfaced.

Deferred to the network/backend-e2e pass (out of kittest reach without live
DAPI or heavy vault fixtures, and flagged as such in the plan): TC-FR8-07
(detail reflecting a load-time Tier-2-sealed node — needs the real
password-at-load seal path), TC-US4-05 voter-row deletion assertion (needs a
seeded voter-identity row), and the live-network credit/vote/claim dispatch
paths behind FR-9/FR-11/DPNS Cast-votes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(user-stories): catalog the Masternodes tab, retire the legacy load story

Adds MN-001..MN-009 (load, card grid, vote, remove, Hub filter, at-load
encryption, credit actions, key management, evonode token-reward cross-link)
and UX-003 (global wallet/identity switcher) per the completed Masternodes
feature. Flips IDN-003 to superseded — its generic-screen masternode load
path was removed when the dedicated tab shipped.

* docs(masternodes): commit final design docs (DOC-002)

Lands the human-accepted requirements, UX spec, test-case spec, and dev
plan under docs/ai-design/, following the existing 2026-04-23-identity-hub-impl
numbered-file convention. Resolves the ~100+ FR-/TC-/US-/§-ID references
already scattered through the feature's code comments and tests, which
pointed at an uncommitted /data/artifacts scratch copy. Internal
cross-file references (requirements.md, ux-spec.md, etc.) are updated to
the new numbered filenames.

* docs(masternodes): trim oversized module docs, catalog global-nav switcher

Shortens the four ui/masternodes/*.rs module doc comments to the
internal-tier length cap (DOC-003) — they weren't published API, so the
5-10 line public-rustdoc relaxation didn't apply. Adds GlobalNavSwitcher
and its top_panel entry point to ui/components/README.md's catalog
(DOC-004), so the next screen needing a page-aware switcher finds it
instead of reimplementing one.

* fix(masternodes): guard identity load against silent overwrite (QA-005/006)

Root-cause storage fix. insert_local_qualified_identity is INSERT OR
REPLACE, so a load with no guard silently clobbers an already-stored
identity and its keys. Thread an IdentityLoadMode through
IdentityInputToLoad so each entry point declares intent:

- RejectIfExists: the masternode load form rejects a duplicate ProTxHash
  with TaskError::DuplicateProTxHash before any network fetch (QA-006).
- MergeIntoExisting: the scoped Add-voting-key prompt merges the new key
  into the stored identity, preserving Owner/Payout it did not resupply
  (QA-005), via merge_existing_keys_into.
- Overwrite: legacy User re-load and headless flows unchanged (default).

Adds get_local_qualified_identity accessor backing the existence check
and merge read. Failing-first TDD: a unit test proving Owner/Payout keys
survive a voting-key-only merge, and an offline test proving a duplicate
ProTxHash is rejected and the first node is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): key routing, network-switch reset, live refresh

QA-007: the detail Keys section pushed the static read-only KeysScreen.
Render a per-key 'Manage keys' list and route the Add-protection CTA to
KeyInfoScreen (interactive view/sign/seal per key), mirroring
identities_screen.

QA-001: MasternodesScreen had no change_context override, so a network
switch left an open load form or cross-network detail view actionable.
Add an explicit change_context arm that resets to the List view and
reloads from the now-active network.

QA-003: both Refresh buttons only re-read the local cache. Wire them to
dispatch IdentityTask::RefreshIdentity (per loaded node on the list, the
open node on detail) plus a QueryDPNSContests re-query, alongside the
optimistic local re-read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): SHOULD-FIX pass + QA-token cleanup + offline tests

- QA-002: remove the legacy Fill-Random HPMN/Masternode bypass from the
  now User-only add-existing-identity screen (it set identity_type to
  Evonode/Masternode directly, defeating the User-only restriction).
- QA-004: the evonode 'Claim token rewards' CTA pushes a real scoped
  ClaimTokensScreen when the node holds exactly one token, falling back to
  My Tokens when the target is ambiguous — no more bare SetMainScreen.
- QA-008: refresh the open detail view after its own backend task, not
  just the card list.
- Diziet-F3: render the missing-voter 'Add voting key' CTA above, outside
  the collapsed DPNS section, so it is visible without expanding.
- QA-009: surface a MessageBanner when node removal fails instead of a
  silent tracing::warn.
- SEC-001: log the testnet-fixture parse error by position only, never
  its Display text (which echoes a private key).
- SEC-002: parse fixture key fields as Secret (redacted/zeroized).
- Strip stale PROJ-003/PROJ-004 markers and all ephemeral QA-xxx review
  IDs from source comments (kept only in commit messages).
- TODOs for the deferred mixed-protection-tier CTA and the
  is_valid_pro_tx_hash/decode_identity_id duplication.
- Offline tests: TC-FR8-07 (protection_tier reflects a Tier-2 seal) and
  TC-US4-05 (Remove deletes the associated voter identity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dashpay): close FR-6 boundary bypass via DashPay identity selectors (SEC-005)

DashPay screens (send_payment, contacts_list, profile_screen,
contact_requests, qr_scanner, qr_code_generator, add_contact_screen,
profile_search) built their IdentitySelector and constructor seed from
the unfiltered load_local_qualified_identities() chained with
.syncing_global(...). IdentitySelector::sync_to_global() writes the
picked id straight to AppContext::selected_identity_id — a separate path
from B1's resolve_selected_identity()/restore filters — so a user could
select a masternode/evonode as the app-global operate-as identity from
inside DashPay, bypassing the FR-6/R1 boundary B1 established.

DashPay operates on User identities only, so every identity list in these
screens is sourced from load_local_user_identities() (the same swap B1
made for the global-nav switcher and Identity Hub). This filters the
masternode out of both the selector write-path and the constructor seed.

Adds a kittest (masternode_never_selectable_in_dashpay_screens) exercising
the FR-6 boundary through five DashPay screens — the existing FR-6 kittest
only covered Identities/Identity Hub, which is how this slipped through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(masternodes): Marvin punch-list — in-flight guard + execution tests

- QA-012: gate re-submission while a node-load is in flight. Add a
  load_in_flight flag on MasternodesScreen, set on Submit dispatch and
  cleared on the task result or a new display_task_error override; the
  '+ Load' toolbar button and empty-state CTA show a spinner + disabled
  'Loading…' while set, so a rapid double-submit of a brand-new ProTxHash
  cannot race two loads past the pre-fetch existence check.
- Extend masternode_never_selectable_in_dashpay_screens to QRScanner,
  QRCodeGenerator (both seed selected_identity in new()) and assert
  ProfileSearchScreen's User-filtered data source excludes the masternode
  — FR-6 coverage now spans all 8 DashPay screens.
- Add manage_keys_button_opens_key_info_screen: clicks a per-key
  'Voting key ›' button and asserts a KeyInfoScreen is pushed with its
  'Key Information' heading (execution-level proof of the QA-007 fix).
- Add refresh_from_network unit test: one RefreshIdentity per loaded node
  plus a trailing QueryDPNSContests, None when empty (QA-003).
- Fix two doc-comment lines mangled by the earlier review-ID strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): add Masternodes tab and global nav switcher (DOC-005)

Covers the user-facing outcomes of the completed Masternodes feature:
the new Expert-Mode-gated tab (card list, detail view, load-time key
encryption, inline DPNS voting, credit actions, Evonode token-reward
claiming) replacing the old generic load path for masternode/evonode
identities, the resulting Identity Hub / Identities picker filter, and
the wallet/identity switcher now present on every root screen instead
of just the Identity Hub.

* fix(masternodes): default GUI build broken — masternode_input feature-gated

The whole model::masternode_input module was gated behind
load form (ui/masternodes/load_form.rs) imports is_valid_pro_tx_hash from
it unconditionally. So a plain 'cargo build --bin dash-evo-tool' (default
features only — no mcp/cli, the documented quick-start build) failed with
E0432 unresolved import. Every gate this feature ran used --all-features,
which always pulls mcp+cli and masked it.

The module can't be blanket-ungated: its parse/decode helpers return
McpToolError (from the feature-gated mcp module). Fix ungates the module
and the pure is_valid_pro_tx_hash validator (the only thing the GUI needs),
and gates precisely the McpToolError-coupled items — KeyMode, parse_node_type,
parse_key_mode, require_at_least_one_signing_key, decode_identity_id, their
imports, and their tests — behind mcp/cli. The pure validator's tests move
to an always-compiled module so they run in the default build too.

Verified: 'cargo build --bin dash-evo-tool' (no flags) compiles; default-
feature clippy clean; both default and --all-features test paths pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(masternodes): correct global-nav coverage claim (F-003)

CHANGELOG and the components README claimed the global wallet/identity
switcher was on "every screen". It ships Phase-A: rendered on Identities,
DashPay, DPNS, Wallets, Identity Hub, and Masternodes, interactive only
on the Hub and Masternodes (the other four render subdued, read-only
pills), and absent from every other root screen (Contracts, Tokens,
Tools, Network Chooser, Withdraws, ...). Names the actual screens and
notes the rest as a tracked follow-up instead of implying full rollout.

* fix(masternodes): Fable final round — Tier-2 merge seal, load gate, ProTxHash error

F-001 (MUST-FIX): "Add voting key" on a password-protected (Tier-2) node no
longer trips the insert's fail-closed guard. load_identity now verifies the
node's object password UP FRONT (before the network fetch, mirroring
add_key_to_identity's verify-before-broadcast order) and seals the merged
plaintext keys Tier-2 via seal_merged_plaintext_keys just before the at-rest
insert. Two regression tests: a scripted-prompt success path proving the new
key flips InVault and reads back Protected, and a headless NullSecretPrompt
path proving the merge fails closed with SecretPromptUnavailable before fetch.

F-002: the list screen's load_in_flight gate is cleared only on the load's own
LoadedIdentity result variant (not any routed result), with a refresh_on_arrival
backstop so a tab switch mid-load can never strand "+ Load" at "Loading…".

F-005: a malformed identity-id input now surfaces MalformedProTxHash for
masternode/evonode loads (where the field IS a ProTxHash) and keeps
IdentifierParsingError for User loads. Regression test added.

F-006: masternodes/evonodes legitimately have no HD wallet, so the
"saving identity without wallet" warning is gated to User identities; nodes log
at debug instead.

F-004: correct the MCP masternode_identity_load comment — Overwrite is a
destructive full-replace of stored keys, not a merge/refresh; TODO for a future
load-mode param.

F-007: strip ephemeral review-ID prefixes (SEC-*, Diziet-*, Smythe) from
masternode-scope source comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): add load-form back link + remove object pill from breadcrumb

Live-walkthrough fixes on real testnet data.

Fix 1 — load form back link: the load form now renders the same
`‹ All masternodes` back link as the detail view (wireframe C shows it on
both), at the top of the form, returning to the card list. New kittest
`load_form_back_link_returns_to_list` covers it; the existing
`load_form_opens_from_cta_and_cancels` gets a taller headless window so the
bottom Cancel button stays reachable now that the back row is present.

Fix 2 — remove the masternode object/identity pill from the Masternodes
breadcrumb. Masternode/evonode identities are never wallet-linked (wallet_info
is always None — locked decision #4), so pairing a wallet pill with an object
pill implied a wallet↔masternode relationship that does not exist. The
breadcrumb now carries only segment-1 + the interactive wallet pill; node
selection is driven entirely by card-click → detail and the back link. The
Masternodes page switches to add_top_panel_with_global_nav (non-capturing),
matching every other non-object page. The masternodes_page_nav_spec builder
drops its items/selected params.

This does NOT touch the FR-6 boundary, which is enforced structurally at the
resolution layer (B1) independent of any pill. The release-blocking FR-6
boundary tests (TC-NAV-12 / TC-FR6-07) remain unchanged and green. The
PageScopedObject / SelectPageObject / add_top_panel_with_global_nav_capturing
machinery is retained as the documented, tested boundary pattern for future
page-scoped-object features (the global_nav_switcher tests still exercise it);
only the Masternodes page's use of it is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): reject load when selected node type mismatches on-chain

Loading a masternode/evonode by ProTxHash trusted the load-form Masternode/
Evonode toggle as ground truth with no cross-check. A regular masternode loaded
with "Evonode" selected was silently accepted, mis-badged "Evonode", and shown
the Evonode-only "Claim token rewards" action.

The load task (authoritative layer) now cross-checks the selected type against
the node's actual on-chain registration. A masternode's Platform identity id is
its ProTxHash, so the node is looked up via Core RPC `protx info`; the `type`
field ("Regular"/"Evo") is classified and compared. On a confirmed mismatch the
load is rejected with a typed, actionable `TaskError::NodeTypeMismatch` naming
both the selected and actual types. When the on-chain type cannot be determined
(Core RPC unreachable — e.g. an SPV-only setup with no Core node) the load
proceeds unverified, so this adds no regression for those users.

Layering per CLAUDE.md: the pure classification (`classify_protx_node_type`)
and rejection decision (`node_type_conflict`) live in `model/masternode_input`
and are exhaustively unit-tested (the reported Evonode-on-regular case
included); the backend task owns the network lookup and enforcement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): surface a visible warning when node type is unverified

Follow-up to the node-type cross-check: when Core RPC is unreachable (the
common case for SPV-only users) the node type cannot be verified, and silently
proceeding with an unverified badge reproduced the original UX bug downgraded
from "wrong" to "unverified". The load task now distinguishes the two success
outcomes via a new `BackendTaskSuccessResult::LoadedIdentityTypeUnverified`
variant, and the Masternodes screen surfaces a visible warning banner (not just
a log line) telling the user the badge reflects their selection and to reload
later to confirm. The MCP masternode-load tool reports the same distinction via
a new `node_type_verified` output field.

Regression tests: the pure reject decision (`node_type_conflict`) and the
`NodeTypeMismatch` user message cover the hard-reject path; a kittest drives the
unverified-load result into the live screen and asserts the warning banner is
surfaced to the UI, not merely logged.

Confirmed with team-lead: hard-reject on a confirmed mismatch; a visible (not
log-only) warning on the unverified path. The upstream platform-wallet SPV
masternode-list passthrough (for verifying node type without Core RPC) is
tracked as a separate follow-up against the platform repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(masternodes): drop Fix #3 node-type validation entirely

Reverts c5167787 and 755eee87. Product decision: trust the user's
Masternode/Evonode toggle as-is, with no on-chain node-type verification.

Rationale: the only non-type-gated actions (Withdraw/Top up/Transfer) are
unaffected by a mis-toggle; the sole type-gated action ("Claim token rewards")
degrades to a clean no-op/failed Platform state transition, not a fund-safety
issue — so the toggle working as the user set it is correct behavior, not a
defect. Dropping verification also removes the dependency on Core RPC (being
deleted in the platform-wallet migration) and on fetching the operator identity
(extra scope), leaving the load path simpler and migration-proof.

Removes: TaskError::NodeTypeMismatch, the onchain_node_type Core RPC cross-check,
the classify_protx_node_type/node_type_conflict model helpers, the
LoadedIdentityTypeUnverified result variant + UI warning banner, the MCP
node_type_verified output field, and all associated tests. Fixes #1 (load-form
back link) and #2 (breadcrumb pill removal) in 5db23f72 are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): adapt retargeted tab to platform-wallet rewrite APIs

Rebasing the Masternodes tab onto the platform-wallet backend rewrite
(PR #860) surfaced three call sites where the rewrite reshaped an API the
masternode-tab code depended on:

- `AppContext::identity_kv()` was renamed to `det_kv()`; the load-path
  existence check (`get_local_qualified_identity`) now calls the new name.
- `ContestState::state_is_votable()` was dead-code-removed by the rewrite,
  but `ContestedName::is_open_for_voter` (Masternodes card DPNS status)
  relies on it — restored as a live, un-gated method.
- The rewrite dropped the `identity-hub` Cargo feature and renders the
  Identity Hub nav entry unconditionally; the left-panel builder no longer
  gates that entry behind the removed `#[cfg(feature = "identity-hub")]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(withdraw): screen-level kittest coverage for default_withdrawal_key fix

WithdrawalScreen::new() pre-selecting via default_withdrawal_key() was only
unit-tested at the QualifiedIdentity model layer. Add tests/kittest/withdraw_screen.rs
to verify the fix at the actual screen layer: ghost-key identities (on-chain-only
TRANSFER key) render the no-keys empty state instead of a form, private-key-backed
TRANSFER/OWNER keys are pre-selected AND rendered correctly in the key-selection
ComboBox (via accesskit value, not label). Also locks in a genuine regression the
fix newly exposes: WithdrawalScreen::new() leaks a raw "No key provided when
getting selected wallet" error banner for any identity with no locally-signable
withdrawal key.

* fix(withdraw): skip wallet resolution when no signable key exists

WithdrawalScreen::new() called get_selected_wallet with selected_key=None
after default_withdrawal_key() correctly began returning None for identities
with no locally-signable withdrawal key. With app_context=None that hits the
"No key provided" String Err path, which .or_show_error() posted verbatim
into a user-facing MessageBanner — violating the plain-language error policy.

Guard the call on selected_key being Some, so the raw-string Err branch is
structurally unreachable here instead of avoided by luck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(masternodes): add user story for network-switch reset behavior

Live QA of the Masternodes tab confirmed MasternodesScreen::reset_for_network_change()
cleanly resets the List view and clears stale banners/form data on a network switch,
with no existing story documenting this PR's fix. Add MN-010.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(withdraw): flip banner-leak test to a regression lock (2edbc18)

WithdrawalScreen::new() now guards get_selected_wallet on selected_key
being Some (2edbc18), so the raw "No key provided..." banner leak for
ghost-key-only identities is fixed. Rename
ghost_key_construction_leaks_raw_error_banner ->
ghost_key_construction_does_not_leak_raw_error_banner and invert the
assertion; also verify the no-keys empty state still renders correctly
for the same identity, so the fix didn't trade the leak for a broken
empty state.

* fix(mcp): stop det-cli double-prefixing the HTTP bearer token

rmcp 1.7's StreamableHttpClientTransportConfig::auth_header takes the
raw token and lets reqwest's bearer_auth prepend "Bearer " itself. The
det-cli client passed format!("Bearer {token}"), so the wire header
became "Authorization: Bearer Bearer <token>". The server middleware
strips one "Bearer " and compares the remaining "Bearer <token>" against
the configured key — never equal — so headless HTTP mode returned 401 on
every request. The auth path had no end-to-end coverage, which is why it
shipped broken.

Pass the raw token and add tests/mcp_http_auth.rs pinning the server's
wire contract: raw token accepted, a double-"Bearer" prefix rejected,
missing credentials rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): share Expert Mode flag app-wide; node-specific load error

Two live-QA bugs on the Masternodes tab (PR #876).

Bug 1 — Expert Mode didn't reveal the Masternodes nav without a restart.
`developer_mode` was an independent per-network `AppContext` AtomicBool, kept
in sync only by a best-effort loop in the Settings checkbox handler over the
contexts that happened to exist at click time. AppState keeps one context per
network (only the active one at startup; others created lazily on switch), so
the left-nav `FeatureGate::DeveloperMode` gate could read a stale value on a
different context than the toggle mutated — the nav entry stayed hidden until a
restart re-read the persisted flag into the single fresh context. Promote the
flag to one shared `Arc<AtomicBool>`, created once in `AppState::new` and
injected into every `AppContext::new` (startup + `SwitchNetwork` via
`developer_mode_handle()`), so all per-network contexts observe one flag. Drop
the fragile sync loop; request a repaint on toggle since enabling Expert Mode
disables animations (which stops continuous repaints).

Bug 2 — loading a masternode by a valid-but-unregistered ProTxHash showed the
generic "Identity not found — check the ID or name" copy, wrong for a ProTxHash
load form. Add a dedicated `TaskError::MasternodeNotFound` variant and return it
from the masternode/evonode load path instead of `IdentityNotFound`.

Regression tests: `developer_mode_is_shared_across_network_contexts` (RED before
the shared-flag fix), `masternode_not_found_message_is_node_specific`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@lklimek lklimek marked this pull request as ready for review July 10, 2026 11:54
Comment thread docs/kv-keys.md Outdated
);
}
}
let _resp = resp.info_tooltip(tip);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this line when _resp is not used ?

//! cross into egui. The lone secret type on the wire is `SecretString`, the
//! `platform_wallet_storage` zeroizing wrapper the project already depends on.

use async_trait::async_trait;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure it belongs to src/ui/components,? Components are designed as items that implement the Component trait. Maybe src/ui/tools?

@@ -0,0 +1,181 @@
//! Shared modal chrome for passphrase entry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/ui/components should implement the Component trait. Implement it if possible.

@@ -0,0 +1,298 @@
//! Per-screen cache of wallets' tracked asset locks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure it belongs to src/ui/components? Components are designed as items that implement the Component trait. Maybe src/ui/tools?

Comment thread docs/user-stories.md
**Persona:** Priya, Jordan

As a user with an imported private key, I want to send Dash from that single-key wallet so that I can move funds to another address.

- Send flow works the same as for HD wallets.
- Temporarily unavailable in this version (Decision #7): single-key send returns a clear, calm "not supported in this version — your data is preserved; use an HD recovery-phrase wallet" message. Single-key wallet data and its UTXOs are retained on disk and load correctly; only the spend action is gated. Re-enabled when single-key moves onto the upstream wallet runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it implemented.

Comment thread docs/user-stories.md
@@ -441,6 +488,19 @@ As a user, I want to view all keys associated with my identity so that I can aud
- Lists all keys with type, purpose, and status.
- View individual key details.

### IDN-013: Password-protect an identity's signing keys (SEC-001) [Implemented]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only applies to non-HD-wallet backed identities. HD-wallet backend identities have per-wallet (seed) password protection capabilities, not identity level.

Comment thread docs/user-stories.md
@@ -481,6 +541,26 @@ As a user, I want to top up identity credits from a Platform address so that I c
- Available as funding method in top-up screen.
- Uses Platform address credits directly.

### IDN-014: Fund identity directly from scanned external payment [Removed — upstream-only funding]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to restore this feature

@thepastaclaw

thepastaclaw commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit ee1ce81)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Source: reviewers: claude-general (opus), codex-general (gpt-5.6-sol, failed rc=1); verifier: opus; specialists: none

Verified both agent findings against the worktree at 4343d03. The PR is a large, well-engineered wallet-backend rewrite; no blocking correctness or security defects were confirmed. Both surviving findings are non-blocking test-coverage observations: a sum-based header-vs-tab reconciliation whose invariant test cannot exercise the immature/locked-funds divergence case (verified at snapshot.rs:506/510-511/1217), and a self-referential wire-layout test that round-trips the current struct rather than pinning a golden pre-change blob (verified at settings.rs:516-546). No prior-finding reconciliation context or CodeRabbit inline findings were supplied, so nothing is carried forward beyond the two current-delta findings.

🟡 1 suggestion(s) | 💬 1 nitpick(s)

2 finding(s) are included in the review details below; GitHub refused the PR diff API because this PR exceeds 300 changed files.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/ui/wallets/wallets_screen/mod.rs`:
- [SUGGESTION] src/ui/wallets/wallets_screen/mod.rs:1225-1231: Header-vs-tab reconciliation is sum-based; untested when balance.total > sum(address_balances)
  The PR claims "visible tabs always sum to the header total, in every mode." But the header total is `snapshot.balance.total` = upstream `core_balance.total()` (src/wallet_backend/snapshot.rs:506), which the PR's own doc (snapshot.rs:58-60) states "counts immature coinbase and locked (CoinJoin) funds" that coin selection cannot touch. The per-account tabs derive entirely from `address_balances`, summed only from `state.utxos()` (snapshot.rs:510-511), and the reconciling System/Other tab computes its value by summing categorized system summaries via `hidden_category_balance` (mod.rs:1225-1231) rather than as a residual `total - visible`. So header == Σtabs holds only if `Σ state.utxos() == core_balance.total()`. If upstream `utxos()` ever omits immature coinbase / locked CoinJoin outputs that `total()` still counts, the visible tabs sum to less than the header — reintroducing the divergence class this PR set out to close. The invariant test `header_total_reconciles_with_core_tab_breakdown_through_real_accessors` (snapshot.rs:1198) fixes `total = address_balances.values().sum()` (snapshot.rs:1217), so it structurally cannot catch this. Display-only (no funds at risk) and the immature/locked state is rare on user wallets, hence suggestion severity. A regression test that publishes a snapshot with `balance.total > Σaddress_balances` and asserts the reconciling tab still closes the gap would either confirm the claim or expose the hole.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Source: reviewers: opus (ok), gpt-5.6-sol (failed: revoked refresh token); verifier: opus; specialists: none

The 4343d03..ee1ce81 delta is a single .gitignore line (ignore local Codex config); no source code changed. Both prior findings reference code that is byte-for-byte identical at this head and remain STILL VALID non-blocking test-coverage observations. No new latest-delta findings. Carried-forward: prior-1 (header-vs-tab reconciliation untested when balance.total > sum(address_balances)) and prior-2 (self-referential reserved-byte wire-layout test).

🟡 1 suggestion(s) | 💬 1 nitpick(s)

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/ui/wallets/wallets_screen/mod.rs`:
- [SUGGESTION] src/ui/wallets/wallets_screen/mod.rs:1225-1231: Header-vs-tab reconciliation is sum-based; untested when balance.total > sum(address_balances)
  Carried forward from the prior review at 4343d034 (STILL VALID — code unchanged in this delta, which touches only .gitignore). The header total is `snapshot.balance.total` = upstream `core_balance.total()` (src/wallet_backend/snapshot.rs:502-506), which counts immature coinbase and locked (CoinJoin) funds that coin selection cannot touch. The per-account tabs derive entirely from `address_balances`, summed only from `state.utxos()` (snapshot.rs:508-518), and the reconciling System/Other tab computes its value by summing categorized system summaries via `hidden_category_balance` (mod.rs:1225-1231) rather than as a residual `total - visible`. So header == Σtabs holds only if `Σ state.utxos() == core_balance.total()`. If upstream `utxos()` ever omits immature coinbase / locked CoinJoin outputs that `total()` still counts, the visible tabs sum to less than the header, reintroducing the divergence this PR set out to close. The invariant test fixes `total = address_balances.values().sum()`, so it structurally cannot catch this. Display-only (no funds at risk) and the immature/locked state is rare, hence suggestion severity. A regression test that publishes a snapshot with `balance.total > Σaddress_balances` and asserts the reconciling tab still closes the gap would either confirm the claim or expose the hole.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants