Skip to content

feat: per-wallet Core RPC routing with auto-detection and selection dialog#695

Merged
lklimek merged 30 commits into
v1.0-devfrom
fix/multi-wallet
Mar 7, 2026
Merged

feat: per-wallet Core RPC routing with auto-detection and selection dialog#695
lklimek merged 30 commits into
v1.0-devfrom
fix/multi-wallet

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Mar 4, 2026

Issue being fixed or feature implemented

Closes #98
Closes #693
Follow-up: #700 (async list_core_wallets())

User Story

Imagine you are running Dash Evo Tool and your Dash Core node has multiple wallets loaded (e.g. a personal wallet and a masternode wallet). Previously, every wallet-related operation — refreshing balances, creating asset locks, registering identities — would fail with a cryptic RPC error about "Wallet file not specified". You'd have to close all but one wallet in Core to make DET work.

Now, DET automatically detects which Core wallet owns each DET wallet's addresses. If only one Core wallet is loaded, it's selected transparently. If multiple wallets exist and auto-detection is inconclusive, a clean selection dialog appears asking which Dash Core wallet to use. Your choice is persisted per-wallet in the database, so you never see the dialog again unless your Core setup changes.

What was done?

31 files changed, +2271 / −356 lines across backend, database, context, and UI layers.

Core Architecture Changes

Per-wallet Core RPC routing (src/context/mod.rs)

  • New core_client_for_wallet(name) builds RPC clients targeting /wallet/<name> URLs
  • Extracted create_core_rpc_client() shared helper — cookie-auth-first fallback used by both AppContext::new() and per-wallet clients (CMT-002)
  • New helpers: ensure_address_imported(), try_import_address(), list_core_wallets(), try_detect_core_wallet_for_address()
  • Wallet names are URL-encoded; path-traversal (..) is rejected

Typed error pipeline (src/backend_task/error.rs)

  • CoreWalletNotConfigured variant — structurally detects RPC error code -19 via From<dashcore_rpc::Error> (no string parsing)
  • MustRetry(String) variant — signals that a prerequisite was auto-fixed and the caller should retry (CMT-003)
  • From<String> also detects "Wallet file not specified" for backwards compatibility
  • Additional #[from] variants: rusqlite::Error, JoinError
  • 7 unit tests covering all conversion paths

Auto-detection and recovery (src/backend_task/core/mod.rs)

  • with_wallet_recovery() intercepts CoreWalletNotConfigured, queries all loaded Core wallets, checks address ownership via getaddressinfo, persists the match, returns MustRetry
  • RefreshWalletInfo catches MustRetry and retries immediately (clones wallet Arc before first attempt)
  • Detection runs inside tokio::task::block_in_place() to avoid blocking Tokio workers (CMT-009)
  • Applied to all wallet task variants: refresh, asset lock creation, top-up, payments, recovery

AppState integration (src/app.rs)

  • MustRetry errors show success banner and trigger refresh() — no error shown to user
  • Generic errors delegate to display_task_error() first — screens can intercept typed errors before the default banner

Database & Model

  • core_wallet_name TEXT DEFAULT NULL column added to both wallet and single_key_wallet tables
  • v28 migration with pragma_table_info() idempotency guards — safe on fresh and upgraded databases (CMT-001)
  • set_wallet_core_wallet_name() and set_single_key_wallet_core_wallet_name() setters
  • Wallet and SingleKeyWallet models carry core_wallet_name: Option<String>

UI Components

SelectionDialog (src/ui/components/selection_dialog.rs) — 402 lines

  • Reusable modal with ComboBox dropdown, overlay, keyboard support (Enter/Escape)
  • Popup ID aligned with egui's internal ComboBox derivation (CMT-017)

WalletsBalancesScreen (src/ui/wallets/wallets_screen/mod.rs)

  • display_task_error() intercepts CoreWalletNotConfigured:
    • Single Core wallet → auto-applies without dialog (CMT-015)
    • Multiple → shows SelectionDialog
    • Zero → error banner
  • apply_core_wallet_selection() takes explicit is_single_key flag (CMT-016)
  • Banner cleanup on error interception (CMT-018)

AddNewWalletScreen / ImportMnemonicScreen

  • Core wallet ComboBox shown when >1 wallets loaded; selection persisted with new wallet
  • Single-wallet case preserved in state (removed len() > 1 filter) — auto-applied at save time (CMT-012, CMT-013)

Error handling improvements

  • render_qr_code() errors use generic banner + .with_details(e) instead of e.to_string() (CMT-010, CMT-011)
  • All refresh_wallet_info, refresh_single_key_wallet_info, recover_asset_locks use ? directly — no duplicated "Failed to create Core RPC client" prefix (CMT-019–022)

Deferred Items

TODO comments added for synchronous list_core_wallets() RPC on UI thread (CMT-004–008) → tracked in #700.

How has this been tested?

  • cargo test --all-features --workspace — all tests pass
  • cargo clippy --all-features --all-targets -- -D warnings — clean
  • cargo +nightly fmt --all — formatted
  • Manual test scenarios: docs/ai-design/2026-03-04-multi-wallet-rpc/manual-test-scenarios.md

Test plan

See manual-test-scenarios.md — 11 scenarios covering:

  • Single wallet auto-selection
  • Multi-wallet dialog flow
  • Persistence across restarts
  • Error recovery (Core unreachable, no wallets loaded)
  • HD and single-key wallet variants

Breaking Changes

None. New core_wallet_name column has DEFAULT NULL — existing wallets work unchanged. v28 migration is idempotent.

Checklist

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Added multi-wallet RPC support enabling selection of Dash Core wallets
    • Introduced automatic Core wallet detection during operations
    • Added wallet selection interface when multiple Core wallets are available
    • Enhanced wallet creation and import with Core wallet selection
  • Improvements

    • Improved error handling and messaging for wallet configuration scenarios
  • Documentation

    • Added comprehensive manual test scenarios for multi-wallet RPC functionality

When Dash Core has multiple wallets loaded, wallet-specific RPC calls
fail with error -19. This change detects that error, enumerates loaded
wallets via `listwallets`, auto-selects when only one is present, and
shows a SelectionDialog when multiple wallets exist. The choice is
persisted as `core_wallet_name` in the .env config.

- Add reusable SelectionDialog component (generalization of ConfirmationDialog)
- Add `core_wallet_name` field to NetworkConfig with .env persistence
- Add `core_rpc_url()` helper that appends `/wallet/<name>` when configured
- Add `set_core_wallet_name()` method on AppContext to persist and reinit
- Intercept error -19 in core task RPC calls with auto-select/dialog flow
- Render wallet selection dialog as foreground overlay in AppState
- Document `core_wallet_name` in .env.example for all networks
- Add 11 manual test scenarios

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

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds multi-wallet Dash Core RPC support: detects RPC error -19 (wallet not specified), attempts auto-detection by address, persists per-wallet core_wallet_name, introduces a SelectionDialog UI for manual selection, converts many backend task errors to TaskError, and migrates DB schema to store wallet-specific core names.

Changes

Cohort / File(s) Summary
Documentation
docs/ai-design/2026-03-04-multi-wallet-rpc/manual-test-scenarios.md
Adds detailed manual test scenarios (MWT-001..MWT-023) for multi-wallet RPC flows, auto-detection, dialog behavior, error cases, and persistence.
Error Infrastructure
src/backend_task/error.rs, src/app.rs
Introduces TaskError variants (CoreWalletNotConfigured, MustRetry, Sqlite, JoinError), maps RPC code -19 to CoreWalletNotConfigured, and routes task errors to screen-specific display_task_error before global banners.
AppContext / Core RPC Helpers
src/context/mod.rs, src/context/wallet_lifecycle.rs
Adds core_client_for_wallet, ensure_address_imported, try_import_address, list_core_wallets, and try_detect_core_wallet_for_address; refactors spawn_blocking result handling for wallet UTXO refresh.
Backend Task Refactor
src/backend_task/core/..., src/backend_task/dashpay*.rs
Converts core and dashpay task error types from String to TaskError, adds with_wallet_recovery and core_wallet_first_address helpers to enable auto-detection and retry (MustRetry) semantics, and scopes Core RPC clients per wallet.
Database & Models
src/database/initialization.rs, src/database/wallet.rs, src/database/single_key_wallet.rs, src/model/wallet/...
DB migration v27→v28: adds core_wallet_name column; model structs (Wallet, SingleKeyWallet) gain core_wallet_name: Option<String>; adds setters to persist core wallet names and propagates field across CRUD paths.
UI Components
src/ui/components/selection_dialog.rs, src/ui/components/message_banner.rs, src/ui/components/mod.rs, src/ui/components/styled.rs
Adds SelectionDialog component (modal, ComboBox, keyboard shortcuts), SelectionStatus, and MessageBanner::set_global_with_error; exports selection dialog from components.
Screens: Wallets & Creation/Import
src/ui/wallets/wallets_screen/mod.rs, src/ui/wallets/add_new_wallet_screen.rs, src/ui/wallets/import_mnemonic_screen.rs
Implements display_task_error handling for CoreWalletNotConfigured, manages core_wallet_dialog and pending selection state, adds core_wallets caching and selected_core_wallet_index to creation/import flows, and applies selected core wallet to persisted wallets.
Identity & Asset Lock Screens
src/ui/identities/..., src/ui/wallets/create_asset_lock_screen.rs
Switches QR/address generation methods to return TaskError; replaces direct Core RPC calls with app_context.ensure_address_imported using the wallet-scoped core client.
UI Error Routing
src/ui/mod.rs
Adds ScreenLike::display_task_error(&TaskError) -> bool and dispatches to concrete screens so screens can handle errors and suppress global banners.
Build / Dependencies
Cargo.toml
Adds urlencoding = "2" dependency for wallet name URL encoding in wallet-scoped RPC URIs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Wallets Screen
    participant Backend as Backend Task
    participant Core as Dash Core RPC

    User->>UI: Trigger operation (e.g., Refresh / Send)
    UI->>Backend: Execute backend task
    Backend->>Core: RPC call (no rpcwallet)
    Core-->>Backend: RpcError code -19 (Wallet not specified)
    Backend->>Backend: Map to TaskError::CoreWalletNotConfigured
    Backend->>Backend: Extract seed_hash + first address
    Backend->>Core: listwallets()
    Core-->>Backend: [wallet_list]

    alt Single wallet or unique owner detected
        Backend->>Backend: Persist core_wallet_name
        Backend-->>UI: Return MustRetry (retry signal)
        UI->>Backend: Retry task with wallet context
        Backend->>Core: RPC call with rpcwallet param
        Core-->>Backend: Success
        Backend-->>UI: Result
    else Multiple/ambiguous matches
        Backend-->>UI: Propagate CoreWalletNotConfigured
        UI->>UI: display_task_error -> try auto-detect
        UI->>User: Show SelectionDialog (options)
        User->>UI: Select wallet or cancel
        UI->>Backend: apply_core_wallet_selection (persist)
        Backend-->>UI: Success / Error
        UI->>Backend: Retry or abort
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Poem

🐰 I found the missing key,

wallets now choose where to be,
Auto-pick or a friendly list,
No more -19 in the mist,
Hops and banners, all set free.

🚥 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 PR title clearly and specifically describes the main change: implementing per-wallet Core RPC routing with automatic detection and selection dialog.
Linked Issues check ✅ Passed The PR addresses all coding requirements from linked issues #98 and #693: implements wallet-file-not-specified (-19) error detection [#98, #693], provides auto-detection and persistence of core_wallet_name [#98, #693], offers selection UI for multiple wallets [#98, #693], and includes wallet-scoped RPC routing [#98, #693].
Out of Scope Changes check ✅ Passed All changes align with objectives: per-wallet RPC routing, error handling for -19, database persistence, UI selection dialog, and helper methods. No unrelated changes detected outside the multi-wallet RPC recovery scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/multi-wallet

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.

lklimek and others added 2 commits March 4, 2026 20:14
Two bugs in the wallet selection dialog:

1. Buttons and ComboBox were not clickable — only Escape worked.
   Added a full-screen input-blocking Area at Order::Middle that
   consumes pointer events before they reach screens underneath.
   Changed overlay paint order from Background to Middle.

2. After canceling the dialog, the wallet screen spinner stayed
   forever. Added display_message() call when CoreWalletSelectionNeeded
   is received, so the screen's progress banner gets dismissed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dialog Window was rendering at default order (Middle), same as its
dark overlay, causing the entire screen to appear greyed out with no
visible dialog. Set Window order to Foreground so it renders above
the overlay layer.

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

Adds multi-wallet Dash Core RPC support by persisting a selected Core wallet name in config and presenting a reusable UI selection dialog when Core returns the “wallet file not specified” (-19) error.

Changes:

  • Introduces a reusable SelectionDialog component for choosing from multiple options.
  • Adds core_wallet_name to NetworkConfig with .env persistence and a core_rpc_url() helper that appends /wallet/<name>.
  • Intercepts Core RPC “wallet not specified” failures to either auto-select the single loaded wallet or request UI-driven wallet selection.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/ui/components/styled.rs Re-exports SelectionDialog types for convenient UI use.
src/ui/components/selection_dialog.rs New modal selection dialog component (ComboBox + confirm/cancel).
src/ui/components/mod.rs Registers the new selection_dialog module.
src/spv/tests.rs Updates test NetworkConfig initializer with core_wallet_name.
src/context/mod.rs Adds core_rpc_url() and set_core_wallet_name(); uses core_rpc_url() for client init/reinit.
src/config.rs Adds core_wallet_name to config and persists it to .env; adds tests for parsing.
src/backend_task/mod.rs Adds CoreWalletSelectionNeeded(Vec<String>) task success result.
src/backend_task/core/mod.rs Detects “wallet not specified” error and calls listwallets to drive selection flow.
src/app.rs Shows selection dialog on CoreWalletSelectionNeeded and persists the chosen wallet name.
docs/ai-design/2026-03-04-multi-wallet-rpc/manual-test-scenarios.md Adds manual test plan for multi-wallet scenarios.
.env.example Documents *_core_wallet_name setting for each network.

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

Comment thread src/app.rs Outdated
Comment thread src/app.rs Outdated
Comment thread src/context/mod.rs Outdated
Comment thread src/config.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
lklimek and others added 17 commits March 4, 2026 21:11
Replace global core_wallet_name (.env config) with per-wallet storage
in SQLite. Each DET wallet now independently maps to a Core wallet,
fixing multi-wallet RPC (-19) errors when different wallets need
different Core wallet targets.

- Revert global NetworkConfig.core_wallet_name approach
- Add DB migration v28: core_wallet_name column on wallet tables
- Add core_client_for_wallet() with path traversal validation
- Per-wallet error-19 detection, auto-assign (single) or prompt (multiple)
- Two-phase wallet creation/import UI with SelectionDialog
- App-level runtime recovery handler for CoreWalletSelectionNeeded
- Update manual test scenarios for per-wallet behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n/import

The SelectionDialog modal overlay in wallet creation/import screens was
not clickable due to rendering conflicts with the screen layout. Replace
with an inline ComboBox that appears in the form when multiple Core
wallets are detected. The runtime recovery modal in app.rs (for legacy
wallets hitting error -19) is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a DET wallet triggers RPC error -19, try getaddressinfo on each
loaded Core wallet before showing the SelectionDialog. If exactly one
Core wallet recognizes the address (is_mine or is_watchonly), auto-assign
it and persist to SQLite. Falls back to manual selection only when
detection is ambiguous.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add MWT-016/017/018 for auto-detection success, ambiguous fallback, and
fresh wallet scenarios. Update existing scenarios to reflect the
getaddressinfo-based auto-detection step before SelectionDialog fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
display_message() only cleared `refreshing` for Error/Warning types,
leaving the spinner running when CoreWalletSelectionNeeded sent Info.
Also implement refresh() to call refresh_on_arrival() so post-selection
and auto-detection paths properly re-fetch wallet data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Address review findings from grumpy-review triage:

- Add check_wallet_not_specified() recovery to RefreshSingleKeyWalletInfo,
  SendWalletPayment, SendSingleKeyWalletPayment, and RecoverAssetLocks
- Extend apply_core_wallet_selection() and auto-detection to handle
  single-key wallets (persist via set_single_key_wallet_core_wallet_name)
- Change CoreWalletSelectionNeeded.wallet_seed_hash from String to [u8; 32]
  eliminating hex encode/decode round-trip
- Extract core_wallet_first_address() helper to deduplicate 3x pattern
- Guard against concurrent dialog overwrites in CoreWalletSelectionNeeded
- Switch core_client_for_wallet() from denylist to allowlist validation
- Remove redundant outer blocker overlay in SelectionDialog rendering
- Fix test scenario button labels (Confirm → Select) and error codes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The full-screen blocker Area was at Order::Foreground, competing with
the SelectionDialog Window for input. Move it to Order::Middle so it
blocks clicks to the background UI while letting the Foreground dialog
receive all input.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add TaskError::CoreWalletNotConfigured variant with actionable message
directing users to the Wallets screen. The From<String> impl now parses
known error patterns into typed variants automatically, catching error -19
from any backend task path (identity registration, top-up, wallet funding)
without modifying individual task files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…reen

Let RPC error -19 flow through the normal TaskError path instead of
intercepting it as a fake success result. Only the wallets screen
catches CoreWalletNotConfigured to show a SelectionDialog; other
screens display a user-friendly error banner.

- Simplify check_wallet_not_specified() to return TaskError directly
- Remove CoreWalletSelectionNeeded variant from BackendTaskSuccessResult
- Add display_task_error() to ScreenLike trait for typed error dispatch
- Add SelectionDialog::show_modal() convenience method
- Add MessageBanner::set_global_error() helper
- Move dialog state and apply_core_wallet_selection() to WalletsBalancesScreen
- Update manual test scenarios for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
display_task_error() now returns bool — true means the screen handled
the error and AppState should skip the default error banner. Fixes the
wallets screen showing a "go to Wallets screen" banner instead of the
Core wallet selection dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Configured

run_core_task() now returns Result<..., TaskError> instead of
Result<..., String>. Previously, CoreWalletNotConfigured was converted
to a user-friendly String, which From<String> could not reconstruct
back into the typed variant — so display_task_error() on the wallets
screen never matched it and the generic error banner appeared instead
of the selection dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UI screens making direct core_client RPC calls (identity QR code,
top-up QR code, create asset lock) bypassed the backend task system
entirely, showing raw "JSON-RPC error: RpcError { code: -19, ... }"
to users.

Add TaskError::user_message() helper that converts raw error strings
to user-friendly text using the same detection as From<String>.
Apply it in all three affected UI screens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…code matching

Replace string-based RPC error detection in UI render functions with
type-safe From<dashcore_rpc::Error> conversion. Matches error code -19
structurally via dashcore_rpc::jsonrpc::error::Error::Rpc instead of
parsing error message strings.

- Add From<dashcore_rpc::Error> impl with code-19 → CoreWalletNotConfigured
- Change render_qr_code() and generate_funding_address() to return
  Result<(), TaskError> instead of Result<(), String>
- Remove RpcResultExt trait and TaskError::user_message() (no longer needed)
- Replace .map_err(|e| e.to_string())? with bare ? on RPC calls
- Add unit tests for From<dashcore_rpc::Error> conversion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On multi-wallet Dash Core nodes, wallet-state RPC calls require the
wallet name in the URL path. Seven call sites used the base core_client
(no wallet path), causing error -19.

Add ensure_address_imported() and try_import_address() helpers on
AppContext that route through core_client_for_wallet(). Update all
call sites to extract core_wallet_name from the Wallet struct and
use the correct client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove wallet_seed_hash from CoreWalletNotConfigured (PROJ-001)
- Propagate TaskError through run_dashpay_task instead of String (RUST-001)
- Merge check_wallet_not_specified variants into with_wallet_recovery (RUST-002)
- Simplify run_core_task arms via with_wallet_recovery helper (RUST-003)
- Propagate dashcore_rpc::Error directly from inner functions (SEC-008)
- Reject '..' sequences in Core wallet name validation (SEC-001)
- Handle CoreWalletAutoDetected in display_task_result (PROJ-004)
- Use defensive .get() for ComboBox index access (PROJ-006)
- Log warning on DB write failure instead of discarding (RUST-004)
- Parameterize SelectionDialog egui Area ID from title (RUST-006)
- Suppress Enter key when ComboBox dropdown is open (RUST-007)
- Avoid unnecessary String clone in ComboBox selected_text (RUST-008)
- Fix wrong variant name in manual test doc (DOC-001)

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

Add unique id_salt to ScrollArea in render_banner() so each banner's
expanded details section gets its own egui ID, preventing visual overlap
when multiple banners have "Show details" expanded simultaneously.

Closes #681

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/app.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/database/wallet.rs
Comment thread src/ui/components/message_banner.rs Outdated
Comment thread src/ui/wallets/wallets_screen/mod.rs Outdated
Comment thread src/ui/wallets/wallets_screen/mod.rs
@lklimek lklimek marked this pull request as ready for review March 6, 2026 13:37
@lklimek lklimek requested a review from Copilot March 6, 2026 13:37
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

Copilot reviewed 31 out of 31 changed files in this pull request and generated 7 comments.


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

Comment thread src/ui/wallets/wallets_screen/mod.rs
Comment thread src/ui/wallets/wallets_screen/mod.rs Outdated
Comment thread src/context/mod.rs
Comment thread src/context/mod.rs
Comment thread src/ui/components/selection_dialog.rs
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/ui/wallets/wallets_screen/mod.rs
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: 10

Caution

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

⚠️ Outside diff range comments (1)
src/backend_task/core/refresh_single_key_wallet_info.rs (1)

37-82: ⚠️ Potential issue | 🟠 Major

Delete old UTXO rows before inserting the refreshed set.

This rebuilds the current set from list_unspent, but the persistence path below only inserts current outputs. Spent rows already stored in utxos survive, and src/database/single_key_wallet.rs loads them back into the wallet on restart, so the single-key wallet can come back with stale outputs and balances. reconcile_spv_wallets() in src/context/wallet_lifecycle.rs already does the per-address delete first; this Core refresh path needs the same cleanup.

Possible fix
         let utxo_map = {
             let utxos = client.list_unspent(Some(0), None, Some(&[&address]), None, None)?;

             let mut map: HashMap<OutPoint, TxOut> = HashMap::new();
             for utxo in utxos {
@@
             map
         };
+
+        self.db
+            .execute(
+                "DELETE FROM utxos WHERE address = ? AND network = ?",
+                rusqlite::params![address.to_string(), self.network.to_string()],
+            )
+            .map_err(|e| e.to_string())?;

         // Step 4: Calculate balance from UTXOs
         let total_balance: u64 = utxo_map.values().map(|tx_out| tx_out.value).sum();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/core/refresh_single_key_wallet_info.rs` around lines 37 -
82, Before inserting refreshed UTXOs, delete existing persisted UTXOs for this
single-key wallet so stale spent rows are removed: immediately before the for
(outpoint, tx_out) in &utxo_map loop call a DB delete method (e.g.
self.db.delete_utxos_for_key_hash(&key_hash) or
self.db.delete_utxos_for_single_key(&key_hash)); if such a method doesn’t exist
add one (implement deletion by key hash or address in the same table used by
insert_utxo), handle and log any error consistently (map_err / tracing::warn or
return the error as done elsewhere), and prefer doing the delete and subsequent
inserts in the same logical operation/transaction if your DB layer supports it.
♻️ Duplicate comments (1)
src/backend_task/core/mod.rs (1)

179-183: ⚠️ Potential issue | 🟡 Minor

Don't stringify typed errors in the task entrypoint.

These conversions throw away the original error types and force the new error flow back through string parsing; ConfigError in particular becomes Generic, and the RPC path skips the dedicated From<dashcore_rpc::Error> impl.

♻️ Minimal fix
                 })
-                .map_err(|e| TaskError::from(e.to_string())),
+                .map_err(TaskError::from),
             CoreTask::GetBestChainLocks => {
                 // Load configs
-                let config = Config::load()
-                    .map_err(|e| TaskError::from(format!("Failed to load config: {}", e)))?;
+                let config = Config::load().map_err(TaskError::from)?;

As per coding guidelines, "Backend tasks should return Result<T, TaskError> from src/backend_task/error.rs. When adding new backend error types, add a #[from] variant to TaskError rather than converting to String."

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

In `@src/backend_task/core/mod.rs` around lines 179 - 183, The code currently
converts typed errors to strings in the CoreTask::GetBestChainLocks branch
(around the Config::load call), losing original error types and bypassing
TaskError's typed From impls; change the map_err calls to propagate the original
error types into TaskError by returning/map_err to TaskError variants using the
#[from] conversions instead of e.to_string()—specifically update the
Config::load error mapping to map into TaskError (add a #[from] variant in
src/backend_task/error.rs for the ConfigError type if missing) and remove
stringification so existing From<dashcore_rpc::Error> and other From impls are
used end-to-end.
🧹 Nitpick comments (9)
src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs (2)

151-157: Consider using .with_details() for technical error context.

Per coding guidelines, raw backend errors shouldn't be exposed directly to users. While TaskError::CoreWalletNotConfigured has a user-friendly message, other error variants (e.g., RPC failures from ensure_address_imported) may expose technical details.

Given that RPC paths are deprecated in the near future (per retrieved learnings), this is low priority but worth noting for consistency with other screens.

♻️ Suggested improvement
                 if let Err(e) = self.render_qr_code(ui, amount_dash) {
-                    MessageBanner::set_global(
+                    MessageBanner::set_global(
                         ui.ctx(),
-                        e.to_string(),
+                        "Failed to prepare funding address",
                         MessageType::Error,
-                    );
+                    )
+                    .with_details(e.to_string());
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs` around lines
151 - 157, The current error path passes raw backend errors into
MessageBanner::set_global when render_qr_code fails; instead map error variants
from render_qr_code/TaskError (e.g., TaskError::CoreWalletNotConfigured and
other cases like RPC failures from ensure_address_imported) to a user-friendly
message and attach the technical details via .with_details(e.to_string()) before
calling MessageBanner::set_global on ui.ctx(); update the error handling around
render_qr_code to pattern-match or use a helper that returns a
(friendly_message, details) pair and pass only the friendly_message to the
banner while preserving e.to_string() in .with_details().

34-51: Consider consolidating the duplicate import logic.

Both branches call ensure_address_imported with identical arguments. The logic could be simplified:

♻️ Suggested simplification
                     if let Some(has_address) = self.core_has_funding_address {
-                        if !has_address {
-                            self.app_context.ensure_address_imported(
-                                &receive_address,
-                                core_wallet_name.as_deref(),
-                                Some("Managed by Dash Evo Tool"),
-                            )?;
-                        }
-                        self.funding_address = Some(receive_address);
+                        if !has_address {
+                            self.app_context.ensure_address_imported(
+                                &receive_address,
+                                core_wallet_name.as_deref(),
+                                Some("Managed by Dash Evo Tool"),
+                            )?;
+                        }
                     } else {
                         self.app_context.ensure_address_imported(
                             &receive_address,
                             core_wallet_name.as_deref(),
                             Some("Managed by Dash Evo Tool"),
                         )?;
-                        self.funding_address = Some(receive_address);
                         self.core_has_funding_address = Some(true);
                     }
+                    self.funding_address = Some(receive_address);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs` around lines
34 - 51, The branches around core_has_funding_address duplicate the call to
app_context.ensure_address_imported and setting of self.funding_address;
refactor by calling self.app_context.ensure_address_imported(&receive_address,
core_wallet_name.as_deref(), Some("Managed by Dash Evo Tool")) once
unconditionally, then set self.funding_address = Some(receive_address) and
ensure self.core_has_funding_address is set to Some(true) if it was None (leave
it as-is when Some(true)); update the block that currently references
core_has_funding_address, funding_address, core_wallet_name, and
app_context.ensure_address_imported to this simplified flow.
docs/ai-design/2026-03-05-banner-details-overlap/manual-test.md (1)

9-15: Consider adding more specific trigger instructions.

Step 1 instructs testers to "trigger 2+ error banners" but relies on vague examples in parentheses. For better reproducibility, consider adding concrete trigger steps or referencing specific test scenarios that produce the required errors (e.g., "disconnect network in settings, then attempt identity operation").

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

In `@docs/ai-design/2026-03-05-banner-details-overlap/manual-test.md` around lines
9 - 15, Update Step 1 ("Trigger 2+ error banners") to replace vague examples
with concrete, reproducible trigger steps: enumerate 2–3 specific scenarios
(e.g., toggle network offline in browser devtools and perform the identity
operation to produce a network error; attempt a protected API call with an
expired token to produce a 401; perform a long-running fetch that will timeout)
and instruct testers to perform those exact actions in sequence so that multiple
distinct error banners appear; ensure the text in the manual-test.md step
explicitly lists the actions, expected error types, and the order to run them.
src/ui/components/selection_dialog.rs (3)

254-261: Unnecessary Arc wrapper around RichText.

RichText is typically cheap to clone. Wrapping it in Arc adds indirection without benefit here, and the pattern differs from the cancel button handling below (lines 280-287) which uses .into().

💡 Suggested fix for consistency
-                            let confirm_label = if let WidgetText::RichText(rich_text) =
-                                confirm_text
-                            {
-                                rich_text.clone()
-                            } else {
-                                Arc::new(egui::RichText::new(confirm_text.text()).color(text_color))
-                            };
+                            let confirm_label = if let WidgetText::RichText(rich_text) = confirm_text {
+                                rich_text.clone()
+                            } else {
+                                egui::RichText::new(confirm_text.text()).color(text_color).into()
+                            };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/selection_dialog.rs` around lines 254 - 261, The confirm
button handling currently wraps a constructed egui::RichText in Arc::new, which
is unnecessary and inconsistent with the cancel button's use of .into(); update
the match for confirm_text in the same block (referencing confirm_label and
confirm_text) to return either the cloned RichText or the constructed
egui::RichText converted with .into() (or just cloned directly) instead of
Arc::new(...), so the branches match the cancel handling and remove the extra
Arc indirection.

91-97: current_value() semantics may be confusing.

When is_open is false, this returns Some(SelectionStatus::Canceled), implying the user canceled. However, a closed dialog might also mean the user confirmed a selection. The status field exists but isn't used here.

Consider returning self.status.clone() to reflect the actual outcome:

💡 Suggested fix
     fn current_value(&self) -> Option<Self::DomainType> {
-        if self.is_open {
-            None
-        } else {
-            Some(SelectionStatus::Canceled)
-        }
+        self.status.clone()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/components/selection_dialog.rs` around lines 91 - 97, The
current_value() implementation incorrectly returns
Some(SelectionStatus::Canceled) whenever is_open is false, ignoring the actual
outcome stored in the status field; update current_value() so it returns None
while the dialog is open (self.is_open) and returns self.status.clone() (or an
owned clone of the DomainType) when closed, referencing the status field and
keeping SelectionStatus as the DomainType to reflect the real result.

38-44: Returning &None from changed_value() is unusual.

The method returns a reference to &None (a static reference to the constant None) in the else branch, while returning &self.dialog_response in the if branch. This works but mixes owned and static references in a subtle way.

Consider storing the conditional result in the response struct at construction time, matching how other ComponentResponse implementations work.

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

In `@src/ui/components/selection_dialog.rs` around lines 38 - 44, The else branch
of changed_value() returns a reference to the constant &None which mixes static
and instance references; instead compute and store the conditional result when
the response struct is constructed and return a reference to that stored field.
Update the response struct (the type that holds dialog_response) to include a
field like changed_value: Option<DomainType> (or Option<Self::DomainType> where
appropriate), set it during construction based on has_changed()/dialog_response,
and change changed_value() to simply return &self.changed_value; keep using
existing symbols dialog_response and changed_value() so callers remain
unchanged.
src/backend_task/dashpay/profile.rs (1)

358-358: Error type conversion maintains backward compatibility.

The send_payment_to_contact now returns TaskError, and this conversion to String keeps the send_payment function signature stable.

Per coding guidelines, backend tasks should return Result<T, TaskError>. Consider migrating this function's return type to TaskError in a follow-up to align with the codebase direction, which would allow preserving richer error context (including CoreWalletNotConfigured if applicable).

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

In `@src/backend_task/dashpay/profile.rs` at line 358, The current .map_err(|e|
e.to_string()) converts errors to String to keep send_payment's signature
stable, but we should migrate to returning Result<_, TaskError> so richer errors
(e.g., CoreWalletNotConfigured) are preserved; update send_payment's return type
to Result<T, TaskError>, change send_payment_to_contact's error conversion to
map_err(TaskError::from) or propagate the TaskError directly (use ? where
possible), and adjust callers to handle TaskError instead of String; ensure
TaskError implements From for the underlying error types used by
send_payment_to_contact.
src/context/wallet_lifecycle.rs (1)

186-198: Use different warning text for join errors vs refresh errors.

The match now separates spawn_blocking failures from inner refresh failures, but both branches still emit the same message. A small branch-specific message or field would make these startup warnings actionable.

Also applies to: 211-224

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

In `@src/context/wallet_lifecycle.rs` around lines 186 - 198, The two match arms
that handle tokio::task::spawn_blocking(...).await for
ctx.refresh_wallet_info(wallet) should log distinct messages: change the Err(e)
arm to indicate a task join/spawn failure (e.g., "Failed to spawn/join
auto-refresh task for wallet: {e}") and keep the Ok(Err(e)) arm to indicate the
inner refresh failed (e.g., "Failed to auto-refresh wallet UTXOs on startup:
{e}"); update both occurrences (the current block around spawn_blocking handling
and the similar block at the second occurrence) to use these branch-specific
texts and include the error variable in the log for actionable diagnostics while
leaving Ok(Ok(_)) unchanged.
src/model/wallet/mod.rs (1)

741-749: Consolidate address import into one guarded path.

In RPC mode, newly derived receive/change addresses go through try_import_address() twice: first unconditionally at line 741 with a descriptive label, then again inside register_address() at line 940–941 when core_backend_mode() == Rpc. The first call has no backend-mode guard, while the second one does. Since try_import_address() ignores errors and has no deduplication logic, the same address is imported twice on the RPC path. In SPV mode, the address is imported once (line 741 only). Prefer passing the optional label into register_address() so the import happens from a single guarded location.

Also applies to: 940-941

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

In `@src/model/wallet/mod.rs` around lines 741 - 749, The unguarded call to
try_import_address causes duplicate imports in RPC mode; move the formatted
label creation (using self.alias and derivation_path) into the register_address
call and remove the earlier unconditional try_import_address invocation so that
register_address is solely responsible for importing addresses when
core_backend_mode() == Rpc; update register_address to accept an Option<&str>
label (pass self.core_wallet_name.as_deref() and the formatted label as
Some(...)) and ensure calls to register_address propagate the label while
leaving SPV behavior unchanged.
🤖 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/backend_task/error.rs`:
- Around line 52-60: The From<String> impl for TaskError currently only matches
the literal "Wallet file not specified" and thus downgrades already-typed
TaskError::CoreWalletNotConfigured that was wrapped with extra context; update
impl From<String> for TaskError to detect the stringified form of the
CoreWalletNotConfigured variant (e.g., check if
s.contains(TaskError::CoreWalletNotConfigured.to_string())) in addition to the
existing literal check, and return TaskError::CoreWalletNotConfigured when
matched so code paths like the post-mining refresh in core::mod (where a
TaskError is re-stringified with context) preserve the original variant instead
of becoming TaskError::Generic.

In `@src/context/mod.rs`:
- Around line 567-599: core_client_for_wallet currently always uses
Auth::UserPass; change it to attempt Auth::CookieFile first and fall back to
Auth::UserPass (the same auth selection logic used in AppContext::new()).
Specifically, when building the Client inside core_client_for_wallet, try
Client::new(&url, Auth::CookieFile(cfg.core_cookie_path.clone())) and if that
returns an Err, attempt Client::new(&url,
Auth::UserPass(cfg.core_rpc_user.clone(), cfg.core_rpc_password.clone())),
returning the first success or a combined/clear error if both fail; keep the
existing wallet name validation in core_client_for_wallet unchanged.

In `@src/database/initialization.rs`:
- Around line 845-855: The v28 migration add_core_wallet_name_column in
add_core_wallet_name_column currently unconditionally ALTERs wallet and
single_key_wallet which can fail if core_wallet_name was already added earlier
(e.g., via initialize_single_key_wallet_table when applying version 18). Make
the migration idempotent by querying PRAGMA table_info('wallet') and PRAGMA
table_info('single_key_wallet') and only run the ALTER TABLE ... ADD COLUMN
core_wallet_name TEXT DEFAULT NULL if that column is not present; locate
add_core_wallet_name_column and modify it to perform the pragma checks (or
alternatively guard in
apply_version_changes(18)/initialize_single_key_wallet_table to avoid re-adding
the column).

In `@src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs`:
- Around line 115-116: The code currently converts errors from
render_qr_code(...) into plain text via MessageBanner::set_global, which loses
TaskError variants (like TaskError::CoreWalletNotConfigured) needed by the
multi-wallet recovery UI and may expose raw backend messages; change the error
handling to pattern-match the returned Err(e) from self.render_qr_code(ui,
amount_dash): if the error is TaskError::CoreWalletNotConfigured (or other
recovery variants) forward/return that specific TaskError so the recovery UI can
handle it, otherwise call MessageBanner::set_global with a generic user-facing
message and attach the technical details using BannerHandle::with_details(e) (do
not convert e to plain text for the recovery path).

In `@src/ui/mod.rs`:
- Around line 860-866: The WalletsScreen::display_task_error override currently
handles CoreWalletNotConfigured by returning true but does not clear any
existing progress banner, so add a call to self.refresh_banner.take_and_clear()
in the CoreWalletNotConfigured match arm before returning true; locate the
display_task_error implementation (the method overriding
AppState::display_task_error), add the self.refresh_banner.take_and_clear() call
just prior to the return in the CoreWalletNotConfigured branch to ensure cleanup
normally done by display_message() still occurs when you suppress the default
banner.

In `@src/ui/wallets/add_new_wallet_screen.rs`:
- Around line 132-133: The code currently collapses RPC failures and the
single-wallet case into None by using
app_context.list_core_wallets().ok().filter(|w| w.len() > 1) for the
core_wallets field and then writing core_wallet_name = None; instead, preserve
the difference: call list_core_wallets() and treat Err as None but Any Ok(vec)
should be Some(vec) (or at least Some if vec.len() >= 1); only drop to None for
Err, not for vec.len() == 1. Update the initialization of core_wallets (in the
struct fields where core_wallets and selected_core_wallet_index are set) to
accept a Some(vec) when the RPC returned one wallet, and in the later
persistence logic (where core_wallet_name is set around the
create_screen()/lines ~278–281) set core_wallet_name = Some(name) when the
returned list has exactly one wallet instead of unconditionally setting None;
reference list_core_wallets(), the core_wallets field,
selected_core_wallet_index, and core_wallet_name to locate the changes.

In `@src/ui/wallets/create_asset_lock_screen.rs`:
- Around line 570-571: The current error handling after calling
render_qr_code(ui) collapses every TaskError into a banner string via
MessageBanner::set_global(e.to_string()), which prevents
TaskError::CoreWalletNotConfigured from triggering the wallet-selection recovery
flow and also exposes raw internals to users; update the match on the error
returned from render_qr_code (or the enclosing if let Err(e)) to explicitly
detect TaskError::CoreWalletNotConfigured and invoke the wallet-selection
recovery flow used by this screen, and for all other error variants call
MessageBanner::set_global with a generic, user-friendly message while attaching
the technical details via BannerHandle::with_details() (do not call
e.to_string() directly in the banner).

In `@src/ui/wallets/import_mnemonic_screen.rs`:
- Around line 92-93: The state currently stores core_wallets filtered with
.ok().filter(|w| w.len() > 1) which removes the single loaded Core wallet and
breaks the wallet-to-Core binding; change state to keep the full list returned
by app_context.list_core_wallets() (e.g., assign core_wallets =
app_context.list_core_wallets().ok() without filtering) and retain
selected_core_wallet_index logic, then hide the ComboBox UI by gating its render
on core_wallets.as_ref().map_or(false, |v| v.len() > 1) (or equivalent) so the
selector is only shown when more than one core wallet exists; apply the same
change patterns wherever list_core_wallets() was filtered (the blocks around
selected_core_wallet_index and core_wallet_name handling at the other locations
mentioned).

In `@src/ui/wallets/wallets_screen/mod.rs`:
- Around line 239-300: The code currently infers the target wallet from live UI
state which can change while backend tasks are in flight; instead, propagate the
originating wallet identity/type from the backend result/error and use that to
persist selection. Modify the backend task result/error to carry the originating
wallet_hash and a wallet_type flag/enum (e.g., SingleKey vs HD), update
display_task_error (and any task-completion handlers) to pass that identity to
apply_core_wallet_selection, and change apply_core_wallet_selection to accept
the explicit wallet_hash and wallet_type (and then call either
set_wallet_core_wallet_name or set_single_key_wallet_core_wallet_name based on
that flag) rather than reading/inferring from shared maps; update all call sites
accordingly so persistence always targets the wallet that started the operation.
- Around line 2079-2088: The UI thread is calling
self.app_context.list_core_wallets() synchronously; move that RPC off the egui
thread by replacing the blocking call with a non-blocking flow: set up the
SelectionDialog/pending state to a “loading” placeholder (e.g., set
core_wallet_dialog and pending_core_wallet_seed_hash) and spawn a background
task (tokio::spawn, spawn_blocking, or the existing backend task flow) to call
app_context.list_core_wallets(); when the task completes send the wallets back
to the UI (via a channel, a message handler, or a callback) and update
pending_core_wallet_options and core_wallet_dialog with the actual wallets or an
error. Ensure you reference app_context.list_core_wallets(), core_wallet_dialog,
pending_core_wallet_seed_hash, and pending_core_wallet_options when implementing
the async flow so the UI is never blocked by the RPC.

---

Outside diff comments:
In `@src/backend_task/core/refresh_single_key_wallet_info.rs`:
- Around line 37-82: Before inserting refreshed UTXOs, delete existing persisted
UTXOs for this single-key wallet so stale spent rows are removed: immediately
before the for (outpoint, tx_out) in &utxo_map loop call a DB delete method
(e.g. self.db.delete_utxos_for_key_hash(&key_hash) or
self.db.delete_utxos_for_single_key(&key_hash)); if such a method doesn’t exist
add one (implement deletion by key hash or address in the same table used by
insert_utxo), handle and log any error consistently (map_err / tracing::warn or
return the error as done elsewhere), and prefer doing the delete and subsequent
inserts in the same logical operation/transaction if your DB layer supports it.

---

Duplicate comments:
In `@src/backend_task/core/mod.rs`:
- Around line 179-183: The code currently converts typed errors to strings in
the CoreTask::GetBestChainLocks branch (around the Config::load call), losing
original error types and bypassing TaskError's typed From impls; change the
map_err calls to propagate the original error types into TaskError by
returning/map_err to TaskError variants using the #[from] conversions instead of
e.to_string()—specifically update the Config::load error mapping to map into
TaskError (add a #[from] variant in src/backend_task/error.rs for the
ConfigError type if missing) and remove stringification so existing
From<dashcore_rpc::Error> and other From impls are used end-to-end.

---

Nitpick comments:
In `@docs/ai-design/2026-03-05-banner-details-overlap/manual-test.md`:
- Around line 9-15: Update Step 1 ("Trigger 2+ error banners") to replace vague
examples with concrete, reproducible trigger steps: enumerate 2–3 specific
scenarios (e.g., toggle network offline in browser devtools and perform the
identity operation to produce a network error; attempt a protected API call with
an expired token to produce a 401; perform a long-running fetch that will
timeout) and instruct testers to perform those exact actions in sequence so that
multiple distinct error banners appear; ensure the text in the manual-test.md
step explicitly lists the actions, expected error types, and the order to run
them.

In `@src/backend_task/dashpay/profile.rs`:
- Line 358: The current .map_err(|e| e.to_string()) converts errors to String to
keep send_payment's signature stable, but we should migrate to returning
Result<_, TaskError> so richer errors (e.g., CoreWalletNotConfigured) are
preserved; update send_payment's return type to Result<T, TaskError>, change
send_payment_to_contact's error conversion to map_err(TaskError::from) or
propagate the TaskError directly (use ? where possible), and adjust callers to
handle TaskError instead of String; ensure TaskError implements From for the
underlying error types used by send_payment_to_contact.

In `@src/context/wallet_lifecycle.rs`:
- Around line 186-198: The two match arms that handle
tokio::task::spawn_blocking(...).await for ctx.refresh_wallet_info(wallet)
should log distinct messages: change the Err(e) arm to indicate a task
join/spawn failure (e.g., "Failed to spawn/join auto-refresh task for wallet:
{e}") and keep the Ok(Err(e)) arm to indicate the inner refresh failed (e.g.,
"Failed to auto-refresh wallet UTXOs on startup: {e}"); update both occurrences
(the current block around spawn_blocking handling and the similar block at the
second occurrence) to use these branch-specific texts and include the error
variable in the log for actionable diagnostics while leaving Ok(Ok(_))
unchanged.

In `@src/model/wallet/mod.rs`:
- Around line 741-749: The unguarded call to try_import_address causes duplicate
imports in RPC mode; move the formatted label creation (using self.alias and
derivation_path) into the register_address call and remove the earlier
unconditional try_import_address invocation so that register_address is solely
responsible for importing addresses when core_backend_mode() == Rpc; update
register_address to accept an Option<&str> label (pass
self.core_wallet_name.as_deref() and the formatted label as Some(...)) and
ensure calls to register_address propagate the label while leaving SPV behavior
unchanged.

In `@src/ui/components/selection_dialog.rs`:
- Around line 254-261: The confirm button handling currently wraps a constructed
egui::RichText in Arc::new, which is unnecessary and inconsistent with the
cancel button's use of .into(); update the match for confirm_text in the same
block (referencing confirm_label and confirm_text) to return either the cloned
RichText or the constructed egui::RichText converted with .into() (or just
cloned directly) instead of Arc::new(...), so the branches match the cancel
handling and remove the extra Arc indirection.
- Around line 91-97: The current_value() implementation incorrectly returns
Some(SelectionStatus::Canceled) whenever is_open is false, ignoring the actual
outcome stored in the status field; update current_value() so it returns None
while the dialog is open (self.is_open) and returns self.status.clone() (or an
owned clone of the DomainType) when closed, referencing the status field and
keeping SelectionStatus as the DomainType to reflect the real result.
- Around line 38-44: The else branch of changed_value() returns a reference to
the constant &None which mixes static and instance references; instead compute
and store the conditional result when the response struct is constructed and
return a reference to that stored field. Update the response struct (the type
that holds dialog_response) to include a field like changed_value:
Option<DomainType> (or Option<Self::DomainType> where appropriate), set it
during construction based on has_changed()/dialog_response, and change
changed_value() to simply return &self.changed_value; keep using existing
symbols dialog_response and changed_value() so callers remain unchanged.

In `@src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs`:
- Around line 151-157: The current error path passes raw backend errors into
MessageBanner::set_global when render_qr_code fails; instead map error variants
from render_qr_code/TaskError (e.g., TaskError::CoreWalletNotConfigured and
other cases like RPC failures from ensure_address_imported) to a user-friendly
message and attach the technical details via .with_details(e.to_string()) before
calling MessageBanner::set_global on ui.ctx(); update the error handling around
render_qr_code to pattern-match or use a helper that returns a
(friendly_message, details) pair and pass only the friendly_message to the
banner while preserving e.to_string() in .with_details().
- Around line 34-51: The branches around core_has_funding_address duplicate the
call to app_context.ensure_address_imported and setting of self.funding_address;
refactor by calling self.app_context.ensure_address_imported(&receive_address,
core_wallet_name.as_deref(), Some("Managed by Dash Evo Tool")) once
unconditionally, then set self.funding_address = Some(receive_address) and
ensure self.core_has_funding_address is set to Some(true) if it was None (leave
it as-is when Some(true)); update the block that currently references
core_has_funding_address, funding_address, core_wallet_name, and
app_context.ensure_address_imported to this simplified flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dd26f353-cfd1-4449-8e35-f3da5a2dc459

📥 Commits

Reviewing files that changed from the base of the PR and between d3f3546 and 4a5b25e.

📒 Files selected for processing (31)
  • docs/ai-design/2026-03-04-multi-wallet-rpc/manual-test-scenarios.md
  • docs/ai-design/2026-03-05-banner-details-overlap/manual-test.md
  • src/app.rs
  • src/backend_task/core/mod.rs
  • src/backend_task/core/recover_asset_locks.rs
  • src/backend_task/core/refresh_single_key_wallet_info.rs
  • src/backend_task/core/refresh_wallet_info.rs
  • src/backend_task/dashpay.rs
  • src/backend_task/dashpay/payments.rs
  • src/backend_task/dashpay/profile.rs
  • src/backend_task/error.rs
  • src/backend_task/mod.rs
  • src/context/mod.rs
  • src/context/wallet_lifecycle.rs
  • src/database/initialization.rs
  • src/database/single_key_wallet.rs
  • src/database/wallet.rs
  • src/model/wallet/mod.rs
  • src/model/wallet/single_key.rs
  • src/model/wallet/utxos.rs
  • src/ui/components/message_banner.rs
  • src/ui/components/mod.rs
  • src/ui/components/selection_dialog.rs
  • src/ui/components/styled.rs
  • src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs
  • src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs
  • src/ui/mod.rs
  • src/ui/wallets/add_new_wallet_screen.rs
  • src/ui/wallets/create_asset_lock_screen.rs
  • src/ui/wallets/import_mnemonic_screen.rs
  • src/ui/wallets/wallets_screen/mod.rs

Comment thread src/backend_task/error.rs
Comment thread src/context/mod.rs
Comment thread src/database/initialization.rs
Comment thread src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs Outdated
Comment thread src/ui/mod.rs
Comment thread src/ui/wallets/add_new_wallet_screen.rs Outdated
Comment thread src/ui/wallets/create_asset_lock_screen.rs Outdated
Comment thread src/ui/wallets/import_mnemonic_screen.rs Outdated
Comment thread src/ui/wallets/wallets_screen/mod.rs Outdated
Comment thread src/ui/wallets/wallets_screen/mod.rs
lklimek and others added 2 commits March 6, 2026 14:56
- CMT-001/007: Use TaskError::from directly for dashcore_rpc::Error and
  JoinError instead of going through e.to_string(). Add From<JoinError>
  and From<rusqlite::Error> to TaskError. Simplify Config::load() via ?.
- CMT-002: set_wallet_core_wallet_name and
  set_single_key_wallet_core_wallet_name now return rusqlite::Result<bool>
  (false = 0 rows, true = 1 row, Err on >1 rows).
- CMT-003: Rename set_global_error -> set_global_with_error.
- CMT-004: apply_core_wallet_selection returns Result<(), String>;
  error display moved to the caller.
- CMT-005: Add rustdoc to display_task_error.
- CMT-006: URL-encode wallet name in core_client_for_wallet using
  urlencoding crate. Add unit test for spaces -> %20.
- CMT-008: Propagate DB errors in with_wallet_recovery instead of
  logging and continuing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek requested a review from Copilot March 6, 2026 14:02
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

Copilot reviewed 31 out of 32 changed files in this pull request and generated 8 comments.


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

Comment thread src/ui/wallets/add_new_wallet_screen.rs Outdated
Comment thread src/backend_task/core/refresh_wallet_info.rs Outdated
Comment thread src/backend_task/core/refresh_single_key_wallet_info.rs Outdated
Comment thread src/backend_task/core/recover_asset_locks.rs Outdated
Comment thread src/model/wallet/utxos.rs Outdated
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/ui/wallets/import_mnemonic_screen.rs Outdated
Comment thread src/ui/wallets/wallets_screen/mod.rs
lklimek and others added 6 commits March 6, 2026 16:22
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…(CMT-002, CMT-019-022)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ace (CMT-003, CMT-009)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ner cleanup (CMT-010-016, CMT-018)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek changed the title feat: multi-wallet RPC support with selection dialog feat: per-wallet Core RPC routing with auto-detection and selection dialog Mar 6, 2026
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: 7

♻️ Duplicate comments (5)
src/backend_task/error.rs (1)

65-72: ⚠️ Potential issue | 🟠 Major

Preserve CoreWalletNotConfigured when it gets stringified.

This still only recognizes the raw Dash Core RPC text. Any path that wraps TaskError::CoreWalletNotConfigured into a contextual string and later converts it back through From<String> will downgrade to Generic, so the wallet-selection recovery flow disappears again.

Possible fix
 impl From<String> for TaskError {
     fn from(s: String) -> Self {
-        if s.contains("Wallet file not specified") {
+        let core_wallet_msg = TaskError::CoreWalletNotConfigured.to_string();
+        if s.contains("Wallet file not specified") || s.contains(&core_wallet_msg) {
             TaskError::CoreWalletNotConfigured
         } else {
             TaskError::Generic(s)
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/error.rs` around lines 65 - 72, The conversion impl
From<String> for TaskError currently only looks for the literal RPC text and
can’t restore TaskError::CoreWalletNotConfigured if that variant was previously
stringified into a contextual message; update the flow so the variant is
preserved by (A) adding a unique marker/token when stringifying
TaskError::CoreWalletNotConfigured (e.g. include "CORE_WALLET_NOT_CONFIGURED" or
the variant name in the stringification site), and (B) changing impl
From<String> for TaskError to detect that marker (or an exact match of the
variant name) and return TaskError::CoreWalletNotConfigured, otherwise fall back
to TaskError::Generic(s); refer to the impl From<String> for TaskError and the
TaskError::CoreWalletNotConfigured / TaskError::Generic variants to locate where
to adjust stringification and parsing.
src/ui/wallets/create_asset_lock_screen.rs (1)

570-576: ⚠️ Potential issue | 🟠 Major

Still swallowing the multi-wallet recovery error.

render_qr_code() now returns TaskError, but Line 570 still reduces every variant to a banner. That prevents TaskError::CoreWalletNotConfigured from reaching the new selection flow, so asset-lock funding can still dead-end on multi-wallet Core nodes.

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

In `@src/ui/wallets/create_asset_lock_screen.rs` around lines 570 - 576, The
current error handling around render_qr_code(ui) reduces every TaskError to a
generic banner and hides TaskError::CoreWalletNotConfigured; update the match so
that when render_qr_code returns Err(TaskError::CoreWalletNotConfigured) you do
not convert it to a MessageBanner but instead propagate or call the multi-wallet
selection flow (the same path used elsewhere for CoreWalletNotConfigured), and
only show MessageBanner::set_global(...) with .with_details(e) for other error
variants; locate this in the same function where render_qr_code(ui) is called
and branch on TaskError::CoreWalletNotConfigured explicitly.
src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs (1)

115-121: ⚠️ Potential issue | 🟠 Major

Still swallowing the multi-wallet recovery error.

Line 115 turns every render_qr_code() failure into a local banner. ensure_address_imported() can surface TaskError::CoreWalletNotConfigured here, and once it is consumed the new wallet-selection flow never gets a chance to run, so this screen still dead-ends when Core has multiple wallets loaded.

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

In `@src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs` around lines
115 - 121, The current code in the top_up_identity_screen handles any error from
render_qr_code(...) by converting it to a local MessageBanner, which swallows
TaskError::CoreWalletNotConfigured and prevents the multi-wallet selection flow
from running; update the error handling around render_qr_code (and any calls to
ensure_address_imported) to match on the error: if it is
TaskError::CoreWalletNotConfigured (or wraps that variant), propagate/return the
error so the caller can trigger the wallet-selection flow instead of consuming
it, and only show the MessageBanner for other error variants; reference
render_qr_code, ensure_address_imported and TaskError::CoreWalletNotConfigured
when making this change.
src/ui/wallets/wallets_screen/mod.rs (2)

2102-2104: ⚠️ Potential issue | 🟠 Major

Move list_core_wallets() off the egui thread.

This is still a synchronous Core RPC in the UI error path. On a slow or unavailable Core node, opening the recovery flow will stall the wallets screen until listwallets returns.

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

In `@src/ui/wallets/wallets_screen/mod.rs` around lines 2102 - 2104, The UI is
calling the synchronous Core RPC via self.app_context.list_core_wallets() on the
egui thread (in wallets_screen mod), which can block rendering; move this call
to a background task/future and return immediately to the UI: create an
async/background worker (or spawn on your existing backend task executor) that
invokes app_context.list_core_wallets() off the UI thread, then send the result
back to the UI (e.g. via a channel, message, or shared state like a
Arc<Mutex>/Atomic/egui::Context.request_repaint) and update the wallets list
when the response arrives; ensure the wallets_screen code no longer calls
list_core_wallets() directly on the egui thread but reads the cached/placeholder
state until the background task populates it.

2085-2100: ⚠️ Potential issue | 🟠 Major

Don't infer the target wallet from live screen selection.

display_task_error() still reconstructs the wallet hash/type from selected_wallet / selected_single_key_wallet when the async error arrives. If the user switches selection or removes a wallet while the task is in flight, the Core wallet choice can be persisted onto the wrong wallet. Carry the originating wallet hash/type through the task error/result instead of reading mutable UI state here.

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

In `@src/ui/wallets/wallets_screen/mod.rs` around lines 2085 - 2100,
display_task_error currently recomputes the target wallet from mutable UI state
(selected_wallet/selected_single_key_wallet), which can be wrong if selection
changes; instead propagate the originating wallet identity through the async
task result/error and use that value in display_task_error. Concretely: modify
the task result/error type (or the closure that sends the error) to include
(Option<WalletHash>, bool is_single_key) captured at task start (use seed_hash /
key_hash to build it), update all places that construct and call
display_task_error to pass that originating pair, and remove the selected_wallet
/ selected_single_key_wallet read logic from display_task_error so it only uses
the supplied wallet identity.
🤖 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 1151-1154: The MustRetry branch currently sets a global banner and
immediately calls self.visible_screen_mut().refresh() without dismissing the
in-progress progress banner; update the
TaskResult::Error(TaskError::MustRetry(msg)) handler to clear the screen's
refresh_banner (call the equivalent of self.refresh_banner.take_and_clear() on
the visible screen or its BannerHandle) before calling
MessageBanner::set_global(...) and before self.visible_screen_mut().refresh() so
screens that rely on display_message() to dismiss progress banners do not leave
stale in-progress banners.

In `@src/backend_task/core/mod.rs`:
- Around line 250-260: CoreTask::RefreshSingleKeyWalletInfo and the other
wallet-scoped branches call with_wallet_recovery(...) which persists an
auto-detected wallet and returns TaskError::MustRetry, but these branches
currently propagate that error immediately; change them to mirror
RefreshWalletInfo: after calling with_wallet_recovery(...) detect if it returned
Err(TaskError::MustRetry) and convert that into the same success/retry behavior
used by RefreshWalletInfo (i.e. treat MustRetry as a handled case so the task
gets retried rather than failing). Update the handling in the CoreTask variants
that call with_wallet_recovery (e.g., CoreTask::RefreshSingleKeyWalletInfo and
the other wallet-scoped branches that call refresh_single_key_wallet_info /
recover / asset-lock / payment) to catch TaskError::MustRetry and return the
equivalent success/trigger-for-retry result instead of propagating the error.

In `@src/context/mod.rs`:
- Around line 574-590: core_client_for_wallet() is passing self.devnet_name
(which is initialized to None) into create_core_rpc_client, causing
wallet-scoped clients to use the wrong devnet name; change the call in
core_client_for_wallet to pass the configured devnet name from cfg (e.g.,
cfg.devnet_name or cfg.devnet_name.as_ref()) instead of self.devnet_name so
create_core_rpc_client(&url, self.network, &cfg.devnet_name, &cfg) receives the
actual configured value used at startup.

In `@src/ui/components/selection_dialog.rs`:
- Around line 223-241: The UI currently returns SelectionStatus::Selected(0)
even when self.options is empty because selected_index defaults to 0; update all
places that produce a selection on confirm/Enter (the handlers around where
selected_index, options, combo_popup_id are used — including the blocks at
~223-241, ~249-274, and ~318-322) to validate self.options.is_empty() first and
return a non-selection (e.g., SelectionStatus::None or an explicit error/state)
instead of Selected(self.selected_index); ensure any code paths that reference
self.options[self.selected_index] are guarded by the same check so no
out-of-bounds access can occur.
- Around line 91-97: current_value() currently ignores the stored dialog result
and only uses is_open, causing callers to not see the emitted SelectionStatus;
update the function to return None while is_open is true and return
Some(self.status.clone() or copy) when closed so the recorded status (e.g.,
SelectionStatus::Confirmed or ::Canceled) is observable. Apply the same fix to
the other current_value implementation referenced in the file (the second impl
at the later block) so both use the component's status field instead of
hardcoding Canceled.

In `@src/ui/wallets/add_new_wallet_screen.rs`:
- Around line 132-135: The constructor new() currently calls the blocking Core
method list_core_wallets() which can freeze screen construction; change new()
(or the AddNewWalletScreen init) to set core_wallets to None or an empty list
and selected_core_wallet_index to 0, immediately return Self with a degraded
state, then spawn an asynchronous background task (or schedule via the existing
UI task runner) that calls app_context.list_core_wallets(), updates the screen
state (core_wallets and selected_core_wallet_index) when the result arrives, and
if the call fails push a MessageBanner error into the screen state; keep
create_screen() unchanged and ensure all error handling is internal to the
screen so callers always get a Self quickly.

In `@src/ui/wallets/wallets_screen/mod.rs`:
- Around line 2014-2031: The selection handler currently drops the outstanding
backend task context after assigning a Core wallet (SelectionStatus::Selected
branch using pending_core_wallet_seed_hash, pending_core_wallet_options,
apply_core_wallet_selection and self.refresh), so failed operations (e.g.,
refresh/asset-lock/top-up that errored with -19) never resume; modify this
branch (and the similar branch at the 2105–2115 range) to capture and persist
the original task context before calling apply_core_wallet_selection, and after
a successful apply/core refresh re-dispatch the original backend task (or
enqueue it) so the refresh/asset-lock/top-up operation is retried automatically;
keep references to the task identifier/context in the struct (e.g., a
pending_backend_task field) or re-create the task from existing fields and
ensure re-dispatch happens immediately after self.refresh() succeeds.

---

Duplicate comments:
In `@src/backend_task/error.rs`:
- Around line 65-72: The conversion impl From<String> for TaskError currently
only looks for the literal RPC text and can’t restore
TaskError::CoreWalletNotConfigured if that variant was previously stringified
into a contextual message; update the flow so the variant is preserved by (A)
adding a unique marker/token when stringifying
TaskError::CoreWalletNotConfigured (e.g. include "CORE_WALLET_NOT_CONFIGURED" or
the variant name in the stringification site), and (B) changing impl
From<String> for TaskError to detect that marker (or an exact match of the
variant name) and return TaskError::CoreWalletNotConfigured, otherwise fall back
to TaskError::Generic(s); refer to the impl From<String> for TaskError and the
TaskError::CoreWalletNotConfigured / TaskError::Generic variants to locate where
to adjust stringification and parsing.

In `@src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs`:
- Around line 115-121: The current code in the top_up_identity_screen handles
any error from render_qr_code(...) by converting it to a local MessageBanner,
which swallows TaskError::CoreWalletNotConfigured and prevents the multi-wallet
selection flow from running; update the error handling around render_qr_code
(and any calls to ensure_address_imported) to match on the error: if it is
TaskError::CoreWalletNotConfigured (or wraps that variant), propagate/return the
error so the caller can trigger the wallet-selection flow instead of consuming
it, and only show the MessageBanner for other error variants; reference
render_qr_code, ensure_address_imported and TaskError::CoreWalletNotConfigured
when making this change.

In `@src/ui/wallets/create_asset_lock_screen.rs`:
- Around line 570-576: The current error handling around render_qr_code(ui)
reduces every TaskError to a generic banner and hides
TaskError::CoreWalletNotConfigured; update the match so that when render_qr_code
returns Err(TaskError::CoreWalletNotConfigured) you do not convert it to a
MessageBanner but instead propagate or call the multi-wallet selection flow (the
same path used elsewhere for CoreWalletNotConfigured), and only show
MessageBanner::set_global(...) with .with_details(e) for other error variants;
locate this in the same function where render_qr_code(ui) is called and branch
on TaskError::CoreWalletNotConfigured explicitly.

In `@src/ui/wallets/wallets_screen/mod.rs`:
- Around line 2102-2104: The UI is calling the synchronous Core RPC via
self.app_context.list_core_wallets() on the egui thread (in wallets_screen mod),
which can block rendering; move this call to a background task/future and return
immediately to the UI: create an async/background worker (or spawn on your
existing backend task executor) that invokes app_context.list_core_wallets() off
the UI thread, then send the result back to the UI (e.g. via a channel, message,
or shared state like a Arc<Mutex>/Atomic/egui::Context.request_repaint) and
update the wallets list when the response arrives; ensure the wallets_screen
code no longer calls list_core_wallets() directly on the egui thread but reads
the cached/placeholder state until the background task populates it.
- Around line 2085-2100: display_task_error currently recomputes the target
wallet from mutable UI state (selected_wallet/selected_single_key_wallet), which
can be wrong if selection changes; instead propagate the originating wallet
identity through the async task result/error and use that value in
display_task_error. Concretely: modify the task result/error type (or the
closure that sends the error) to include (Option<WalletHash>, bool
is_single_key) captured at task start (use seed_hash / key_hash to build it),
update all places that construct and call display_task_error to pass that
originating pair, and remove the selected_wallet / selected_single_key_wallet
read logic from display_task_error so it only uses the supplied wallet identity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d51f437b-d6c2-4eb3-9454-7f5304ee4f4a

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5b25e and f6ab7e1.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • Cargo.toml
  • src/app.rs
  • src/backend_task/core/mod.rs
  • src/backend_task/core/recover_asset_locks.rs
  • src/backend_task/core/refresh_single_key_wallet_info.rs
  • src/backend_task/core/refresh_wallet_info.rs
  • src/backend_task/error.rs
  • src/context/mod.rs
  • src/database/initialization.rs
  • src/database/single_key_wallet.rs
  • src/database/wallet.rs
  • src/model/wallet/utxos.rs
  • src/ui/components/message_banner.rs
  • src/ui/components/selection_dialog.rs
  • src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs
  • src/ui/wallets/add_new_wallet_screen.rs
  • src/ui/wallets/create_asset_lock_screen.rs
  • src/ui/wallets/import_mnemonic_screen.rs
  • src/ui/wallets/wallets_screen/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/components/message_banner.rs

Comment thread src/app.rs
Comment thread src/backend_task/core/mod.rs Outdated
Comment thread src/context/mod.rs Outdated
Comment thread src/ui/components/selection_dialog.rs
Comment thread src/ui/components/selection_dialog.rs
Comment thread src/ui/wallets/add_new_wallet_screen.rs
Comment thread src/ui/wallets/wallets_screen/mod.rs
@lklimek lklimek added the human review This PR is ready for human review. label Mar 6, 2026
- CMT-001: Remove unused devnet_name field from AppContext, use cfg.devnet_name
- CMT-002: Add clone-and-retry for RefreshSingleKeyWalletInfo on MustRetry
- CMT-003: Show actionable warning after Core wallet selection/auto-detection
- CMT-004: Call display_message() in MustRetry handler for banner cleanup
- CMT-005: Fix SelectionDialog::current_value() to return stored status
- CMT-006: Guard confirm/Enter against empty options in SelectionDialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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: 1

♻️ Duplicate comments (3)
src/backend_task/core/mod.rs (1)

278-285: ⚠️ Potential issue | 🟠 Major

Retry these wallet-scoped tasks after MustRetry, too.

Unlike the refresh branches above, these arms return with_wallet_recovery(...) directly. When auto-detection succeeds, TaskError::MustRetry bubbles to src/app.rs, Lines 1151-1156, which only shows a banner and refreshes the screen, so the original asset-lock/payment/recovery action is still dropped on the first attempt.

Mirror the RefreshWalletInfo / RefreshSingleKeyWalletInfo pattern here: catch Err(TaskError::MustRetry(_)) and rerun the original operation once with the now-persisted Core wallet selection.

Also applies to: 286-293, 294-301, 302-311, 313-318

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

In `@src/backend_task/core/mod.rs` around lines 278 - 285, The
CreateRegistrationAssetLock arm (and the other similar arms listed) returns
with_wallet_recovery(...) directly so TaskError::MustRetry bubbles out and the
original operation is dropped; update each of these CoreTask arms (e.g.,
CoreTask::CreateRegistrationAssetLock) to follow the
RefreshWalletInfo/RefreshSingleKeyWalletInfo pattern: call
create_registration_asset_lock(...) and if it returns
Err(TaskError::MustRetry(_)) then perform the wallet-recovery/refresh logic and
retry the original create_* / pay_* / recover_* operation once using the
persisted Core wallet selection (i.e., call with_wallet_recovery to persist
selection, then re-invoke the original method like
create_registration_asset_lock with the same args), otherwise propagate other
errors unchanged.
src/ui/wallets/wallets_screen/mod.rs (2)

2088-2103: ⚠️ Potential issue | 🟠 Major

Don't recover against the live wallet selection.

This handler derives wallet_hash and is_single_key from whichever wallet is selected when the error arrives. If the user switches or removes wallets while the backend task is in flight, the Core wallet choice can be persisted onto the wrong wallet—or the recovery UI can become a no-op when the originating wallet is no longer selected.

Carry the originating wallet id/type with the error/result instead of reconstructing it from mutable screen state.

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

In `@src/ui/wallets/wallets_screen/mod.rs` around lines 2088 - 2103, The handler
currently reconstructs wallet_hash/is_single_key from mutable UI state (using
selected_wallet and selected_single_key_wallet) which can be wrong if selection
changes; instead modify the async task/result/error type to carry the
originating wallet identifier and a flag for single-key (e.g., include
seed_hash/key_hash and is_single_key in the result or error), populate those
fields at task creation, and then use that carried identifier when handling the
completion (replace code that computes wallet_hash/is_single_key from
selected_wallet/selected_single_key_wallet with the values from the task
result/error); update the places that construct the error/result and the handler
that reads wallet_hash/is_single_key so they use the new fields.

2105-2107: ⚠️ Potential issue | 🟠 Major

Move list_core_wallets() off the egui thread.

This is still a synchronous RPC round-trip inside display_task_error(). If Core is slow or unreachable, the UI stalls right before the recovery dialog should appear.

Fetch the wallet list via a backend task or spawned worker and update the dialog when the result arrives.

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

In `@src/ui/wallets/wallets_screen/mod.rs` around lines 2105 - 2107,
display_task_error currently calls self.app_context.list_core_wallets()
synchronously on the egui thread causing UI stalls; remove that direct call and
instead kick off an asynchronous/background fetch (e.g., spawn_blocking,
tokio::spawn or a backend task) to call list_core_wallets(), store the
pending/loading state on the WalletsScreen struct (or a new CoreWalletsState
field), and update the dialog when the async result arrives (invoke
display_task_error to render the recovered data or error once the background
task completes). Ensure you reference and update the same state the UI reads so
the dialog shows immediately with a loading indicator and is replaced with the
wallet list or error when the background task resolves.
🤖 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 1162-1173: Replace the direct use of err.to_string() with a
generic user-facing error message when creating the global banner and when
calling display_message to avoid leaking internals; keep attaching the technical
details only via MessageBanner::set_global(...)/BannerHandle::with_details(&err)
inside the developer-mode branch (current_app_context().is_developer_mode()),
and ensure self.visible_screen_mut().display_message(...) is passed the same
generic MessageType::Error text instead of the raw err string.

---

Duplicate comments:
In `@src/backend_task/core/mod.rs`:
- Around line 278-285: The CreateRegistrationAssetLock arm (and the other
similar arms listed) returns with_wallet_recovery(...) directly so
TaskError::MustRetry bubbles out and the original operation is dropped; update
each of these CoreTask arms (e.g., CoreTask::CreateRegistrationAssetLock) to
follow the RefreshWalletInfo/RefreshSingleKeyWalletInfo pattern: call
create_registration_asset_lock(...) and if it returns
Err(TaskError::MustRetry(_)) then perform the wallet-recovery/refresh logic and
retry the original create_* / pay_* / recover_* operation once using the
persisted Core wallet selection (i.e., call with_wallet_recovery to persist
selection, then re-invoke the original method like
create_registration_asset_lock with the same args), otherwise propagate other
errors unchanged.

In `@src/ui/wallets/wallets_screen/mod.rs`:
- Around line 2088-2103: The handler currently reconstructs
wallet_hash/is_single_key from mutable UI state (using selected_wallet and
selected_single_key_wallet) which can be wrong if selection changes; instead
modify the async task/result/error type to carry the originating wallet
identifier and a flag for single-key (e.g., include seed_hash/key_hash and
is_single_key in the result or error), populate those fields at task creation,
and then use that carried identifier when handling the completion (replace code
that computes wallet_hash/is_single_key from
selected_wallet/selected_single_key_wallet with the values from the task
result/error); update the places that construct the error/result and the handler
that reads wallet_hash/is_single_key so they use the new fields.
- Around line 2105-2107: display_task_error currently calls
self.app_context.list_core_wallets() synchronously on the egui thread causing UI
stalls; remove that direct call and instead kick off an asynchronous/background
fetch (e.g., spawn_blocking, tokio::spawn or a backend task) to call
list_core_wallets(), store the pending/loading state on the WalletsScreen struct
(or a new CoreWalletsState field), and update the dialog when the async result
arrives (invoke display_task_error to render the recovered data or error once
the background task completes). Ensure you reference and update the same state
the UI reads so the dialog shows immediately with a loading indicator and is
replaced with the wallet list or error when the background task resolves.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 595a8580-5f97-4851-91fe-0cd5d7b38288

📥 Commits

Reviewing files that changed from the base of the PR and between f6ab7e1 and 76f45af.

📒 Files selected for processing (5)
  • src/app.rs
  • src/backend_task/core/mod.rs
  • src/context/mod.rs
  • src/ui/components/selection_dialog.rs
  • src/ui/wallets/wallets_screen/mod.rs

Comment thread src/app.rs
Comment on lines +1162 to +1173
if !handled {
let msg = err.to_string();
let handle = MessageBanner::set_global(ctx, &msg, MessageType::Error);
if self.current_app_context().is_developer_mode() {
// INTENTIONAL(SEC-003): TaskError Debug output is shown to users
// in developer mode. This is a local UI app — no third parties
// see this output. Ensure inner error types don't expose secrets
// (see #667).
handle.with_details(&err);
}
self.visible_screen_mut()
.display_message(&msg, MessageType::Error);
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.

⚠️ Potential issue | 🟠 Major

Use a generic fallback message here instead of err.to_string().

Unrecognized TaskError variants now include DB/runtime failures too, so this fallback can expose raw internal details in both the global banner and display_message(). Keep this branch generic and reserve verbatim error text for developer-mode details/logging.

Small fix
-                        let msg = err.to_string();
-                        let handle = MessageBanner::set_global(ctx, &msg, MessageType::Error);
+                        let msg = "Operation failed. Please try again.";
+                        let handle = MessageBanner::set_global(ctx, msg, MessageType::Error);
                         if self.current_app_context().is_developer_mode() {
                             // INTENTIONAL(SEC-003): TaskError Debug output is shown to users
                             // in developer mode. This is a local UI app — no third parties
                             // see this output. Ensure inner error types don't expose secrets
                             // (see `#667`).
                             handle.with_details(&err);
                         }
                         self.visible_screen_mut()
-                            .display_message(&msg, MessageType::Error);
+                            .display_message(msg, MessageType::Error);

As per coding guidelines, "Never expose raw backend/database errors to users. Use a generic user-friendly message in the banner and attach technical details via BannerHandle::with_details()."

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

In `@src/app.rs` around lines 1162 - 1173, Replace the direct use of
err.to_string() with a generic user-facing error message when creating the
global banner and when calling display_message to avoid leaking internals; keep
attaching the technical details only via
MessageBanner::set_global(...)/BannerHandle::with_details(&err) inside the
developer-mode branch (current_app_context().is_developer_mode()), and ensure
self.visible_screen_mut().display_message(...) is passed the same generic
MessageType::Error text instead of the raw err string.

@PastaPastaPasta
Copy link
Copy Markdown
Member

Concept ACK

@lklimek lklimek merged commit d3e364e into v1.0-dev Mar 7, 2026
10 checks passed
@lklimek lklimek deleted the fix/multi-wallet branch March 7, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human review This PR is ready for human review.

Projects

None yet

3 participants