Skip to content

feat: default to SPV connection; gate Dash Core RPC behind Expert mode#836

Merged
lklimek merged 7 commits into
v1.0-devfrom
feat/spv-default-connection
Apr 22, 2026
Merged

feat: default to SPV connection; gate Dash Core RPC behind Expert mode#836
lklimek merged 7 commits into
v1.0-devfrom
feat/spv-default-connection

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Apr 22, 2026

User Story

Imagine you are a new user installing Dash Evo Tool for the first time. You want to check your balance, send a payment, or register a DPNS name — but you don't run a full Dash Core node, don't know what RPC is, and don't want to install dashd.

Today, Dash Evo Tool would silently require you to have a local Dash Core node running and complain when it can't connect. After this PR, it just works: the built-in SPV light client connects you to the Dash network directly. No node, no port forwarding, no RPC credentials. For experienced users who do run a local node, Expert mode still exposes the Dash Core RPC option.

Summary

Flips the default connection axis in Dash Evo Tool:

  • SPV is the default for every user. New installs start syncing via the built-in light client out of the box.
  • Dash Core RPC moves behind the Expert mode checkbox. Power users who run a local node still get full control.
  • Existing Dash Core users are preserved automatically. A one-shot DB migration (v33 → v34) keeps their RPC connection if they had it configured (developer_mode=true + a core_rpc_password in .env). Everyone else is migrated to SPV.
  • Expert mode becomes a pure visibility toggle. Turning it off no longer mutates the stored connection mode — the user's explicit choice is preserved across the toggle.

Key code changes

  • CoreBackendMode::default() flipped from Rpc to Spv; From<u8> unknown-byte fallback flipped accordingly.
  • AppContext::new() no longer silently force-flips SPV → RPC when developer mode is off.
  • New FeatureGate::RpcBackend variant so connection-mode predicates go through the central registry (Phase 5 DRY rule).
  • spawn_zmq_listener gated on FeatureGate::RpcBackend — no burning a socket + retry loop for SPV-only users.
  • try_auto_start_spv no longer requires developer mode.
  • New DB migration v33 → v34 in initialization.rs. Reads .env via dotenvy::from_path_iter (the non-mutating iterator API) to avoid polluting process environment during DB init — important for test parallelism and one-shot migration hygiene. Config::load_from() / dotenvy::from_path_override would have set_var'd every key and raced across parallel migration tests.
  • UX regression fix: SPV sync progress in the Network Chooser no longer hides from non-Expert users. They are the default SPV cohort now; they need to see it.
  • Three stale "once SPV is production-ready" TODOs retired. Two remaining dev-mode gates (advanced SPV peer config + SPV maintenance diagnostics) now have comments explaining why they are legitimately Expert-only.

Follow-up TODOs (filed as deferred work)

  • Migrate existing ~25 raw ctx.is_developer_mode() call sites across tokens, wallets, identities, etc. to FeatureGate::DeveloperMode. Pure refactor.
  • Migrate predicate-style core_backend_mode() == … comparisons (backend_task/core/, context/connection_status.rs, etc.) to FeatureGate::RpcBackend / SpvBackend. Leave genuine two-branch match dispatches alone.
  • Diziet's full Connection card redesign (jargon-free Everyday User status line, parity-gap micro-notices, read-only RPC endpoint display, Reconnect button).
  • Consider splitting developer_mode into two orthogonal flags: one for the node-operator case, one for signing/security overrides.

Test plan

  • cargo build --all-features
  • cargo clippy --all-features --all-targets -- -D warnings
  • cargo +nightly fmt --all -- --check
  • cargo test --all-features --workspace — 461 lib + 10 e2e + 72 kittest + 3 doctest; 0 failed, 2 ignored (testnet-bound)
  • 5 v34 migration unit tests covering: preserve-mode for Expert+RPC configured, flip-to-SPV when developer_mode=false, flip-to-SPV when Expert but no RPC password set, flip-to-SPV when .env missing, idempotence on re-run
  • 1 fresh-DB integration test asserting core_backend_mode == Spv post-init
  • Phase 5 DRY / layering audit: 8/8 pass
  • Manual QA — fresh install UX: connects via SPV without any configuration; no mention of "SPV" or "RPC" in default UI
  • Manual QA — existing Dash Core user (Expert + core_rpc_password set): mode preserved after migration; app connects via RPC as before
  • Manual QA — existing non-Expert user: migrated to SPV; can turn on Expert mode and switch to Local Dash Core node
  • Manual QA — Expert toggle lifecycle: turn on → selector appears → pick Local Dash Core node; turn off → selector hides; turn on → mode still Local Dash Core node (not silently flipped to SPV)
  • Manual QA — MCP headless (`det_cli`): forces SPV via volatile override, no RPC attempts
  • Manual QA — SPV sync progress widget visible in Network Chooser for default (non-Expert) users

Rollback

Revert as a single-PR revert. Existing v34 databases do not auto-downgrade — users needing full rollback must restore the backup (timestamped backups are created automatically by the migration framework before each version bump).

Migration

Runs once inside the existing DB migration framework (v33 → v34, inside a transaction, with automatic DB backup). Rules:

Conditions Outcome
`developer_mode == false` (or unset in `.env`) `core_backend_mode = Spv`
`developer_mode == true` AND no network has `core_rpc_password` set `core_backend_mode = Spv`
`developer_mode == true` AND at least one network has `core_rpc_password` set leave `core_backend_mode` untouched

No new tracking variables; the DB schema version bump is the one-shot gate.

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

Release Notes

  • New Features

    • SPV (built-in light client) now defaults for fresh installs and new connections
    • Simplified connection type labels: "Built-in (SPV)" and "Local Dash Core node"
    • Added capability-based feature gating for RPC functionality
  • Documentation

    • Updated user story documenting fresh-install experience with SPV backend and zero-configuration setup
  • Tests

    • Updated backend mode tests to reflect SPV as the new default

lklimek and others added 3 commits April 22, 2026 12:50
SPV becomes the out-of-the-box connection for every user. Dash Core RPC
becomes an opt-in Expert-mode configuration for power users who actually
run a local Dash Core node.

Behavior changes:
- `CoreBackendMode::default() == Spv`; `From<u8>` falls back to Spv
  (consistent with the DB column default). Runtime default in
  `ConnectionStatus::new()` flipped to match.
- `AppContext::new()` no longer silently force-flips SPV→RPC when
  developer mode is off. The saved `core_backend_mode` is the single
  source of truth; developer mode is a visibility toggle only.
- `NetworkChooserScreen`: removed the "SPV is experimental" warning,
  renamed connection options to "Built-in (SPV)" / "Local Dash Core
  node", updated the Expert-mode tooltip, and removed the legacy
  force-RPC-when-Expert-off loop.
- `try_auto_start_spv` no longer requires developer mode; `spawn_zmq_listener`
  is now gated through `FeatureGate::RpcBackend` so SPV mode doesn't burn
  a ZMQ socket retry loop for a local node that isn't there.
- New `FeatureGate::RpcBackend` variant centralises the RPC-mode
  predicate so callers don't reach for raw `core_backend_mode() == Rpc`
  comparisons.

Migration (v33 → v34):
- Users with `developer_mode=true` AND at least one network's
  `core_rpc_password` set keep their existing backend mode.
- Everyone else is pinned to SPV.
- The migration parses `.env` directly (not via `Config::load_from`) to
  avoid mutating process environment during DB init — important for
  test parallelism and for not leaking one-shot migration state into
  the live process.

Tests:
- `CoreBackendMode` roundtrip test updated for the new default.
- Five new v34 migration unit tests covering all four migration
  scenarios plus re-run idempotence.
- New startup integration test asserting a fresh DB resolves to SPV.

Removes three "once SPV is production-ready" TODOs:
- `src/ui/network_chooser_screen.rs` connection combobox gate
- `src/ui/network_chooser_screen.rs` force-RPC-on-Expert-off loop
- `src/app.rs` `try_auto_start_spv` developer-mode guard

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ings

Round of QA fix-ups on top of 815eb35 (SPV-default flip).

- **ui/network_chooser_screen.rs (Adams PROJ-001, MEDIUM):** the SPV sync
  progress widget was previously hidden unless `developer_mode` was ON.
  After the SPV-default flip, the default cohort is exactly the non-Expert
  users, so hiding their sync feedback is a real regression — they'd see
  a silent minutes-long sync with no indication. Drop the `developer_mode`
  gate, replace the raw `core_backend_mode == Spv` check with
  `FeatureGate::SpvBackend.is_available(...)` to satisfy the Phase 5
  DRY rule on feature gating, and kill the stale TODO comment.

- **database/initialization.rs (Marvin QA-001, MEDIUM):** the
  `test_v33_migration_fresh_install` test asserted DB version but not
  that `core_backend_mode` landed on SPV (1). It does now — this is the
  user-visible contract of the v34 default-flip and deserves an explicit
  assertion, not an implicit one tucked into schema checks.

- **database/initialization.rs (Marvin QA-002, LOW):** replace the naive
  `trim_matches('"' | '\'')` .env value strip with a small `parse_env_value`
  helper that mirrors dotenvy's quote + inline-comment handling:
  * `KEY=value # comment` → `value`
  * `KEY="value # with hash"` / `KEY='value # with hash'` → preserved
  * `KEY=value#nohash` → `value#nohash` (matches dotenvy — `#` only starts
    a comment when preceded by whitespace outside quotes)
  Adds three unit tests covering each case. Backslash escapes are
  intentionally not interpreted — the migration only cares about a bool
  flag and whether a password is non-empty.

- **ui/network_chooser_screen.rs (Adams PROJ-001 / Marvin, LOW):** the
  SPV Peer Source and SPV Maintenance sections are legitimately Expert-only
  (advanced configuration + destructive diagnostics). Rewrite the stale
  `TODO: Remove once SPV is production-ready` comments to explain *why*
  these are gated rather than implying they should be ungated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add NET-015 (Alex persona) covering zero-config SPV connection as the
new default, now that SPV is the default connection mechanism and Dash
Core RPC is gated behind Expert mode.

No CHANGELOG.md exists in this repository; the CHANGELOG entry is
noted in the commit message only. See the plan at
.claude/plans/clever-booping-lemon.md for the full entry text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@lklimek has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 4 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 30 minutes and 4 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a1b1585-dd82-455e-a1c5-99d1c3697083

📥 Commits

Reviewing files that changed from the base of the PR and between ddec5fb and aea0ce5.

📒 Files selected for processing (3)
  • src/context/mod.rs
  • src/database/initialization.rs
  • src/ui/network_chooser_screen.rs
📝 Walkthrough

Walkthrough

This pull request changes the default backend from RPC to SPV (Simple Payment Verification) across the application. It includes database schema migration logic to preserve existing RPC configurations while defaulting fresh installs to SPV, updates core initialization and context to reflect SPV as the primary backend, adds RPC feature gating, and refactors UI terminology and defaults accordingly.

Changes

Cohort / File(s) Summary
Documentation
docs/user-stories.md
Added user story NET-015 describing fresh installs using a built-in SPV light client with zero configuration and sync progress indication, with RPC terminology reserved for Expert mode.
Core Backend Mode Enum & Defaults
src/spv/manager.rs, src/spv/tests.rs
Updated CoreBackendMode to use #[repr(u8)] with explicit discriminants (Rpc = 0, Spv = 1), changed From<u8> conversion to default unknown values to Spv, and updated test assertions to match new default behavior.
Context Initialization
src/context/connection_status.rs, src/context/mod.rs
Changed ConnectionStatus initial backend mode from Rpc to Spv; refactored AppContext::new() to initialize from saved settings with Spv as the fallback default and bind SPV provider first, then conditionally rebind RPC. Added unit test verifying fresh database defaults to Spv.
Feature Gating
src/model/feature_gate.rs, src/app.rs
Added FeatureGate::RpcBackend variant to gate RPC/ZMQ functionality; updated SPV auto-start logic to remove developer-mode requirement and check only FeatureGate::SpvBackend availability; updated ZMQ listener spawning to depend on FeatureGate::RpcBackend.
Database Migration
src/database/initialization.rs
Incremented schema version from 33 to 34; implemented v34 migration that reads .env for developer mode and RPC password presence, then sets core_backend_mode to Spv (=1) unless both conditions indicate local RPC is configured; added .env snapshot reading and comprehensive test coverage for migration scenarios.
CLI/Headless Mode
src/mcp/server.rs
Updated inline comment to clarify that Dash Core RPC credentials absence triggers volatile SPV-mode forcing in headless contexts, without changing control flow.
Network Configuration UI
src/ui/network_chooser_screen.rs
Changed default backend mode preference to Spv; replaced developer-mode gating with FeatureGate::SpvBackend availability checks for SPV sync-progress rendering; updated terminology ("SPV Client" → "Built-in (SPV)", "Dash Core RPC" → "Local Dash Core node"); removed experimental SPV warning and developer-mode-disable logic that forced RPC; expanded Expert mode tooltip description.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Hops and cheers for SPV's new reign,
Fresh installs dance in the light-client lane,
RPC tucked away, an expert's friend,
Default to sync from start to end!

🚥 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 clearly and specifically summarizes the main change: making SPV the default connection and gating Dash Core RPC behind Expert mode, which aligns with the primary objective of the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/spv-default-connection

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Shifts Dash Evo Tool’s default connection from Dash Core RPC to the built-in SPV light client, while keeping RPC available behind the Expert mode UI and preserving existing RPC users via a v34 DB migration.

Changes:

  • Flip CoreBackendMode defaults/fallbacks to SPV and update startup/runtime codepaths to treat persisted settings.core_backend_mode as the source of truth.
  • Add FeatureGate::RpcBackend and use feature gates to avoid RPC/ZMQ initialization work when running in SPV mode.
  • Add a v34 DB migration that pins most users to SPV while preserving RPC for Expert users with a configured RPC password (via .env parsing).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ui/network_chooser_screen.rs Defaults UI state to SPV, gates SPV progress display via FeatureGate, and stops Expert toggle from mutating backend mode.
src/spv/tests.rs Updates roundtrip tests to match SPV default/fallback behavior.
src/spv/manager.rs Changes CoreBackendMode default + From<u8> fallback to SPV and documents the invariants.
src/model/feature_gate.rs Adds RpcBackend gate and centralizes backend predicates.
src/mcp/server.rs Forces SPV backend in headless mode via a volatile override.
src/database/initialization.rs Bumps schema version to v34 and adds one-shot migration driven by direct .env parsing + new tests.
src/context/mod.rs Stops forcing RPC when Expert mode is off; defaults to SPV when settings are absent/unreadable; adds a fresh-DB assertion test.
src/context/connection_status.rs Changes initial backend_mode atomic default to SPV.
src/app.rs Gates ZMQ listener spawn behind FeatureGate::RpcBackend; allows SPV autostart without Expert mode.
docs/user-stories.md Adds NET-015 user story documenting “no local node required” behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/user-stories.md Outdated
Comment thread src/spv/manager.rs
Comment thread src/ui/network_chooser_screen.rs
lklimek and others added 2 commits April 22, 2026 14:22
Retires the custom line-by-line .env parser and the 3 `parse_env_value`
unit tests that tested it. dotenvy now handles all .env edge cases
(escapes, variable substitution, multi-line quoted values) correctly and
consistently with the main Config::load_from() code path.

The non-mutating `from_path_iter` iterator is safe inside the migration
transaction and for parallel test runs — unlike Config::load_from() /
dotenvy::from_path_override which pollute the process environment.

Strictly shorter (-105 lines) and more correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CMT-1 (docs/user-stories.md): Reword NET-015 acceptance criteria to
  reflect what this PR actually ships. The default everyday-user UI
  avoids SPV/RPC/node jargon, while technical terminology may still
  appear in Expert mode or advanced settings.

- CMT-2 (src/spv/manager.rs): Add #[repr(u8)] to CoreBackendMode so
  its u8 persistence guarantee is explicit, matching the existing
  SpvStatus enum below it.
@lklimek lklimek marked this pull request as ready for review April 22, 2026 13:01
@lklimek lklimek enabled auto-merge (squash) April 22, 2026 13:02
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented Apr 22, 2026

Review Gate

Commit: aea0ce5d

  • Debounce: 15m ago (need 30m)

  • CI checks: builds passed, 0/0 tests passed

  • CodeRabbit review: comment found

  • Off-peak hours: peak window (5am-11am PT) — currently 06:47 AM PT Wednesday

  • Run review now (check to override)

@lklimek lklimek disabled auto-merge April 22, 2026 13:04
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ui/network_chooser_screen.rs (1)

524-527: ⚠️ Potential issue | 🟡 Minor

Inconsistent fallback default — still Rpc here while sibling sites were flipped to Spv.

Lines 260 and 415 were updated to .or_insert(CoreBackendMode::Spv) to match the SPV-default flip, but this third occurrence on line 527 in the Connection Status card still inserts Rpc. Today this is effectively dead code because render_network_table runs the Connection Settings grid first and always inserts an entry for self.current_network, but it's a footgun: any future refactor that renders the status card independently (or for a different network) would silently fall back to RPC and desync from the actual AppContext::core_backend_mode(). Align it with the other two sites.

🔧 Proposed fix
             let current_backend_mode = *self
                 .backend_modes
                 .entry(self.current_network)
-                .or_insert(CoreBackendMode::Rpc);
+                .or_insert(CoreBackendMode::Spv);

Better yet, seed this once from ctx.core_backend_mode() rather than hard-coding a default in three places.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/network_chooser_screen.rs` around lines 524 - 527, The fallback
default for backend_modes when accessing
self.backend_modes.entry(self.current_network).or_insert(...) is inconsistent
(currently CoreBackendMode::Rpc) and should match the other occurrences; change
the insertion to use the global seed instead of hard-coding Rpc — either replace
CoreBackendMode::Rpc with CoreBackendMode::Spv or, preferably, call
ctx.core_backend_mode() when inserting so backend_modes is seeded from the
actual AppContext value; update the entry call in the Connection Status card
code (the backend_modes / current_network usage) to use ctx.core_backend_mode()
to keep behavior consistent with render_network_table.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app.rs`:
- Around line 725-730: When toggling backend modes the code must tear down or
recreate the ZMQ listeners stored in zmq_listeners so they don't persist across
mode switches; update set_core_backend_mode_inner() to, when
FeatureGate::RpcBackend.is_available(ctx) becomes false, iterate and gracefully
stop/close all entries in zmq_listeners (cancel retry loops, close sockets, join
tasks) and clear the collection, and when RpcBackend becomes available again
ensure any missing listeners are spawned (re-use the startup/network-switch
listener creation logic or call the same spawn function used at boot) so that
switching to SPV drops active listeners and switching back to RPC recreates
them; coordinate this with start_spv()/stop_spv() so SPV status and listener
lifecycle are managed together and avoid double-spawning or leaked sockets.

In `@src/database/initialization.rs`:
- Around line 57-64: The suffix check in the migration loop currently uses
key.ends_with("core_rpc_password") which is case-sensitive while DEVELOPER_MODE
uses eq_ignore_ascii_case; change the suffix check to a case-insensitive form
(e.g., normalize key with to_ascii_lowercase().ends_with("core_rpc_password") or
otherwise compare ignoring ASCII case) in the loop that contains
key.eq_ignore_ascii_case("DEVELOPER_MODE") and has_any_rpc_password handling,
and add a unit test variant (alongside
v34_preserves_mode_when_local_core_configured) named something like
v34_preserves_mode_with_uppercase_password_key that writes an
uppercase/mixed-case key (e.g., MAINNET_CORE_RPC_PASSWORD) into the temp .env,
runs try_perform_migration(33, 34, Some(tmp.path())), and asserts the core
backend mode is preserved.

---

Outside diff comments:
In `@src/ui/network_chooser_screen.rs`:
- Around line 524-527: The fallback default for backend_modes when accessing
self.backend_modes.entry(self.current_network).or_insert(...) is inconsistent
(currently CoreBackendMode::Rpc) and should match the other occurrences; change
the insertion to use the global seed instead of hard-coding Rpc — either replace
CoreBackendMode::Rpc with CoreBackendMode::Spv or, preferably, call
ctx.core_backend_mode() when inserting so backend_modes is seeded from the
actual AppContext value; update the entry call in the Connection Status card
code (the backend_modes / current_network usage) to use ctx.core_backend_mode()
to keep behavior consistent with render_network_table.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 17275b62-0711-4cb8-8f9b-c9f3ab617e50

📥 Commits

Reviewing files that changed from the base of the PR and between cd4b52f and ddec5fb.

📒 Files selected for processing (10)
  • docs/user-stories.md
  • src/app.rs
  • src/context/connection_status.rs
  • src/context/mod.rs
  • src/database/initialization.rs
  • src/mcp/server.rs
  • src/model/feature_gate.rs
  • src/spv/manager.rs
  • src/spv/tests.rs
  • src/ui/network_chooser_screen.rs

Comment thread src/app.rs
Comment thread src/database/initialization.rs
CR-2 — make core_rpc_password suffix match case-insensitive in the
v34 migration, matching DEVELOPER_MODE's treatment. Uppercase keys
(MAINNET_CORE_RPC_PASSWORD) would have silently been ignored,
flipping the user to SPV despite a configured local Core node.
Adds v34_preserves_mode_with_uppercase_password_key test.

CR-3 — flip the third or_insert(CoreBackendMode::Rpc) site in
network_chooser_screen.rs to Spv, matching the other two
already-flipped sites. Dead-code footgun removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lklimek
Copy link
Copy Markdown
Contributor Author

lklimek commented Apr 22, 2026

On CodeRabbit's outside-diff finding about the third or_insert(CoreBackendMode::Rpc) site in src/ui/network_chooser_screen.rs:524-527 (Connection Status card): fixed in 1cc25ca4. Flipped to CoreBackendMode::Spv to match the other two already-flipped sites on lines 260 and 443. Kept the minimal one-line fix rather than the bigger "seed from ctx.core_backend_mode()" refactor CodeRabbit also suggested — staying in scope for this PR.

🤖 Co-authored by Claudius the Magnificent AI Agent

Add a TODO in set_core_backend_mode_inner flagging that mid-session
backend-mode switches do not touch the ZMQ listener lifecycle: an RPC
→ SPV switch leaves any existing listener running against an unused
Core node (pre-existing bug predating the SPV-default flip), and an
SPV → RPC switch never spawns a listener, so Expert users toggling to
Local Dash Core node lose real-time ZMQ events until restart or
network switch (the FeatureGate::RpcBackend gate in spawn_zmq_listener
blocks the startup spawn while in SPV mode). Fixing this needs an
AppContext → AppState signal to manage the listener map owned by
AppState — a cross-module refactor deliberately scoped out of PR #836.
Ref: CR-1 on #836.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lklimek lklimek enabled auto-merge (squash) April 22, 2026 13:35
@lklimek lklimek merged commit a474503 into v1.0-dev Apr 22, 2026
5 checks passed
@lklimek lklimek deleted the feat/spv-default-connection branch April 22, 2026 13:46
lklimek added a commit that referenced this pull request Apr 23, 2026
* feat(error): add OperationRequiresDashCore task error variant

Introduces a typed TaskError for backend operations that cannot run
when the app is connected via SPV. The variant carries the static
operation name so the user-facing message is specific without any
string concatenation, and the `#[error(...)]` template keeps the
Display/Debug separation consistent with the rest of TaskError.

Used by follow-up commits that gate single-key wallet refresh on
Dash Core availability and will also back future UI "disable in SPV"
affordances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(shielded): route asset-lock broadcast through backend-mode-aware helper

The `shield_from_asset_lock()` bundle was reaching directly for
`app_context.core_client.read()?.send_raw_transaction(...)`, which
would unconditionally hit Dash Core even when the user had selected
SPV mode. In SPV the `core_client` is still constructed for
legacy/devtool reasons but there is no expectation that it can reach
a live `dashd` — the broadcast failed with `ConnectionRefused`.

Switch to the shared `AppContext::broadcast_raw_transaction()`
dispatcher (used by `create_registration_asset_lock`, `send_wallet_
payment`, `broadcast_and_commit_asset_lock`, etc.). It picks the
right path per `core_backend_mode()` — Core RPC or
`SpvManager::broadcast_transaction` — and keeps the finality tracker
leak-free by cleaning the waiting entry when the broadcast itself
fails.

Addresses the shielded half of the backend-E2E failures flagged for
`tc_014_wallet_platform_lifecycle` and `tc_018_fund_platform_address
_from_asset_lock` (`ConfirmationTimeout` in SPV mode triggered by the
RPC path never actually broadcasting in SPV).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wallet-spv): broadcast single-key payments via backend-mode helper

`send_single_key_wallet_payment()` built and signed the transaction
locally but then dispatched the broadcast through
`core_client.send_raw_transaction()` without checking
`core_backend_mode()`. In SPV mode that blew up with
`ConnectionRefused` from the unused Core RPC socket instead of going
out on the wire via `SpvManager::broadcast_transaction`.

Replace the direct RPC call with the shared
`AppContext::broadcast_raw_transaction()` dispatcher, which routes
per backend mode. The rest of the function — UTXO selection and
signing from the wallet's in-memory cache — is unchanged and works
in both modes; the caller is now responsible for populating those
UTXOs (see `refresh_single_key_wallet_info`, still Core-RPC only).

Addresses the broadcast half of `core_tasks::tc_009_send_single_key_
wallet_payment`. The UTXO-refresh prerequisite is handled in a
follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wallet-spv): surface typed error when refreshing single-key wallet in SPV

`refresh_single_key_wallet_info()` relies exclusively on Core RPC
(`import_address`, `list_unspent`) to discover UTXOs for a
standalone WIF/private-key wallet. The SPV subsystem only tracks
addresses derived from HD-wallet account paths registered with its
bloom filter, so there is no SPV-side equivalent for an imported
single-key wallet.

Previously calling the task in SPV mode hit the unused Core RPC
socket and reported `ConnectionRefused` via a raw
`CoreRpcConnectionFailed` banner. Guard the task with an
early-return that produces the new
`TaskError::OperationRequiresDashCore` variant, so the UI shows an
actionable "connect to Dash Core and retry" message instead of a
connection-level error the user cannot map to the actual cause.

Update `core_tasks::tc_003_refresh_single_key_wallet_info` and the
`tc_009` pre-flight refresh to assert the mode-dependent outcome:
typed `OperationRequiresDashCore` in SPV, normal `RefreshedWallet`
in RPC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wallet-spv): short-circuit asset lock recovery in SPV mode

`recover_asset_locks()` rescans the Core wallet's transaction and
UTXO indices for asset-lock payloads that weren't tracked locally.
In SPV mode none of the RPC queries it needs (`list_unspent`,
`get_raw_transaction`, `get_raw_transaction_info`) are available,
so the whole pass collapsed into a `ConnectionRefused` failure.

In SPV the reconciliation that this pass performs on RPC is handled
automatically by the InstantLock/ChainLock event stream — whenever
an asset-lock transaction arrives, `received_asset_lock_finality()`
populates `unused_asset_locks` and the DB row directly, so a
standalone "recover" pass has nothing to do. Return an empty
`RecoveredAssetLocks { 0, 0 }` result in SPV mode instead of
failing.

Addresses `core_tasks::tc_006_recover_asset_locks`, which expects
a `RecoveredAssetLocks` response in both backends (0 recoveries is
documented as a valid outcome in the test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ui-spv): disable MasternodeListDiff dev tool when not in RPC mode

The MasternodeListDiff inspector issues ~16 direct Dash Core RPC calls
(see audit section C-1 and `src/backend_task/mnlist.rs:81`). Running it
while the app is on its built-in SPV backend produces a storm of
connection-refused errors, so dim the entry in the Tools side panel and
show a tooltip explaining the constraint instead.

The check is a plain `core_backend_mode() == CoreBackendMode::Rpc`
comparison per the dim-the-menu design chosen for v1.0-dev; the
`FeatureGate::RpcBackend` predicate lands separately with #836.

Complements backend commits 8975d5d / 7e74e69 / 4ee1a5a / b29f416
which surface typed "requires Core" errors for wallet flows.

* fix(ui-spv): grey out single-key wallet actions in SPV mode

Per audit section C-2: single-key wallets are not in the SPV bloom
filter (no `Account` entry to feed addresses from), so `refresh` and
`send` fail even though broadcast itself can now route via
`broadcast_raw_transaction`. Commit 4ee1a5a already surfaces a typed
`OperationRequiresDashCore` error — this commit adds the matching UI
preventive step so users cannot reach that error from the GUI.

Changes:
* In the single-key wallet detail panel, grey out the Send button with
  a disabled-hover tooltip and surface an in-panel warning label
  explaining the constraint. Receive stays enabled — it only displays
  the cached local address.
* On the single-key send screen, prepend an info banner and disable the
  Send action with the same tooltip. Guards against users already sitting
  on the send screen when they switch the backend mode mid-session.

Raw `core_backend_mode() == CoreBackendMode::Rpc` comparisons per the
v1.0-dev pattern; `FeatureGate::RpcBackend` lands separately with #836.

Complements backend commits 8975d5d / 7e74e69 / 4ee1a5a / b29f416.

* fix(spv): address review comments on single-key wallet SPV gating

- Consolidate duplicate SINGLE_KEY_REQUIRES_CORE copy between single_key_view
  and single_key_send_screen.
- Render the SPV-mode warning through MessageBanner instead of inline RichText /
  Frame styling (lklimek review feedback).
- Drop the dead CoreBackendMode::Rpc arm in backend-e2e tc_009 — backend-e2e is
  SPV-only, so only the typed-error path is exercised.
- Gate explicit button text color on enabled state so egui's disabled visuals
  apply correctly (Copilot review).
- Log a warning and recover through the poisoned guard when the
  transactions_waiting_for_finality mutex is poisoned, so finality tracking
  entries cannot leak silently across retries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(backend-e2e): restore tc_009 send flow as commented-out future work

The removed Rpc-arm assertions were the shape of the full single-key send
test. Keep them commented out next to the SPV typed-error check so the
intent and the assertion shape survive the SPV-only gap — when single-key
wallets gain SPV parity, un-comment the block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ui-spv): store single-key SPV warning banner to stop per-frame log spam

The persistent SPV-mode warning on the single-key wallet detail view and
the single-key send screen was constructed as a fresh `MessageBanner`
every frame. Each fresh `BannerState` starts with `logged: false`, so
`process_banner()` emits a `tracing::warn!` once and then flips the
local flag — but the local is dropped at end of frame and the next frame
repeats the whole dance. Result: a banner warn per repaint (~60/sec when
egui is active) for the entire time the screen is visible on SPV.

Store the banner as a screen-struct field instead, populate it once per
SPV-mode entry (guarded by `has_message()`), clear it on mode change,
and disable auto-dismiss so the state notice persists without needing
the fresh-each-frame hack. Adds a small `MessageBanner::disable_auto_dismiss()`
helper since the component previously only exposed an overriding setter.

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

* fix(shielded): block on finality tracker cleanup after asset-lock timeout

On the asset-lock timeout path, `try_lock()` could return `WouldBlock`
and silently skip cleanup of `transactions_waiting_for_finality`. That
leaves a stale entry behind for a tx this flow has already abandoned,
so the finality listener keeps servicing it across retries/runs.

Switch to blocking `lock()` with the same poisoned-guard handling the
broadcast-failure branch already uses. The critical section is a single
`BTreeMap::remove`, so holding the std::sync Mutex across the await
boundary is bounded and matches the surrounding pattern.

Addresses copilot review on PR #837.

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

* fix(spv): address iteration-2 review findings on PR #837

- Narrow single_key_view module scope: re-export SINGLE_KEY_REQUIRES_CORE
  via pub(crate) use instead of publicising the whole submodule.
- Replace TaskError::OperationRequiresDashCore Display fragment with a
  complete sentence; keep `operation` as a Debug-only field so log
  output still identifies the originating callsite.
- Reword the single-key SPV banner to be action-specific
  (sending/refresh need Core; Receive still works) per coderabbit +
  copilot-pull-request-reviewer feedback.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <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