Skip to content

fix: masternode key-protection dialog, nav reset-to-list, and network-switch passphrase wiring#913

Merged
lklimek merged 10 commits into
v1.0-devfrom
fix/masternodes-and-secret-prompt-batch
Jul 21, 2026
Merged

fix: masternode key-protection dialog, nav reset-to-list, and network-switch passphrase wiring#913
lklimek merged 10 commits into
v1.0-devfrom
fix/masternodes-and-secret-prompt-batch

Conversation

@Claudius-Maginificent

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

Copy link
Copy Markdown
Collaborator

Why this PR exists

  • Problem: three independent UX/correctness bugs surfaced during live GUI testing of the Masternodes screens and wallet withdrawals:
    1. The Masternodes "Add password protection…" action redirects to the Key Info screen but never opens the protection dialog it promises, leaving the user stranded with no next step.
    2. Clicking "Masternodes" in the left nav or top nav breadcrumb reopens whatever masternode was last viewed instead of the list, because nothing ever resets the screen's sticky detail-view state on root navigation.
    3. Switching networks in-session (not the persisted boot network) permanently strands every passphrase-protected operation (withdrawals, protected seeds/keys) behind a headless-only error — "This wallet is protected by a passphrase, which can only be entered in the app window" — even while running inside the real GUI, until the app is restarted.
  • What breaks without it: (1) an operator trying to protect a masternode's keys gets stuck with no visible action; (2) navigating the Masternodes tab via its own nav entry is unreliable — you never land on the list once you've viewed a node; (3) after any in-session network switch, every protected-key withdrawal on that network silently fails with a confusing, actionable-looking error that is actually unactionable, since the user is already in the one place the message tells them to be.

What was done

  1. Masternode protection dialog auto-opens (fix(masternodes): open key protection prompt directly): added KeyInfoScreen::new_with_protection_prompt, which starts the screen directly in the add-protection confirmation flow instead of the ordinary idle state, and forces the "Key Protection" section open while that flow is active. The Masternodes detail screen's aggregate CTA now routes through this constructor.
  2. Masternodes nav resets to the list (fix(masternodes): reset detail view to list on root navigation): added a default no-op ScreenLike::reset_to_root_view(), implemented on MasternodesScreen to reset to MasternodesView::List, called only from set_main_screen() — not from refresh_on_arrival() — so returning from a pushed KeyInfoScreen/ClaimTokensScreen still preserves the detail view, and on_leave()'s existing "keep the view, drop secrets" contract is unaffected.
  3. Secret-prompt wiring order on network switch (fix(context): install secret prompt before wiring wallet backend on network switch): the SwitchNetwork handler wired the new context's wallet backend before installing the real prompt host, so SecretAccess permanently snapshotted NullSecretPrompt at construction (no interior mutability). Now the outgoing context's prompt is propagated onto the new context immediately after creation, before any wallet-backend wiring, in both BackendTask::SwitchNetwork and the MCP network_switch tool (identical gap). Boot-time context creation was already correct and is unaffected.

User story

Imagine you are a masternode operator. You open the Masternodes tab, add password protection to a node's keys and see the confirmation dialog right away instead of a dead end; you bounce between Wallets and Masternodes and always land back on the node list, not a stale detail view; and after switching networks mid-session, your protected identity still prompts you for its password when you withdraw, instead of telling you to go somewhere you already are.

Testing

Each fix was written test-first (RED before the change, GREEN after) and re-verified together on the merged tree:

  • cargo test --test kittest masternode_tab --all-features — 15 passed (includes all three new/changed regression tests: add_password_protection_opens_confirmation_dialog, left_nav_return_to_masternodes_resets_detail_to_list, go_to_main_screen_from_key_info_preserves_masternode_detail)
  • cargo test --lib backend_task --all-features — 389 passed, including switch_network_propagates_secret_prompt_before_wallet_backend_wiring
  • cargo test --lib mcp::tools::network --all-features — 1 passed (network_switch_tool_preserves_secret_prompt_identity)
  • cargo clippy --bin dash-evo-tool --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean

Breaking changes

None.

Checklist

  • Each fix is test-first (RED confirmed before the change, GREEN after)
  • Masternodes protection-dialog fix does not force-close the "Key Protection" section, only force-open while active
  • Nav-reset fix does not touch refresh_on_arrival()/on_leave() — return-from-sub-screen and leave-preserves-view behavior verified unchanged
  • Secret-prompt fix covers both call sites (backend_task::SwitchNetwork and the MCP network_switch tool); boot-time wiring confirmed already correct
  • Full merged-tree verification (clippy + fmt + all touched test scopes) re-run after combining the three fixes into one branch

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Added a direct confirmation flow for enabling password protection on masternode keys.
    • Key Protection now automatically stays visible when an active protection flow is in progress.
  • Bug Fixes

    • Switching networks preserves secret-prompt identity availability during protected key resolution.
    • Returning to the Masternodes screen now reliably resets to the list view (without stale details).
    • Navigating back from key information preserves the previously opened masternode detail view.
    • Secret-prompt installation now recovers correctly after a poisoned prompt slot.
  • Tests

    • Expanded UI and network integration coverage for secret-prompt propagation and navigation/reset behavior.

lklimek and others added 3 commits July 20, 2026 16:09
…etwork switch

BackendTask::SwitchNetwork built the new AppContext and immediately wired
its wallet backend (ensure_wallet_backend_and_start_spv), which snapshots
the context's secret prompt into the SecretAccess chokepoint at
construction time (no interior mutability). The real egui prompt host was
only installed after the task result returned, by which point the
snapshot already happened — so any passphrase-protected operation on a
network reached via an in-session switch (not the persisted boot
network) permanently fell back to NullSecretPrompt, producing the
headless-only "This wallet is protected by a passphrase, which can only
be entered in the app window" error inside the actual GUI. The MCP
network-switch tool had the identical ordering gap.

Propagate the outgoing context's secret prompt onto the new context
immediately after creation, before any wallet-backend wiring, in both
call sites. Boot-time context creation was already correct and is
unaffected.

Co-Authored-By: Codex Sol <noreply@openai.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Route the aggregate masternode protection action through a KeyInfoScreen
constructor that starts the add-protection confirmation flow. Keep the
protection section expanded while that flow is active so the next step stays
visible.

Co-Authored-By: Codex Sol (GPT-5) <noreply@openai.com>
set_main_screen() only called refresh_on_arrival(), which intentionally
preserves nested view state for the return-from-sub-screen case. That
meant clicking the left-nav or top-nav "Masternodes" entry re-rendered
whatever masternode was last viewed instead of the list, since nothing
ever reset MasternodesScreen's sticky `view` field back to List.

Add a default no-op ScreenLike::reset_to_root_view() (delegated through
the Screen enum), implement it on MasternodesScreen to reset to
MasternodesView::List, and call it from set_main_screen() only -- not
from refresh_on_arrival() itself, which stays untouched so returning
from a pushed KeyInfoScreen/ClaimTokensScreen still preserves the detail
view, and on_leave()'s existing "keep the view, drop secrets" contract
is unaffected.

Co-Authored-By: Codex Sol (GPT-5) <noreply@openai.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@lklimek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18556874-1cca-490f-90c5-c63a78b4fd79

📥 Commits

Reviewing files that changed from the base of the PR and between d557b6b and f223237.

📒 Files selected for processing (1)
  • src/ui/masternodes/list_screen.rs
📝 Walkthrough

Walkthrough

The changes preserve secret prompts across network switches, recover poisoned prompt locks, reset root UI views during main-screen changes, and open masternode key protection directly in its confirmation flow. Tests cover backend, MCP, navigation, pending forms, and key-protection behavior.

Changes

Network context prompt preservation

Layer / File(s) Summary
Context prompt initialization
src/context/mod.rs, src/backend_task/mod.rs, src/mcp/tools/network.rs, src/app.rs
Network switches propagate the source secret prompt before wallet-backend wiring, context installation recovers poisoned prompt locks, and MCP/backend tests verify protected-key resolution.

UI navigation and key protection

Layer / File(s) Summary
Root-view lifecycle reset
src/ui/mod.rs, src/ui/masternodes/list_screen.rs, src/app.rs, tests/kittest/masternode_tab.rs
Main-screen changes delegate root-view resets; masternode navigation resets detail views while preserving pending load form state.
Key protection entry flow
src/ui/identities/keys/key_info_screen.rs, src/ui/masternodes/detail_screen.rs, tests/kittest/masternode_tab.rs
The masternode protection action opens KeyInfoScreen with confirmation active and the protection section expanded, using seeded-key UI coverage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MasternodeDetailScreen
  participant KeyInfoScreen
  participant ProtectionFlow
  User->>MasternodeDetailScreen: Click Add password protection
  MasternodeDetailScreen->>KeyInfoScreen: Create with protection prompt
  KeyInfoScreen->>ProtectionFlow: Open confirmation flow
  ProtectionFlow-->>User: Show Yes, add protection action
Loading

Possibly related PRs

Suggested labels: claudius-review

Suggested reviewers: lklimek

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: key-protection dialog, navigation reset-to-list, and network-switch passphrase wiring.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/masternodes-and-secret-prompt-batch

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 marked this pull request as ready for review July 20, 2026 17:33
@thepastaclaw

thepastaclaw commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit f223237)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/mcp/tools/network.rs (1)

241-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant and late secret prompt installation.

Since BackendTask::SwitchNetwork now explicitly installs the secret prompt before wiring the wallet backend, this line is redundant. Furthermore, because dispatch_task awaits the task's completion, the backend is already wired by the time this line executes (since start_spv is true). As documented on install_secret_prompt, calling it after the backend is wired has no effect on the SecretAccess boundary, making this assignment conceptually late.

♻️ Proposed fix
-                context.install_secret_prompt(ctx.secret_prompt());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/tools/network.rs` at line 241, Remove the redundant
context.install_secret_prompt(ctx.secret_prompt()) call from the network
handling flow, leaving BackendTask::SwitchNetwork responsible for installing the
secret prompt before wiring the wallet backend.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/mcp/tools/network.rs`:
- Line 241: Remove the redundant
context.install_secret_prompt(ctx.secret_prompt()) call from the network
handling flow, leaving BackendTask::SwitchNetwork responsible for installing the
secret prompt before wiring the wallet backend.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08072b62-a252-4beb-b988-5ed1c7ad2e7a

📥 Commits

Reviewing files that changed from the base of the PR and between 04b8212 and cb60820.

📒 Files selected for processing (8)
  • src/app.rs
  • src/backend_task/mod.rs
  • src/mcp/tools/network.rs
  • src/ui/identities/keys/key_info_screen.rs
  • src/ui/masternodes/detail_screen.rs
  • src/ui/masternodes/list_screen.rs
  • src/ui/mod.rs
  • tests/kittest/masternode_tab.rs

@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.

Final validation — Codex + Sonnet

Three independent GUI fixes — direct-open of the masternode key-protection prompt, resetting the masternodes tab's detail view to List on root navigation, and moving install_secret_prompt ahead of wallet-backend wiring on network switch (both the shared backend-task path and its MCP call site). The core production fix (src/backend_task/mod.rs:1017, before the start_spv wiring at 1024-1040) is correct and properly ordered against the SecretAccess chokepoint's requirement. Two real gaps survive verification: the new regression tests for that fix only prove the prompt is stored on the context field, never that it is the prompt actually consumed by the wired WalletBackend/SecretAccess, and the MCP NetworkSwitch::invoke re-installs the prompt after dispatch_task has already run the same install-then-wire sequence, making that line a no-op. Additionally, the new reset_to_root_view call fires unconditionally on every SetMainScreen dispatch — including a same-tab reclick that never touches on_leave — silently discarding an in-progress or failed Load form that on_leave's own documented design intentionally preserves for a corrected resubmit; this path is untested by the new kittest coverage, which only exercises navigate-away-and-back.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (failed), claude-sonnet-5 — rust-quality (failed), claude-sonnet-5 — general (failed), claude-sonnet-5 — rust-quality (failed), claude-sonnet-5 — general (failed), claude-sonnet-5 — rust-quality (completed), claude-sonnet-5 — general (failed), claude-sonnet-5 — general (completed)

🟡 3 suggestion(s)

🤖 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/backend_task/mod.rs`:
- [SUGGESTION] src/backend_task/mod.rs:1281-1316: Regression test proves prompt is stored on the context, not that it's the prompt the wired backend actually uses
  Verified directly: this test uses `start_spv: false` and asserts `context.wallet_backend().is_err()`, i.e. it deliberately prevents `ensure_wallet_backend_and_start_spv` from ever running. The only real assertion, `Arc::ptr_eq(&prompt, &context.secret_prompt())`, is true the instant `install_secret_prompt` runs regardless of its ordering relative to backend wiring — it would still pass if wiring somehow read the prompt before this call, as long as nothing later clobbers the field. `WalletBackend::new(..., self.secret_prompt())` (src/context/mod.rs:1082) is the actual point that snapshots the prompt into `SecretAccess`; this test never drives that path with `start_spv: true`, nor exercises a protected-secret operation to prove the specific `TestPrompt` instance is what the backend consults. A future bug where the backend captures the prompt at the wrong time (e.g. before this `install_secret_prompt` call, or from a stale reference) would slip past this test even though its name claims to guard exactly that ordering. Wire the backend (`start_spv: true`) and assert behavior through `wallet_backend().secret_access()`, or add a test-only observation point inside `SecretAccess`.

In `src/mcp/tools/network.rs`:
- [SUGGESTION] src/mcp/tools/network.rs:241: install_secret_prompt re-assignment in NetworkSwitch::invoke is dead code that masks a false sense of coverage
  Verified the call chain: `dispatch_task` (src/mcp/dispatch.rs:29) invokes `app_context.run_backend_task(task, sender)` on the exact same `ctx` used at network.rs:241. Inside the `SwitchNetwork` arm, that `self` already calls `new_ctx.install_secret_prompt(self.secret_prompt())` at mod.rs:1017, and since `NetworkSwitch` always sets `start_spv: true`, it also already wires the backend via `ensure_wallet_backend_and_start_spv` (mod.rs:1024-1040) before `run_backend_task` returns — all before control reaches `network.rs:241`. By the time this line executes, `context.secret_prompt()` already equals `ctx.secret_prompt()` and any wallet backend has already been built with it, so this re-assignment cannot change behavior; it only replaces the mutable context field a second time, after the point where it could matter. Worse, the new regression test `network_switch_tool_preserves_secret_prompt_identity` only asserts on this already-redundant field, so removing the *actual* fix at mod.rs:1017 would still leave this test green while the backend silently retained the stale prompt/NullSecretPrompt. Remove this line (or replace it with a comment clarifying it's an intentional defensive no-op) and retarget the MCP test at the wired backend's behavior instead of the context field.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1954-1961: reset_to_root_view fires on a same-tab reclick too, discarding an in-progress/failed Load form that on_leave deliberately preserves
  Verified directly: `select_main_screen` (src/app.rs:1688-1696) early-returns without calling `on_leave` when the target screen is already selected, but `set_main_screen` (src/app.rs:1954-1961) calls `active_screen.reset_to_root_view()` unconditionally regardless of that early return. `left_panel.rs` dispatches `AppAction::SetMainScreenThenGoToMainScreen`/`SetMainScreen` purely on `clicked()`, with no `is_selected` guard (lines ~198-238), so a user can reclick the already-active Masternodes tab. `MasternodesScreen::on_leave`'s own doc comment (list_screen.rs:523-533) states a failed load 'deliberately keeps its form (and its fields) for a corrected resubmit' — but `reset_to_root_view` (list_screen.rs:519-521) unconditionally sets `self.view = MasternodesView::List`, discarding the entire Load form (ProTxHash, alias, node type) on a same-tab reclick even though no real navigation away-and-back occurred and `on_leave` never ran. This regression is new: before this PR there was no reset-on-arrival at all. The added kittest (`left_nav_return_to_masternodes_resets_detail_to_list`, tests/kittest/masternode_tab.rs:883) only covers navigating to a different tab and back, not a same-tab reclick while a Load form or failed load is open.

Comment thread src/backend_task/mod.rs
Comment thread src/mcp/tools/network.rs Outdated
Comment thread src/app.rs
lklimek and others added 6 commits July 20, 2026 18:41
@lklimek lklimek added the claudius-review Triggers automated code review using claudius plugin, runs as a CI job label Jul 20, 2026
Gate root-view resets on the visible load form so a failed load remains
available for correction after its pending task state has cleared.

Co-Authored-By: Codex GPT-5 <noreply@openai.com>

@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.

Final validation — Codex + Sonnet

Verified exact HEAD a03c545 against combined Codex full-PR evidence and Sonnet's delta_from_last_sonnet coverage (cb60820..a03c545). Both prior prompt-wiring findings (55e35b5e107e, 6c8fdc41b891) are genuinely fixed. The third prior finding is not fully resolved: source inspection confirms reconcile_pending_load clears pending_load to None on load failure while leaving the Load form displayed, so the pending_load-based guard at list_screen.rs:519-522 wipes that preserved failed form on the next reset_to_root_view call. The same guard also over-preserves an unrelated Detail view while pending_load is Some. These are the same root cause and are combined into one finding. Codex's two other state-discrimination findings are also confirmed. No blocking issues; all three findings are suggestions.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (failed), claude-sonnet-5 — rust-quality (failed), claude-sonnet-5 — general (failed), claude-sonnet-5 — rust-quality (failed), claude-sonnet-5 — general (completed), claude-sonnet-5 — rust-quality (completed)

🟡 3 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 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/masternodes/list_screen.rs`:
- [SUGGESTION] src/ui/masternodes/list_screen.rs:519-522: pending_load-gated view reset both loses failed-load forms and over-preserves unrelated views
  `reset_to_root_view` gates entirely on `self.pending_load.is_none()`, but `pending_load` tracks whether a load task is outstanding, not what the screen is currently displaying — and it diverges from the displayed view in both directions. (1) Under-preservation: `reconcile_pending_load` clears `pending_load` to `None` as soon as the load stops being outstanding, including on failure, while intentionally leaving `MasternodesView::Load(form)` open with its fields intact for a corrected resubmit. Because `pending_load` is already `None`, a subsequent `reset_to_root_view` resets the form to `List`, silently discarding its fields. (2) Over-preservation: `pending_load` can be `Some` while `self.view` is `MasternodesView::Detail(...)`; Cancel can dismiss the form without clearing `pending_load`, and the global node picker can open a Detail view while a load is still outstanding. Root navigation then refuses to reset the Detail view to List. Guard on whether the displayed view is actually a Load form worth protecting, not merely whether `pending_load` is Some or None.

In `src/ui/masternodes/detail_screen.rs`:
- [SUGGESTION] src/ui/masternodes/detail_screen.rs:691-699: Protection routing can select a key whose screen cannot render the dialog
  `first_protectable_key` treats any `PrivateKeyData` variant present in `private_keys` as protectable, but `KeyInfoScreen` only renders `render_key_protection_section` in the `InVault` branch. A masternode holding both a wallet-derived key and an unprotected vault key can have the aggregate Add password protection CTA route to the wallet-derived key if it appears first in `identity_keys()`. `new_with_protection_prompt` then sets `protection_confirm` on a screen that never handles it for that variant, so the dialog remains invisible. Filter to keys whose `PrivateKeyData` is actually `InVault` before selecting the routing target.

In `src/ui/identities/keys/key_info_screen.rs`:
- [SUGGESTION] src/ui/identities/keys/key_info_screen.rs:802-815: Revalidation still opens the add flow for already-protected keys
  `new_with_protection_prompt`'s status recheck only special-cases `IdentityProtectionStatus::NoVaultKeys`; every other variant, including `Protected`, calls `open_add_confirm()`. If another GUI or MCP operation completes protection between the detail view's stale status probe and this constructor, the resulting screen reports protection as active while presenting an Add-protection confirmation. Submitting the existing password against that stale flow returns `IdentityKeysProtected { count: 0 }` and can rewrite the hint; a different password appears to fail as though a real migration were attempted. Match the enum exhaustively and only open the add flow for `Unprotected` or `Mixed`.

Comment thread src/ui/masternodes/list_screen.rs
Comment on lines +802 to +815
let mut screen = Self::new(identity, key, private_key_data, app_context);
let status = screen.compute_protection_status();
if status == IdentityProtectionStatus::NoVaultKeys {
screen.protection_stage = ProtectionStage::Idle;
MessageBanner::set_global(
app_context.egui_ctx(),
"Password protection is not available yet. Wait for wallet setup to finish, then try again.",
MessageType::Warning,
);
} else {
screen.protection_status = Some(status);
screen.open_add_confirm();
}
screen

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.

🟡 Suggestion: Revalidation still opens the add flow for already-protected keys

new_with_protection_prompt's status recheck only special-cases IdentityProtectionStatus::NoVaultKeys; every other variant, including Protected, calls open_add_confirm(). If another GUI or MCP operation completes protection between the detail view's stale status probe and this constructor, the resulting screen reports protection as active while presenting an Add-protection confirmation. Submitting the existing password against that stale flow returns IdentityKeysProtected { count: 0 } and can rewrite the hint; a different password appears to fail as though a real migration were attempted. Match the enum exhaustively and only open the add flow for Unprotected or Mixed.

Suggested change
let mut screen = Self::new(identity, key, private_key_data, app_context);
let status = screen.compute_protection_status();
if status == IdentityProtectionStatus::NoVaultKeys {
screen.protection_stage = ProtectionStage::Idle;
MessageBanner::set_global(
app_context.egui_ctx(),
"Password protection is not available yet. Wait for wallet setup to finish, then try again.",
MessageType::Warning,
);
} else {
screen.protection_status = Some(status);
screen.open_add_confirm();
}
screen
let mut screen = Self::new(identity, key, private_key_data, app_context);
let status = screen.compute_protection_status();
screen.protection_status = Some(status);
match status {
IdentityProtectionStatus::NoVaultKeys => {
screen.protection_stage = ProtectionStage::Idle;
MessageBanner::set_global(
app_context.egui_ctx(),
"Password protection is not available yet. Wait for wallet setup to finish, then try again.",
MessageType::Warning,
);
}
IdentityProtectionStatus::Unprotected | IdentityProtectionStatus::Mixed => {
screen.open_add_confirm();
}
IdentityProtectionStatus::Protected => {}
}
screen

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still valid at f223237. new_with_protection_prompt continues to special-case only NoVaultKeys; the else branch fires open_add_confirm() for Protected and Mixed alike. A key protected out-of-band between the detail view's probe and this constructor still lands the user in an Add-protection flow against already-sealed keys. Leaving this thread open — the exhaustive-match fix you proposed remains the right call.

🤖 Claudius the Magnificent

@Claudius-Maginificent

Copy link
Copy Markdown
Collaborator Author

Pushed a fix for the failed-load form being discarded on navigation: reset_to_root_view now gates on the actual view state instead of pending_load, so a still-open failed load form survives a same-tab reclick or back-navigation, matching on_leave's documented intent.

Commit: f2232376

🤖 Co-authored by Claudius the Magnificent AI Agent

@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.

Final validation — Codex + Sonnet

Verified exact HEAD f223237 via git and gh pr view. Prior finding ae59c2746083 (pending_load-gated view reset) is genuinely fixed by the head commit: reset_to_root_view now gates on the displayed MasternodesView::Load variant instead of pending_load state, resolving both the under-preservation (failed-load form wiped) and over-preservation (stuck Detail view) halves, backed by a new regression assertion. The two protection-flow suggestions (routing to a non-InVault key; revalidation still opening the add-confirm flow for an already-protected key) remain valid at exact HEAD on direct source inspection. No blocking issues; review remains COMMENT.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (failed), gpt-5.6-sol — rust-quality (failed), gpt-5.6-sol — general (completed), gpt-5.6-sol — rust-quality (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (failed), claude-sonnet-5 — rust-quality (failed), claude-sonnet-5 — general (completed), claude-sonnet-5 — rust-quality (completed)

🟡 1 suggestion(s)

1 additional finding(s) omitted (not in diff).

1 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/masternodes/detail_screen.rs`:
- [SUGGESTION] src/ui/masternodes/detail_screen.rs:691-699: Protection routing can select a key whose screen cannot render the dialog
  first_protectable_key() treats any PrivateKeyData variant present in private_keys as protectable (it only checks `.is_some()`), but KeyInfoScreen only calls render_key_protection_section — the code path that draws the add/remove-protection modal — inside the PrivateKeyData::InVault match arm (key_info_screen.rs:559-579). A masternode holding both a wallet-derived key and a later unprotected vault key can have the aggregate "Add password protection…" CTA route to the wallet-derived key if it appears first in identity_keys(). new_with_protection_prompt then sets protection_confirm/opens the add-confirm flow on a screen that never renders that section for the non-InVault branch, so the promised confirmation dialog silently never appears — reproducing the exact class of bug this PR sets out to fix. Filter the routing target to keys whose PrivateKeyData is actually InVault.

@lklimek
lklimek merged commit f2c355a into v1.0-dev Jul 21, 2026
4 of 6 checks passed
@lklimek
lklimek deleted the fix/masternodes-and-secret-prompt-batch branch July 21, 2026 07:15

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claudius grumpy review — PR #913

Three specialists (security/opus, project/opus, QA/sonnet) walked the 9 changed files. The verdict is a genuinely pleasant surprise: the security core is sound. The network-switch secret-prompt reorder correctly installs the interactive host before the wallet backend snapshots it into SecretAccess, closing the passphrase-stranding gap for real, and the two regression tests actually exercise the wired WalletBackend/SecretAccess path (with_secret(...) + ask_count()==1) — not merely the stored context field, as an earlier reviewer worried. The poisoned-lock recovery in install_secret_prompt is panic- and deadlock-free. No secret leaks, no new panics on reachable paths.

The masternode half is where the gremlins live:

  • 🟡 MEDIUM (CODE-001)first_protectable_key selects by .is_some() regardless of PrivateKeyData variant, but the seal dialog only renders for InVault keys. For a realistic multi-key masternode identity, the new "Add password protection…" CTA opens a dead-end screen with no dialog and no error — the exact bug this PR fixes, reintroduced through the back door. Flagged inline. This is the one worth fixing before merge.
  • 🔵 LOW (CODE-002)new_with_protection_prompt opens the Add-protection flow for Protected/Mixed too; safe today only because the sole caller is tier-gated. Latent for future callers. (Matches the still-open reviewer thread on that line.)
  • 🔵 LOW (CODE-003)reset_to_root_view also clears the Detail view's unsubmitted vote picks / voting-key entry on root navigation, asymmetric with the Load-form preservation the same batch engineered. Partly by-design (resetting to List is the feature); worth an explicit contract decision.
  • 🔵 LOW (CALL-001) — the reset_to_root_view trait doc overstates that pushed-sub-screen returns always preserve the sub-view; three compound SetMainScreen* actions route through the reset.

No CRITICAL or HIGH. Full report (HTML + JSON) generated locally. Recommend addressing the MEDIUM; the LOWs are polish and can be follow-ups.

🤖 Claudius the Magnificent
📊 View full HTML review report

&& ui.button("Add password protection…").clicked()
{
action = Some(self.open_key_info(target, &key));
action = Some(self.open_key_info_with_protection_prompt(target, &key));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM — this new routing makes the headline feature a silent no-op for multi-key masternodes

This line newly routes the "Add password protection…" CTA into open_key_info_with_protection_prompt, which selects its target via first_protectable_key (detail_screen.rs:686). Two independent reviewers flagged the same trap, and I'm afraid they're right.

first_protectable_key picks the first key whose held data is merely .is_some() — matching any PrivateKeyData variant (Clear, AlwaysClear, Encrypted, AtWalletDerivationPath, InVault). But KeyInfoScreen only draws the protection confirmation dialog inside the PrivateKeyData::InVault match arm (key_info_screen.rs:559-579). So when the selection lands on a non-InVault key, new_with_protection_prompt dutifully arms ConfirmAdd, the screen opens… and nothing renders. No dialog, no error, no next step — precisely the dead-end this PR set out to abolish.

It's reachable, not hypothetical: identity_keys() iterates in KeyID order, while the CTA visibility gate counts vault keys via view.scheme — divergent criteria. migrate_keystore_to_vault migrates only plaintext (Clear/AlwaysClear); AtWalletDerivationPath and legacy Encrypted keys are left untouched. An evonode/masternode identity holding a wallet-derived auth key (lower KeyID) alongside an InVault owner/voting key routes the CTA straight to the key the seal screen can't render.

The existing add_password_protection_opens_confirmation_dialog kittest seeds a single Clear key that migrates to InVault, so the selected key and the only vault key are trivially identical — it cannot catch this.

Fix: filter first_protectable_key to keys whose held PrivateKeyData is InVault (ideally also IdentityKeyView::scheme == Unprotected, mirroring the tier gate). Or make new_with_protection_prompt fail closed with a MessageBanner warning — as it already does for NoVaultKeys — when the routed key isn't InVault. Its doc comment ("the only keys that can be sealed") is also false for the non-vault variants. Please add a kittest seeding a wallet-derived/encrypted key at a lower KeyID than the Unprotected vault key so this stays buried.

🤖 Claudius the Magnificent — grumpy review, MEDIUM (CODE-001)

@github-actions github-actions Bot removed the claudius-review Triggers automated code review using claudius plugin, runs as a CI job label Jul 21, 2026
orchardpaytl pushed a commit to orchardpaytl/orchardpay that referenced this pull request Jul 21, 2026
Brings in: shutdown fix (dashpay#905), duplicate-DPNS-name error message
(dashpay#915), startup banner clearing (dashpay#916), nav pointer cursor + tooltips
and wallet-less masternode indication (dashpay#917), onboarding disconnected-
banner suppression (dashpay#907), masternode dialog/nav/passphrase fixes
(dashpay#913), DAPI auto-refresh during pre-1.0 migration (dashpay#908), "Add
Receiving Address" wiring + its test hardening (dashpay#914, dashpay#920), and a CI
timeout bump (dashpay#912). dashpay#906 (shielded re-enable) was already pulled in
individually last session, so its squashed commit merged as a no-op.

Conflicts (6 files) were rebrand-naming overlaps (dash_evo_tool::  vs
orchardpay::  imports) plus one real merge in left_panel.rs, where
OrchardPay's green-icon tint had to combine with upstream's new nav
tooltip. Also fixed 5 files upstream's auto-merged (non-conflicting)
additions left un-rebranded: a stray DASH_EVO_DATA_DIR_LOCK/env-var
name in a new app.rs test, and dash_evo_tool:: references in three
kittest test files.

Added tooltip strings for OrchardPay's own nav entries (OrchardPay,
DashPay) so the new every_nav_entry_has_a_tooltip test covers them —
upstream's version only knows its own nav items. Fixed the new
nav_label_hover_shows_pointer_cursor kittest test: OrchardPay's nav
rail carries two more always-visible entries than upstream's, pushing
"Settings" below the scrollable list's default-size visible viewport;
scroll it into view first, matching what a real user would do.

Verified: cargo check (both feature modes), cargo clippy --all-features
--all-targets -- -D warnings, cargo fmt --all, cargo test --all-features
--workspace (2066 lib + 257 kittest + doc tests, 0 failed), all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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