Skip to content

fix: replace expect()/unwrap() panics with error propagation#562

Merged
PastaPastaPasta merged 28 commits intov1.0-devfrom
fix/replace-panics-with-error-propagation
Feb 19, 2026
Merged

fix: replace expect()/unwrap() panics with error propagation#562
PastaPastaPasta merged 28 commits intov1.0-devfrom
fix/replace-panics-with-error-propagation

Conversation

@PastaPastaPasta
Copy link
Member

@PastaPastaPasta PastaPastaPasta commented Feb 12, 2026

Summary

  • Replace ~40 .expect() / .unwrap() / panic!() / unreachable!() / unimplemented!() calls across backend, database, context, and UI layers with proper error propagation or safe fallbacks
  • Backend tasks: contested names, identity registration/top-up, contract loading, platform info, contacts — all now return Result instead of panicking
  • Database layer: wallet loading, contested names, tokens, scheduled votes, contacts, asset lock DB, initialization/migration — errors propagate instead of crashing
  • AppContext::new(): 13 .expect() calls replaced with match blocks that log errors and return None, so a misconfigured network context fails gracefully instead of taking down the whole app
  • Config/SDK: dapi_address_list(), insight_api_uri(), and initialize_sdk() now return Result instead of panicking on bad input
  • UI: SystemTime::now().duration_since(UNIX_EPOCH).expect() replaced with .unwrap_or_default() across 26 files; unimplemented!() in marketplace settings replaced with informational label

Motivation

The application had numerous panic-prone code paths that could crash the entire GUI if any upstream data was malformed, a network was misconfigured, or a database query returned unexpected results. This PR systematically eliminates these crash vectors by:

  1. Returning Result/Option from fallible functions
  2. Using match blocks with tracing::error! logging before returning None
  3. Using .unwrap_or_default() for non-critical timestamp operations
  4. Replacing unreachable!() with safe default branches

Test plan

  • cargo build passes
  • cargo clippy --all-features --all-targets -- -D warnings passes clean
  • cargo +nightly fmt --all applied
  • Manual testing: verify app starts and functions normally on mainnet/testnet
  • Verify graceful failure when a network config is intentionally broken (should log error, not crash)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Converted many crash paths into graceful errors, warnings, and safe fallbacks (app initialization, network/context selection, ZMQ listeners, background tasks, DB, wallets, identity flows).
    • Improved validation and automatic fallbacks for unavailable network contexts.
  • UI

    • Made time/elapsed displays resilient to clock anomalies.
    • Replaced an unimplemented crash with a "marketplace not supported" label.
    • Expanded public profile text and lowered avatar-fetch log severity.
  • Tests

    • Updated tests to handle fallible app-state initialization.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Widespread removal of panic-prone unwrap/expect/unimplemented! in favor of Result/Option returns, mapped errors, guarded fallbacks, and warnings across app initialization, SDK/context creation, backend tasks, DB layers, and many UI time computations; AppState and SDK constructors became fallible.

Changes

Cohort / File(s) Summary
App init & main
src/app.rs, src/main.rs
AppState::new now returns Result; initialization propagates errors; network context selection, ZMQ/listener creation and fallbacks use guarded logic and warnings instead of panics.
App context & dir
src/context/mod.rs, src/app_dir.rs
AppContext initialization made fallible (returns None on failures); cookie path resolution returns IO errors for unsupported networks; core client auth uses guarded cookie→password fallback.
SDK & config
src/sdk_wrapper.rs, src/config.rs
SDK initialization returns Result; NetworkConfig::dapi_address_list now returns Result with descriptive errors; address list propagation uses ?.
Core networking
src/components/core_p2p_handler.rs
Unsupported network case returns Err instead of panicking; constructor error-path hardened.
Backend — contested names / DPNS
src/backend_task/contested_names/*
Replaced expects/unwraps with map_err/Results; non-string contested entries filtered; semaphore permits and channel sends are guarded; tasks log and return errors instead of panicking.
Backend — contracts & identity
src/backend_task/contract.rs, src/backend_task/identity/*
Token config and identity transition creation now handle and log errors (skip or propagate) instead of unwrap/panic; richer error messages added.
Backend — platform_info & dashpay
src/backend_task/platform_info.rs, src/backend_task/dashpay/*
Formatting helpers now return Result; timestamp/status/address parsing propagate errors; dashpay contact parsing/decryption logs and skips invalid entries.
Database layer
src/database/* (wallet.rs, contested_names.rs, contacts.rs, scheduled_votes.rs, initialization.rs)
NULL-safe row extraction, mapped parsing/ID conversion errors to rusqlite::Error with context, safer UNIX_EPOCH handling, added WalletError and conversion to rusqlite::Error, structured migration error reporting.
Transaction processing
src/context/transaction_processing.rs
Asset-lock flows now return structured DB errors for missing credit outputs/address parsing; added early break after updating the matching wallet.
UI — time/tolerance hardening
src/ui/... (many files under src/ui/contracts_documents/*, src/ui/identities/*, src/ui/tokens/*, src/ui/tools/*, src/ui/wallets/*, src/ui/network_chooser_screen.rs)
Replaced SystemTime::duration_since(...).expect(...) with .unwrap_or_default() widely to avoid panics on clock regressions; elapsed-time values default to zero on failure.
UI — small behavior & messaging
src/ui/tokens/update_token_config.rs, src/ui/dashpay/contact_profile_viewer.rs
Replaced unimplemented!() with a user-facing label for marketplace settings; avatar fetch failures downgraded to warnings and public profile text expanded.
Tests & harnesses
tests/e2e/*, tests/kittest/*
Test code updated to call AppState::new(...).expect("Failed to create AppState") before chaining builder methods to accommodate fallible AppState construction.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hopped through unwraps, quiet and sly,
Replaced loud panics with a patient why.
Warnings whisper, fallbacks softly tread,
No crashing thumps where anxious code once led.
A calmer burrow — safe beds for every thread.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the changeset: replacing expect()/unwrap() panics with error propagation across the codebase.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/replace-panics-with-error-propagation

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

❤️ Share

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

Copy link
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

This PR hardens the application by removing panic-based error handling (expect/unwrap/panic!/unreachable!/unimplemented!) across the app, database, context initialization, SDK/config parsing, and UI, replacing it with Result/Option propagation, logging, and safe fallbacks to avoid crashing the GUI on malformed data or misconfiguration.

Changes:

  • Convert several constructors/helpers (notably AppState::new() and initialize_sdk()) to return Result and update call sites accordingly.
  • Replace many SystemTime “time went backwards” panics with .unwrap_or_default() in UI screens and background tasks.
  • Improve DB/data parsing robustness by propagating errors instead of panicking; add warnings + safe defaults for unexpected stored values.

Reviewed changes

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

Show a summary per file
File Description
tests/kittest/wallets_screen.rs Updates harness construction to handle AppState::new() returning Result.
tests/kittest/startup.rs Same: uses expect(...) on AppState::new() result in tests.
tests/kittest/network_chooser.rs Same: updates test harness app initialization for new Result API.
tests/kittest/identities_screen.rs Same: updates kittest harness initialization.
tests/kittest/create_asset_lock_screen.rs Same: updates kittest harness initialization.
tests/e2e/wallet_flows.rs Same: updates e2e harness initialization.
tests/e2e/navigation.rs Same: updates e2e harness initialization.
src/ui/wallets/send_screen.rs Replaces SystemTime panic with .unwrap_or_default().
src/ui/tools/transition_visualizer_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/update_token_config.rs Replaces unimplemented!() with a non-crashing UI label.
src/ui/tokens/unfreeze_tokens_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/transfer_tokens_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/set_token_price_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/resume_tokens_screen.rs Replaces unwrap()/panic-prone SystemTime usage with .unwrap_or_default().
src/ui/tokens/pause_tokens_screen.rs Replaces unwrap()/panic-prone SystemTime usage with .unwrap_or_default().
src/ui/tokens/mint_tokens_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/freeze_tokens_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/direct_token_purchase_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/destroy_frozen_funds_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/tokens/claim_tokens_screen.rs Replaces unwrap()/panic-prone SystemTime usage with .unwrap_or_default().
src/ui/tokens/burn_tokens_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/network_chooser_screen.rs Replaces SystemTime panic with .unwrap_or_default() for periodic checks.
src/ui/identities/withdraw_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/identities/transfer_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/identities/register_dpns_name_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/identities/keys/add_key_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/identities/add_existing_identity_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/contracts_documents/update_contract_screen.rs Replaces unwrap() on SystemTime with .unwrap_or_default() for elapsed display.
src/ui/contracts_documents/register_contract_screen.rs Same: safer time handling for elapsed display.
src/ui/contracts_documents/group_actions_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/ui/contracts_documents/document_action_screen.rs Replaces unwrap() on SystemTime with .unwrap_or_default() in multiple status paths.
src/ui/contracts_documents/contracts_documents_screen.rs Replaces SystemTime panic with .unwrap_or_default() in query timing.
src/ui/contracts_documents/add_contracts_screen.rs Replaces SystemTime panic with .unwrap_or_default() in status timing.
src/sdk_wrapper.rs Changes initialize_sdk() to return Result and propagates build/config errors.
src/main.rs Updates eframe app creation to propagate AppState::new() errors via ?.
src/database/wallet.rs Replaces multiple panics with error propagation while decoding/parsing DB wallet data.
src/database/scheduled_votes.rs Handles unexpected DB values safely (warn + default) instead of unreachable!().
src/database/initialization.rs Converts migration failure panic into a returned rusqlite error.
src/database/contested_names.rs Propagates identifier parse errors; replaces time panics with .unwrap_or_default().
src/database/contacts.rs Avoids unwrap_or_default() on row.get(...) errors; propagates DB read errors instead.
src/context/transaction_processing.rs Propagates errors for malformed asset lock transactions instead of panicking.
src/context/mod.rs Makes AppContext::new() fail gracefully with logging; updates SDK init and network fallback behavior.
src/config.rs Changes parsing helpers to return Result and updates unit tests accordingly (incomplete).
src/components/core_p2p_handler.rs Replaces unsupported-network panic with a returned error.
src/backend_task/platform_info.rs Converts formatting helpers to return Result and propagates errors instead of panicking.
src/backend_task/identity/top_up_identity.rs Avoids panic when rebuilding transitions for debugging; returns richer error info.
src/backend_task/identity/register_identity.rs Propagates identity creation errors; avoids panic in debug transition recreation.
src/backend_task/dashpay/payments.rs Replaces SystemTime unwrap with .unwrap_or_default() for IDs/timestamps.
src/backend_task/dashpay/contacts.rs Avoids panics on invalid identifiers; skips with optional warnings.
src/backend_task/contract.rs Avoids panicking on missing token config; warns and skips invalid entries.
src/backend_task/contested_names/vote_on_dpns_name.rs Propagates missing document type error instead of panicking.
src/backend_task/contested_names/query_dpns_vote_contenders.rs Propagates missing document type error instead of panicking.
src/backend_task/contested_names/query_dpns_contested_resources.rs Replaces multiple unwraps with safe handling + logging; improves async channel/semaphore error handling.
src/app_dir.rs Replaces unimplemented!() for unsupported networks with a returned io::Error.
src/app.rs Makes AppState::new() return Result; replaces hard exits/panics with propagated errors and graceful ZMQ fallback.
Comments suppressed due to low confidence (1)

src/config.rs:487

  • NetworkConfig::insight_api_uri() now returns Result<Uri, String>, so this unit test annotated with #[should_panic] will no longer panic and will fail. Update the test to assert that an empty URL returns Err (and optionally check the error message) instead of expecting a panic.
    #[test]
    #[should_panic(expected = "invalid insight API URL")]
    fn test_insight_api_uri_empty_panics() {
        let config = make_network_config("https://127.0.0.1:443", "", 9998);
        let _uri = config.insight_api_uri();
    }

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

Comment on lines +438 to +443
ExtendedPubKey::decode(&master_ecdsa_bip44_account_0_epk_bytes).map_err(|e| {
rusqlite::Error::InvalidParameterName(format!(
"Failed to decode ExtendedPubKey: {}",
e
))
})?;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

These parse/decode failures are being mapped to rusqlite::Error::InvalidParameterName, which is intended for SQL parameter binding issues and can be misleading for callers/logging. Consider using a more appropriate rusqlite error variant (e.g., FromSqlConversionFailure with the relevant column index/type) to represent corrupted row data.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Audit note (Claude Code): This comment is still unresolved.

Both ExtendedPubKey::decode (line 444) and seed hash conversion (line 451) still use rusqlite::Error::InvalidParameterName. The identical fix (switching to FromSqlConversionFailure) was applied in contested_names.rs but was not carried over to wallet.rs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Tracked in #612

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (6)
src/ui/tokens/update_token_config.rs (1)

774-774: ⚠️ Potential issue | 🟠 Major

Remaining .expect() on line 774 can still panic at runtime.

This PR's objective is to replace panic-prone calls with error propagation or safe fallbacks. self.signing_key.clone().expect("Signing key must be set") will panic if signing_key is None. Consider guarding the button click or showing an error message instead.

Suggested fix
-                    signing_key: self.signing_key.clone().expect("Signing key must be set"),
+                    signing_key: match self.signing_key.clone() {
+                        Some(key) => key,
+                        None => {
+                            self.error_message = Some("No signing key selected".to_string());
+                            return action;
+                        }
+                    },
src/database/contacts.rs (1)

101-113: ⚠️ Potential issue | 🟡 Minor

Inconsistent NULL handling with load_contact_private_info.

load_contact_private_info (lines 76-78) now safely handles NULL nickname/notes columns via Option<String>, but this function still uses direct row.get::<_, String> which will return InvalidColumnType error on NULL values. Since the schema allows NULLs for these columns, apply the same pattern here for consistency.

Proposed fix
             .query_map(params![owner_identity_id.to_buffer().to_vec()], |row| {
                 Ok(ContactPrivateInfo {
                     owner_identity_id: row.get(0)?,
                     contact_identity_id: row.get(1)?,
-                    nickname: row.get(2)?,
-                    notes: row.get(3)?,
+                    nickname: row.get::<_, Option<String>>(2)?.unwrap_or_default(),
+                    notes: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
                     is_hidden: row.get::<_, i32>(4)? != 0,
                 })
             })?
src/backend_task/contested_names/query_dpns_contested_resources.rs (1)

30-31: ⚠️ Potential issue | 🟡 Minor

Pre-existing bug: retries counter resets on every outer-loop iteration.

retries is initialized to 0 at line 46 inside the pagination loop. When a retryable error triggers continue (line 115), control returns to line 33, re-executing line 46 and resetting retries to 0. The retry limit of MAX_RETRIES is therefore never reached.

Not introduced by this PR, but worth fixing separately — move retries before the outer loop or use an inner retry loop.

Proposed fix
         const MAX_RETRIES: usize = 3;
+        let mut retries = 0;
         let mut start_at_value = None;
         let mut names_to_be_updated = Vec::new();
         loop {
             ...
-            // Initialize retry counter
-            let mut retries = 0;
 
             let contested_resources = match ContestedResource::fetch_many(sdk, query.clone()).await

Also applies to: 46-47, 101-116

src/database/contested_names.rs (1)

493-503: ⚠️ Potential issue | 🟡 Minor

Remaining expect()/unwrap() calls in insert_or_update_contenders panic on malformed contender data.

Four panic sites remain that process untrusted network data:

  • Line 495: .expect("expect a contender document deserialization")
  • Line 497: .as_ref().unwrap().clone()
  • Line 501: .expect("expected name")
  • Line 503: .unwrap() on as_str()

These should be converted to proper error propagation with map_err/ok_or and ? for consistency with the rest of the database layer and to avoid panics on malformed DPNS data.

src/config.rs (1)

483-487: ⚠️ Potential issue | 🟠 Major

Update the "empty insight URL" test to expect Err instead of panic

insight_api_uri() returns Result<Uri, String>, so the #[should_panic] test will fail. Update it to assert is_err().

🔧 Suggested fix
-    #[test]
-    #[should_panic(expected = "invalid insight API URL")]
-    fn test_insight_api_uri_empty_panics() {
+    #[test]
+    fn test_insight_api_uri_empty_returns_error() {
         let config = make_network_config("https://127.0.0.1:443", "", 9998);
-        let _uri = config.insight_api_uri();
+        assert!(config.insight_api_uri().is_err());
     }
src/ui/contracts_documents/update_contract_screen.rs (1)

275-285: ⚠️ Potential issue | 🟡 Minor

Use saturating_sub to guard against underflow in elapsed time calculations

The unwrap_or_default() call returns Duration::ZERO if SystemTime::now() fails (e.g., clock issues), resulting in now = 0. When start_time is captured at a real timestamp, now - start_time will underflow—panicking in debug mode and wrapping to a huge value in release mode. Use saturating_sub to safely handle this edge case.

This affects the elapsed time calculations in:

  • Line 281: FetchingNonce match arm
  • Line 293: Broadcasting match arm
  • Line 305: ProofError match arm
🔧 Suggested fix
-                let elapsed = now - start_time;
+                let elapsed = now.saturating_sub(start_time);

Apply the same change to the other two locations.

🤖 Fix all issues with AI agents
In `@src/database/contested_names.rs`:
- Around line 77-86: The code is using rusqlite::Error::InvalidParameterName for
column conversion failures; replace those with
rusqlite::Error::FromSqlConversionFailure carrying the column index, expected
Type::Blob, and Box::new(e) when Identifier::from_bytes fails (e.g., in the
map/transposed blocks that call Identifier::from_bytes), so the error
semantically represents a failed column-to-type conversion; also update all six
occurrences in this file to this pattern (matching other files like wallet.rs),
and in insert_or_update_contenders replace the .expect() calls that parse
documents and names with proper error propagation (return a
rusqlite::Error::FromSqlConversionFailure or convert the parse error into the
function's Result error) instead of panicking so callers receive errors rather
than aborting.

In `@src/ui/identities/withdraw_screen.rs`:
- Around line 596-600: The subtraction computing elapsed_seconds (now -
start_time) can underflow if
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
returns 0; change the subtraction to use saturating_sub (i.e., let
elapsed_seconds = now.saturating_sub(start_time)) to avoid panics/wraps, and
apply the same safeguard where the start time is written/stored earlier (the
block around lines 261–264 that sets start_time) so that any subsequent
elapsed-time calculations also use saturating_sub and/or log a trace when now
defaults to 0 to aid diagnosis.
🟡 Minor comments (21)
src/backend_task/platform_info.rs-270-270 (1)

270-270: ⚠️ Potential issue | 🟡 Minor

saturating_sub(0) is a no-op.

daily_withdrawal_limit.saturating_sub(0) always equals daily_withdrawal_limit. The comment notes the 24-hour amount isn't available, but this line is misleading — "Remaining Today" will always show the full daily limit.

Consider either tracking the actual 24h withdrawn amount or labeling this as the daily limit directly to avoid confusing users.

src/ui/identities/keys/add_key_screen.rs-659-663 (1)

659-663: ⚠️ Potential issue | 🟡 Minor

Unsigned subtraction can still panic if now < start_time.

With unwrap_or_default(), if the second call defaults to 0 while start_time holds a real timestamp, now - start_time will underflow and panic — defeating the purpose of removing expect(). Use saturating_sub for consistency with the defensive intent of this PR.

Proposed fix
-                    let elapsed_seconds = now - start_time;
+                    let elapsed_seconds = now.saturating_sub(*start_time);
src/ui/tokens/destroy_frozen_funds_screen.rs-615-620 (1)

615-620: ⚠️ Potential issue | 🟡 Minor

Potential u64 underflow on now - start_time.

If SystemTime::now().duration_since(UNIX_EPOCH) fails and defaults to 0, then now is 0 while start_time holds a real epoch timestamp. The subtraction now - start_time will panic in debug mode (overflow) or silently wrap in release mode.

Use saturating_sub to guard against this:

Proposed fix
-                        let elapsed = now - start_time;
+                        let elapsed = now.saturating_sub(*start_time);
src/ui/tokens/set_token_price_screen.rs-1143-1149 (1)

1143-1149: ⚠️ Potential issue | 🟡 Minor

Potential u64 underflow on now - start_time.

If SystemTime::now().duration_since(UNIX_EPOCH) returns Err here (defaulting now to 0) but succeeded when start_time was recorded at line 731, the subtraction on line 1148 will panic due to unsigned integer underflow.

Use saturating_sub to be consistent with the defensive intent of the unwrap_or_default change.

Proposed fix
-                        let elapsed = now - start_time;
+                        let elapsed = now.saturating_sub(*start_time);
src/ui/identities/add_existing_identity_screen.rs-454-458 (1)

454-458: ⚠️ Potential issue | 🟡 Minor

unwrap_or_default() is a good replacement — but protect the subtraction at Line 1119.

The four unwrap_or_default() changes are a clear improvement over expect(). However, if now ever falls back to 0 (default Duration) while start_time holds a real timestamp, Line 1119's now - start_time will underflow (u64 subtraction). Use saturating_sub to keep this safe end-to-end.

Proposed fix
-                            let elapsed_seconds = now - start_time;
+                            let elapsed_seconds = now.saturating_sub(*start_time);

Also applies to: 656-660, 813-817, 1115-1119

src/ui/tokens/mint_tokens_screen.rs-700-704 (1)

700-704: ⚠️ Potential issue | 🟡 Minor

u64 subtraction underflow when unwrap_or_default() yields zero.

If SystemTime::now().duration_since(UNIX_EPOCH) fails here (returning Duration::ZEROnow = 0), but start_time was set to a real timestamp on line 302-306, then now - start_time will panic in debug or silently wrap in release. Use saturating_sub to avoid this.

Proposed fix
-                        let elapsed = now - start_time;
+                        let elapsed = now.saturating_sub(*start_time);
src/ui/contracts_documents/register_contract_screen.rs-267-272 (1)

267-272: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow.
If the clock moves backward (or defaults to zero), now - start_time can panic or wrap; use saturating_sub instead.

🔧 Suggested fix
-                let elapsed = now - start_time;
+                let elapsed = now.saturating_sub(start_time);

Also applies to: 278-283

src/ui/tokens/resume_tokens_screen.rs-518-524 (1)

518-524: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow with correct syntax.
If the clock moves backward, now - start_time can underflow and wrap to a large incorrect value. Use saturating subtraction to return 0 instead.

🔧 Suggested fix
-                        let elapsed = now - start_time;
+                        let elapsed = now.saturating_sub(start_time);
src/ui/tokens/direct_token_purchase_screen.rs-640-645 (1)

640-645: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow.
A backward clock can cause now - start_time to underflow and panic. Use saturating_sub() instead.

🔧 Suggested fix
-                        let elapsed = now - start_time;
+                        let elapsed = now.saturating_sub(start_time);
src/ui/tokens/transfer_tokens_screen.rs-544-549 (1)

544-549: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow using saturating arithmetic.
If the system clock is adjusted backward, now can be less than start_time, causing integer underflow and displaying incorrect elapsed time. Use saturating_sub instead.

🔧 Suggested fix
-                        let elapsed_seconds = now - start_time;
+                        let elapsed_seconds = now.saturating_sub(*start_time);
src/app_dir.rs-55-69 (1)

55-69: ⚠️ Potential issue | 🟡 Minor

Require a devnet name when resolving the cookie path for Devnet networks.

When Network::Devnet is used with a None or empty devnet_name, the current code silently uses an empty string, which results in the cookie path pointing to the base .dashcore directory instead of a network-specific subdirectory. This masks misconfiguration errors and should be rejected.

🔧 Suggested fix
-            Network::Devnet => devnet_name.as_deref().unwrap_or(""),
+            Network::Devnet => devnet_name
+                .as_deref()
+                .filter(|s| !s.is_empty())
+                .ok_or_else(|| {
+                    io::Error::new(io::ErrorKind::InvalidInput, "Devnet name is required")
+                })?,
src/ui/contracts_documents/add_contracts_screen.rs-369-374 (1)

369-374: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow with correct syntax.
If the system clock goes backward, now - start_time can panic; use now.saturating_sub(start_time).

🔧 Suggested fix
-                    let elapsed_seconds = now - start_time;
+                    let elapsed_seconds = now.saturating_sub(start_time);
src/ui/tools/transition_visualizer_screen.rs-265-270 (1)

265-270: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow.
A backward clock (or zeroed duration) can make now - start_time wrap; use saturating_sub to prevent this.

🔧 Suggested fix
-                let elapsed_seconds = now - start_time;
+                let elapsed_seconds = now.saturating_sub(start_time);
src/ui/tokens/freeze_tokens_screen.rs-268-271 (1)

268-271: ⚠️ Potential issue | 🟡 Minor

Harden elapsed calculation against system time failures

At line 618, now - start_time can underflow if the second SystemTime::now() call fails and returns 0 via unwrap_or_default(). Use saturating_sub to safely handle this case.

Suggested fix
- let elapsed = now - start_time;
+ let elapsed = now.saturating_sub(start_time);
src/ui/tokens/burn_tokens_screen.rs-265-268 (1)

265-268: ⚠️ Potential issue | 🟡 Minor

Guard elapsed time against underflow when now defaults to 0

If the clock jumps before UNIX_EPOCH after start_time is set, now becomes 0 and now - start_time can underflow in debug mode (panic) or wrap in release mode. Prefer saturating_sub for elapsed calculations.

✅ Suggested fix
- let elapsed = now - start_time;
+ let elapsed = now.saturating_sub(start_time);

Also applies to: 656-659

src/ui/tokens/claim_tokens_screen.rs-206-209 (1)

206-209: ⚠️ Potential issue | 🟡 Minor

Use saturating_sub to prevent underflow in elapsed calculation

When SystemTime::now() fails, unwrap_or_default() yields 0. The subsequent subtraction now - start_time underflows if start_time was captured successfully, resulting in incorrect elapsed values.

✅ Suggested fix
- let elapsed = now - start_time;
+ let elapsed = now.saturating_sub(start_time);

Also applies to: 567-570

src/ui/tokens/pause_tokens_screen.rs-210-213 (1)

210-213: ⚠️ Potential issue | 🟡 Minor

Prevent elapsed-time underflow from clock rollback

When SystemTime::now().duration_since(UNIX_EPOCH) fails due to a clock rollback, unwrap_or_default() returns 0, making elapsed = now - start_time underflow if start_time was set to a positive value earlier. Use saturating_sub to handle this safely:

Suggested fix
- let elapsed = now - start_time;
+ let elapsed = now.saturating_sub(start_time);

Also applies to: 519-523

src/ui/contracts_documents/group_actions_screen.rs-563-567 (1)

563-567: ⚠️ Potential issue | 🟡 Minor

Use saturating subtraction for elapsed time to prevent underflow

When duration_since(UNIX_EPOCH) fails (e.g., system clock issues), unwrap_or_default() returns Duration::ZERO, resulting in now = 0. Subtracting a non-zero start_time from this causes an underflow. Use saturating_sub to safely handle this edge case, consistent with the pattern used in other UI screens like send_screen.rs.

Suggested fix
- let elapsed = now - start_time;
+ let elapsed = now.saturating_sub(*start_time);

Also applies to: 598–601

src/ui/contracts_documents/document_action_screen.rs-383-387 (1)

383-387: ⚠️ Potential issue | 🟡 Minor

Use saturating_sub for elapsed time calculations to prevent underflow

When SystemTime::now() defaults to a duration of 0 (via unwrap_or_default() on error), the elapsed calculation can underflow. Apply saturating_sub to each elapsed computation.

✅ Suggested fix (apply to each elapsed calculation)
let elapsed = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap_or_default()
    .as_secs()
-   - start;
+   .saturating_sub(start);

Applies to: 465–469, 523–528, 589–594, 939–943, 948–952

src/ui/contracts_documents/contracts_documents_screen.rs-212-215 (1)

212-215: ⚠️ Potential issue | 🟡 Minor

Use saturating_sub for elapsed time calculation to prevent potential underflow

If the first SystemTime::now() call succeeds but a later call fails and returns unwrap_or_default() (Duration of 0 → 0 seconds), the subtraction 0 - start_time will underflow. Use saturating_sub to handle this edge case defensively.

🔧 Suggested fix
                        let time_elapsed = SystemTime::now()
                            .duration_since(UNIX_EPOCH)
                            .unwrap_or_default()
                            .as_secs()
-                           - start_time;
+                           .saturating_sub(start_time);

Also applies to: lines 391-408, 424-428

src/ui/tokens/unfreeze_tokens_screen.rs-270-274 (1)

270-274: ⚠️ Potential issue | 🟡 Minor

Use saturating_sub to prevent elapsed time underflow

When unwrap_or_default() returns 0 due to SystemTime::duration_since() failure, subtracting start_time can cause integer underflow. This results in a panic in debug mode or wrapped value in release mode. Use saturating_sub to clamp at zero instead.

🔧 Suggested fix
-                        let elapsed = now - start_time;
+                        let elapsed = now.saturating_sub(*start_time);

Also applies to: 603-609

🧹 Nitpick comments (12)
src/ui/identities/register_dpns_name_screen.rs (1)

561-566: Use saturating_sub to guard against underflow.

now - start_time on line 566 is unsigned arithmetic. If now ever resolves to 0 (via the new default) while start_time holds a real timestamp, this will panic in debug or silently wrap in release. Using saturating_sub is a trivial hardening that keeps the no-panic guarantee consistent.

Proposed fix
-                    let elapsed_seconds = now - start_time;
+                    let elapsed_seconds = now.saturating_sub(*start_time);
src/backend_task/dashpay/contacts.rs (1)

309-315: Nit: log message includes length that is always 32.

decrypt_to_user_id returns [u8; 32], so decrypted_id.len() is invariantly 32. Consider logging a hex snippet of the bytes instead for more useful diagnostics:

Suggested improvement
                     let Ok(contact_id) = Identifier::from_bytes(&decrypted_id) else {
                         tracing::warn!(
-                            "Failed to parse decrypted contact ID (length {}), skipping contact info entry",
-                            decrypted_id.len()
+                            "Failed to parse decrypted contact ID (bytes: {}), skipping contact info entry",
+                            hex::encode(&decrypted_id)
                         );
                         continue;
                     };
src/ui/network_chooser_screen.rs (1)

840-840: Remaining .expect() panics in UI code contradict PR objectives.

Lines 840 and 860 still use .expect("Expected to save db settings"), which can panic the UI thread if the DB write fails. Given this PR's goal of eliminating panic-prone calls, consider propagating or logging these errors instead.

Suggested fix
-                                self.save().expect("Expected to save db settings");
+                                if let Err(e) = self.save() {
+                                    tracing::error!("Failed to save db settings: {e}");
+                                }

Apply the same pattern at both lines 840 and 860.

Also applies to: 860-860

src/backend_task/platform_info.rs (1)

244-246: Inconsistent Address::from_script error handling between the two format functions.

In format_withdrawal_documents_with_daily_limit (Line 244), a failed address parse gracefully degrades to "Invalid Address: ...", while in format_withdrawal_documents_to_bare_info (Line 318) and in the RecentlyCompletedWithdrawals branch (Line 618), the same failure propagates as an error and aborts the entire listing.

Consider aligning these: the graceful-degradation approach is arguably better UX for display-only formatting, since one bad address shouldn't prevent showing the rest of the withdrawals.

Also applies to: 318-319

src/ui/tokens/destroy_frozen_funds_screen.rs (2)

197-199: Remaining .expect() is inconsistent with the PR's goal.

load_local_qualified_identities().expect("Identities not loaded") can still panic. Consider propagating the error or falling back to an empty Vec with an error message, consistent with the approach taken elsewhere in this PR.


306-306: Remaining .expect("No key selected") can panic on a user action.

This is reached from confirmation_ok() which is triggered by user interaction. If selected_key is None, this panics. Consider returning an error to the UI instead (e.g., set self.error_message and return AppAction::None), consistent with the PR's intent.

src/ui/identities/add_existing_identity_screen.rs (1)

59-59: Pre-existing expect() in load_testnet_nodes_from_yml is inconsistent with this PR's goal.

This function already uses .ok()? for file I/O but then panics on malformed YAML via .expect("expected proper yaml"). Since this PR is specifically about removing panic-prone calls, consider returning None here too (e.g., .ok()?).

Proposed fix
 fn load_testnet_nodes_from_yml(file_path: &str) -> Option<TestnetNodes> {
     let file_content = fs::read_to_string(file_path).ok()?;
-    serde_yaml_ng::from_str(&file_content).expect("expected proper yaml")
+    serde_yaml_ng::from_str(&file_content).ok()
 }
src/backend_task/identity/register_identity.rs (1)

295-297: Remaining .expect() on create_identifier() could be converted to ? for consistency with this PR.

This is a data-dependent fallible call (not a lock-poisoning unwrap), so it's a natural candidate for the same treatment applied elsewhere in this PR.

Suggested fix
-        let identity_id = asset_lock_proof
-            .create_identifier()
-            .expect("expected to create an identifier");
+        let identity_id = asset_lock_proof
+            .create_identifier()
+            .map_err(|e| format!("Failed to create identifier from asset lock proof: {}", e))?;
src/backend_task/contract.rs (1)

126-126: unwrap_or_default() on token_id silently produces a zeroed identifier.

If token_id() returns None, the resulting TokenInfo will contain a default (all-zeros) Identifier, which could be confusing downstream (e.g., displayed in the UI or used in lookups). Consider matching on None and either skipping the token (consistent with the error handling above) or logging a warning.

src/context/transaction_processing.rs (1)

243-254: Use rusqlite::Error::UserFunctionError for domain-level errors instead of InvalidParameterName.

InvalidParameterName is semantically meant for SQL parameter binding issues. Using it for "no credit outputs" or "address parsing failure" is misleading and inconsistent with the precedent in src/database/wallet.rs, which converts WalletError via UserFunctionError(Box::new(err)).

Suggested fix
-                let first = payload.credit_outputs.first().ok_or_else(|| {
-                    rusqlite::Error::InvalidParameterName(
-                        "Asset lock transaction has no credit outputs".to_string(),
-                    )
-                })?;
+                let first = payload.credit_outputs.first().ok_or_else(|| {
+                    rusqlite::Error::UserFunctionError(Box::from(
+                        "Asset lock transaction has no credit outputs",
+                    ))
+                })?;
 
-                let address =
-                    Address::from_script(&first.script_pubkey, self.network).map_err(|e| {
-                        rusqlite::Error::InvalidParameterName(format!(
-                            "Failed to derive address from asset lock credit output script: {e}"
-                        ))
-                    })?;
+                let address =
+                    Address::from_script(&first.script_pubkey, self.network).map_err(|e| {
+                        rusqlite::Error::UserFunctionError(Box::from(format!(
+                            "Failed to derive address from asset lock credit output script: {e}"
+                        )))
+                    })?;
src/database/contested_names.rs (1)

361-364: Silent error suppression differs from the rest of the file's approach.

Here .ok() swallows the from_bytes error, treating corrupt awarded_to bytes as None. The practical effect is benign — it forces a needless UPDATE that writes the correct value — but it's inconsistent with the other sites in this file that propagate conversion errors.

A brief tracing::warn! on the .ok() path would help diagnose corrupt DB data without changing the control flow.

src/context/mod.rs (1)

604-612: Catch-all arm silently defaults unknown networks to PLATFORM_V11.

The _ => &PLATFORM_V11 arm replaces what was likely a panic!/unreachable!. This is safer, but note that if a new Network variant is introduced with a different platform version, this will silently apply the wrong version. The existing TODO on line 605 already tracks the longer-term fix (reading from the SDK at runtime). Consider adding a tracing::warn! in the catch-all so unexpected variants don't go unnoticed.

🔧 Optional: log a warning for unrecognized network variants

This can't be a const fn if you add logging, so you'd need to decide whether the const qualifier is more valuable here. An alternative is a code comment.

-        _ => &PLATFORM_V11,
+        other => {
+            // If we reach here, a new Network variant was added without updating this match.
+            // Defaulting to PLATFORM_V11 as a safe fallback.
+            tracing::warn!(?other, "Unknown network variant, defaulting to PLATFORM_V11");
+            &PLATFORM_V11
+        }

Copy link
Contributor

@lklimek lklimek left a comment

Choose a reason for hiding this comment

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

See comments, ones prefixed with [HIGH] etc. are from Claude, others are mine.

Most important:

  1. We silently ignore errors, without notifying user that something went wrong. See https://github.com/dashpay/dash-evo-tool/pull/562/changes#r2797870053
  2. Copilot and rabbit also gave good feedback, please address AI's feedback before assigning ticket to me
  3. Tests red.

src/app.rs Outdated
if let Some(ctx) = self.testnet_app_context.as_ref() {
ctx
} else {
tracing::warn!("Testnet app context not available, falling back to mainnet");
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] Silent mainnet fallback can cause fund loss

When the user selects Testnet/Devnet/Regtest but the corresponding AppContext is None (failed to initialize), this silently falls back to the mainnet context. The user believes they are on Testnet but all operations — wallet transactions, identity registration, token transfers — silently target mainnet with real funds.

The tracing::warn! is only visible in the log file, not in the UI.

Suggestion: Return Result or Option from current_app_context() so callers can display a visible error. Alternatively, prevent the user from selecting a network whose context failed to initialize (e.g., grey out the option in the network chooser). The same applies to Devnet (line 713), Regtest (line 719), and the catch-all (line 727).

Copy link
Contributor

Choose a reason for hiding this comment

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

Audit note (Claude Code): This comment is still unresolved.

The log level was upgraded from warn! to error! and messages now include a "BUG:" prefix, which improves debuggability. However, the core concern remains: if this fallback triggers, the user silently operates on mainnet with no UI indication. The tracing::error! is only visible in log files.

The reviewer's suggestion to return Result/Option from current_app_context() or to prevent selecting unavailable networks (e.g. grey out the option) has not been implemented.

@lklimek
Copy link
Contributor

lklimek commented Feb 12, 2026

Audit Summary

Reviewed by: Claude Code with a 4-agent team:

  • backend-reviewer (code-reviewer) — backend tasks, context, config, app.rs
  • db-reviewer (code-reviewer) — database layer (9 files)
  • ui-reviewer (code-reviewer) — UI screens, components, tests
  • security-auditor (security-engineer) — security audit across all changes

The PR accomplishes its stated goal — replacing ~40 expect()/unwrap()/panic!()/unreachable!()/unimplemented!() calls with proper error propagation or safe fallbacks. The vast majority of changes are correct and improve robustness. Database improvements (N+1 elimination, transaction atomicity, indexes) and the eprintln!tracing migration are well done.

Key concern: silent mainnet fallback (HIGH)

The most significant issue is the current_app_context() fallback behavior (see inline on src/app.rs). When a non-mainnet context is None, the code silently falls back to mainnet. A user who selected Testnet could unknowingly execute operations on mainnet with real funds.

Issues outside the diff

[HIGH] src/config.rs:483-487#[should_panic] test is now incorrect. insight_api_uri() was changed to return Result<Uri, String> (line 338), but test_insight_api_uri_empty_panics still uses #[should_panic(expected = "invalid insight API URL")]. No panic will occur, so the test will fail. The analogous test_insight_api_uri_invalid_panics was correctly updated (line 478), but this one was missed. Fix:

#[test]
fn test_insight_api_uri_empty_returns_error() {
    let config = make_network_config("https://127.0.0.1:443", "", 9998);
    assert!(config.insight_api_uri().is_err());
}

[LOW] Inconsistent log level for avatar fetch failures. src/ui/dashpay/contact_profile_viewer.rs:196 uses error!() for a failed avatar fetch, while src/ui/dashpay/contacts_list.rs:284 and src/ui/dashpay/profile_screen.rs:469 use tracing::warn!() for the identical scenario. Network failures fetching avatars are expected in degraded conditions and should consistently be warn!.

Positive observations

  • New wallet model tests (981 lines) are comprehensive and well-structured
  • Test harness updates for Result-returning AppState::new() are correct
  • unimplemented!() → UI label for marketplace settings is the right approach
  • ZMQ listener failures becoming non-fatal is a good resilience improvement

Findings Summary

# Severity Location Finding
H1 HIGH src/app.rs:705 Silent mainnet fallback when testnet/devnet context is None — can cause fund loss
H2 HIGH src/config.rs:483 #[should_panic] test incorrect — insight_api_uri() now returns Result
H4 HIGH src/database/contested_names.rs:363 .ok() silently swallows awarded_to data corruption
M2 MEDIUM src/backend_task/platform_info.rs:319 One bad address aborts entire withdrawal display (inconsistent with line 246)
M3 MEDIUM src/context/mod.rs:611 Catch-all _ => &PLATFORM_V11 silently accepts future network variants
L2 LOW src/backend_task/dashpay/contacts.rs:264 Silent skip without logging (inconsistent with line 310-314)
L3 LOW src/ui/dashpay/contact_profile_viewer.rs:196 error!() for avatar fetch vs warn!() in other files

Inline comments are posted as a separate pending review for in-diff findings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (3)
src/ui/identities/withdraw_screen.rs (1)

320-328: ⚠️ Potential issue | 🟠 Major

refresh() still contains two .unwrap() calls that can panic.

Given this PR's goal of eliminating unwrap/expect panics, these two calls are inconsistent:

  1. load_local_qualified_identities().unwrap() — will panic if the DB query fails.
  2. .find(...).unwrap() — will panic if the identity was deleted or is otherwise missing.

A safer approach: propagate a user-visible error or silently keep the stale identity.

Suggested safer fallback
     fn refresh(&mut self) {
-        self.identity = self
-            .app_context
-            .load_local_qualified_identities()
-            .unwrap()
-            .into_iter()
-            .find(|identity| identity.identity.id() == self.identity.identity.id())
-            .unwrap();
-        self.max_amount = self.identity.identity.balance();
+        match self.app_context.load_local_qualified_identities() {
+            Ok(identities) => {
+                if let Some(updated) = identities
+                    .into_iter()
+                    .find(|i| i.identity.id() == self.identity.identity.id())
+                {
+                    self.identity = updated;
+                    self.max_amount = self.identity.identity.balance();
+                } else {
+                    tracing::warn!("Identity no longer found during refresh");
+                }
+            }
+            Err(e) => {
+                tracing::warn!("Failed to refresh identities: {}", e);
+            }
+        }
     }

Based on learnings: "Error handling refactoring is needed across the Dash-EVO-Tool codebase, particularly to avoid panics with .expect() and instead propagate errors properly."

src/database/contested_names.rs (1)

502-512: ⚠️ Potential issue | 🟠 Major

Remaining .expect() / .unwrap() calls in insert_or_update_contenders still panic on data issues.

Lines 504, 506, 510, and 512 use .expect() and .unwrap() for document deserialization and field extraction. These are exactly the kind of panics this PR aims to eliminate. A malformed contender document or missing "label" field would crash the application.

Proposed fix
-            let deserialized_contender = contender
-                .try_to_contender(dpns_domain_document_type, app_context.platform_version())
-                .expect("expect a contender document deserialization");
-
-            let document = deserialized_contender.document().as_ref().unwrap().clone();
-
-            let name = document
-                .get("label")
-                .expect("expected name")
-                .as_str()
-                .unwrap();
+            let deserialized_contender = contender
+                .try_to_contender(dpns_domain_document_type, app_context.platform_version())
+                .map_err(|e| {
+                    rusqlite::Error::InvalidParameterName(format!(
+                        "Failed to deserialize contender document: {}",
+                        e
+                    ))
+                })?;
+
+            let document = deserialized_contender
+                .document()
+                .as_ref()
+                .ok_or_else(|| {
+                    rusqlite::Error::InvalidParameterName(
+                        "Contender has no document".to_string(),
+                    )
+                })?
+                .clone();
+
+            let name = document
+                .get("label")
+                .ok_or_else(|| {
+                    rusqlite::Error::InvalidParameterName(
+                        "Document missing 'label' field".to_string(),
+                    )
+                })?
+                .as_str()
+                .ok_or_else(|| {
+                    rusqlite::Error::InvalidParameterName(
+                        "'label' field is not a string".to_string(),
+                    )
+                })?;

Based on learnings: "Error handling refactoring is needed across the Dash-EVO-Tool (DET) codebase, particularly to avoid panics with .expect() and instead propagate errors properly using the ? operator."

src/database/wallet.rs (1)

864-870: ⚠️ Potential issue | 🟡 Minor

Remaining .expect() on seed hash conversion in identity loading.

This is the only seed hash conversion in get_wallets that still panics. All other seed hash conversions (lines 445, 525, 710, 805, 908) were converted to map_err. This one was missed.

Proposed fix
-            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
-                .try_into()
-                .expect("Seed hash should be 32 bytes");
+            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash.try_into().map_err(|_| {
+                rusqlite::Error::InvalidParameterName(
+                    "Seed hash should be 32 bytes".to_string(),
+                )
+            })?;
🤖 Fix all issues with AI agents
In `@src/backend_task/platform_info.rs`:
- Line 270: The expression using daily_withdrawal_limit.saturating_sub(0) is a
no-op and incorrectly shows the full daily limit as "Remaining Today"; update
the code that computes or displays this value (the usage around
daily_withdrawal_limit and the dash_to_credits! conversion) to either remove the
"Remaining Today" output when 24h usage data is unavailable or replace it with a
clear placeholder/annotation such as "(24h usage data unavailable)"; ensure you
adjust any formatting logic that depends on daily_withdrawal_limit so it no
longer misleadingly subtracts zero.
- Around line 618-622: The Address::from_script call in the withdrawal
formatting code currently uses map_err(...)? which aborts on a single bad
address; change it to the soft-fallback pattern used in
format_withdrawal_documents_with_daily_limit and
format_withdrawal_documents_to_bare_info by replacing the map_err + ? handling
around Address::from_script(&output_script, self.network) so that it returns a
string like format!("Invalid Address: {}", e) (or similar) instead of
propagating the error, ensuring the document list still renders even if one
address is invalid.
🧹 Nitpick comments (5)
src/ui/identities/withdraw_screen.rs (1)

247-271: Two .expect() calls remain in show_confirmation_popup.

Lines 249 and 270 both use .expect("Withdrawal amount should be present"). While these are partially guarded by the UI (the button is only enabled when withdrawal_amount.is_some()), a defensive approach would be more consistent with the PR's intent:

Suggested safer handling
-                    "Are you sure you want to withdraw {} to {}",
-                    self.withdrawal_amount
-                        .as_ref()
-                        .expect("Withdrawal amount should be present"),
+                    "Are you sure you want to withdraw {} to {}",
+                    self.withdrawal_amount
+                        .as_ref()
+                        .map(|a| a.to_string())
+                        .unwrap_or_else(|| "unknown".to_string()),

For line 270, return an error status instead of panicking:

-                let credits = self
-                    .withdrawal_amount
-                    .as_ref()
-                    .expect("Withdrawal amount should be present")
-                    .value() as u128;
+                let Some(amount) = self.withdrawal_amount.as_ref() else {
+                    self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage(
+                        "No withdrawal amount specified".to_string(),
+                    );
+                    self.confirmation_dialog = None;
+                    return AppAction::None;
+                };
+                let credits = amount.value() as u128;
src/backend_task/platform_info.rs (1)

456-504: One bad document aborts the entire queued-withdrawal display.

Because both format_withdrawal_documents_with_daily_limit and format_withdrawal_documents_to_bare_info use collect::<Result<Vec<String>, String>>()?, a single corrupt or incomplete document will cause the entire view to return an error. For a read-only display of multiple independent documents, consider using filter_map to skip/log bad entries and still show the valid ones.

src/database/wallet.rs (2)

438-449: Inconsistent rusqlite::Error variant usage within this file.

Lines 438-443 use InvalidParameterName for data conversion errors, while lines 534-538 correctly use FromSqlConversionFailure for the same category of error (converting DB data to a Rust type). These two patterns coexist in the same function (get_wallets), making the error handling inconsistent.

Consider standardizing on FromSqlConversionFailure for all data conversion failures (ExtendedPubKey decoding, seed hash conversion, address parsing, etc.) since that's the semantically correct variant. InvalidParameterName is for SQL named-parameter binding mismatches.


592-604: Non-idiomatic error skip pattern.

row.is_err() followed by row? on the same binding is valid but unnecessarily indirect. A more idiomatic approach:

Suggested fix
     for row in address_rows {
-        if row.is_err() {
-            continue;
-        }
-        let (
+        let Ok((
             seed_array,
             address,
             derivation_path,
             balance,
             path_reference,
             path_type,
             total_received,
-        ) = row?;
+        )) = row else {
+            continue;
+        };
src/app.rs (1)

1026-1029: Remaining .unwrap() on SystemTime — same pattern replaced with .unwrap_or_default() in 26 other files.

This is an existing line not touched in this PR, but it's the exact same duration_since(UNIX_EPOCH).unwrap() pattern being systematically replaced elsewhere. Worth addressing for consistency.

Suggested fix
         let current_time = SystemTime::now()
             .duration_since(SystemTime::UNIX_EPOCH)
-            .unwrap()
+            .unwrap_or_default()
             .as_millis() as u64;

Copy link
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

Caution

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

⚠️ Outside diff range comments (4)
src/database/wallet.rs (4)

862-864: ⚠️ Potential issue | 🟠 Major

Missed .expect() — this will still panic on corrupted data.

Every other seed_hash.try_into() in this file was converted to .map_err(|_| rusqlite::Error::InvalidParameterName(...)), but this one in the identity-loading closure was missed. A corrupted or truncated wallet column in the identity table would crash the application.

Proposed fix
-            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
-                .try_into()
-                .expect("Seed hash should be 32 bytes");
+            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash.try_into().map_err(|_| {
+                rusqlite::Error::InvalidParameterName(
+                    "Seed hash should be 32 bytes".to_string(),
+                )
+            })?;

Based on learnings: "Error handling refactoring is needed across the Dash-EVO-Tool (DET) codebase, particularly to avoid panics with .expect() and instead propagate errors properly using the ? operator."

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/database/wallet.rs` around lines 862 - 864, Replace the panic-causing
.expect() on wallet_seed_hash.try_into() inside the identity-loading closure by
mapping the conversion error to a rusqlite::Error and propagating it with ?;
specifically change the wallet_seed_hash.try_into().expect(...) usage (where
wallet_seed_hash_array and wallet_seed_hash are defined) to
wallet_seed_hash.try_into().map_err(|_|
rusqlite::Error::InvalidParameterName("invalid wallet seed hash
length".into()))? so the closure returns a rusqlite::Result instead of
panicking.

590-602: ⚠️ Potential issue | 🟡 Minor

Silent error swallowing — address row errors are skipped without logging.

When row.is_err(), the error is silently discarded. This can hide data corruption or parsing issues that the operator should be aware of. At minimum, log before continuing.

Also note the pattern is slightly odd: checking is_err() then calling row? afterward is redundant — if you reach line 602, row is guaranteed Ok, so ? is a no-op unwrap.

Proposed fix
     for row in address_rows {
-        if row.is_err() {
-            continue;
-        }
         let (
             seed_array,
             address,
             derivation_path,
             balance,
             path_reference,
             path_type,
             total_received,
-        ) = row?;
+        ) = match row {
+            Ok(v) => v,
+            Err(e) => {
+                tracing::warn!("Skipping wallet address row due to error: {}", e);
+                continue;
+            }
+        };
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/database/wallet.rs` around lines 590 - 602, The loop over address_rows
currently swallows errors by continuing when row.is_err() and then uses row?
redundantly; change it to explicitly match or use if let Ok((seed_array,
address, derivation_path, balance, path_reference, path_type, total_received)) =
row { ... } else { let err = row.unwrap_err(); log the error (including err) via
the module's logger (e.g., error! or process_logger) with context like "failed
to read address row" and continue } so errors are logged and the redundant
is_err()/row? pattern is removed.

916-941: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: parse failures silently skip, but network validation aborts all.

Lines 917-919 use if let Ok(...) chains that silently skip rows where the row extraction or address parsing fails. But line 921-928 propagates require_network errors via ?, aborting the entire wallet load.

This means a malformed address string is silently ignored, but an address that parses but fails network validation kills the whole get_wallets() call. Consider making these consistent — either skip-and-log both, or propagate both.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/database/wallet.rs` around lines 916 - 941, The code currently skips rows
when row extraction or Address::<NetworkUnchecked>::from_str fails but aborts
the whole get_wallets() when address.require_network(...) returns Err; make the
behavior consistent by not using ? there: replace the
address.require_network(...).map_err(...)? call with a match or if let that logs
the validation error (including address_str and the error) and continues the for
loop so malformed or wrong-network addresses are skipped rather than aborting;
ensure you still compute canonical_address via
Wallet::canonical_address(&address_checked, *network) and insert into
wallet.platform_address_info as before, keeping references to platform_rows,
wallets_map and platform_address_info the same.

438-463: ⚠️ Potential issue | 🟡 Minor

Replace InvalidParameterName with FromSqlConversionFailure for data conversion errors, and convert the remaining .expect() at line 862.

Throughout this file, rusqlite::Error::InvalidParameterName is used for errors when decoding/converting data read from the database (e.g., ExtendedPubKey::decode, seed_hash.try_into(), Address::from_str(), deserialize() calls). This is semantically incorrect—InvalidParameterName is for SQL parameter binding failures, not column value conversion failures.

rusqlite::Error::FromSqlConversionFailure(column_index, sql_type, error) is the appropriate variant. Align all ~19 occurrences with the pattern already used elsewhere in the file (see lines with Type::Text, Type::Integer).

Additionally, line 862-864 still has .expect("Seed hash should be 32 bytes") in production code, which contradicts the PR's goal of eliminating panics:

Remaining .expect() to convert
let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
    .try_into()
    .expect("Seed hash should be 32 bytes");

Convert to:

let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash.try_into().map_err(|_| {
    rusqlite::Error::FromSqlConversionFailure(
        1,
        rusqlite::types::Type::Blob,
        Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, "Seed hash should be 32 bytes")),
    )
})?;
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/database/wallet.rs` around lines 438 - 463, Replace inappropriate
rusqlite::Error::InvalidParameterName usages used for data conversion failures
(e.g., around ExtendedPubKey::decode, seed_hash.try_into(), Address::from_str(),
deserialize() calls) with rusqlite::Error::FromSqlConversionFailure supplying
the column index, the expected rusqlite::types::Type (e.g., Type::Blob or
Type::Text/Integer as appropriate), and Box::new(the underlying error) to match
the pattern used elsewhere; also change the remaining panic in the
wallet_seed_hash_array conversion (the .expect("Seed hash should be 32 bytes")
around wallet_seed_hash.try_into()) to map_err(...) returning
rusqlite::Error::FromSqlConversionFailure with Type::Blob and an io::Error
describing "Seed hash should be 32 bytes". Ensure all ~19 occurrences follow
this pattern and use the same error construction style found in other parts of
the file.
🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@src/app.rs`:
- Around line 719-762: current_app_context silently falls back to mainnet on
unreachable branches which risks running the UI on mainnet without user notice;
change current_app_context to return a Result<&Arc<AppContext>, NetworkFallback>
(or include an accompanying Option/enum indicating a fallback) so callers are
notified when a fallback occurred, and populate that error/indicator when the
code currently logs the "BUG:" cases (use the existing fields chosen_network,
mainnet_app_context, testnet_app_context, devnet_app_context, local_app_context
to detect the fallback). Update callers to surface the error to the UI (or set
chosen_network appropriately at a mutable callsite) so the user can be informed
instead of silently operating on mainnet.

In `@src/database/contested_names.rs`:
- Around line 19-179: Both get_all_contested_names and
get_ongoing_contested_names duplicate the large row-mapping closure; extract
that logic into a shared helper (e.g., map_contested_row) that accepts a
rusqlite::Row (or &Row), a mutable reference to the contested_name_map
(HashMap<String, ContestedName>), and contest_duration (and any small context
like network if needed) and returns rusqlite::Result<()>; move identifier
parsing (Identifier::from_bytes), state computation (locked/awarded/created_at
-> ContestState), and contestant construction into that helper, then call it
from each query’s query_map closure to avoid duplication and centralize error
handling and mapping logic for get_all_contested_names and
get_ongoing_contested_names.

In `@src/database/wallet.rs`:
- Around line 660-667: The UTXO address parsing uses
Address::from_str(...).assume_checked() and skips the network validation done
for wallet addresses by check_address_for_network(); update the UTXO loading
path to parse the string into an Address, call
check_address_for_network(&address) (or the same validation helper used at line
566) and propagate an error if the network mismatches before calling
.assume_checked(), ensuring the mapped rusqlite::Error includes the validation
failure context.
- Around line 862-864: Replace the panic-causing .expect() on
wallet_seed_hash.try_into() inside the identity-loading closure by mapping the
conversion error to a rusqlite::Error and propagating it with ?; specifically
change the wallet_seed_hash.try_into().expect(...) usage (where
wallet_seed_hash_array and wallet_seed_hash are defined) to
wallet_seed_hash.try_into().map_err(|_|
rusqlite::Error::InvalidParameterName("invalid wallet seed hash
length".into()))? so the closure returns a rusqlite::Result instead of
panicking.
- Around line 590-602: The loop over address_rows currently swallows errors by
continuing when row.is_err() and then uses row? redundantly; change it to
explicitly match or use if let Ok((seed_array, address, derivation_path,
balance, path_reference, path_type, total_received)) = row { ... } else { let
err = row.unwrap_err(); log the error (including err) via the module's logger
(e.g., error! or process_logger) with context like "failed to read address row"
and continue } so errors are logged and the redundant is_err()/row? pattern is
removed.
- Around line 916-941: The code currently skips rows when row extraction or
Address::<NetworkUnchecked>::from_str fails but aborts the whole get_wallets()
when address.require_network(...) returns Err; make the behavior consistent by
not using ? there: replace the address.require_network(...).map_err(...)? call
with a match or if let that logs the validation error (including address_str and
the error) and continues the for loop so malformed or wrong-network addresses
are skipped rather than aborting; ensure you still compute canonical_address via
Wallet::canonical_address(&address_checked, *network) and insert into
wallet.platform_address_info as before, keeping references to platform_rows,
wallets_map and platform_address_info the same.
- Around line 438-463: Replace inappropriate
rusqlite::Error::InvalidParameterName usages used for data conversion failures
(e.g., around ExtendedPubKey::decode, seed_hash.try_into(), Address::from_str(),
deserialize() calls) with rusqlite::Error::FromSqlConversionFailure supplying
the column index, the expected rusqlite::types::Type (e.g., Type::Blob or
Type::Text/Integer as appropriate), and Box::new(the underlying error) to match
the pattern used elsewhere; also change the remaining panic in the
wallet_seed_hash_array conversion (the .expect("Seed hash should be 32 bytes")
around wallet_seed_hash.try_into()) to map_err(...) returning
rusqlite::Error::FromSqlConversionFailure with Type::Blob and an io::Error
describing "Seed hash should be 32 bytes". Ensure all ~19 occurrences follow
this pattern and use the same error construction style found in other parts of
the file.
🧹 Nitpick comments (2)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@src/database/contested_names.rs`:
- Around line 19-179: Both get_all_contested_names and
get_ongoing_contested_names duplicate the large row-mapping closure; extract
that logic into a shared helper (e.g., map_contested_row) that accepts a
rusqlite::Row (or &Row), a mutable reference to the contested_name_map
(HashMap<String, ContestedName>), and contest_duration (and any small context
like network if needed) and returns rusqlite::Result<()>; move identifier
parsing (Identifier::from_bytes), state computation (locked/awarded/created_at
-> ContestState), and contestant construction into that helper, then call it
from each query’s query_map closure to avoid duplication and centralize error
handling and mapping logic for get_all_contested_names and
get_ongoing_contested_names.

In `@src/database/wallet.rs`:
- Around line 660-667: The UTXO address parsing uses
Address::from_str(...).assume_checked() and skips the network validation done
for wallet addresses by check_address_for_network(); update the UTXO loading
path to parse the string into an Address, call
check_address_for_network(&address) (or the same validation helper used at line
566) and propagate an error if the network mismatches before calling
.assume_checked(), ensuring the mapped rusqlite::Error includes the validation
failure context.
src/database/contested_names.rs (1)

19-179: Consider extracting the shared row-mapping logic.

get_all_contested_names and get_ongoing_contested_names have nearly identical ~80-line row-mapping closures (identifier parsing, state computation, contestant construction). The only difference is the SQL WHERE clause and parameters. Extracting the common row-processing logic into a helper would reduce the surface area for divergence and make the new error-handling patterns easier to maintain in one place.

Also applies to: 181-349

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/database/contested_names.rs` around lines 19 - 179, Both
get_all_contested_names and get_ongoing_contested_names duplicate the large
row-mapping closure; extract that logic into a shared helper (e.g.,
map_contested_row) that accepts a rusqlite::Row (or &Row), a mutable reference
to the contested_name_map (HashMap<String, ContestedName>), and contest_duration
(and any small context like network if needed) and returns rusqlite::Result<()>;
move identifier parsing (Identifier::from_bytes), state computation
(locked/awarded/created_at -> ContestState), and contestant construction into
that helper, then call it from each query’s query_map closure to avoid
duplication and centralize error handling and mapping logic for
get_all_contested_names and get_ongoing_contested_names.
src/database/wallet.rs (1)

660-667: UTXO addresses skip network validation unlike wallet addresses.

At line 566, wallet addresses go through check_address_for_network(), but UTXO addresses at line 667 use .assume_checked() without network validation. If the DB contains a UTXO with an address for the wrong network, it would be silently accepted.

This is likely safe since UTXOs are filtered by network in SQL and were validated at insertion time, but flagging the inconsistency.

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/database/wallet.rs` around lines 660 - 667, The UTXO address parsing uses
Address::from_str(...).assume_checked() and skips the network validation done
for wallet addresses by check_address_for_network(); update the UTXO loading
path to parse the string into an Address, call
check_address_for_network(&address) (or the same validation helper used at line
566) and propagate an error if the network mismatches before calling
.assume_checked(), ensuring the mapped rusqlite::Error includes the validation
failure context.

Copy link
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.

Caution

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

⚠️ Outside diff range comments (3)
src/database/wallet.rs (3)

595-607: ⚠️ Potential issue | 🟡 Minor

Silent error swallowing is inconsistent with the rest of get_wallets.

Steps 4–8 propagate row errors via row?, but step 3 silently skips them with continue. A corrupted or invalid address row will be silently dropped, potentially hiding data issues. This is especially inconsistent given the function's doc comment about stopping on the first corrupted blob (line 412–415).

Proposed fix — propagate consistently
     for row in address_rows {
-        if row.is_err() {
-            continue;
-        }
         let (
             seed_array,
             address,
             derivation_path,
             balance,
             path_reference,
             path_type,
             total_received,
         ) = row?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/wallet.rs` around lines 595 - 607, The loop over address_rows in
get_wallets silently skips Err rows (using if row.is_err() { continue; }) which
is inconsistent with the function's error propagation; remove the manual is_err
check and let the row? operator propagate errors for the tuple deconstruction
(seed_array, address, derivation_path, balance, path_reference, path_type,
total_received) so any corrupted/invalid address row returns an Err instead of
being dropped; update the loop that iterates address_rows and any related
control flow to rely on row? for consistent error handling.

790-790: ⚠️ Potential issue | 🟡 Minor

Duplicate step number in trace log.

Line 779 is "step 7" and line 790 is also "step 7". Should be "step 8" here (subsequent steps at lines 854 and 906 are already labeled 8 and 9).

Fix
-        tracing::trace!("step 7: load wallet transactions for each wallet");
+        tracing::trace!("step 8: load wallet transactions for each wallet");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/wallet.rs` at line 790, The trace log in src/database/wallet.rs
incorrectly repeats "step 7" — update the tracing::trace! call that currently
reads "step 7: load wallet transactions for each wallet" to "step 8: load wallet
transactions for each wallet" so step numbering is sequential with the later
"step 8" and "step 9" logs; modify the string literal passed to tracing::trace!
accordingly.

866-869: ⚠️ Potential issue | 🟠 Major

Remaining .expect() not converted to error propagation.

Line 869 still has .expect("Seed hash should be 32 bytes") which will panic on corrupted data, contradicting this PR's goal. Every other seed hash conversion in this file was converted to .map_err(...).

Proposed fix
-            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
-                .try_into()
-                .expect("Seed hash should be 32 bytes");
+            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
+                .try_into()
+                .map_err(|_| {
+                    rusqlite::Error::InvalidParameterName(
+                        "Seed hash should be 32 bytes".to_string(),
+                    )
+                })?;

Based on learnings: "Error handling refactoring is needed across the Dash-EVO-Tool (DET) codebase, particularly to avoid panics with .expect() and instead propagate errors properly using the ? operator."

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

In `@src/database/wallet.rs` around lines 866 - 869, Replace the panic-causing
.expect on converting wallet_seed_hash into wallet_seed_hash_array with proper
error propagation: change the expression that creates wallet_seed_hash_array
(the .try_into() call on wallet_seed_hash) to map the conversion error into the
function's error type and propagate it with ? (e.g., .try_into().map_err(|e| /*
convert to appropriate error: WalletError/DbError */ )?), so the code returns an
Err instead of panicking; update the error mapping to use the same error variant
pattern used elsewhere in wallet.rs for seed hash conversions to keep
consistency.
🧹 Nitpick comments (3)
src/ui/network_chooser_screen.rs (1)

889-889: Pre-existing .expect() calls remain in this file.

Lines 889, 909, and 959 still use self.save().expect("Expected to save db settings"), which can panic if the DB write fails. These aren't part of this diff, but since the PR's goal is eliminating panic-prone calls, consider converting them to log-and-warn in a follow-up.

// e.g. replace:
self.save().expect("Expected to save db settings");
// with:
if let Err(e) = self.save() {
    tracing::error!("Failed to save db settings: {e}");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/network_chooser_screen.rs` at line 889, Replace the panic-prone calls
to self.save().expect("Expected to save db settings") (occurring in methods that
call save()) with non-panicking error handling: call self.save(), match or
if-let on the Result, and log the error via tracing::error! (or tracing::warn!)
with a descriptive message including the error (e.g., "Failed to save db
settings: {e}"), then continue without panicking; ensure you update all
occurrences (the repeated self.save() calls in this module) and keep the save()
call semantics otherwise unchanged.
src/ui/identities/withdraw_screen.rs (1)

264-288: Two .expect() calls remain in show_confirmation_popup.

Lines 266 and 287 still use .expect("Withdrawal amount should be present"). While they're guarded by the UI (the Withdraw button is only enabled when withdrawal_amount.is_some()), this is inconsistent with the PR's goal of eliminating panics. Consider using early-return with an error status instead.

Proposed fix
-                format!(
-                    "Are you sure you want to withdraw {} to {}",
-                    self.withdrawal_amount
-                        .as_ref()
-                        .expect("Withdrawal amount should be present"),
-                    message_address
-                ),
+                {
+                    let Some(amount) = self.withdrawal_amount.as_ref() else {
+                        self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage(
+                            "No withdrawal amount specified".to_string(),
+                        );
+                        self.confirmation_dialog = None;
+                        return AppAction::None;
+                    };
+                    format!(
+                        "Are you sure you want to withdraw {} to {}",
+                        amount, message_address
+                    )
+                },

And similarly for line 284-288:

-                let credits = self
-                    .withdrawal_amount
-                    .as_ref()
-                    .expect("Withdrawal amount should be present")
-                    .value() as u128;
+                let Some(amount) = self.withdrawal_amount.as_ref() else {
+                    self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage(
+                        "No withdrawal amount specified".to_string(),
+                    );
+                    self.confirmation_dialog = None;
+                    return AppAction::None;
+                };
+                let credits = amount.value() as u128;

Based on learnings: "Error handling refactoring is needed across the Dash-EVO-Tool (DET) codebase, particularly to avoid panics with .expect() and instead propagate errors properly."

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

In `@src/ui/identities/withdraw_screen.rs` around lines 264 - 288, In
show_confirmation_popup, remove the two .expect("Withdrawal amount should be
present") calls on self.withdrawal_amount and instead early-return if the amount
is missing: use if let Some(amount) = self.withdrawal_amount.as_ref() { ... } to
capture amount.value() for credits and proceed to set
self.withdraw_from_identity_status =
WithdrawFromIdentityStatus::WaitingForResult(now); otherwise clear
self.confirmation_dialog (and optionally set an error/idle status) and return so
the UI cannot panic when withdrawal_amount is None.
src/database/wallet.rs (1)

443-448: InvalidParameterName is semantically misleading for data corruption errors.

Throughout this file, rusqlite::Error::InvalidParameterName is used to wrap deserialization/validation failures. This error variant is intended for SQL parameter binding issues, not data integrity errors. Consider using FromSqlConversionFailure (as done elsewhere in this file, e.g., line 537) or the custom WalletErrorUserFunctionError path already defined at line 1249, which would provide a more accurate error surface.

Not blocking since the pattern is consistent within this PR, but it may confuse callers trying to distinguish parameter errors from data corruption.

Also applies to: 450-452, 464-468

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

In `@src/database/wallet.rs` around lines 443 - 448, Replace uses of
rusqlite::Error::InvalidParameterName wrapping deserialization/validation
failures (e.g., the ExtendedPubKey::decode call on
master_ecdsa_bip44_account_0_epk_bytes) with a semantically appropriate error
variant such as rusqlite::Error::FromSqlConversionFailure or route the failure
into the crate's WalletError → UserFunctionError path used elsewhere (see the
WalletError/UserFunctionError usage pattern), preserving the original error
message and source; update the map_err closures around ExtendedPubKey::decode
and the other occurrences noted (lines near the other decode/validation sites)
to construct FromSqlConversionFailure with the offending value/type and
Box::new(original_error) or convert into WalletError::UserFunctionError as
appropriate so callers can distinguish data corruption from SQL parameter
errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/database/wallet.rs`:
- Around line 595-607: The loop over address_rows in get_wallets silently skips
Err rows (using if row.is_err() { continue; }) which is inconsistent with the
function's error propagation; remove the manual is_err check and let the row?
operator propagate errors for the tuple deconstruction (seed_array, address,
derivation_path, balance, path_reference, path_type, total_received) so any
corrupted/invalid address row returns an Err instead of being dropped; update
the loop that iterates address_rows and any related control flow to rely on row?
for consistent error handling.
- Line 790: The trace log in src/database/wallet.rs incorrectly repeats "step 7"
— update the tracing::trace! call that currently reads "step 7: load wallet
transactions for each wallet" to "step 8: load wallet transactions for each
wallet" so step numbering is sequential with the later "step 8" and "step 9"
logs; modify the string literal passed to tracing::trace! accordingly.
- Around line 866-869: Replace the panic-causing .expect on converting
wallet_seed_hash into wallet_seed_hash_array with proper error propagation:
change the expression that creates wallet_seed_hash_array (the .try_into() call
on wallet_seed_hash) to map the conversion error into the function's error type
and propagate it with ? (e.g., .try_into().map_err(|e| /* convert to appropriate
error: WalletError/DbError */ )?), so the code returns an Err instead of
panicking; update the error mapping to use the same error variant pattern used
elsewhere in wallet.rs for seed hash conversions to keep consistency.

---

Nitpick comments:
In `@src/database/wallet.rs`:
- Around line 443-448: Replace uses of rusqlite::Error::InvalidParameterName
wrapping deserialization/validation failures (e.g., the ExtendedPubKey::decode
call on master_ecdsa_bip44_account_0_epk_bytes) with a semantically appropriate
error variant such as rusqlite::Error::FromSqlConversionFailure or route the
failure into the crate's WalletError → UserFunctionError path used elsewhere
(see the WalletError/UserFunctionError usage pattern), preserving the original
error message and source; update the map_err closures around
ExtendedPubKey::decode and the other occurrences noted (lines near the other
decode/validation sites) to construct FromSqlConversionFailure with the
offending value/type and Box::new(original_error) or convert into
WalletError::UserFunctionError as appropriate so callers can distinguish data
corruption from SQL parameter errors.

In `@src/ui/identities/withdraw_screen.rs`:
- Around line 264-288: In show_confirmation_popup, remove the two
.expect("Withdrawal amount should be present") calls on self.withdrawal_amount
and instead early-return if the amount is missing: use if let Some(amount) =
self.withdrawal_amount.as_ref() { ... } to capture amount.value() for credits
and proceed to set self.withdraw_from_identity_status =
WithdrawFromIdentityStatus::WaitingForResult(now); otherwise clear
self.confirmation_dialog (and optionally set an error/idle status) and return so
the UI cannot panic when withdrawal_amount is None.

In `@src/ui/network_chooser_screen.rs`:
- Line 889: Replace the panic-prone calls to self.save().expect("Expected to
save db settings") (occurring in methods that call save()) with non-panicking
error handling: call self.save(), match or if-let on the Result, and log the
error via tracing::error! (or tracing::warn!) with a descriptive message
including the error (e.g., "Failed to save db settings: {e}"), then continue
without panicking; ensure you update all occurrences (the repeated self.save()
calls in this module) and keep the save() call semantics otherwise unchanged.

Copy link
Contributor

@lklimek lklimek left a comment

Choose a reason for hiding this comment

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

Not all comments are resolved. I manually checked some of them, and they are still there. See comments.

PastaPastaPasta and others added 12 commits February 19, 2026 09:33
…n with Result

Changed dapi_address_list() and insight_api_uri() to return Result
instead of panicking. Updated initialize_sdk() to propagate errors
via Result<Sdk, String>. Updated callers and tests accordingly.
Replaced 13 .expect() calls in AppContext::new() with match blocks
that log errors and return None. Covers SPV/RPC provider init,
5 system data contract loads, cookie path, CoreClient creation, and
DB wallet queries. Also replaced panic!("unsupported network") in
default_platform_version() with safe fallback, and changed
initialize_sdk callers in update_core_rpc_config to propagate errors.
…cessing

In received_asset_lock_finality(), replaced two .expect() calls with
proper ? error propagation: credit_outputs.first() now returns an error
if empty, and Address::from_script() maps its error to a descriptive
rusqlite error. Malformed asset lock data no longer panics.
Replace unimplemented!("marketplace settings") with a UI label so
users see an informational message instead of a crash. Also reverts
read_or_recover/write_or_recover calls that were inadvertently
introduced during cherry-pick back to standard lock calls.
- [HIGH] Prevent silent mainnet fallback by guarding change_network()
  against switching to unavailable network contexts, and validate saved
  network on startup. Upgrade fallback logging to error level.
- [HIGH] Fix incorrect should_panic test for insight_api_uri() which
  now returns Result instead of panicking.
- [HIGH] Replace .ok() with proper error propagation for awarded_to
  Identifier parsing in contested_names.rs insert_or_update_name_contest.
- [MEDIUM] Make withdrawal address error handling consistent between
  format_withdrawal_documents_to_bare_info and _with_daily_limit.
- [MEDIUM] Restore panic for unsupported network in const fn
  default_platform_version to prevent silent misconfiguration.
- [LOW] Add tracing::warn for Identifier::from_bytes failure in
  contacts.rs mutual contact matching for consistency.
- [LOW] Downgrade avatar fetch failure log from error to warn
  in contact_profile_viewer.rs for consistency.
- Replace remaining seed_hash .expect() panics with map_err in wallet.rs.
- Use saturating_sub for elapsed time calculation in withdraw_screen.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…aturating_sub(0) no-op

Replace InvalidParameterName with FromSqlConversionFailure in
contested_names.rs for all Identifier byte conversion errors, since
these are column data conversion failures, not SQL parameter binding
mismatches. Also remove the no-op saturating_sub(0) in platform_info.rs
withdrawal display. Includes formatting fixes from cargo +nightly fmt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@PastaPastaPasta PastaPastaPasta force-pushed the fix/replace-panics-with-error-propagation branch from de97545 to b9d88af Compare February 19, 2026 15:36
Copy link
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: 3

Caution

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

⚠️ Outside diff range comments (10)
src/database/contacts.rs (1)

101-111: ⚠️ Potential issue | 🟠 Major

load_all_contact_private_info has the same NULL-handling gap that was just fixed in load_contact_private_info.

Lines 106–107 use row.get(2)? and row.get(3)? to extract String from the same nickname TEXT and notes TEXT columns that are nullable in the schema (no NOT NULL constraint). If any row has a NULL for either column rusqlite will return Err(InvalidColumnType), silently failing the entire load — exactly the problem fixed on lines 76–77. The fix must be applied consistently here too.

🐛 Proposed fix
         let infos = stmt
             .query_map(params![owner_identity_id.to_buffer().to_vec()], |row| {
                 Ok(ContactPrivateInfo {
                     owner_identity_id: row.get(0)?,
                     contact_identity_id: row.get(1)?,
-                    nickname: row.get(2)?,
-                    notes: row.get(3)?,
+                    nickname: row.get::<_, Option<String>>(2)?.unwrap_or_default(),
+                    notes: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
                     is_hidden: row.get::<_, i32>(4)? != 0,
                 })
             })?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/contacts.rs` around lines 101 - 111, In
load_all_contact_private_info change the nullable column reads for nickname and
notes to fetch Option<String> instead of String (use row.get::<_,
Option<String>>(2) and row.get::<_, Option<String>>(3)) and map/unwrap them to
the desired value (e.g. unwrap_or_default() or .map(|s| s) depending on
ContactPrivateInfo field types) so NULLs no longer cause Err(InvalidColumnType);
update the ContactPrivateInfo construction in the query_map closure accordingly.
src/ui/dashpay/contact_profile_viewer.rs (1)

145-199: ⚠️ Potential issue | 🟠 Major

avatar_loading is never reset to false on fetch failure — causes an infinite spinner.

self.avatar_loading is set to true at line 306 when the fetch is kicked off. On the success path (lines 279–296) the UI thread resets it to false after the texture is created. However, on the Err path (lines 195–197) the async task only logs and exits — nothing signals the UI thread to clear avatar_loading. The render loop's else branch (lines 313–319) then shows a spinner indefinitely, and the else if !self.avatar_loading guard (line 304) prevents any retry.

The fix is to write a sentinel into egui's temp storage from the error branch, check for it in the render loop, and reset avatar_loading accordingly.

🐛 Proposed fix

In load_avatar_texture (async error branch):

                 Err(e) => {
                     warn!("Failed to fetch contact avatar image: {}", e);
+                    ctx_clone.data_mut(|data| {
+                        data.insert_temp(
+                            egui::Id::new(format!("contact_avatar_failed_{}", url_clone)),
+                            true,
+                        );
+                    });
+                    ctx_clone.request_repaint();
                 }

In render, after the color_image check and before the else if !self.avatar_loading branch:

+                                            // Check for async fetch failure sentinel
+                                            let failed_id =
+                                                format!("contact_avatar_failed_{}", avatar_url);
+                                            let fetch_failed = ui.ctx().data_mut(|data| {
+                                                data.get_temp::<bool>(egui::Id::new(&failed_id))
+                                                    .unwrap_or(false)
+                                            });
+                                            if fetch_failed {
+                                                ui.ctx().data_mut(|data| {
+                                                    data.remove::<bool>(egui::Id::new(&failed_id));
+                                                });
+                                                self.avatar_loading = false;
+                                            } else
                                             if let Some(color_image) = color_image {

Also applies to: 304-319

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

In `@src/ui/dashpay/contact_profile_viewer.rs` around lines 145 - 199, The async
error path in load_avatar_texture doesn’t clear self.avatar_loading so the UI
spinner never stops; in the Err branch of load_avatar_texture write a sentinel
into egui temp storage (using the same egui::Id key pattern e.g.
"contact_avatar_data_{url}" or a dedicated "contact_avatar_error_{url}") and
call ctx_clone.request_repaint() so the UI thread can observe the failure; then
update render (the code that reads ColorImage from ctx.data()) to check for that
sentinel and set self.avatar_loading = false when it is present (and remove the
sentinel) so the spinner and retry guard are correctly reset.
src/backend_task/contested_names/query_dpns_contested_resources.rs (2)

167-168: ⚠️ Potential issue | 🟡 Minor

Stale comment: says "15 permits" but code uses 24.

📝 Fix the comment
-        // Create a semaphore with 15 permits
+        // Create a semaphore with 24 permits
         let semaphore = Arc::new(Semaphore::new(24));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/contested_names/query_dpns_contested_resources.rs` around
lines 167 - 168, Update the stale comment above the semaphore creation to match
the actual permit count: change the text "// Create a semaphore with 15 permits"
to reflect 24 permits (or rephrase generically) where the semaphore is
constructed via Arc::new(Semaphore::new(24)) so the comment matches the
semaphore variable and its initialization.

46-46: ⚠️ Potential issue | 🟠 Major

Bug: Retry counter resets on every continue, creating a potential infinite loop.

retries is declared at line 46 inside the loop (line 33). When continue executes at line 115, control jumps back to line 33, which re-executes let mut retries = 0;. The retries > MAX_RETRIES check at line 107 can therefore never become true, resulting in an infinite loop on persistent transient errors like "try another server".

Compare with query_dpns_vote_contenders.rs (lines 48-51) where retries is correctly declared before the loop.

🐛 Move `retries` before the outer loop
         const MAX_RETRIES: usize = 3;
         let mut start_at_value = None;
         let mut names_to_be_updated = Vec::new();
+        let mut retries = 0;
         loop {
             let query = VotePollsByDocumentTypeQuery {
                 contract_id: data_contract.id(),
@@ -43,9 +44,6 @@
             };

-            // Initialize retry counter
-            let mut retries = 0;
-
             let contested_resources = match ContestedResource::fetch_many(sdk, query.clone()).await
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/contested_names/query_dpns_contested_resources.rs` at line
46, The retry counter "retries" is being reset on every iteration because it's
declared inside the outer loop; move the declaration "let mut retries = 0;" to
immediately before the outer loop so it persists across iterations, remove any
duplicate declarations inside the loop, and ensure you still increment "retries"
on transient-error paths (the branch that currently does "continue" on "try
another server") and keep the existing "if retries > MAX_RETRIES" check to break
out when exceeded.
src/backend_task/platform_info.rs (1)

488-496: ⚠️ Potential issue | 🟡 Minor

Silent error discard in fallback path — add a warn! log.

The TotalCreditsInPlatform::fetch_current error is silently discarded. The PR migrates logging to tracing, so this fallback should emit at least a tracing::warn! before proceeding, making the degraded-mode transition observable in logs.

🛠️ Proposed fix
-                            Err(_) => {
+                            Err(e) => {
+                                tracing::warn!(
+                                    "Failed to fetch total credits for withdrawal limit, \
+                                     falling back to bare info: {}",
+                                    e
+                                );
                                 // Fall back to simple format without daily limits
                                 let formatted = format_withdrawal_documents_to_bare_info(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/platform_info.rs` around lines 488 - 496, The fallback
branch currently swallows the error from TotalCreditsInPlatform::fetch_current;
change the match arm to capture the error (Err(e)) and emit a tracing::warn!
that includes the error and context (e.g. that we are falling back to
format_withdrawal_documents_to_bare_info) before continuing to call
format_withdrawal_documents_to_bare_info and returning
PlatformInfoTaskResult::TextResult; reference the identifiers
TotalCreditsInPlatform::fetch_current, format_withdrawal_documents_to_bare_info,
BackendTaskSuccessResult::PlatformInfo, and PlatformInfoTaskResult::TextResult
so the warning is added in the correct match arm.
src/ui/identities/add_existing_identity_screen.rs (1)

58-61: ⚠️ Potential issue | 🟡 Minor

Remaining .expect() missed by this PR — will still panic on malformed YAML.

load_testnet_nodes_from_yml already handles fs::read_to_string gracefully with .ok()?, but a malformed .testnet_nodes.yml will still crash the process via .expect("expected proper yaml"). The function's return type is Option<TestnetNodes>, so .ok() is the correct fallback.

🛡️ Proposed fix
 fn load_testnet_nodes_from_yml(file_path: &str) -> Option<TestnetNodes> {
     let file_content = fs::read_to_string(file_path).ok()?;
-    serde_yaml_ng::from_str(&file_content).expect("expected proper yaml")
+    serde_yaml_ng::from_str(&file_content).ok()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/add_existing_identity_screen.rs` around lines 58 - 61, The
function load_testnet_nodes_from_yml currently calls
serde_yaml_ng::from_str(...).expect(...) which will panic on malformed YAML;
change that call to return an Option by using .ok() (or map the Result to
Option) so the function returns None on parse failure instead of panicking,
preserving the function's Option<TestnetNodes> contract and handling failures
consistently for TestnetNodes.
src/ui/tokens/direct_token_purchase_screen.rs (1)

286-310: ⚠️ Potential issue | 🟠 Major

Unguarded .expect("Expected a key") on line 302 can panic at runtime

selected_key can be None at confirmation time due to a developer-mode divergence:

  • has_keys check (line 415) uses public_keys().is_empty() in dev mode, enabling the purchase flow if ANY key exists
  • But selected_key is initialized (line 82) with only CRITICAL-level keys via get_first_public_key_matching
  • An identity with non-CRITICAL keys but no CRITICAL key will reach confirmation with selected_key = None and crash

The confirmation flow (lines 582, 267–310) validates amount and price but never checks that selected_key is Some(...) before the .expect() call.

Add a guard before line 302 to return an error state if selected_key is None, or validate the key earlier and prevent the confirmation dialog from appearing.

src/backend_task/identity/register_identity.rs (1)

112-114: ⚠️ Potential issue | 🟡 Minor

Two .expect() calls were missed by this PR.

Line 114self.core_client.read().expect("Core client lock was poisoned") is the exact same pattern that was converted to map_err(|e| format!(...))? in top_up_identity.rs lines 116-118 as part of this PR. The register path was overlooked:

🛡️ Proposed fix – line 114
-                            wallet
-                                .reload_utxos(
-                                    &self
-                                        .core_client
-                                        .read()
-                                        .expect("Core client lock was poisoned"),
-                                    self.network,
-                                    Some(self),
-                                )
-                                .map_err(|e| e.to_string())?;
+                            let core_client = self
+                                .core_client
+                                .read()
+                                .map_err(|e| format!("Core client lock was poisoned: {}", e))?;
+                            wallet
+                                .reload_utxos(&core_client, self.network, Some(self))
+                                .map_err(|e| e.to_string())?;

Line 294asset_lock_proof.create_identifier().expect("expected to create an identifier") panics if identifier derivation fails. Given create_identifier() returns Result, this should be propagated:

🛡️ Proposed fix – line 294
-        let identity_id = asset_lock_proof
-            .create_identifier()
-            .expect("expected to create an identifier");
+        let identity_id = asset_lock_proof
+            .create_identifier()
+            .map_err(|e| format!("Failed to derive identity ID from asset lock proof: {}", e))?;

Also applies to: 292-294

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

In `@src/backend_task/identity/register_identity.rs` around lines 112 - 114,
Replace the two panicking .expect() calls with proper error propagation: change
the self.core_client.read().expect("Core client lock was poisoned") usage in
register_identity.rs to handle the PoisonError by mapping it to a string (e.g.,
map_err(|e| format!("failed to acquire core_client lock: {}", e))?) and
returning the error via ?; likewise replace
asset_lock_proof.create_identifier().expect("expected to create an identifier")
with propagating the Result (e.g., map_err(|e| format!("failed to create
identifier: {}", e))?) so identifier creation errors are returned instead of
panicking, updating any surrounding function signature to propagate the error if
necessary.
src/database/wallet.rs (1)

867-869: ⚠️ Potential issue | 🟡 Minor

Remaining .expect() panic missed by this PR.

All analogous seed_hash.try_into() conversions in get_wallets (lines 450–452, 528–532, 713–715, 808–810, 920–922) were replaced with map_err, but this one in the identity-loading block was not updated. It will panic if the stored wallet seed hash blob is not exactly 32 bytes.

🐛 Proposed fix
-            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
-                .try_into()
-                .expect("Seed hash should be 32 bytes");
+            let wallet_seed_hash_array: [u8; 32] = wallet_seed_hash
+                .try_into()
+                .map_err(|_| {
+                    rusqlite::Error::InvalidParameterName(
+                        "Wallet seed hash should be 32 bytes".to_string(),
+                    )
+                })?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/wallet.rs` around lines 867 - 869, Replace the panic-causing
conversion of wallet_seed_hash to a 32-byte array in the identity-loading block:
instead of using wallet_seed_hash.try_into().expect(...), perform the same
fallible conversion used elsewhere (use try_into().map_err(...) and propagate
the error) so malformed/incorrect-length seed blobs return a proper error rather
than panicking; update the code around wallet_seed_hash_array and its creation
to mirror the map_err-based handling used in get_wallets for the other
seed_hash.try_into() occurrences.
src/app.rs (1)

1060-1063: ⚠️ Potential issue | 🟡 Minor

Replace .unwrap() with .unwrap_or_default() for consistency.

This instance at line 1062 was missed while other ~50 instances of duration_since(UNIX_EPOCH) across the codebase were updated to .unwrap_or_default(). This is the only remaining .unwrap() call on this pattern in the repository.

Proposed fix
             let current_time = SystemTime::now()
                 .duration_since(SystemTime::UNIX_EPOCH)
-                .unwrap()
+                .unwrap_or_default()
                 .as_millis() as u64;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app.rs` around lines 1060 - 1063, Replace the panic-prone unwrap() call
used when computing current_time with unwrap_or_default() to match the rest of
the codebase: locate the
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) chain that assigns
current_time and call unwrap_or_default() instead of unwrap() so a zero/default
Duration is returned on error; keep the cast to as_millis() as u64 and the
current_time binding name unchanged.
🧹 Nitpick comments (14)
src/components/core_p2p_handler.rs (1)

310-311: Two remaining .unwrap() calls on try_into() were not addressed by this PR.

// Line 310
while u32::from_le_bytes(header_buf[0..4].try_into().unwrap()) != self.network.magic() {
// Line 341
let payload_len_u32 = u32::from_le_bytes(header_buf[16..20].try_into().unwrap());

Both are infallible in practice (the slice lengths are fixed by the compile-time [u8; 24] buffer), so they will never actually panic. But for consistency with the PR's stated goal of removing .unwrap(), consider replacing with direct byte indexing, which is truly panic-free without relying on a runtime invariant:

♻️ Proposed cleanup using direct byte indexing
-        while u32::from_le_bytes(header_buf[0..4].try_into().unwrap()) != self.network.magic() {
+        while u32::from_le_bytes([header_buf[0], header_buf[1], header_buf[2], header_buf[3]]) != self.network.magic() {
-        let payload_len_u32 = u32::from_le_bytes(header_buf[16..20].try_into().unwrap());
+        let payload_len_u32 = u32::from_le_bytes([header_buf[16], header_buf[17], header_buf[18], header_buf[19]]);

Also applies to: 341-341

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

In `@src/components/core_p2p_handler.rs` around lines 310 - 311, Replace the
remaining try_into().unwrap() uses on header_buf slices with explicit,
panic-free byte indexing: instead of calling
header_buf[0..4].try_into().unwrap() and header_buf[16..20].try_into().unwrap(),
build the 4-byte arrays by directly reading header_buf[0], header_buf[1],
header_buf[2], header_buf[3] and header_buf[16], header_buf[17], header_buf[18],
header_buf[19] respectively; then feed those arrays to u32::from_le_bytes so the
comparisons against self.network.magic() and the payload_len_u32 computation are
done without unwraps or reliance on slice-to-array conversion.
src/ui/network_chooser_screen.rs (2)

2040-2055: LGTM — unwrap_or_default() prevents the panic; consider an explicit if let Ok(...) guard for clarity.

The substitution is correct: Duration::ZERO is the natural sentinel when the clock is before UNIX_EPOCH. Under that condition current_time = 0, so 0 >= recheck_time (> 0) is always false and GetBestChainLocks is silently suppressed until the clock normalises — which is far better than a panic. No data is lost; the next normal-clock frame resumes scheduling correctly.

An optional alternative that makes the suppression intention explicit:

♻️ Optional: explicit `if let Ok(...)` guard
-        let current_time = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .unwrap_or_default();
-        if let Some(time) = self.recheck_time {
+        if let Ok(current_time) = SystemTime::now().duration_since(UNIX_EPOCH) {
+        if let Some(time) = self.recheck_time {
             if current_time.as_millis() as u64 >= time {
                 action = AppAction::BackendTask(BackendTask::CoreTask(
                     CoreTask::GetBestChainLocks,
                 ));
                 self.recheck_time = Some((current_time + recheck_time).as_millis() as u64);
             }
         } else {
             self.recheck_time = Some((current_time + recheck_time).as_millis() as u64);
         }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/network_chooser_screen.rs` around lines 2040 - 2055, Replace the
current use of unwrap_or_default() when computing current_time with an explicit
success guard: call SystemTime::now().duration_since(UNIX_EPOCH) and use if let
Ok(duration) = ... to bind current_time only when the clock is >= UNIX_EPOCH;
keep the existing logic that checks self.recheck_time and schedules
AppAction::BackendTask(CoreTask::GetBestChainLocks) and updates
self.recheck_time, and set self.recheck_time = None in the else branch as
before—this makes the suppression of scheduling on pre-epoch clock explicit
while preserving the current behavior of self.recheck_time and the
GetBestChainLocks scheduling.

886-910: Three residual .expect() panics in the render path — inconsistent with this PR's stated goals.

Lines 889, 909, and 959 all call self.save().expect("Expected to save db settings") from inside render_network_table, which is invoked from ui() every frame. If the SQLite layer returns an error (e.g., disk full, locked DB), the application panics in the middle of a render pass. This directly contradicts the PR's explicit goal of replacing .expect() with proper error propagation throughout the codebase.

♻️ Proposed fix — surface save errors via the existing `custom_dash_qt_error_message` field (or tracing)
-                                self.save().expect("Expected to save db settings");
+                                if let Err(e) = self.save() {
+                                    tracing::error!("Failed to save settings: {e}");
+                                    self.custom_dash_qt_error_message = Some(format!("Failed to save: {e}"));
+                                }

Apply the same pattern at lines 909 and 959 (the "Clear" button and the overwrite_dash_conf checkbox).

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

In `@src/ui/network_chooser_screen.rs` around lines 886 - 910, Replace the
in-render .expect() calls on self.save() inside render_network_table/ui flow
with non-panicking error handling: call self.save() and match the Result; on Ok
do nothing, on Err set self.custom_dash_qt_error_message (or another
user-visible error field) to a concise message including the error (use
format!("{:?}", err) or tracing::error!) so the UI surfaces failures instead of
panicking. Apply this pattern for the branches that modify
self.custom_dash_qt_path (the file-picker accept path), the Clear button handler
that currently sets custom_dash_qt_path, and the overwrite_dash_conf checkbox
handler so all save() invocations in render_network_table are handled
gracefully. Ensure you still return early or keep UI state consistent after an
error and use tracing::error! where appropriate for additional diagnostics.
src/database/contacts.rs (1)

63-63: conn.lock().unwrap() remains a panic path — consider mapping to a rusqlite::Error.

Both functions still call .unwrap() on the mutex guard. While mutex poisoning is uncommon, it contradicts the PR's goal of eliminating all panic-prone calls. You can map the PoisonError to a rusqlite::Error:

♻️ Proposed refactor (same pattern for line 94)
-        let conn = self.conn.lock().unwrap();
+        let conn = self.conn.lock().map_err(|e| {
+            rusqlite::Error::InvalidParameterName(format!("mutex poisoned: {e}"))
+        })?;

Also applies to: 94-94

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

In `@src/database/contacts.rs` at line 63, Replace the panic-prone
conn.lock().unwrap() calls in contacts.rs with a non-panicking conversion: in
the functions that call conn.lock().unwrap() (e.g., the method containing the
line with conn.lock().unwrap() at ~63 and the other at ~94), map a PoisonError
into a rusqlite::Error (for example by using conn.lock().map_err(|e|
rusqlite::Error::ToSqlConversionFailure(Box::new(e))) or another appropriate
rusqlite::Error variant) and propagate that Result instead of unwrapping; update
the function return types to return rusqlite::Result or convert the mapped error
into the existing error path so the mutex-poison case becomes a handled
rusqlite::Error rather than a panic.
src/ui/dashpay/contact_profile_viewer.rs (1)

146-146: Remove the unused _texture_id local — it's dead computation.

_texture_id is computed but never read; the actual texture key is re-derived at line 260. The _ prefix silences the compiler warning, but the allocation still happens on every load_avatar_texture call.

♻️ Proposed fix
 fn load_avatar_texture(&mut self, ctx: &egui::Context, url: &str) {
-    let _texture_id = format!("contact_avatar_{}", url);
     let ctx_clone = ctx.clone();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/dashpay/contact_profile_viewer.rs` at line 146, Remove the dead
allocation for `_texture_id` in load_avatar_texture by deleting the line that
formats "contact_avatar_{}" into `_texture_id`; the texture key is re-derived
later (see where the texture key is computed again around the subsequent code),
so eliminating this unused local removes the unnecessary string allocation and
keeps behavior unchanged.
src/backend_task/contested_names/query_dpns_vote_contenders.rs (1)

47-131: Consider adding a brief delay between retries.

The retry loop re-issues the request immediately on transient errors ("try another server"). A short tokio::time::sleep with exponential backoff would avoid hammering the server, especially when the error explicitly suggests trying another server.

♻️ Example: add backoff between retries
+use tokio::time::{sleep, Duration};
 // ...
                     retries += 1;
                     if retries > MAX_RETRIES {
                         // ...
                     } else {
+                        sleep(Duration::from_millis(500 * retries as u64)).await;
                         continue;
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/contested_names/query_dpns_vote_contenders.rs` around lines
47 - 131, The retry loop (MAX_RETRIES / retries) that calls
ContenderWithSerializedDocument::fetch_many currently retries immediately; add a
short exponential backoff before continuing to avoid hammering servers: when the
error branch that checks error_str.contains("try another server") ||
error_str.contains("contract not found when querying from value with contract
info") increments retries and decides to continue, insert an awaited
tokio::time::sleep with a computed duration (e.g., base_delay_ms << (retries -
1) capped to a max) so the function (query_dpns_vote_contenders / the
surrounding async function that calls fetch_many and then
insert_or_update_contenders) pauses before the next loop iteration; ensure
imports/use of tokio::time::sleep and that the delay calculation is bounded to
prevent overflow.
src/backend_task/platform_info.rs (2)

201-340: Significant code duplication between the two helper functions — consider merging.

The per-document formatting closure (timestamp → amount → status → owner_id → address) is byte-for-byte identical between format_withdrawal_documents_with_daily_limit (Lines 218–257) and format_withdrawal_documents_to_bare_info (Lines 292–331). The only behavioural difference is whether a pre-computed daily_withdrawal_limit block is appended to the output. Merging them via an Option<Credits> parameter would eliminate the duplication without changing any observable behaviour:

♻️ Suggested refactor: merge into a single function
-fn format_withdrawal_documents_with_daily_limit(
-    withdrawal_documents: &[Document],
-    total_credits_on_platform: Credits,
-    network: Network,
-) -> Result<String, String> {
-    // ... identical body ...
-}
-
-fn format_withdrawal_documents_to_bare_info(
-    withdrawal_documents: &[Document],
-    network: Network,
-) -> Result<String, String> {
-    // ... identical body, minus daily limit section ...
-}
+fn format_withdrawal_documents(
+    withdrawal_documents: &[Document],
+    total_credits_on_platform: Option<Credits>, // None → bare info, Some → include daily limit
+    network: Network,
+) -> Result<String, String> {
+    let total_amount: Credits = withdrawal_documents
+        .iter()
+        .map(|document| {
+            document
+                .properties()
+                .get_integer::<Credits>(AMOUNT)
+                .map_err(|e| format!("Failed to get withdrawal amount: {}", e))
+        })
+        .collect::<Result<Vec<Credits>, String>>()?
+        .into_iter()
+        .sum();
+
+    let amounts: Vec<String> = withdrawal_documents
+        .iter()
+        .map(|document| {
+            // ... shared per-document closure ...
+        })
+        .collect::<Result<Vec<String>, String>>()?;
+
+    let daily_limit_section = match total_credits_on_platform {
+        Some(total_credits) => {
+            let limit = daily_withdrawal_limit(total_credits, PlatformVersion::latest())
+                .map_err(|e| format!("Failed to calculate daily withdrawal limit: {}", e))?;
+            format!(
+                "Daily Withdrawal Limit: {:.8} Dash\n         Remaining Today: {:.8} Dash\n\n         ",
+                limit as f64 / (dash_to_credits!(1) as f64),
+                limit as f64 / (dash_to_credits!(1) as f64), // TODO: subtract actual 24h amount
+            )
+        }
+        None => String::new(),
+    };
+
+    Ok(format!(
+        "Withdrawal Information:\n\n\
+         Total Amount: {:.8} Dash\n\
+         {}Recent Withdrawals:\n    {}",
+        total_amount as f64 / (dash_to_credits!(1) as f64),
+        daily_limit_section,
+        amounts.join("\n    ")
+    ))
+}

Call sites become:

// With daily limit:
format_withdrawal_documents(&withdrawal_docs, Some(total_credits.0), self.network)?
// Without:
format_withdrawal_documents(&withdrawal_docs, None, self.network)?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/platform_info.rs` around lines 201 - 340, Both functions
format_withdrawal_documents_with_daily_limit and
format_withdrawal_documents_to_bare_info duplicate the per-document formatting
logic; merge them into one function (e.g., format_withdrawal_documents) that
takes withdrawal_documents: &[Document], network: Network, and an
Option<Credits> (daily_limit_opt) to control whether the daily withdrawal limit
block is computed/appended. Move the shared closure that builds each formatted
line into this new function (reuse the same extraction of created_at → utc/local
conversion, AMOUNT, STATUS, owner_id, OUTPUT_SCRIPT → Address, and status
parsing) and compute total_amount once; when daily_limit_opt is
Some(total_credits_on_platform) calculate daily_withdrawal_limit and include the
extra lines, otherwise omit them; update all call sites to pass
Some(total_credits) or None accordingly.

503-649: RecentlyCompletedWithdrawals inline loop repeats the same per-document pattern a third time.

The closure at Lines 574–627 is structurally the same as those inside the two helper functions above, differing only in using updated_at instead of created_at and adding transaction_index. If the refactor suggested above is adopted, this arm could call the same shared helper (with an extra parameter for the extra fields), further reducing duplication.

This is a non-blocking observation—no action required if the helper-merge refactor is deferred.

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

In `@src/backend_task/platform_info.rs` around lines 503 - 649,
RecentlyCompletedWithdrawals arm duplicates per-document formatting logic;
extract a shared helper (e.g., format_withdrawal_documents) that accepts the
document list and parameters for timestamp field (created_at vs updated_at) and
optional extra fields like transaction_index, then replace the inline closure in
PlatformInfoTaskRequestType::RecentlyCompletedWithdrawals with a call to that
helper; ensure the helper reuses the same steps (parse amount, status, owner_id,
output_script → Address, timestamp conversion) and accepts a flag or callback to
include transaction_index in the output string so this arm only passes the "use
updated_at + include transaction_index" options.
src/ui/identities/transfer_screen.rs (1)

83-86: Pre-existing .expect() in TransferScreen::new() not addressed by this PR

Line 85 still panics if load_local_qualified_identities() fails. The PR converts all the time-related panics in this file, but this constructor panic would crash the app (not just display an error) if the DB call fails. Consider propagating the error or defaulting to an empty Vec with a logged warning, consistent with the refresh() method at line 508–513 that already handles this gracefully.

♻️ Suggested fix consistent with refresh()
-        let known_identities = app_context
-            .load_local_qualified_identities()
-            .expect("Identities not loaded");
+        let known_identities = app_context
+            .load_local_qualified_identities()
+            .unwrap_or_else(|e| {
+                tracing::warn!("Failed to load identities on TransferScreen init: {}", e);
+                Vec::new()
+            });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/transfer_screen.rs` around lines 83 - 86, In
TransferScreen::new(), remove the panic-causing expect on
app_context.load_local_qualified_identities() and instead handle failure the
same way refresh() does: call load_local_qualified_identities(), if it returns
an error log a warning and default known_identities to an empty Vec (or
propagate the Result from TransferScreen::new if that constructor is allowed to
return a Result); ensure you use the same logger and message style as refresh()
so the constructor no longer panics on DB errors.
src/database/initialization.rs (2)

179-183: Remaining .expect() panic in try_perform_migration contradicts the PR's goal.

try_perform_migration already returns Result<bool, String>, so the mutex-lock panic on line 182 can be propagated cleanly with map_err:

♻️ Proposed fix
-                let mut conn = self
-                    .conn
-                    .lock()
-                    .expect("Failed to lock database connection");
+                let mut conn = self
+                    .conn
+                    .lock()
+                    .map_err(|e| format!("Failed to lock database connection: {}", e))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/initialization.rs` around lines 179 - 183, The call to
.expect(...) when locking the mutex in try_perform_migration (the
self.conn.lock() line) causes a panic instead of returning an Err, contradicting
the function's Result<bool, String> signature; replace the expect with
propagating the lock error via map_err (or map_err(|e| format!(...))) so
try_perform_migration returns Err(String) when the mutex lock fails, then use
the resulting guard (e.g., conn) as before to proceed with migrations.

41-44: rusqlite::Error::InvalidParameterName is semantically wrong for this error path.

InvalidParameterName(String) means "error when using SQL named parameters and passing a parameter name not present in the SQL" — it has nothing to do with migration failures. Any caller (or future maintainer) that inspects or pattern-matches this error will be confused.

rusqlite::Error has no generic "execution failed" variant, but ToSqlConversionFailure(Box<dyn Error + Send + Sync>) is already used in this file's backup_db method (line 252) to carry arbitrary IO errors, so at minimum switching to that is consistent with the existing pattern. Ideally, initialize should return a richer error type (e.g. anyhow::Error or a project-defined enum), but that is a larger scope change.

♻️ Suggested minimal fix for consistency
-                    return Err(rusqlite::Error::InvalidParameterName(format!(
-                        "Database migration from version {} to {} failed (database is at version {}): {}",
-                        current_version, DEFAULT_DB_VERSION, version_after_migration, e
-                    )));
+                    return Err(rusqlite::Error::ToSqlConversionFailure(
+                        format!(
+                            "Database migration from version {} to {} failed \
+                             (database is at version {}): {}",
+                            current_version, DEFAULT_DB_VERSION, version_after_migration, e
+                        )
+                        .into(),
+                    ));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/initialization.rs` around lines 41 - 44, The migration failure
currently constructs rusqlite::Error::InvalidParameterName which is semantically
wrong; instead wrap the migration error into
rusqlite::Error::ToSqlConversionFailure like the pattern used in backup_db:
replace the Err(...) return in initialize where you reference current_version,
DEFAULT_DB_VERSION, version_after_migration and e with
Err(rusqlite::Error::ToSqlConversionFailure(Box::new(e))) so the underlying
error is preserved and consistent with existing error boxing semantics.
src/ui/identities/add_existing_identity_screen.rs (1)

872-905: Residual .unwrap() on testnet_loaded_nodes — guarded, but inconsistent with PR goal.

fill_random_hpmn and fill_random_masternode both call self.testnet_loaded_nodes.as_ref().unwrap(). These are guarded by the is_some() check in render_by_identity (line 150), so a panic is impossible in practice, but they're the only remaining unwrap() calls in this file not addressed by the PR. A straightforward fix is to use if let Some(nodes) = &self.testnet_loaded_nodes instead of asserting.

♻️ Proposed fix for fill_random_hpmn
 fn fill_random_hpmn(&mut self) {
-    if let Some((name, hpmn)) = self
-        .testnet_loaded_nodes
-        .as_ref()
-        .unwrap()
-        .hp_masternodes
+    let Some(nodes) = &self.testnet_loaded_nodes else { return };
+    if let Some((name, hpmn)) = nodes
+        .hp_masternodes
         .iter()
         .choose(&mut thread_rng())
     {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/add_existing_identity_screen.rs` around lines 872 - 905,
Replace the remaining .unwrap() on self.testnet_loaded_nodes in fill_random_hpmn
and fill_random_masternode with safe pattern matching: use if let Some(nodes) =
&self.testnet_loaded_nodes and then operate on nodes.hp_masternodes and
nodes.masternodes respectively; update the blocks in fill_random_hpmn and
fill_random_masternode to early-return / skip when testnet_loaded_nodes is None
so you no longer call unwrap() (keep the rest of the assignments to
identity_id_input, identity_type, alias_input, voting_private_key_input,
owner_private_key_input, payout_address_private_key_input as-is).
src/backend_task/identity/register_identity.rs (1)

323-327: LGTM — error is now properly propagated instead of panicking.

Optional micro-optimisation: existing_identity.clone() at line 323 clones the full Option<Identity>. Since you only need the value in the Some arm, you can avoid one full Identity clone in that branch and read more idiomatically:

♻️ Optional refactor
-        let identity = match existing_identity.clone() {
-            Some(id) => id,
-            None => Identity::new_with_id_and_keys(identity_id, public_keys, sdk.version())
-                .map_err(|e| format!("Failed to create identity: {}", e))?,
-        };
+        let identity = match &existing_identity {
+            Some(id) => id.clone(),
+            None => Identity::new_with_id_and_keys(identity_id, public_keys, sdk.version())
+                .map_err(|e| format!("Failed to create identity: {}", e))?,
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/identity/register_identity.rs` around lines 323 - 327, Avoid
cloning the entire Option by matching on existing_identity and only cloning the
contained Identity when needed; replace match existing_identity.clone() with
something like match existing_identity { Some(ref id) => id.clone(), None =>
Identity::new_with_id_and_keys(identity_id, public_keys,
sdk.version()).map_err(|e| format!("Failed to create identity: {}", e))?, } so
only the Some branch performs an Identity.clone() rather than cloning the whole
Option.
src/database/wallet.rs (1)

595-598: Consider logging when address rows are silently dropped.

The new map_err wrappers on lines 565–579 convert address-parsing failures into rusqlite::Errors. However, the pre-existing outer loop (if row.is_err() { continue; }) silently discards those errors with no log emission, making corrupted or mismatched addresses completely invisible in production logs.

🔍 Proposed fix to surface dropped errors
 for row in address_rows {
-    if row.is_err() {
-        continue;
-    }
+    if let Err(ref e) = row {
+        tracing::warn!(error = %e, "Skipping corrupted address row; wallet may have reduced address coverage");
+        continue;
+    }
     let (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/wallet.rs` around lines 595 - 598, The loop over address_rows is
silently dropping parse errors (those turned into rusqlite::Error by the map_err
wrappers on the address parsing code) — update the loop that currently does `if
row.is_err() { continue; }` to explicitly handle the Err case and emit a log
entry (e.g., via log::error! or the module's logger) including the error string
and context like which address/row failed, then continue; keep Ok(row) path
unchanged so valid rows are processed as before. Ensure you reference the
existing address_rows iterator and the parsing/map_err-produced error when
constructing the log message.
🤖 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/ui/identities/add_existing_identity_screen.rs`:
- Around line 1116-1120: The subtraction now - start_time can underflow if
SystemTime::now() falls back to 0; update the calculation that sets
elapsed_seconds (the code using SystemTime::now() -> .duration_since(UNIX_EPOCH)
-> as_secs()) to use saturating_sub to compute elapsed_seconds =
now.saturating_sub(start_time) (or otherwise clamp to 0) so the value never
underflows when now < start_time; modify the block around SystemTime::now(),
start_time and elapsed_seconds accordingly.

In `@src/ui/identities/transfer_screen.rs`:
- Around line 772-776: The subtraction calculating elapsed_seconds can underflow
if SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
yields 0 (pre-epoch path), causing a panic/wrap when computing now - start_time;
change the computation in transfer_screen.rs to use a safe subtraction (e.g.,
replace the direct subtraction of now - start_time with a saturating subtraction
on the u64 result) so elapsed_seconds is computed as
now.saturating_sub(start_time) (locate the SystemTime::now() call, the
unwrap_or_default() path, and the elapsed_seconds binding).

In `@src/ui/tokens/direct_token_purchase_screen.rs`:
- Around line 639-645: The subtraction now - start_time inside the
PurchaseTokensStatus::WaitingForResult arm can overflow if SystemTime::now()
fell back to 0; replace the direct u64 subtraction with a non-panicking
alternative (e.g., use saturating_sub or checked_sub with a fallback) so elapsed
= now.saturating_sub(start_time) (or now.checked_sub(start_time).unwrap_or(0));
update the same pattern wherever start_time is captured/used to ensure elapsed
is computed safely without panics or wrapping (references:
PurchaseTokensStatus::WaitingForResult, start_time, SystemTime::now(),
UNIX_EPOCH).

---

Outside diff comments:
In `@src/app.rs`:
- Around line 1060-1063: Replace the panic-prone unwrap() call used when
computing current_time with unwrap_or_default() to match the rest of the
codebase: locate the SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
chain that assigns current_time and call unwrap_or_default() instead of unwrap()
so a zero/default Duration is returned on error; keep the cast to as_millis() as
u64 and the current_time binding name unchanged.

In `@src/backend_task/contested_names/query_dpns_contested_resources.rs`:
- Around line 167-168: Update the stale comment above the semaphore creation to
match the actual permit count: change the text "// Create a semaphore with 15
permits" to reflect 24 permits (or rephrase generically) where the semaphore is
constructed via Arc::new(Semaphore::new(24)) so the comment matches the
semaphore variable and its initialization.
- Line 46: The retry counter "retries" is being reset on every iteration because
it's declared inside the outer loop; move the declaration "let mut retries = 0;"
to immediately before the outer loop so it persists across iterations, remove
any duplicate declarations inside the loop, and ensure you still increment
"retries" on transient-error paths (the branch that currently does "continue" on
"try another server") and keep the existing "if retries > MAX_RETRIES" check to
break out when exceeded.

In `@src/backend_task/identity/register_identity.rs`:
- Around line 112-114: Replace the two panicking .expect() calls with proper
error propagation: change the self.core_client.read().expect("Core client lock
was poisoned") usage in register_identity.rs to handle the PoisonError by
mapping it to a string (e.g., map_err(|e| format!("failed to acquire core_client
lock: {}", e))?) and returning the error via ?; likewise replace
asset_lock_proof.create_identifier().expect("expected to create an identifier")
with propagating the Result (e.g., map_err(|e| format!("failed to create
identifier: {}", e))?) so identifier creation errors are returned instead of
panicking, updating any surrounding function signature to propagate the error if
necessary.

In `@src/backend_task/platform_info.rs`:
- Around line 488-496: The fallback branch currently swallows the error from
TotalCreditsInPlatform::fetch_current; change the match arm to capture the error
(Err(e)) and emit a tracing::warn! that includes the error and context (e.g.
that we are falling back to format_withdrawal_documents_to_bare_info) before
continuing to call format_withdrawal_documents_to_bare_info and returning
PlatformInfoTaskResult::TextResult; reference the identifiers
TotalCreditsInPlatform::fetch_current, format_withdrawal_documents_to_bare_info,
BackendTaskSuccessResult::PlatformInfo, and PlatformInfoTaskResult::TextResult
so the warning is added in the correct match arm.

In `@src/database/contacts.rs`:
- Around line 101-111: In load_all_contact_private_info change the nullable
column reads for nickname and notes to fetch Option<String> instead of String
(use row.get::<_, Option<String>>(2) and row.get::<_, Option<String>>(3)) and
map/unwrap them to the desired value (e.g. unwrap_or_default() or .map(|s| s)
depending on ContactPrivateInfo field types) so NULLs no longer cause
Err(InvalidColumnType); update the ContactPrivateInfo construction in the
query_map closure accordingly.

In `@src/database/wallet.rs`:
- Around line 867-869: Replace the panic-causing conversion of wallet_seed_hash
to a 32-byte array in the identity-loading block: instead of using
wallet_seed_hash.try_into().expect(...), perform the same fallible conversion
used elsewhere (use try_into().map_err(...) and propagate the error) so
malformed/incorrect-length seed blobs return a proper error rather than
panicking; update the code around wallet_seed_hash_array and its creation to
mirror the map_err-based handling used in get_wallets for the other
seed_hash.try_into() occurrences.

In `@src/ui/dashpay/contact_profile_viewer.rs`:
- Around line 145-199: The async error path in load_avatar_texture doesn’t clear
self.avatar_loading so the UI spinner never stops; in the Err branch of
load_avatar_texture write a sentinel into egui temp storage (using the same
egui::Id key pattern e.g. "contact_avatar_data_{url}" or a dedicated
"contact_avatar_error_{url}") and call ctx_clone.request_repaint() so the UI
thread can observe the failure; then update render (the code that reads
ColorImage from ctx.data()) to check for that sentinel and set
self.avatar_loading = false when it is present (and remove the sentinel) so the
spinner and retry guard are correctly reset.

In `@src/ui/identities/add_existing_identity_screen.rs`:
- Around line 58-61: The function load_testnet_nodes_from_yml currently calls
serde_yaml_ng::from_str(...).expect(...) which will panic on malformed YAML;
change that call to return an Option by using .ok() (or map the Result to
Option) so the function returns None on parse failure instead of panicking,
preserving the function's Option<TestnetNodes> contract and handling failures
consistently for TestnetNodes.

---

Duplicate comments:
In `@src/database/contested_names.rs`:
- Around line 529-539: In insert_or_update_contenders, replace the four panic
sites by propagating errors instead of calling expect/unwrap: change the call to
contender.try_to_contender(...) to use ? and map its Err into a rusqlite::Error
(e.g., with map_err) rather than expect("expect a contender document
deserialization"); replace
deserialized_contender.document().as_ref().unwrap().clone() by checking the
Option and returning a rusqlite::Error when None; change
document.get("label").expect("expected name") to
document.get("label").ok_or_else(|| rusqlite::Error::Other(Box::new(...)))? with
a clear message; and replace .as_str().unwrap() with .as_str().ok_or_else(||
rusqlite::Error::Other(Box::new(...)))? so all failures are converted to
rusqlite::Error and propagated from insert_or_update_contenders.

---

Nitpick comments:
In `@src/backend_task/contested_names/query_dpns_vote_contenders.rs`:
- Around line 47-131: The retry loop (MAX_RETRIES / retries) that calls
ContenderWithSerializedDocument::fetch_many currently retries immediately; add a
short exponential backoff before continuing to avoid hammering servers: when the
error branch that checks error_str.contains("try another server") ||
error_str.contains("contract not found when querying from value with contract
info") increments retries and decides to continue, insert an awaited
tokio::time::sleep with a computed duration (e.g., base_delay_ms << (retries -
1) capped to a max) so the function (query_dpns_vote_contenders / the
surrounding async function that calls fetch_many and then
insert_or_update_contenders) pauses before the next loop iteration; ensure
imports/use of tokio::time::sleep and that the delay calculation is bounded to
prevent overflow.

In `@src/backend_task/identity/register_identity.rs`:
- Around line 323-327: Avoid cloning the entire Option by matching on
existing_identity and only cloning the contained Identity when needed; replace
match existing_identity.clone() with something like match existing_identity {
Some(ref id) => id.clone(), None => Identity::new_with_id_and_keys(identity_id,
public_keys, sdk.version()).map_err(|e| format!("Failed to create identity: {}",
e))?, } so only the Some branch performs an Identity.clone() rather than cloning
the whole Option.

In `@src/backend_task/platform_info.rs`:
- Around line 201-340: Both functions
format_withdrawal_documents_with_daily_limit and
format_withdrawal_documents_to_bare_info duplicate the per-document formatting
logic; merge them into one function (e.g., format_withdrawal_documents) that
takes withdrawal_documents: &[Document], network: Network, and an
Option<Credits> (daily_limit_opt) to control whether the daily withdrawal limit
block is computed/appended. Move the shared closure that builds each formatted
line into this new function (reuse the same extraction of created_at → utc/local
conversion, AMOUNT, STATUS, owner_id, OUTPUT_SCRIPT → Address, and status
parsing) and compute total_amount once; when daily_limit_opt is
Some(total_credits_on_platform) calculate daily_withdrawal_limit and include the
extra lines, otherwise omit them; update all call sites to pass
Some(total_credits) or None accordingly.
- Around line 503-649: RecentlyCompletedWithdrawals arm duplicates per-document
formatting logic; extract a shared helper (e.g., format_withdrawal_documents)
that accepts the document list and parameters for timestamp field (created_at vs
updated_at) and optional extra fields like transaction_index, then replace the
inline closure in PlatformInfoTaskRequestType::RecentlyCompletedWithdrawals with
a call to that helper; ensure the helper reuses the same steps (parse amount,
status, owner_id, output_script → Address, timestamp conversion) and accepts a
flag or callback to include transaction_index in the output string so this arm
only passes the "use updated_at + include transaction_index" options.

In `@src/components/core_p2p_handler.rs`:
- Around line 310-311: Replace the remaining try_into().unwrap() uses on
header_buf slices with explicit, panic-free byte indexing: instead of calling
header_buf[0..4].try_into().unwrap() and header_buf[16..20].try_into().unwrap(),
build the 4-byte arrays by directly reading header_buf[0], header_buf[1],
header_buf[2], header_buf[3] and header_buf[16], header_buf[17], header_buf[18],
header_buf[19] respectively; then feed those arrays to u32::from_le_bytes so the
comparisons against self.network.magic() and the payload_len_u32 computation are
done without unwraps or reliance on slice-to-array conversion.

In `@src/database/contacts.rs`:
- Line 63: Replace the panic-prone conn.lock().unwrap() calls in contacts.rs
with a non-panicking conversion: in the functions that call conn.lock().unwrap()
(e.g., the method containing the line with conn.lock().unwrap() at ~63 and the
other at ~94), map a PoisonError into a rusqlite::Error (for example by using
conn.lock().map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e))) or
another appropriate rusqlite::Error variant) and propagate that Result instead
of unwrapping; update the function return types to return rusqlite::Result or
convert the mapped error into the existing error path so the mutex-poison case
becomes a handled rusqlite::Error rather than a panic.

In `@src/database/initialization.rs`:
- Around line 179-183: The call to .expect(...) when locking the mutex in
try_perform_migration (the self.conn.lock() line) causes a panic instead of
returning an Err, contradicting the function's Result<bool, String> signature;
replace the expect with propagating the lock error via map_err (or map_err(|e|
format!(...))) so try_perform_migration returns Err(String) when the mutex lock
fails, then use the resulting guard (e.g., conn) as before to proceed with
migrations.
- Around line 41-44: The migration failure currently constructs
rusqlite::Error::InvalidParameterName which is semantically wrong; instead wrap
the migration error into rusqlite::Error::ToSqlConversionFailure like the
pattern used in backup_db: replace the Err(...) return in initialize where you
reference current_version, DEFAULT_DB_VERSION, version_after_migration and e
with Err(rusqlite::Error::ToSqlConversionFailure(Box::new(e))) so the underlying
error is preserved and consistent with existing error boxing semantics.

In `@src/database/wallet.rs`:
- Around line 595-598: The loop over address_rows is silently dropping parse
errors (those turned into rusqlite::Error by the map_err wrappers on the address
parsing code) — update the loop that currently does `if row.is_err() { continue;
}` to explicitly handle the Err case and emit a log entry (e.g., via log::error!
or the module's logger) including the error string and context like which
address/row failed, then continue; keep Ok(row) path unchanged so valid rows are
processed as before. Ensure you reference the existing address_rows iterator and
the parsing/map_err-produced error when constructing the log message.

In `@src/ui/dashpay/contact_profile_viewer.rs`:
- Line 146: Remove the dead allocation for `_texture_id` in load_avatar_texture
by deleting the line that formats "contact_avatar_{}" into `_texture_id`; the
texture key is re-derived later (see where the texture key is computed again
around the subsequent code), so eliminating this unused local removes the
unnecessary string allocation and keeps behavior unchanged.

In `@src/ui/identities/add_existing_identity_screen.rs`:
- Around line 872-905: Replace the remaining .unwrap() on
self.testnet_loaded_nodes in fill_random_hpmn and fill_random_masternode with
safe pattern matching: use if let Some(nodes) = &self.testnet_loaded_nodes and
then operate on nodes.hp_masternodes and nodes.masternodes respectively; update
the blocks in fill_random_hpmn and fill_random_masternode to early-return / skip
when testnet_loaded_nodes is None so you no longer call unwrap() (keep the rest
of the assignments to identity_id_input, identity_type, alias_input,
voting_private_key_input, owner_private_key_input,
payout_address_private_key_input as-is).

In `@src/ui/identities/transfer_screen.rs`:
- Around line 83-86: In TransferScreen::new(), remove the panic-causing expect
on app_context.load_local_qualified_identities() and instead handle failure the
same way refresh() does: call load_local_qualified_identities(), if it returns
an error log a warning and default known_identities to an empty Vec (or
propagate the Result from TransferScreen::new if that constructor is allowed to
return a Result); ensure you use the same logger and message style as refresh()
so the constructor no longer panics on DB errors.

In `@src/ui/network_chooser_screen.rs`:
- Around line 2040-2055: Replace the current use of unwrap_or_default() when
computing current_time with an explicit success guard: call
SystemTime::now().duration_since(UNIX_EPOCH) and use if let Ok(duration) = ...
to bind current_time only when the clock is >= UNIX_EPOCH; keep the existing
logic that checks self.recheck_time and schedules
AppAction::BackendTask(CoreTask::GetBestChainLocks) and updates
self.recheck_time, and set self.recheck_time = None in the else branch as
before—this makes the suppression of scheduling on pre-epoch clock explicit
while preserving the current behavior of self.recheck_time and the
GetBestChainLocks scheduling.
- Around line 886-910: Replace the in-render .expect() calls on self.save()
inside render_network_table/ui flow with non-panicking error handling: call
self.save() and match the Result; on Ok do nothing, on Err set
self.custom_dash_qt_error_message (or another user-visible error field) to a
concise message including the error (use format!("{:?}", err) or
tracing::error!) so the UI surfaces failures instead of panicking. Apply this
pattern for the branches that modify self.custom_dash_qt_path (the file-picker
accept path), the Clear button handler that currently sets custom_dash_qt_path,
and the overwrite_dash_conf checkbox handler so all save() invocations in
render_network_table are handled gracefully. Ensure you still return early or
keep UI state consistent after an error and use tracing::error! where
appropriate for additional diagnostics.

Comment on lines 1116 to 1120
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.unwrap_or_default()
.as_secs();
let elapsed_seconds = now - start_time;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

u64 underflow risk in now - start_time — use saturating_sub.

With unwrap_or_default() now able to return 0 for now, if the click timestamp was captured with a working clock (start_time ≈ 1_739_000_000) but now later evaluates to 0 (clock error at render time), the bare u64 subtraction now - start_time will panic in debug builds and wrap to ~u64::MAX in release builds, producing a nonsensical elapsed display.

🛡️ Proposed fix
-                            let elapsed_seconds = now - start_time;
+                            let elapsed_seconds = now.saturating_sub(*start_time);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.unwrap_or_default()
.as_secs();
let elapsed_seconds = now - start_time;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let elapsed_seconds = now.saturating_sub(start_time);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/add_existing_identity_screen.rs` around lines 1116 - 1120,
The subtraction now - start_time can underflow if SystemTime::now() falls back
to 0; update the calculation that sets elapsed_seconds (the code using
SystemTime::now() -> .duration_since(UNIX_EPOCH) -> as_secs()) to use
saturating_sub to compute elapsed_seconds = now.saturating_sub(start_time) (or
otherwise clamp to 0) so the value never underflows when now < start_time;
modify the block around SystemTime::now(), start_time and elapsed_seconds
accordingly.

Comment on lines 772 to 776
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.unwrap_or_default()
.as_secs();
let elapsed_seconds = now - start_time;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

now - start_time can underflow when unwrap_or_default() yields 0

now is u64; if the new unwrap_or_default() path fires (pre-epoch clock) it returns 0, while start_time holds the previously stored positive timestamp. The subtraction 0u64 - start_time panics in debug builds (arithmetic overflow) and silently wraps to ~u64::MAX in release builds — trading one impossible panic for another.

🛡️ Proposed fix using saturating_sub
-        let now = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .unwrap_or_default()
-            .as_secs();
-        let elapsed_seconds = now - start_time;
+        let now = SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .unwrap_or_default()
+            .as_secs();
+        let elapsed_seconds = now.saturating_sub(*start_time);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.unwrap_or_default()
.as_secs();
let elapsed_seconds = now - start_time;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let elapsed_seconds = now.saturating_sub(*start_time);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/identities/transfer_screen.rs` around lines 772 - 776, The subtraction
calculating elapsed_seconds can underflow if
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
yields 0 (pre-epoch path), causing a panic/wrap when computing now - start_time;
change the computation in transfer_screen.rs to use a safe subtraction (e.g.,
replace the direct subtraction of now - start_time with a saturating subtraction
on the u64 result) so elapsed_seconds is computed as
now.saturating_sub(start_time) (locate the SystemTime::now() call, the
unwrap_or_default() path, and the elapsed_seconds binding).

Comment on lines 639 to 645
PurchaseTokensStatus::WaitingForResult(start_time) => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.unwrap_or_default()
.as_secs();
let elapsed = now - start_time;
ui.label(format!("Purchasing... elapsed: {} seconds", elapsed));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

u64 subtraction on line 644 can still panic (or silently wrap) after the unwrap_or_default() change

.unwrap_or_default() returns Duration::ZERO when the system clock is before UNIX_EPOCH, making now = 0. If start_time was set to a real timestamp (e.g., 1_700_000_000), then 0u64 - 1_700_000_000u64 panics in debug mode (overflow check) and silently wraps to u64::MAX - N in release mode. The same unwrap_or_default() pattern is applied at line 290~ when start_time is captured, so both timestamps can independently fall back to 0 — the two calls are not guaranteed to either both fail or both succeed.

🛡️ Proposed fix
 PurchaseTokensStatus::WaitingForResult(start_time) => {
     let now = SystemTime::now()
         .duration_since(UNIX_EPOCH)
         .unwrap_or_default()
         .as_secs();
-    let elapsed = now - start_time;
+    let elapsed = now.saturating_sub(*start_time);
     ui.label(format!("Purchasing... elapsed: {} seconds", elapsed));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/tokens/direct_token_purchase_screen.rs` around lines 639 - 645, The
subtraction now - start_time inside the PurchaseTokensStatus::WaitingForResult
arm can overflow if SystemTime::now() fell back to 0; replace the direct u64
subtraction with a non-panicking alternative (e.g., use saturating_sub or
checked_sub with a fallback) so elapsed = now.saturating_sub(start_time) (or
now.checked_sub(start_time).unwrap_or(0)); update the same pattern wherever
start_time is captured/used to ensure elapsed is computed safely without panics
or wrapping (references: PurchaseTokensStatus::WaitingForResult, start_time,
SystemTime::now(), UNIX_EPOCH).

@PastaPastaPasta
Copy link
Member Author

@coderabbitai is this feedback relevant?

│ 9   │ src/app.rs:719-762                                                     │ UNRESOLVED │ Silent mainnet fallback — no UI notification added           │

  The comment flagged that current_app_context() silently falls back to mainnet when testnet/devnet/regtest context is None. The tracing::error! with "BUG:" prefix was added, but the user would still silently operate on mainnet with no UI
  indication. The suggestion was to return Result/Option or prevent selecting unavailable networks.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the relevant section of src/app.rs to check current_app_context() implementation
# and any related network context handling

# First, let's find the current_app_context function
echo "=== current_app_context() implementation ==="
ast-grep --pattern 'fn current_app_context($$$) {
  $$$
}'

echo -e "\n=== Lines 719-762 in src/app.rs ==="
sed -n '719,762p' src/app.rs | cat -n

echo -e "\n=== Looking for enforce_network_context_invariant ==="
ast-grep --pattern 'fn enforce_network_context_invariant($$$) {
  $$$
}'

echo -e "\n=== Looking for context_available_for_network ==="
ast-grep --pattern 'fn context_available_for_network($$$) {
  $$$
}'

echo -e "\n=== Looking for change_network implementation ==="
ast-grep --pattern 'fn change_network($$$) {
  $$$
}'

Length of output: 3503


🏁 Script executed:

#!/bin/bash
# Check if enforce_network_context_invariant is called in the update loop
echo "=== Searching for enforce_network_context_invariant calls ==="
rg -n "enforce_network_context_invariant" src/app.rs -A2 -B2

echo -e "\n=== Searching for context_available_for_network function ==="
rg -n "fn context_available_for_network" src/app.rs -A10

echo -e "\n=== Checking change_network for pre-validation ==="
rg -n "fn change_network" src/app.rs -A20

echo -e "\n=== Checking update method for enforce_network_context_invariant call ==="
rg -n "fn update\(" src/app.rs -A30 | head -50

Length of output: 3978


@PastaPastaPasta The feedback has been substantially addressed but not completely eliminated. Here's the current state:

What Was Added (Mitigations)

Three protective mechanisms were introduced:

  1. enforce_network_context_invariant() (lines 774-793):

    • Checks if the current network has an available context
    • If violated, it switches to mainnet AND shows a UI error message via display_message()
    • Called on every frame in update() (line 912)
  2. change_network() pre-validation (lines 844-850):

    • Prevents switching to unavailable networks entirely
    • Logs error and returns early if context unavailable
  3. context_available_for_network() (lines 764-772):

    • Helper to check availability before operations

What Remains (The "Last-Resort" Fallback)

The silent mainnet fallback in current_app_context() (lines 719-762) still exists, but:

  • It's now documented as a "last-resort safety to avoid hard crashes in production"
  • The comment states: "change_network() and enforce_network_context_invariant() keep this valid in normal flow"
  • It only logs tracing::error! with "BUG:" prefix but doesn't show UI notification itself

Assessment

Severity downgraded from HIGH to LOW-MEDIUM:

  • Primary concern mitigated: The invariant is actively enforced with UI notification every frame
  • Prevention: Users cannot select unavailable networks through normal UI flow
  • ⚠️ Remaining edge case: The fallback in current_app_context() itself is silent, but should only trigger in defensive/edge cases

The feedback is relevant for completeness, but the practical risk is now minimal because enforce_network_context_invariant() should catch and notify about any violations before users perform operations. The silent fallback would only trigger in timing edge cases between validation checks.

If you want zero tolerance for silent fallbacks, you could return Result from current_app_context() or panic in these branches (since they're marked as "BUG"), but the current defensive approach is reasonable for production reliability.


🧠 Learnings used
Learnt from: lklimek
Repo: dashpay/dash-evo-tool PR: 264
File: src/database/wallet.rs:95-99
Timestamp: 2025-05-13T06:55:34.019Z
Learning: Error handling refactoring is needed across the Dash-EVO-Tool (DET) codebase, particularly to avoid panics with `.expect()` and instead propagate errors properly using the `?` operator.

Learnt from: pauldelucia
Repo: dashpay/dash-evo-tool PR: 16
File: src/app.rs:105-111
Timestamp: 2024-10-22T12:56:05.355Z
Learning: Existing error handling in `AppState::new` in `src/app.rs` for configuration loading is sufficient; additional error handling is unnecessary because errors are logged in `explorer.log`, displayed in the UI on the `NetworkChooserScreen`, and the terminal displays a clear error message if neither configuration loads.

Learnt from: asalomeri
Repo: dashpay/dash-evo-tool PR: 196
File: src/aaaa:16-18
Timestamp: 2025-03-19T14:04:00.597Z
Learning: In Rust Bitcoin applications, proper error handling with the `?` operator should be used when parsing sensitive information like mnemonics from environment variables, rather than using `unwrap()` or `expect()` which can cause runtime panics.

Learnt from: asalomeri
Repo: dashpay/dash-evo-tool PR: 196
File: src/aaaa:16-18
Timestamp: 2025-03-19T14:02:15.588Z
Learning: For Bitcoin transactions in this project, amounts should be parameterized instead of hardcoded, and proper error handling should be used with `?` instead of `unwrap()`.

Copy link
Contributor

@lklimek lklimek left a comment

Choose a reason for hiding this comment

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

Follow up #612

@PastaPastaPasta PastaPastaPasta merged commit c68b17e into v1.0-dev Feb 19, 2026
4 checks passed
@PastaPastaPasta PastaPastaPasta deleted the fix/replace-panics-with-error-propagation branch February 19, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants