Skip to content

feat(masternodes): add Masternodes tab, retargeted onto platform-wallet rewrite#876

Merged
lklimek merged 43 commits into
docs/platform-wallet-migration-designfrom
feat/masternodes-tab
Jul 10, 2026
Merged

feat(masternodes): add Masternodes tab, retargeted onto platform-wallet rewrite#876
lklimek merged 43 commits into
docs/platform-wallet-migration-designfrom
feat/masternodes-tab

Conversation

@Claudius-Maginificent

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

Copy link
Copy Markdown
Collaborator

Why this PR exists

  • Problem: Dash Evo Tool has no dedicated surface for masternode/evonode identities — operators had to treat them as ordinary identities, losing ProTx status, key-role context (owner/voting/payout/transfer), and any guardrails around which key is actually usable for signing.
  • What breaks without it: a masternode operator loading their node's identity sees a generic identity screen with no ProTx-hash validation, no node-type awareness, and (until fixes folded into this branch) a Withdraw screen that could silently pre-select a signing key the wallet holds no private material for, an Expert Mode toggle that didn't propagate to already-open network contexts (nav entry stuck until restart), and a generic "Identity not found" error on an unregistered ProTxHash instead of an actionable message.
  • Blocking relationship: this branch was originally built on v1.0-dev, which turned out to be stale relative to the in-flight platform-wallet backend rewrite (feat: platform-wallet backend rewrite (spec + implementation) #860, this PR's base). It has been retargeted onto that rewrite's head and includes the adaptations that required.

What was done

  • New Masternodes tab: list/load/detail screens, ProTx-hash input validation, key-role display, an Expert-Mode-gated nav entry, and a network-switch reset so a stale load form/detail view can't survive switching networks.
  • Retargeted onto feat: platform-wallet backend rewrite (spec + implementation) #860: rebased the 31 masternode-tab commits onto the rewrite's head and adapted call sites to its new APIs — RootScreenType relocation into model/settings.rs, the set_ctx!/delegate_to_screen! macro dispatch, the rewrite's model-pure MasternodeInputError (dropped the old MCP-coupled/feature-gated version), verify_key_input relocation, and a few renamed methods (identity_kvdet_kv, restored ContestState::state_is_votable).
  • Withdrawal key-selection fix: QualifiedIdentity::default_withdrawal_key() now sources the pre-selected signing key from private-key-backed keys only (Transfer-preferred, Owner-fallback), instead of the prior unfiltered on-chain key lookup that could pick a key with no local private material. Added screen-level kittest coverage (tests/kittest/withdraw_screen.rs) and fixed a raw-error-banner leak in the ghost-key/no-signable-key path.
  • Fixed the det-cli headless HTTP auth bug (double-Bearer-prefixing) found while live-testing the masternode withdrawal path over the CLI against a running headless daemon.
  • Fixed two bugs found during live QA of the full masternode tab flow on testnet: Expert Mode was an independent per-network-context AtomicBool rather than a shared flag, so toggling it left every other network's already-created context stale (nav entry wouldn't appear without a restart) — promoted to a single Arc<AtomicBool> shared across all contexts. And masternode/evonode identity load on an unregistered ProTxHash returned the generic IdentityNotFound error — added a dedicated TaskError::MasternodeNotFound with an actionable, node-specific message.
  • Two independent implementation attempts (rebase vs. wholesale merge) were run in parallel and diffed against each other to catch divergence during the retarget — the merge attempt was found to have silently dropped the network-switch reset wiring for the Masternodes screen; the rebase attempt (this branch) preserves it correctly.

Testing

  • cargo build (default + all features) — clean
  • cargo +nightly fmt --all — clean
  • cargo clippy --all-features --all-targets -- -D warnings — clean
  • cargo test --all-features --workspace — 1455 tests passing (unit, kittest UI integration, doctests)
  • Live testnet verification, CLI: det-cli masternode identity load + withdrawal (owner-key mode) broadcast and settled on Platform, confirmed via platform-withdrawals-get (status pooled).
  • Live testnet verification, GUI: real click-through withdrawal via the running app — owner key correctly auto-selected in the Withdraw screen (no raw error, no ghost key), broadcast confirmed via the app's own broadcast_and_wait completion in its log (not just the success banner). A second withdrawal to an arbitrary destination address correctly failed with a clean, non-technical error banner — Platform's own consensus rule that an OWNER-key signature can only pay the registered payout address, not a bug.
  • Live GUI verification: Expert Mode toggle now updates the Masternodes nav entry immediately, no restart required (regression-tested and re-confirmed live).
  • det-cli standalone smoke tests for masternode-related MCP tools — pass

Breaking changes

None expected — additive feature plus bug fixes; no public API removed.

Checklist

  • Tests added (model-level withdrawal-key tests, error-mapping tests, tests/kittest/masternode_tab.rs, tests/kittest/withdraw_screen.rs, tests/mcp_http_auth.rs)
  • cargo fmt / cargo clippy clean
  • CHANGELOG.md updated
  • docs/user-stories.md updated (MN-010: masternode tab stays consistent across a network switch)

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

lklimek and others added 30 commits July 10, 2026 07:36
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>
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>
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>
…her (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>
…plier (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>
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>
…omment

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>
…or (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>
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>
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>
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>
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>
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>
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>
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>
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>
…ad 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.
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.
…tcher

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.
…5/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>
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>
- 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>
…rs (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>
- 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>
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.
…-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>
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.
…roTxHash 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>
…readcrumb

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>
…hain

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>
lklimek and others added 4 commits July 10, 2026 08:07
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>
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>
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>
Merge the finished withdraw-key-selection fix (fix/withdraw-key-selection)
onto the retargeted Masternodes tab. Both branches share PR #860's head as
base; the overlapping files (model/qualified_identity, backend_task/error)
change additively.

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

# Conflicts:
#	src/model/qualified_identity/mod.rs
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • master
  • v1.0-dev

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

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: df421354-6830-4223-a88c-345bba3e631d

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

Use the checkbox below for a quick retry:

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

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 and others added 9 commits July 10, 2026 09:13
…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.
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>
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>
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.
…nner leak

Adds tests/kittest/withdraw_screen.rs covering the default_withdrawal_key
fix at the actual screen layer (ghost-key, happy-path, owner-fallback,
ComboBox-level assertion), and fixes a real regression the key-selection
fix exposed: WithdrawalScreen::new() called get_selected_wallet
unconditionally, surfacing a raw internal error string in the
user-facing MessageBanner whenever no locally-signable key exists.
Verified independently by a second agent, both directions of the fix
observed under test execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
… 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>
Folds in the CLI/MCP headless-auth fix found while live-testing the
masternode withdrawal path over det-cli on testnet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folds in the two live-QA-found bugs from the masternode tab flow test:
Expert Mode not propagating to already-created per-network contexts,
and a misleading "Identity not found" error for unregistered ProTxHash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lklimek lklimek marked this pull request as ready for review July 10, 2026 11:45
@lklimek lklimek merged commit 4343d03 into docs/platform-wallet-migration-design Jul 10, 2026
5 checks passed
@lklimek lklimek deleted the feat/masternodes-tab branch July 10, 2026 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants