Skip to content

fix: identity & token screen safety and validation#563

Merged
PastaPastaPasta merged 9 commits into
v1.0-devfrom
fix/identity-token-screen-safety
Feb 19, 2026
Merged

fix: identity & token screen safety and validation#563
PastaPastaPasta merged 9 commits into
v1.0-devfrom
fix/identity-token-screen-safety

Conversation

@PastaPastaPasta
Copy link
Copy Markdown
Member

@PastaPastaPasta PastaPastaPasta commented Feb 12, 2026

Summary

  • Replace panic/unwrap() with proper error propagation for unsupported identity key types in IdentityKeys::to_public_keys_map()
  • Add validation for missing signing key in 8 token screens (claim, destroy, freeze, mint, pause, resume, transfer, unfreeze) — shows user-friendly error instead of panicking
  • Handle deleted identity gracefully in transfer/withdraw screen refresh
  • Log silenced DB errors (.ok() / let _ =) in identity deletion, contact save, and contact list operations with tracing::warn!
  • Enforce security level validation for ENCRYPTION/DECRYPTION key purposes in Identity Create screen (auto-sets MEDIUM, locks selection)
  • Replace expect() on DocumentQuery creation in query_tokens.rs with map_err + ? error propagation
  • Replace unwrap() on identity/key submission in token creator with validation check and user error message

Test plan

  • Verify identity creation with ECDSA keys still works
  • Verify token creation flow with selected identity + key works
  • Verify adding ENCRYPTION/DECRYPTION purpose keys auto-sets security level to MEDIUM
  • Verify transfer/withdraw screens handle missing identity without crash
  • Run cargo clippy --all-features --all-targets -- -D warnings (passes clean)

Summary by CodeRabbit

  • Bug Fixes

    • Hardened global error handling: many operations now return and surface errors instead of panicking.
    • Added precondition checks for missing signing keys across token flows to prevent crashes and show clear errors.
    • Database operations for contacts and identity deletes now log warnings on failure instead of being ignored.
  • Improvements

    • Safer identity refresh/replace logic in transfer/withdraw flows to avoid unintended overwrites.
    • Automatic security-level adjustment when a key's purpose changes for clearer UX.
    • Query construction and parsing now fail gracefully with user-facing errors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Convert several panic/unwrap cases to propagated errors or guarded early returns; add logging for previously ignored DB results; validate signing-key and identity selection in multiple token/UI flows; and enforce detailed validation when converting identity keys to public key maps.

Changes

Cohort / File(s) Summary
Backend identity validation & propagation
src/backend_task/identity/mod.rs, src/backend_task/identity/register_identity.rs
IdentityKeys::to_public_keys_map now returns Result<..., String> with per-key purpose/security validation and descriptive Err paths; callers updated to use ? and propagate errors instead of assuming success.
Backend query error propagation
src/backend_task/tokens/query_tokens.rs
Replaced .expect("create query") with map_err(...)? so DocumentQuery::new failures become propagated error strings rather than panics.
DB result handling / logging
src/ui/dashpay/contacts_list.rs, src/ui/identities/identities_screen.rs
Previously-ignored Results from DB ops are now checked; failures are logged as warnings and, where applicable, surfaced to UI state instead of silently discarding or panicking.
Identity UI updates
src/ui/identities/add_new_identity_screen/mod.rs, src/ui/identities/transfer_screen.rs, src/ui/identities/withdraw_screen.rs
Auto-adjust security_level when a key's purpose changes; replace unwraps with safe conditional refresh logic that preserves existing state when refresh yields no match.
Token flows — signing-key & parsing guards
src/ui/tokens/..., src/ui/tokens/tokens_screen/token_creator.rs
Across many token-related screens (claim, destroy_frozen_funds, freeze, mint, pause, resume, transfer, unfreeze, token_creator): added precondition checks for selected signing key/identity; replace expect/unwrap with guarded parsing, local bindings, and user-facing error/status on missing/invalid inputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through lines with careful care,
Replaced bold panics with a gentle snare.
Keys now checked and errors shown,
Warnings whisper where silence had grown.
A careful rabbit guards your code — hooray! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (29 files):

⚔️ .claude/settings.json (content)
⚔️ .github/workflows/claude.yml (content)
⚔️ CLAUDE.md (content)
⚔️ Cargo.lock (content)
⚔️ Cargo.toml (content)
⚔️ src/backend_task/core/mod.rs (content)
⚔️ src/backend_task/identity/mod.rs (content)
⚔️ src/backend_task/identity/register_identity.rs (content)
⚔️ src/backend_task/tokens/query_tokens.rs (content)
⚔️ src/context/connection_status.rs (content)
⚔️ src/spv/manager.rs (content)
⚔️ src/ui/dashpay/contact_requests.rs (content)
⚔️ src/ui/dashpay/contacts_list.rs (content)
⚔️ src/ui/dashpay/mod.rs (content)
⚔️ src/ui/dashpay/send_payment.rs (content)
⚔️ src/ui/identities/add_new_identity_screen/mod.rs (content)
⚔️ src/ui/identities/identities_screen.rs (content)
⚔️ src/ui/identities/transfer_screen.rs (content)
⚔️ src/ui/identities/withdraw_screen.rs (content)
⚔️ src/ui/network_chooser_screen.rs (content)
⚔️ src/ui/tokens/claim_tokens_screen.rs (content)
⚔️ src/ui/tokens/destroy_frozen_funds_screen.rs (content)
⚔️ src/ui/tokens/freeze_tokens_screen.rs (content)
⚔️ src/ui/tokens/mint_tokens_screen.rs (content)
⚔️ src/ui/tokens/pause_tokens_screen.rs (content)
⚔️ src/ui/tokens/resume_tokens_screen.rs (content)
⚔️ src/ui/tokens/tokens_screen/token_creator.rs (content)
⚔️ src/ui/tokens/transfer_tokens_screen.rs (content)
⚔️ src/ui/tokens/unfreeze_tokens_screen.rs (content)

These conflicts must be resolved before merging into v1.0-dev.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: identity & token screen safety and validation' accurately and concisely summarizes the main objective of the pull request: replacing panics/unwraps with proper error handling and validation across identity and token screens.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/identity-token-screen-safety
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/identity-token-screen-safety
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens identity/key handling across the UI and backend by removing panic/unwrap paths, adding user-facing validation for missing signing keys in token flows, and improving resilience/logging around identity/contact persistence.

Changes:

  • Add “no signing key selected” validation to multiple token action screens and the token contract creator flow.
  • Replace panic/expect-based error handling with Result propagation in identity key mapping and token query construction.
  • Make identity refresh more resilient to deleted/missing identities and start logging previously-silenced DB errors in identity deletion and DashPay contacts persistence.

Reviewed changes

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

Show a summary per file
File Description
src/ui/tokens/unfreeze_tokens_screen.rs Adds selected-key validation before dispatching unfreeze task.
src/ui/tokens/transfer_tokens_screen.rs Adds selected-key validation before dispatching transfer task.
src/ui/tokens/tokens_screen/token_creator.rs Validates identity + signing key before creating token registration tasks.
src/ui/tokens/resume_tokens_screen.rs Adds selected-key validation before dispatching resume task.
src/ui/tokens/pause_tokens_screen.rs Adds selected-key validation before dispatching pause task.
src/ui/tokens/mint_tokens_screen.rs Adds selected-key validation before dispatching mint task.
src/ui/tokens/freeze_tokens_screen.rs Adds selected-key validation before dispatching freeze task.
src/ui/tokens/destroy_frozen_funds_screen.rs Adds selected-key validation before dispatching destroy-frozen-funds task.
src/ui/tokens/claim_tokens_screen.rs Adds selected-key validation before dispatching claim task.
src/ui/identities/withdraw_screen.rs Avoids panic on refresh when identity no longer exists locally.
src/ui/identities/transfer_screen.rs Avoids panic on refresh when identity no longer exists locally; refreshes known identities list.
src/ui/identities/identities_screen.rs Logs DB deletion failures instead of silently ignoring them.
src/ui/identities/add_new_identity_screen/mod.rs Adds ENCRYPTION/DECRYPTION purposes and enforces MEDIUM security level when selected.
src/ui/dashpay/contacts_list.rs Logs DB failures for clear/save contact operations instead of silently ignoring them.
src/backend_task/tokens/query_tokens.rs Replaces expect() on DocumentQuery::new with error propagation.
src/backend_task/identity/register_identity.rs Propagates identity key mapping errors via ?.
src/backend_task/identity/mod.rs Changes to_public_keys_map() to return Result and removes panic on unsupported key types.

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

Comment thread src/ui/tokens/freeze_tokens_screen.rs Outdated
fn confirmation_ok(&mut self) -> AppAction {
if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = FreezeTokensStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The displayed error message is sourced from FreezeTokensStatus::ErrorMessage(...), but this branch sets it to "No key selected" while error_message is "No signing key selected". Consider aligning these so the user sees the intended message.

Suggested change
self.status = FreezeTokensStatus::ErrorMessage("No key selected".into());
self.status = FreezeTokensStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
Comment on lines +322 to 326
if let Some(refreshed) = self
.app_context
.load_local_qualified_identities()
.unwrap()
.unwrap_or_default()
.into_iter()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

refresh() uses load_local_qualified_identities().unwrap_or_default(), which will silently treat a load failure the same as “no identities” and hide the underlying error. Consider handling the Err case explicitly (e.g., if let Err(e) = ... { tracing::warn!(...); }) so DB/load issues are visible while still avoiding a panic.

Copilot uses AI. Check for mistakes.
Comment thread src/ui/tokens/resume_tokens_screen.rs Outdated

if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = ResumeTokensStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The displayed error message is sourced from ResumeTokensStatus::ErrorMessage(...), but this branch sets it to "No key selected" while error_message is "No signing key selected". Consider making these consistent (or only setting the rendered one) so the user sees the intended message.

Suggested change
self.status = ResumeTokensStatus::ErrorMessage("No key selected".into());
self.status =
ResumeTokensStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
Comment thread src/ui/tokens/mint_tokens_screen.rs Outdated
fn confirmation_ok(&mut self) -> AppAction {
if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = MintTokensStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The displayed error message is sourced from MintTokensStatus::ErrorMessage(...), but this branch sets it to "No key selected" while error_message is "No signing key selected". Consider using one consistent message (and/or only setting the field that’s rendered).

Suggested change
self.status = MintTokensStatus::ErrorMessage("No key selected".into());
self.status = MintTokensStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
fn confirmation_ok(&mut self) -> AppAction {
if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = DestroyFrozenFundsStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The displayed error message is sourced from DestroyFrozenFundsStatus::ErrorMessage(...), but this branch sets it to "No key selected" while error_message is "No signing key selected". Consider using a single consistent string to avoid ambiguity.

Suggested change
self.status = DestroyFrozenFundsStatus::ErrorMessage("No key selected".into());
self.status = DestroyFrozenFundsStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
Comment thread src/ui/tokens/claim_tokens_screen.rs Outdated

if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = ClaimTokensStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The displayed error message is sourced from ClaimTokensStatus::ErrorMessage(...), but this branch sets it to "No key selected" while error_message is "No signing key selected". Consider making these consistent so the user sees a clear error.

Suggested change
self.status = ClaimTokensStatus::ErrorMessage("No key selected".into());
self.status = ClaimTokensStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
Comment thread src/ui/identities/identities_screen.rs Outdated
Comment on lines +937 to +943
if let Err(e) = self
.app_context
.db
.delete_local_qualified_identity(&identity_id, &self.app_context)
.ok();
{
tracing::warn!("Failed to delete identity from database: {}", e);
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The warning log here doesn’t include which identity ID failed to delete, which can make troubleshooting harder (especially if multiple deletions happen). Consider including identity_id (and voter_identity_id below) in the log fields/message.

Copilot uses AI. Check for mistakes.
Comment thread src/ui/identities/transfer_screen.rs Outdated
Comment on lines +504 to +507
let identities = self
.app_context
.load_local_qualified_identities()
.unwrap()
.into_iter()
.unwrap_or_default();
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

refresh() uses load_local_qualified_identities().unwrap_or_default(), which discards load errors and can make DB problems hard to diagnose. Consider matching on the result and logging the error before falling back, so “identity deleted” and “load failed” aren’t conflated.

Copilot uses AI. Check for mistakes.
Comment thread src/ui/tokens/unfreeze_tokens_screen.rs Outdated
fn confirmation_ok(&mut self) -> AppAction {
if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = UnfreezeTokensStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The UI error text shown to the user comes from UnfreezeTokensStatus::ErrorMessage(...), but this branch sets that message to "No key selected" while error_message is set to "No signing key selected". Consider using a single, consistent string (and/or only setting the field that is actually rendered) to avoid confusing/ambiguous errors.

Suggested change
self.status = UnfreezeTokensStatus::ErrorMessage("No key selected".into());
self.status = UnfreezeTokensStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
Comment thread src/ui/tokens/pause_tokens_screen.rs Outdated

if self.selected_key.is_none() {
self.error_message = Some("No signing key selected".into());
self.status = PauseTokensStatus::ErrorMessage("No key selected".into());
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The displayed error message is sourced from PauseTokensStatus::ErrorMessage(...), but this branch sets it to "No key selected" while error_message is "No signing key selected". Consider making these consistent (or only setting the rendered one) to avoid confusing UX.

Suggested change
self.status = PauseTokensStatus::ErrorMessage("No key selected".into());
self.status = PauseTokensStatus::ErrorMessage("No signing key selected".into());

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/ui/identities/add_new_identity_screen/mod.rs (1)

671-715: ⚠️ Potential issue | 🟠 Major

Auto-set ECDSA_SECP256K1 and required contract bounds for ENCRYPTION/DECRYPTION.

When ENCRYPTION or DECRYPTION purposes are selected, the code must auto-set the key type to ECDSA_SECP256K1 and apply SingleContractDocumentType bounds with "contactRequest" document type (per DIP-15 requirements). Currently, these constraints are not enforced, allowing users to register keys with wrong key types or missing bounds, which causes platform validation failures.

🔧 Suggested fix
-                    for (i, ((key, _), key_type, purpose, security_level, _contract_bounds)) in
+                    for (i, ((key, _), key_type, purpose, security_level, contract_bounds)) in
                         self.identity_keys.keys_input.iter_mut().enumerate()
                     {
@@
                                     if *purpose != prev_purpose {
                                         match *purpose {
                                             Purpose::TRANSFER => {
                                                 *security_level = SecurityLevel::CRITICAL;
                                             }
                                             Purpose::ENCRYPTION | Purpose::DECRYPTION => {
                                                 *security_level = SecurityLevel::MEDIUM;
+                                                *key_type = KeyType::ECDSA_SECP256K1;
+                                                if contract_bounds.is_none() {
+                                                    *contract_bounds = Some(
+                                                        ContractBounds::SingleContractDocumentType {
+                                                            id: self.app_context.dashpay_contract.id(),
+                                                            document_type_name: "contactRequest".to_string(),
+                                                        },
+                                                    );
+                                                }
                                             }

Also applies to lines 744–748 where ENCRYPTION/DECRYPTION security levels are locked to MEDIUM—extend to also prevent key type changes and validate contract bounds exist.

🤖 Fix all issues with AI agents
In `@src/ui/identities/transfer_screen.rs`:
- Around line 504-515: Replace the use of unwrap_or_default() on
self.app_context.load_local_qualified_identities() with explicit Result
handling: call load_local_qualified_identities(), match on Ok(identities) to use
the returned Vec, and on Err(err) log a warning that includes the error (so
failures are diagnosable) and set identities to an empty Vec as a graceful
fallback; then continue using identities to update self.identity,
self.max_amount and self.known_identities as before (referencing
load_local_qualified_identities, identities, self.identity, self.max_amount, and
self.known_identities).
🧹 Nitpick comments (3)
src/ui/tokens/freeze_tokens_screen.rs (2)

253-257: Guard is good, but .unwrap() on line 304 still leaves a latent panic.

The early return at lines 253–257 ensures selected_key is Some before reaching line 304 in the current flow, but a future refactor could break that invariant. Since the PR's explicit goal is eliminating panics, consider using a pattern that doesn't rely on .unwrap():

Suggested improvement

At line 304, replace the .unwrap() with a safe extraction that's consistent with the guard:

-            signing_key: self.selected_key.clone().unwrap(),
+            signing_key: match self.selected_key.clone() {
+                Some(key) => key,
+                None => {
+                    self.error_message = Some("No signing key selected".into());
+                    self.status = FreezeTokensStatus::ErrorMessage("No key selected".into());
+                    return AppAction::None;
+                }
+            },

Or more concisely, restructure confirmation_ok to bind the key from the guard:

-        if self.selected_key.is_none() {
-            self.error_message = Some("No signing key selected".into());
-            self.status = FreezeTokensStatus::ErrorMessage("No key selected".into());
-            return AppAction::None;
-        }
+        let signing_key = match self.selected_key.clone() {
+            Some(key) => key,
+            None => {
+                self.error_message = Some("No signing key selected".into());
+                self.status = FreezeTokensStatus::ErrorMessage("No key selected".into());
+                return AppAction::None;
+            }
+        };

Then use signing_key directly at line 304, eliminating the .unwrap() entirely.

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."

Also applies to: 304-304


267-272: Same is_err() + .unwrap() pattern — prefer match or let-else.

Minor nit, same idea as the key guard above. This can be tightened:

Suggested improvement
-        if parsed.is_err() {
-            self.error_message = Some("Please enter a valid identity ID.".into());
-            self.status = FreezeTokensStatus::ErrorMessage("Invalid identity".into());
-            return AppAction::None;
-        }
-        let freeze_id = parsed.unwrap();
+        let freeze_id = match parsed {
+            Ok(id) => id,
+            Err(_) => {
+                self.error_message = Some("Please enter a valid identity ID.".into());
+                self.status = FreezeTokensStatus::ErrorMessage("Invalid identity".into());
+                return AppAction::None;
+            }
+        };
src/backend_task/identity/mod.rs (1)

149-219: Add inline tests for new error paths.

Since unsupported key types now return Err, add #[test] coverage to lock this behavior in.

💡 Example inline test scaffold
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use dash_sdk::dpp::key_wallet::bip32::DerivationPath;
+
+    #[test]
+    fn to_public_keys_map_rejects_unsupported_key_type() {
+        let keys = IdentityKeys {
+            master_private_key: None,
+            master_private_key_type: KeyType::BLS12_381, // unsupported
+            keys_input: vec![],
+        };
+        assert!(keys.to_public_keys_map().is_err());
+    }
+}

As per coding guidelines: “Unit tests should be inline in source files using #[test] attribute.”

Comment thread src/ui/identities/transfer_screen.rs Outdated
Comment thread src/ui/dashpay/contacts_list.rs
@lklimek
Copy link
Copy Markdown
Contributor

lklimek commented Feb 12, 2026

Audit Summary

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

  • code-reviewer — Correctness, edge cases, duplication, behavioral changes
  • security-engineer — Panics, race conditions, key security, concurrency
  • rust-developer — Rust idioms, error handling patterns, edition compatibility

Overall Assessment

The PR is well-structured and achieves its stated goals. It systematically replaces 10+ panic/unwrap/expect paths with proper error handling across 17 files. No critical issues found. The changes significantly improve application resilience.

Cross-checked against PR #562 (fix/replace-panics-with-error-propagation) — no overlap. PR #562 addresses SystemTime expects and identity transition expects; this PR addresses signing key validation, identity refresh safety, DB error logging, and key type handling.

Findings

# Severity Location Description
1 MEDIUM transfer_tokens_screen.rs refresh() (~L273-291) refresh() still uses double .unwrap() on identity lookup — can panic if identity is deleted while screen is open. Inconsistent with the graceful fix applied to transfer_screen.rs and withdraw_screen.rs in this same PR.
2 MEDIUM 8 token screens (see inline) Guard-then-unwrap anti-pattern: is_none() + early return, then later .unwrap(). Safe but fragile. token_creator.rs already uses the idiomatic match destructure — the 8 screens should follow that pattern.
3 MEDIUM transfer_tokens_screen.rs:231-233 Pre-existing double expect() on get_unqualified_contract_by_id() — same code path as the selected_key fix but not addressed. Other token screens use self.identity_token_info.data_contract.contract.clone() which cannot panic.
4 MEDIUM add_new_identity_screen/mod.rs:696-715 Security level enforcement for ENCRYPTION/DECRYPTION keys is UI-only. Backend register_identity.rs passes IdentityKeys through without validating purpose/security-level constraints. Low risk for desktop app (see inline).
5 LOW identities_screen.rs:934-943 Identity removed from in-memory map before DB deletion. If DB fails, identity disappears from UI but persists in DB, reappearing on restart.
6 LOW pause_tokens_screen.rs, resume_tokens_screen.rs Key validation placed inside dialog match (after user confirms), unlike the other 6 screens which validate in confirmation_ok(). Minor UX inconsistency.

Pre-existing Issues (outside diff, for awareness)

  • transfer_tokens_screen.rs:231-233 — double expect() on data contract lookup (Finding feat: register usernames #3 above)
  • register_identity.rs:297asset_lock_proof.create_identifier().expect(...) still present
  • identity/mod.rs:107-145 — sibling method to_key_storage() doesn't distinguish ECDSA_HASH160 from ECDSA_SECP256K1, inconsistent with the newly-fixed to_public_keys_map()

Positive Observations

  • to_public_keys_map() refactor from iterator+panic to for-loop+Result is clean and idiomatic
  • Token creator validation uses proper match destructure — good reference pattern
  • DB error logging with tracing::warn! is appropriate for the context
  • Transfer/withdraw graceful refresh prevents real user-facing panics
  • Let-chain syntax (if let Some(...) && let Err(e)) is valid for Rust Edition 2024
  • No TOCTOU risk in egui single-threaded frame rendering
  • SQL injection properly mitigated throughout (parameterized queries)

Comment thread src/ui/tokens/mint_tokens_screen.rs
Comment thread src/ui/identities/identities_screen.rs Outdated
Comment thread src/ui/identities/add_new_identity_screen/mod.rs
…er screen refresh

Log identity-load failures with tracing::warn instead of silently
swallowing them via unwrap_or_default().

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🤖 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/ui/identities/transfer_screen.rs`:
- Around line 82-85: The constructor TransferScreen::new currently calls
app_context.load_local_qualified_identities().expect("Identities not loaded")
which can still panic; change new (the TransferScreen::new function) to avoid
expect by either returning a Result<TransferScreen, Error> and propagating the
load error (use ? on load_local_qualified_identities), or handle the error
non-panically by logging the failure and using a safe default (e.g., empty
known_identities Vec) before constructing Self — mirror the non-panicking
approach used in refresh() to keep behavior consistent.
🧹 Nitpick comments (1)
🤖 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/ui/identities/transfer_screen.rs`:
- Around line 82-85: The constructor TransferScreen::new currently calls
app_context.load_local_qualified_identities().expect("Identities not loaded")
which can still panic; change new (the TransferScreen::new function) to avoid
expect by either returning a Result<TransferScreen, Error> and propagating the
load error (use ? on load_local_qualified_identities), or handle the error
non-panically by logging the failure and using a safe default (e.g., empty
known_identities Vec) before constructing Self — mirror the non-panicking
approach used in refresh() to keep behavior consistent.
src/ui/identities/transfer_screen.rs (1)

82-85: Pre-existing expect() in new() remains.

load_local_qualified_identities().expect("Identities not loaded") on Line 85 can still panic. This is outside the current diff, but given the PR's goal of replacing panic paths, it's worth noting for a follow-up — especially since the same pattern was already fixed in refresh().

🤖 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/ui/identities/transfer_screen.rs` around lines 82 - 85, The constructor
TransferScreen::new currently calls
app_context.load_local_qualified_identities().expect("Identities not loaded")
which can still panic; change new (the TransferScreen::new function) to avoid
expect by either returning a Result<TransferScreen, Error> and propagating the
load error (use ? on load_local_qualified_identities), or handle the error
non-panically by logging the failure and using a safe default (e.g., empty
known_identities Vec) before constructing Self — mirror the non-panicking
approach used in refresh() to keep behavior consistent.

thepastaclaw added a commit to thepastaclaw/dash-evo-tool that referenced this pull request Feb 17, 2026
- Replace guard-then-unwrap with match/let-else in all 8 token screens
- Move identity UI removal after successful DB deletion
- Add backend security level validation for identity key purposes
- Show user-facing error message on identity deletion failure
- Replace guard-then-unwrap with match/let-else in all 8 token screens
- Move identity UI removal after successful DB deletion
- Add backend security level validation for identity key purposes
- Show user-facing error message on identity deletion failure

Co-authored-by: PastaClaw <thepastaclaw@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (5)
src/ui/tokens/pause_tokens_screen.rs (1)

96-115: ⚠️ Potential issue | 🟡 Minor

Pre-existing copy-paste: error messages say "Burning" / "burn" instead of "Pausing" / "pause".

Lines 98, 106, and 114 reference "Burning" and "burn" but this is the Pause screen. These appear to have been copied from destroy_tokens_screen.rs and never updated.

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

In `@src/ui/tokens/pause_tokens_screen.rs` around lines 96 - 115, Error messages
in the pause_tokens_screen logic still refer to "Burning"/"burn" instead of
"Pausing"/"pause"; update the strings in the match arms for
AuthorizedActionTakers::NoOne, ::ContractOwner, and ::Identity(identifier) so
they say "Pausing is not allowed on this token", "You are not allowed to pause
this token. Only the contract owner is.", and "You are not allowed to pause this
token" respectively, modifying the error_message assignments that reference
identity_token_info and AuthorizedActionTakers to use the correct wording and
casing.
src/ui/tokens/resume_tokens_screen.rs (1)

96-115: ⚠️ Potential issue | 🟡 Minor

Pre-existing copy-paste: error messages say "Burning" / "burn" instead of "Resuming" / "resume".

Same issue as pause_tokens_screen.rs — Lines 97, 105, and 113 reference "Burning" / "burn" but this is the Resume screen.

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

In `@src/ui/tokens/resume_tokens_screen.rs` around lines 96 - 115, The error
strings in the AuthorizedActionTakers match arms still refer to "Burning"/"burn"
on the Resume screen; update the messages in the NoOne, ContractOwner, and
Identity(identifier) arms to say "Resuming"/"resume" (e.g., replace "Burning is
not allowed on this token" with "Resuming is not allowed on this token", and
"You are not allowed to burn this token" with "You are not allowed to resume
this token"), keeping the same logic around identity_token_info and
AuthorizedActionTakers checks.
src/ui/tokens/transfer_tokens_screen.rs (2)

232-237: ⚠️ Potential issue | 🟡 Minor

Double expect() on data contract lookup can also panic.

If the data contract is somehow missing or the DB query fails, both .expect() calls on Lines 235-236 will panic. This is pre-existing (not changed in this PR), but it's on the same code path that was improved above and was flagged in the audit (finding #3).

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

In `@src/ui/tokens/transfer_tokens_screen.rs` around lines 232 - 237, The code
currently calls
self.app_context.get_unqualified_contract_by_id(&self.identity_token_balance.data_contract_id).expect(...).expect(...),
which can panic on both the Result and Option; change this to handle both error
cases explicitly: call get_unqualified_contract_by_id(...), match or use ? to
propagate the Result error (or convert it into a user-facing error), then handle
the Option (None) with a controlled error path (e.g., return Err, show a UI
error, or log and early-return) instead of using expect; update the creation of
data_contract (Arc::new(...)) to use the safely-unwrapped value after these
checks and reference the symbols data_contract, get_unqualified_contract_by_id,
and identity_token_balance.data_contract_id to locate the code to modify.

276-294: ⚠️ Potential issue | 🟠 Major

refresh() still contains double .unwrap() and can panic if the identity is deleted.

Lines 281 and 284 both call .unwrap() — if the DB load fails or the identity was deleted while this screen is open, the app panics. This was identified in the PR audit (finding #1) and is inconsistent with the graceful handling added to other screens (e.g., mint_tokens_screen.rs Line 381 uses if let Ok(...) + .find()). Consider the same pattern here:

🐛 Proposed fix
 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();
+    if let Ok(all_identities) = self.app_context.load_local_qualified_identities()
+        && let Some(updated_identity) = all_identities
+            .into_iter()
+            .find(|identity| identity.identity.id() == self.identity.identity.id())
+    {
+        self.identity = updated_identity;
+    }
     let token_balances = self
         .app_context
         .db

Based on learnings, error handling refactoring is needed across the codebase 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/tokens/transfer_tokens_screen.rs` around lines 276 - 294, The
refresh() method currently uses two .unwrap() calls that can panic if loading
identities fails or the identity was deleted; change it to gracefully handle
errors: call self.app_context.load_local_qualified_identities() with if let
Ok(identities) and then use .into_iter().find(|i| i.identity.id() ==
self.identity.identity.id()) to update self.identity only if found (otherwise
return early or set an appropriate fallback), and replace the expect on
self.app_context.db.get_identity_token_balances(&self.app_context) with if let
Ok(token_balances) (or handle Err) and then compute self.max_amount by finding
the matching balance and mapping with Amount::from, defaulting to
Amount::default() when absent; ensure no unwrap()/expect() remain in refresh().
src/backend_task/identity/mod.rs (1)

107-145: ⚠️ Potential issue | 🟠 Major

Inconsistency in to_key_storage: ECDSA_HASH160 key data not handled like to_public_keys_map.

to_key_storage (line 126) always stores keys using private_key.public_key(&secp).to_bytes().into() regardless of key_type. However, to_public_keys_map (lines 224-231) correctly uses pubkey_hash().to_byte_array() for ECDSA_HASH160 keys. This causes a data mismatch: if an ECDSA_HASH160 key is registered via to_public_keys_map, the data field stored by to_key_storage will not match the on-chain key. The same issue exists for the master key (line 88 vs lines 159-166). Since ECDSA_HASH160 is the default key type in the application, this should be fixed in to_key_storage to match the behavior of to_public_keys_map.

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

In `@src/backend_task/identity/mod.rs` around lines 107 - 145, The code in
to_key_storage constructs IdentityPublicKey::V0 using
private_key.public_key(&secp).to_bytes().into() unconditionally, which
mismatches to_public_keys_map for KeyType::ECDSA_HASH160; update to_key_storage
(and the master key creation path that mirrors it) to check for
KeyType::ECDSA_HASH160 and set data =
private_key.pubkey_hash().to_byte_array().into() for that case, otherwise
continue using private_key.public_key(&secp).to_bytes().into(); ensure you
modify the IdentityPublicKey::V0 construction sites (the per-key loop producing
qualified_identity_public_key and the master key creation) and use the same
pubkey_hash() logic as to_public_keys_map to keep on-chain and stored data
consistent.
🧹 Nitpick comments (1)
src/backend_task/identity/mod.rs (1)

210-222: Nit: Consider matches! for readability.

♻️ Suggested simplification
-                Purpose::AUTHENTICATION => {
-                    if *security_level != SecurityLevel::CRITICAL
-                        && *security_level != SecurityLevel::HIGH
-                        && *security_level != SecurityLevel::MEDIUM
-                    {
+                Purpose::AUTHENTICATION => {
+                    if !matches!(
+                        security_level,
+                        SecurityLevel::CRITICAL | SecurityLevel::HIGH | SecurityLevel::MEDIUM
+                    ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/identity/mod.rs` around lines 210 - 222, Replace the chained
inequality checks in the Purpose::AUTHENTICATION match arm with a matches!
expression for readability: in the block handling Purpose::AUTHENTICATION
(around the match on purpose/security checks), use matches!(security_level,
SecurityLevel::CRITICAL | SecurityLevel::HIGH | SecurityLevel::MEDIUM) and
invert the condition to return the same Err message when it does not match; keep
the existing Err(...) text and the id and security_level references intact so
behavior and error content remain unchanged.
🤖 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/backend_task/identity/mod.rs`:
- Around line 107-145: The code in to_key_storage constructs
IdentityPublicKey::V0 using private_key.public_key(&secp).to_bytes().into()
unconditionally, which mismatches to_public_keys_map for KeyType::ECDSA_HASH160;
update to_key_storage (and the master key creation path that mirrors it) to
check for KeyType::ECDSA_HASH160 and set data =
private_key.pubkey_hash().to_byte_array().into() for that case, otherwise
continue using private_key.public_key(&secp).to_bytes().into(); ensure you
modify the IdentityPublicKey::V0 construction sites (the per-key loop producing
qualified_identity_public_key and the master key creation) and use the same
pubkey_hash() logic as to_public_keys_map to keep on-chain and stored data
consistent.

In `@src/ui/tokens/pause_tokens_screen.rs`:
- Around line 96-115: Error messages in the pause_tokens_screen logic still
refer to "Burning"/"burn" instead of "Pausing"/"pause"; update the strings in
the match arms for AuthorizedActionTakers::NoOne, ::ContractOwner, and
::Identity(identifier) so they say "Pausing is not allowed on this token", "You
are not allowed to pause this token. Only the contract owner is.", and "You are
not allowed to pause this token" respectively, modifying the error_message
assignments that reference identity_token_info and AuthorizedActionTakers to use
the correct wording and casing.

In `@src/ui/tokens/resume_tokens_screen.rs`:
- Around line 96-115: The error strings in the AuthorizedActionTakers match arms
still refer to "Burning"/"burn" on the Resume screen; update the messages in the
NoOne, ContractOwner, and Identity(identifier) arms to say "Resuming"/"resume"
(e.g., replace "Burning is not allowed on this token" with "Resuming is not
allowed on this token", and "You are not allowed to burn this token" with "You
are not allowed to resume this token"), keeping the same logic around
identity_token_info and AuthorizedActionTakers checks.

In `@src/ui/tokens/transfer_tokens_screen.rs`:
- Around line 232-237: The code currently calls
self.app_context.get_unqualified_contract_by_id(&self.identity_token_balance.data_contract_id).expect(...).expect(...),
which can panic on both the Result and Option; change this to handle both error
cases explicitly: call get_unqualified_contract_by_id(...), match or use ? to
propagate the Result error (or convert it into a user-facing error), then handle
the Option (None) with a controlled error path (e.g., return Err, show a UI
error, or log and early-return) instead of using expect; update the creation of
data_contract (Arc::new(...)) to use the safely-unwrapped value after these
checks and reference the symbols data_contract, get_unqualified_contract_by_id,
and identity_token_balance.data_contract_id to locate the code to modify.
- Around line 276-294: The refresh() method currently uses two .unwrap() calls
that can panic if loading identities fails or the identity was deleted; change
it to gracefully handle errors: call
self.app_context.load_local_qualified_identities() with if let Ok(identities)
and then use .into_iter().find(|i| i.identity.id() ==
self.identity.identity.id()) to update self.identity only if found (otherwise
return early or set an appropriate fallback), and replace the expect on
self.app_context.db.get_identity_token_balances(&self.app_context) with if let
Ok(token_balances) (or handle Err) and then compute self.max_amount by finding
the matching balance and mapping with Amount::from, defaulting to
Amount::default() when absent; ensure no unwrap()/expect() remain in refresh().

---

Nitpick comments:
In `@src/backend_task/identity/mod.rs`:
- Around line 210-222: Replace the chained inequality checks in the
Purpose::AUTHENTICATION match arm with a matches! expression for readability: in
the block handling Purpose::AUTHENTICATION (around the match on purpose/security
checks), use matches!(security_level, SecurityLevel::CRITICAL |
SecurityLevel::HIGH | SecurityLevel::MEDIUM) and invert the condition to return
the same Err message when it does not match; keep the existing Err(...) text and
the id and security_level references intact so behavior and error content remain
unchanged.

@PastaPastaPasta PastaPastaPasta merged commit 397472c into v1.0-dev Feb 19, 2026
6 checks passed
@PastaPastaPasta PastaPastaPasta deleted the fix/identity-token-screen-safety branch February 19, 2026 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants