fix: replace expect()/unwrap() panics with error propagation#562
fix: replace expect()/unwrap() panics with error propagation#562PastaPastaPasta merged 28 commits intov1.0-devfrom
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughWidespread 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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()andinitialize_sdk()) to returnResultand 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 returnsResult<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 returnsErr(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.
| ExtendedPubKey::decode(&master_ecdsa_bip44_account_0_epk_bytes).map_err(|e| { | ||
| rusqlite::Error::InvalidParameterName(format!( | ||
| "Failed to decode ExtendedPubKey: {}", | ||
| e | ||
| )) | ||
| })?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | 🟠 MajorRemaining
.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 ifsigning_keyisNone. 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 | 🟡 MinorInconsistent NULL handling with
load_contact_private_info.
load_contact_private_info(lines 76-78) now safely handles NULLnickname/notescolumns viaOption<String>, but this function still uses directrow.get::<_, String>which will returnInvalidColumnTypeerror 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 | 🟡 MinorPre-existing bug:
retriescounter resets on every outer-loop iteration.
retriesis initialized to0at line 46 inside the paginationloop. When a retryable error triggerscontinue(line 115), control returns to line 33, re-executing line 46 and resettingretriesto0. The retry limit ofMAX_RETRIESis therefore never reached.Not introduced by this PR, but worth fixing separately — move
retriesbefore the outerloopor 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()).awaitAlso applies to: 46-47, 101-116
src/database/contested_names.rs (1)
493-503:⚠️ Potential issue | 🟡 MinorRemaining
expect()/unwrap()calls ininsert_or_update_contenderspanic 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()onas_str()These should be converted to proper error propagation with
map_err/ok_orand?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 | 🟠 MajorUpdate the "empty insight URL" test to expect Err instead of panic
insight_api_uri()returnsResult<Uri, String>, so the#[should_panic]test will fail. Update it to assertis_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 | 🟡 MinorUse
saturating_subto guard against underflow in elapsed time calculationsThe
unwrap_or_default()call returnsDuration::ZEROifSystemTime::now()fails (e.g., clock issues), resulting innow = 0. Whenstart_timeis captured at a real timestamp,now - start_timewill underflow—panicking in debug mode and wrapping to a huge value in release mode. Usesaturating_subto safely handle this edge case.This affects the elapsed time calculations in:
- Line 281:
FetchingNoncematch arm- Line 293:
Broadcastingmatch arm- Line 305:
ProofErrormatch 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 equalsdaily_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 | 🟡 MinorUnsigned subtraction can still panic if
now < start_time.With
unwrap_or_default(), if the second call defaults to0whilestart_timeholds a real timestamp,now - start_timewill underflow and panic — defeating the purpose of removingexpect(). Usesaturating_subfor 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 | 🟡 MinorPotential u64 underflow on
now - start_time.If
SystemTime::now().duration_since(UNIX_EPOCH)fails and defaults to 0, thennowis0whilestart_timeholds a real epoch timestamp. The subtractionnow - start_timewill panic in debug mode (overflow) or silently wrap in release mode.Use
saturating_subto 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 | 🟡 MinorPotential u64 underflow on
now - start_time.If
SystemTime::now().duration_since(UNIX_EPOCH)returnsErrhere (defaultingnowto 0) but succeeded whenstart_timewas recorded at line 731, the subtraction on line 1148 will panic due to unsigned integer underflow.Use
saturating_subto be consistent with the defensive intent of theunwrap_or_defaultchange.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 overexpect(). However, ifnowever falls back to0(defaultDuration) whilestart_timeholds a real timestamp, Line 1119'snow - start_timewill underflow (u64subtraction). Usesaturating_subto 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
u64subtraction underflow whenunwrap_or_default()yields zero.If
SystemTime::now().duration_since(UNIX_EPOCH)fails here (returningDuration::ZERO→now = 0), butstart_timewas set to a real timestamp on line 302-306, thennow - start_timewill panic in debug or silently wrap in release. Usesaturating_subto 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 | 🟡 MinorGuard elapsed time against underflow.
If the clock moves backward (or defaults to zero),now - start_timecan 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 | 🟡 MinorGuard elapsed time against underflow with correct syntax.
If the clock moves backward,now - start_timecan 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 | 🟡 MinorGuard elapsed time against underflow.
A backward clock can causenow - start_timeto underflow and panic. Usesaturating_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 | 🟡 MinorGuard elapsed time against underflow using saturating arithmetic.
If the system clock is adjusted backward,nowcan be less thanstart_time, causing integer underflow and displaying incorrect elapsed time. Usesaturating_subinstead.🔧 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 | 🟡 MinorRequire a devnet name when resolving the cookie path for Devnet networks.
When
Network::Devnetis used with aNoneor emptydevnet_name, the current code silently uses an empty string, which results in the cookie path pointing to the base.dashcoredirectory 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 | 🟡 MinorGuard elapsed time against underflow with correct syntax.
If the system clock goes backward,now - start_timecan panic; usenow.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 | 🟡 MinorGuard elapsed time against underflow.
A backward clock (or zeroed duration) can makenow - start_timewrap; usesaturating_subto 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 | 🟡 MinorHarden elapsed calculation against system time failures
At line 618,
now - start_timecan underflow if the secondSystemTime::now()call fails and returns 0 viaunwrap_or_default(). Usesaturating_subto 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 | 🟡 MinorGuard elapsed time against underflow when
nowdefaults to 0If the clock jumps before UNIX_EPOCH after
start_timeis set,nowbecomes 0 andnow - start_timecan underflow in debug mode (panic) or wrap in release mode. Prefersaturating_subfor 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 | 🟡 MinorUse
saturating_subto prevent underflow in elapsed calculationWhen
SystemTime::now()fails,unwrap_or_default()yields 0. The subsequent subtractionnow - start_timeunderflows ifstart_timewas 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 | 🟡 MinorPrevent elapsed-time underflow from clock rollback
When
SystemTime::now().duration_since(UNIX_EPOCH)fails due to a clock rollback,unwrap_or_default()returns 0, makingelapsed = now - start_timeunderflow ifstart_timewas set to a positive value earlier. Usesaturating_subto 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 | 🟡 MinorUse saturating subtraction for elapsed time to prevent underflow
When
duration_since(UNIX_EPOCH)fails (e.g., system clock issues),unwrap_or_default()returnsDuration::ZERO, resulting innow = 0. Subtracting a non-zerostart_timefrom this causes an underflow. Usesaturating_subto safely handle this edge case, consistent with the pattern used in other UI screens likesend_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 | 🟡 MinorUse saturating_sub for elapsed time calculations to prevent underflow
When
SystemTime::now()defaults to a duration of 0 (viaunwrap_or_default()on error), the elapsed calculation can underflow. Applysaturating_subto 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 | 🟡 MinorUse saturating_sub for elapsed time calculation to prevent potential underflow
If the first
SystemTime::now()call succeeds but a later call fails and returnsunwrap_or_default()(Duration of 0 → 0 seconds), the subtraction0 - start_timewill underflow. Usesaturating_subto 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 | 🟡 MinorUse
saturating_subto prevent elapsed time underflowWhen
unwrap_or_default()returns0due toSystemTime::duration_since()failure, subtractingstart_timecan cause integer underflow. This results in a panic in debug mode or wrapped value in release mode. Usesaturating_subto 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: Usesaturating_subto guard against underflow.
now - start_timeon line 566 is unsigned arithmetic. Ifnowever resolves to 0 (via the new default) whilestart_timeholds a real timestamp, this will panic in debug or silently wrap in release. Usingsaturating_subis 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_idreturns[u8; 32], sodecrypted_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: InconsistentAddress::from_scripterror 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 informat_withdrawal_documents_to_bare_info(Line 318) and in theRecentlyCompletedWithdrawalsbranch (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 emptyVecwith 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. Ifselected_keyisNone, this panics. Consider returning an error to the UI instead (e.g., setself.error_messageand returnAppAction::None), consistent with the PR's intent.src/ui/identities/add_existing_identity_screen.rs (1)
59-59: Pre-existingexpect()inload_testnet_nodes_from_ymlis 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 returningNonehere 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()oncreate_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()ontoken_idsilently produces a zeroed identifier.If
token_id()returnsNone, the resultingTokenInfowill contain a default (all-zeros)Identifier, which could be confusing downstream (e.g., displayed in the UI or used in lookups). Consider matching onNoneand either skipping the token (consistent with the error handling above) or logging a warning.src/context/transaction_processing.rs (1)
243-254: Userusqlite::Error::UserFunctionErrorfor domain-level errors instead ofInvalidParameterName.
InvalidParameterNameis semantically meant for SQL parameter binding issues. Using it for "no credit outputs" or "address parsing failure" is misleading and inconsistent with the precedent insrc/database/wallet.rs, which convertsWalletErrorviaUserFunctionError(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 thefrom_byteserror, treating corruptawarded_tobytes asNone. 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 toPLATFORM_V11.The
_ => &PLATFORM_V11arm replaces what was likely apanic!/unreachable!. This is safer, but note that if a newNetworkvariant 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 atracing::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 fnif you add logging, so you'd need to decide whether theconstqualifier 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 + }
lklimek
left a comment
There was a problem hiding this comment.
See comments, ones prefixed with [HIGH] etc. are from Claude, others are mine.
Most important:
- We silently ignore errors, without notifying user that something went wrong. See https://github.com/dashpay/dash-evo-tool/pull/562/changes#r2797870053
- Copilot and rabbit also gave good feedback, please address AI's feedback before assigning ticket to me
- 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"); |
There was a problem hiding this comment.
[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).
There was a problem hiding this comment.
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.
Audit SummaryReviewed by: Claude Code with a 4-agent team:
The PR accomplishes its stated goal — replacing ~40 Key concern: silent mainnet fallback (HIGH)The most significant issue is the Issues outside the diff[HIGH] #[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. Positive observations
Findings Summary
Inline comments are posted as a separate pending review for in-diff findings. |
There was a problem hiding this comment.
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/expectpanics, these two calls are inconsistent:
load_local_qualified_identities().unwrap()— will panic if the DB query fails..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 | 🟠 MajorRemaining
.expect()/.unwrap()calls ininsert_or_update_contendersstill 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 | 🟡 MinorRemaining
.expect()on seed hash conversion in identity loading.This is the only seed hash conversion in
get_walletsthat still panics. All other seed hash conversions (lines 445, 525, 710, 805, 908) were converted tomap_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 inshow_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 whenwithdrawal_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_limitandformat_withdrawal_documents_to_bare_infousecollect::<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 usingfilter_mapto skip/log bad entries and still show the valid ones.src/database/wallet.rs (2)
438-449: Inconsistentrusqlite::Errorvariant usage within this file.Lines 438-443 use
InvalidParameterNamefor data conversion errors, while lines 534-538 correctly useFromSqlConversionFailurefor 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
FromSqlConversionFailurefor all data conversion failures (ExtendedPubKey decoding, seed hash conversion, address parsing, etc.) since that's the semantically correct variant.InvalidParameterNameis for SQL named-parameter binding mismatches.
592-604: Non-idiomatic error skip pattern.
row.is_err()followed byrow?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()onSystemTime— 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;
There was a problem hiding this comment.
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 | 🟠 MajorMissed
.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 truncatedwalletcolumn in theidentitytable 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 | 🟡 MinorSilent 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 callingrow?afterward is redundant — if you reach line 602,rowis guaranteedOk, 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 | 🟡 MinorInconsistent 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 propagatesrequire_networkerrors 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 | 🟡 MinorReplace
InvalidParameterNamewithFromSqlConversionFailurefor data conversion errors, and convert the remaining.expect()at line 862.Throughout this file,
rusqlite::Error::InvalidParameterNameis 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—InvalidParameterNameis 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 withType::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_namesandget_ongoing_contested_nameshave nearly identical ~80-line row-mapping closures (identifier parsing, state computation, contestant construction). The only difference is the SQLWHEREclause 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.
There was a problem hiding this comment.
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 | 🟡 MinorSilent error swallowing is inconsistent with the rest of
get_wallets.Steps 4–8 propagate row errors via
row?, but step 3 silently skips them withcontinue. 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 | 🟡 MinorDuplicate 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 | 🟠 MajorRemaining
.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 inshow_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 whenwithdrawal_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:InvalidParameterNameis semantically misleading for data corruption errors.Throughout this file,
rusqlite::Error::InvalidParameterNameis used to wrap deserialization/validation failures. This error variant is intended for SQL parameter binding issues, not data integrity errors. Consider usingFromSqlConversionFailure(as done elsewhere in this file, e.g., line 537) or the customWalletError→UserFunctionErrorpath 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.
lklimek
left a comment
There was a problem hiding this comment.
Not all comments are resolved. I manually checked some of them, and they are still there. See comments.
…t.rs data loading
…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>
de97545 to
b9d88af
Compare
There was a problem hiding this comment.
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_infohas the same NULL-handling gap that was just fixed inload_contact_private_info.Lines 106–107 use
row.get(2)?androw.get(3)?to extractStringfrom the samenickname TEXTandnotes TEXTcolumns that are nullable in the schema (noNOT NULLconstraint). If any row has aNULLfor either column rusqlite will returnErr(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_loadingis never reset tofalseon fetch failure — causes an infinite spinner.
self.avatar_loadingis set totrueat line 306 when the fetch is kicked off. On the success path (lines 279–296) the UI thread resets it tofalseafter the texture is created. However, on theErrpath (lines 195–197) the async task only logs and exits — nothing signals the UI thread to clearavatar_loading. The render loop'selsebranch (lines 313–319) then shows a spinner indefinitely, and theelse if !self.avatar_loadingguard (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_loadingaccordingly.🐛 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 thecolor_imagecheck and before theelse if !self.avatar_loadingbranch:+ // 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 | 🟡 MinorStale 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 | 🟠 MajorBug: Retry counter resets on every
continue, creating a potential infinite loop.
retriesis declared at line 46 inside theloop(line 33). Whencontinueexecutes at line 115, control jumps back to line 33, which re-executeslet mut retries = 0;. Theretries > MAX_RETRIEScheck 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) whereretriesis 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 | 🟡 MinorSilent error discard in fallback path — add a
warn!log.The
TotalCreditsInPlatform::fetch_currenterror is silently discarded. The PR migrates logging totracing, so this fallback should emit at least atracing::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 | 🟡 MinorRemaining
.expect()missed by this PR — will still panic on malformed YAML.
load_testnet_nodes_from_ymlalready handlesfs::read_to_stringgracefully with.ok()?, but a malformed.testnet_nodes.ymlwill still crash the process via.expect("expected proper yaml"). The function's return type isOption<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 | 🟠 MajorUnguarded
.expect("Expected a key")on line 302 can panic at runtime
selected_keycan beNoneat confirmation time due to a developer-mode divergence:
has_keyscheck (line 415) usespublic_keys().is_empty()in dev mode, enabling the purchase flow if ANY key exists- But
selected_keyis initialized (line 82) with only CRITICAL-level keys viaget_first_public_key_matching- An identity with non-CRITICAL keys but no CRITICAL key will reach confirmation with
selected_key = Noneand crashThe confirmation flow (lines 582, 267–310) validates amount and price but never checks that
selected_keyisSome(...)before the.expect()call.Add a guard before line 302 to return an error state if
selected_keyisNone, or validate the key earlier and prevent the confirmation dialog from appearing.src/backend_task/identity/register_identity.rs (1)
112-114:⚠️ Potential issue | 🟡 MinorTwo
.expect()calls were missed by this PR.Line 114 —
self.core_client.read().expect("Core client lock was poisoned")is the exact same pattern that was converted tomap_err(|e| format!(...))?intop_up_identity.rslines 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 294 —
asset_lock_proof.create_identifier().expect("expected to create an identifier")panics if identifier derivation fails. Givencreate_identifier()returnsResult, 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 | 🟡 MinorRemaining
.expect()panic missed by this PR.All analogous
seed_hash.try_into()conversions inget_wallets(lines 450–452, 528–532, 713–715, 808–810, 920–922) were replaced withmap_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 | 🟡 MinorReplace
.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 ontry_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 explicitif let Ok(...)guard for clarity.The substitution is correct:
Duration::ZEROis the natural sentinel when the clock is before UNIX_EPOCH. Under that conditioncurrent_time = 0, so0 >= recheck_time (> 0)is always false andGetBestChainLocksis 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 insiderender_network_table, which is invoked fromui()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 theoverwrite_dash_confcheckbox).🤖 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 arusqlite::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 thePoisonErrorto arusqlite::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_idlocal — it's dead computation.
_texture_idis 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 everyload_avatar_texturecall.♻️ 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 shorttokio::time::sleepwith 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) andformat_withdrawal_documents_to_bare_info(Lines 292–331). The only behavioural difference is whether a pre-computeddaily_withdrawal_limitblock is appended to the output. Merging them via anOption<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:RecentlyCompletedWithdrawalsinline 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_atinstead ofcreated_atand addingtransaction_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()inTransferScreen::new()not addressed by this PRLine 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 emptyVecwith a logged warning, consistent with therefresh()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 intry_perform_migrationcontradicts the PR's goal.
try_perform_migrationalready returnsResult<bool, String>, so the mutex-lock panic on line 182 can be propagated cleanly withmap_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::InvalidParameterNameis 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::Errorhas no generic "execution failed" variant, butToSqlConversionFailure(Box<dyn Error + Send + Sync>)is already used in this file'sbackup_dbmethod (line 252) to carry arbitrary IO errors, so at minimum switching to that is consistent with the existing pattern. Ideally,initializeshould return a richer error type (e.g.anyhow::Erroror 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()ontestnet_loaded_nodes— guarded, but inconsistent with PR goal.
fill_random_hpmnandfill_random_masternodeboth callself.testnet_loaded_nodes.as_ref().unwrap(). These are guarded by theis_some()check inrender_by_identity(line 150), so a panic is impossible in practice, but they're the only remainingunwrap()calls in this file not addressed by the PR. A straightforward fix is to useif let Some(nodes) = &self.testnet_loaded_nodesinstead 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 fullOption<Identity>. Since you only need the value in theSomearm, you can avoid one fullIdentityclone 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_errwrappers on lines 565–579 convert address-parsing failures intorusqlite::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.
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("Time went backwards") | ||
| .unwrap_or_default() | ||
| .as_secs(); | ||
| let elapsed_seconds = now - start_time; |
There was a problem hiding this comment.
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.
| 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.
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("Time went backwards") | ||
| .unwrap_or_default() | ||
| .as_secs(); | ||
| let elapsed_seconds = now - start_time; |
There was a problem hiding this comment.
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.
| 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).
| 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)); |
There was a problem hiding this comment.
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).
|
@coderabbitai is this feedback relevant? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 3503 🏁 Script executed: Length of output: 3978
What Was Added (Mitigations)Three protective mechanisms were introduced:
What Remains (The "Last-Resort" Fallback)The silent mainnet fallback in
AssessmentSeverity downgraded from HIGH to LOW-MEDIUM:
The feedback is relevant for completeness, but the practical risk is now minimal because If you want zero tolerance for silent fallbacks, you could return 🧠 Learnings used |
Summary
.expect()/.unwrap()/panic!()/unreachable!()/unimplemented!()calls across backend, database, context, and UI layers with proper error propagation or safe fallbacksResultinstead of panicking.expect()calls replaced with match blocks that log errors and returnNone, so a misconfigured network context fails gracefully instead of taking down the whole appdapi_address_list(),insight_api_uri(), andinitialize_sdk()now returnResultinstead of panicking on bad inputSystemTime::now().duration_since(UNIX_EPOCH).expect()replaced with.unwrap_or_default()across 26 files;unimplemented!()in marketplace settings replaced with informational labelMotivation
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:
Result/Optionfrom fallible functionsmatchblocks withtracing::error!logging before returningNone.unwrap_or_default()for non-critical timestamp operationsunreachable!()with safe default branchesTest plan
cargo buildpassescargo clippy --all-features --all-targets -- -D warningspasses cleancargo +nightly fmt --allapplied🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
UI
Tests