From 869006c8a7c18096e291744cfab62451e340bf60 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:29:56 -0600 Subject: [PATCH 1/9] fix: replace panic with error return on unsupported identity key types --- src/backend_task/identity/mod.rs | 68 +++++++++++-------- .../identity/register_identity.rs | 4 +- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index c9ceb3e6a..554668e47 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -146,7 +146,7 @@ impl IdentityKeys { key_map.into() } - pub fn to_public_keys_map(&self) -> BTreeMap { + pub fn to_public_keys_map(&self) -> Result, String> { let Self { master_private_key, master_private_key_type, @@ -164,7 +164,12 @@ impl IdentityKeys { .to_byte_array() .to_vec() .into(), - _ => panic!("need a ECDSA Key for now"), + other => { + return Err(format!( + "Unsupported master key type: {:?}. Only ECDSA_SECP256K1 and ECDSA_HASH160 are supported.", + other + )); + } }; let key = IdentityPublicKey::V0(IdentityPublicKeyV0 { id: 0, @@ -179,34 +184,39 @@ impl IdentityKeys { key_map.insert(0, key); } - key_map.extend(keys_input.iter().enumerate().map( - |(i, ((private_key, _), key_type, purpose, security_level, contract_bounds))| { - let id = (i + 1) as KeyID; - let data = match key_type { - KeyType::ECDSA_SECP256K1 => private_key.public_key(&secp).to_bytes().into(), - KeyType::ECDSA_HASH160 => private_key - .public_key(&secp) - .pubkey_hash() - .to_byte_array() - .to_vec() - .into(), - _ => panic!("need a ECDSA Key for now"), - }; - let identity_public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { - id, - purpose: *purpose, - security_level: *security_level, - contract_bounds: contract_bounds.clone(), - key_type: *key_type, - read_only: false, - data, - disabled_at: None, - }); - (id, identity_public_key) - }, - )); + for (i, ((private_key, _), key_type, purpose, security_level, contract_bounds)) in + keys_input.iter().enumerate() + { + let id = (i + 1) as KeyID; + let data = match key_type { + KeyType::ECDSA_SECP256K1 => private_key.public_key(&secp).to_bytes().into(), + KeyType::ECDSA_HASH160 => private_key + .public_key(&secp) + .pubkey_hash() + .to_byte_array() + .to_vec() + .into(), + other => { + return Err(format!( + "Unsupported key type for key {}: {:?}. Only ECDSA_SECP256K1 and ECDSA_HASH160 are supported.", + id, other + )); + } + }; + let identity_public_key = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id, + purpose: *purpose, + security_level: *security_level, + contract_bounds: contract_bounds.clone(), + key_type: *key_type, + read_only: false, + data, + disabled_at: None, + }); + key_map.insert(id, identity_public_key); + } - key_map + Ok(key_map) } } diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index 47ae6fa5d..ab15d026b 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -296,7 +296,7 @@ impl AppContext { .create_identifier() .expect("expected to create an identifier"); - let public_keys = keys.to_public_keys_map(); + let public_keys = keys.to_public_keys_map()?; // Debug: Log the keys being registered to verify contract bounds are set for (key_id, key) in &public_keys { @@ -651,7 +651,7 @@ impl AppContext { guard.clone() }; - let public_keys = keys.to_public_keys_map(); + let public_keys = keys.to_public_keys_map()?; // Calculate fee estimate for identity creation from platform addresses let key_count = public_keys.len(); From 7ca86605cbb861ee47053f59ff06b4eb1c46101b Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:30:03 -0600 Subject: [PATCH 2/9] fix: replace panic on missing signing key with validation in 8 token screens --- src/ui/tokens/claim_tokens_screen.rs | 9 ++++++++- src/ui/tokens/destroy_frozen_funds_screen.rs | 8 +++++++- src/ui/tokens/freeze_tokens_screen.rs | 8 +++++++- src/ui/tokens/mint_tokens_screen.rs | 8 +++++++- src/ui/tokens/pause_tokens_screen.rs | 9 ++++++++- src/ui/tokens/resume_tokens_screen.rs | 9 ++++++++- src/ui/tokens/transfer_tokens_screen.rs | 8 +++++++- src/ui/tokens/unfreeze_tokens_screen.rs | 8 +++++++- 8 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 4d95f23d2..3dfa20487 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -203,6 +203,13 @@ impl ClaimTokensScreen { match dialog.show(ui).inner.dialog_response { Some(ConfirmationStatus::Confirmed) => { self.confirmation_dialog = None; + + if self.selected_key.is_none() { + self.error_message = Some("No signing key selected".into()); + self.status = ClaimTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") @@ -216,7 +223,7 @@ impl ClaimTokensScreen { token_position: self.identity_token_basic_info.token_position, actor_identity: self.identity.clone(), distribution_type, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: self.public_note.clone(), })), BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs b/src/ui/tokens/destroy_frozen_funds_screen.rs index 11d5f8784..32496ae36 100644 --- a/src/ui/tokens/destroy_frozen_funds_screen.rs +++ b/src/ui/tokens/destroy_frozen_funds_screen.rs @@ -260,6 +260,12 @@ impl DestroyFrozenFundsScreen { } 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()); + return AppAction::None; + } + let maybe_frozen_id = Identifier::from_string_try_encodings( &self.frozen_identity_id, &[ @@ -303,7 +309,7 @@ impl DestroyFrozenFundsScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/freeze_tokens_screen.rs b/src/ui/tokens/freeze_tokens_screen.rs index 35ac3d6e4..43b26ec96 100644 --- a/src/ui/tokens/freeze_tokens_screen.rs +++ b/src/ui/tokens/freeze_tokens_screen.rs @@ -250,6 +250,12 @@ impl FreezeTokensScreen { /// Handle confirmation OK action 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()); + return AppAction::None; + } + // Validate user input let parsed = Identifier::from_string_try_encodings( &self.freeze_identity_id, @@ -295,7 +301,7 @@ impl FreezeTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/mint_tokens_screen.rs b/src/ui/tokens/mint_tokens_screen.rs index 0244d6797..2a57d6553 100644 --- a/src/ui/tokens/mint_tokens_screen.rs +++ b/src/ui/tokens/mint_tokens_screen.rs @@ -278,6 +278,12 @@ impl MintTokensScreen { } 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()); + return AppAction::None; + } + if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); self.error_message = Some("Invalid amount".into()); @@ -327,7 +333,7 @@ impl MintTokensScreen { sending_identity: self.identity_token_info.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/pause_tokens_screen.rs b/src/ui/tokens/pause_tokens_screen.rs index c6624398c..ffb498130 100644 --- a/src/ui/tokens/pause_tokens_screen.rs +++ b/src/ui/tokens/pause_tokens_screen.rs @@ -207,6 +207,13 @@ impl PauseTokensScreen { match dialog.show(ui).inner.dialog_response { Some(ConfirmationStatus::Confirmed) => { self.confirmation_dialog = None; + + if self.selected_key.is_none() { + self.error_message = Some("No signing key selected".into()); + self.status = PauseTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") @@ -237,7 +244,7 @@ impl PauseTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/resume_tokens_screen.rs b/src/ui/tokens/resume_tokens_screen.rs index 7e20dac1e..d53273408 100644 --- a/src/ui/tokens/resume_tokens_screen.rs +++ b/src/ui/tokens/resume_tokens_screen.rs @@ -207,6 +207,13 @@ impl ResumeTokensScreen { match dialog.show(ui).inner.dialog_response { Some(ConfirmationStatus::Confirmed) => { self.confirmation_dialog = None; + + if self.selected_key.is_none() { + self.error_message = Some("No signing key selected".into()); + self.status = ResumeTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") @@ -237,7 +244,7 @@ impl ResumeTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index 8fffe040b..c3bdd3e87 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -193,6 +193,12 @@ impl TransferTokensScreen { } fn confirmation_ok(&mut self) -> AppAction { + if self.selected_key.is_none() { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("No signing key selected".into()); + return AppAction::None; + } + if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { self.transfer_tokens_status = TransferTokensStatus::ErrorMessage("Invalid amount".into()); @@ -234,7 +240,7 @@ impl TransferTokensScreen { amount: self.amount.clone().unwrap_or(Amount::new(0, 0)).value(), data_contract, token_position: self.identity_token_balance.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: self.public_note.clone(), }, ))) diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs b/src/ui/tokens/unfreeze_tokens_screen.rs index a0b97f777..859dffb1f 100644 --- a/src/ui/tokens/unfreeze_tokens_screen.rs +++ b/src/ui/tokens/unfreeze_tokens_screen.rs @@ -252,6 +252,12 @@ impl UnfreezeTokensScreen { } 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()); + return AppAction::None; + } + // Validate user input let parsed = Identifier::from_string_try_encodings( &self.unfreeze_identity_id, @@ -298,7 +304,7 @@ impl UnfreezeTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), + signing_key: self.selected_key.clone().unwrap(), public_note: if self.group_action_id.is_some() { None } else { From edbcb2ad104c785c394d72d15a243229c12ba243 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:30:03 -0600 Subject: [PATCH 3/9] fix: handle deleted identity gracefully in transfer/withdraw refresh --- src/ui/identities/transfer_screen.rs | 14 +++++++++----- src/ui/identities/withdraw_screen.rs | 10 ++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 9d4f35ee9..3c8545814 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -501,14 +501,18 @@ impl ScreenLike for TransferScreen { fn refresh(&mut self) { // Refresh the identity because there might be new keys - self.identity = self + let identities = self .app_context .load_local_qualified_identities() - .unwrap() - .into_iter() + .unwrap_or_default(); + if let Some(refreshed) = identities + .iter() .find(|identity| identity.identity.id() == self.identity.identity.id()) - .unwrap(); - self.max_amount = self.identity.identity.balance(); + { + self.identity = refreshed.clone(); + self.max_amount = self.identity.identity.balance(); + } + self.known_identities = identities; } /// Renders the UI components for the withdrawal screen diff --git a/src/ui/identities/withdraw_screen.rs b/src/ui/identities/withdraw_screen.rs index 1ea5aebee..01ecffda3 100644 --- a/src/ui/identities/withdraw_screen.rs +++ b/src/ui/identities/withdraw_screen.rs @@ -319,14 +319,16 @@ impl ScreenLike for WithdrawalScreen { fn refresh(&mut self) { // Refresh the identity because there might be new keys - self.identity = self + if let Some(refreshed) = self .app_context .load_local_qualified_identities() - .unwrap() + .unwrap_or_default() .into_iter() .find(|identity| identity.identity.id() == self.identity.identity.id()) - .unwrap(); - self.max_amount = self.identity.identity.balance(); + { + self.identity = refreshed; + self.max_amount = self.identity.identity.balance(); + } } /// Renders the UI components for the withdrawal screen From 840f27775111e5f750887d5c2e387e4d7258b56e Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:32:33 -0600 Subject: [PATCH 4/9] fix: log silenced DB errors in identity deletion and contact save --- src/ui/dashpay/contacts_list.rs | 31 +++++++++++++++++++------- src/ui/identities/identities_screen.rs | 23 +++++++++++-------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/ui/dashpay/contacts_list.rs b/src/ui/dashpay/contacts_list.rs index 0abd74e1d..8d17f7612 100644 --- a/src/ui/dashpay/contacts_list.rs +++ b/src/ui/dashpay/contacts_list.rs @@ -1044,10 +1044,13 @@ impl ScreenLike for ContactsList { // Clear all existing contacts for this identity from database first // This prevents stale contacts from persisting - let _ = self + if let Err(e) = self .app_context .db - .clear_dashpay_contacts(&owner_id, &network_str); + .clear_dashpay_contacts(&owner_id, &network_str) + { + tracing::warn!("Failed to clear dashpay contacts from database: {}", e); + } // Convert ContactData to Contact structs and save to database for contact_data in contacts_data { @@ -1073,7 +1076,7 @@ impl ScreenLike for ContactsList { self.contacts.insert(contact_data.identity_id, contact); // Save to database - let _ = self.app_context.db.save_dashpay_contact( + if let Err(e) = self.app_context.db.save_dashpay_contact( &owner_id, &contact_data.identity_id, &network_str, @@ -1082,16 +1085,23 @@ impl ScreenLike for ContactsList { contact_data.avatar_url.as_deref(), None, // public_message - not yet fetched "accepted", // Only accepted contacts are returned from load_contacts - ); + ) { + tracing::warn!("Failed to save dashpay contact to database: {}", e); + } // Save private info if present - if let Some(nickname) = &contact_data.nickname { - let _ = self.app_context.db.save_contact_private_info( + if let Some(nickname) = &contact_data.nickname + && let Err(e) = self.app_context.db.save_contact_private_info( &owner_id, &contact_data.identity_id, nickname, &contact_data.note.unwrap_or_default(), contact_data.is_hidden, + ) + { + tracing::warn!( + "Failed to save contact private info to database: {}", + e ); } } @@ -1163,7 +1173,7 @@ impl ScreenLike for ContactsList { if let Some(identity) = &self.selected_identity { let owner_id = identity.identity.id(); let network_str = self.app_context.network.to_string(); - let _ = self.app_context.db.save_dashpay_contact( + if let Err(e) = self.app_context.db.save_dashpay_contact( &owner_id, &contact_id, &network_str, @@ -1172,7 +1182,12 @@ impl ScreenLike for ContactsList { contact.avatar_url.as_deref(), public_message.as_deref(), "accepted", - ); + ) { + tracing::warn!( + "Failed to save updated contact profile to database: {}", + e + ); + } } } } diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index 6d6e07d30..a1c561c74 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -934,22 +934,27 @@ impl IdentitiesScreen { let mut lock = self.identities.lock().unwrap(); lock.shift_remove(&identity_id); - self.app_context + 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); + } if let Some((voter_identity, _)) = &identity_to_remove.associated_voter_identity { let voter_identity_id = voter_identity.id(); - self.app_context - .db - .delete_local_qualified_identity( - &voter_identity_id, - &self.app_context, - ) - .ok(); + if let Err(e) = self.app_context.db.delete_local_qualified_identity( + &voter_identity_id, + &self.app_context, + ) { + tracing::warn!( + "Failed to delete voter identity from database: {}", + e + ); + } } self.identity_to_remove = None; From b26fa7e05bce2b910cdbeadb6390d28040f085d7 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:32:37 -0600 Subject: [PATCH 5/9] fix: enforce security level validation for ENCRYPTION/DECRYPTION keys in Identity Create --- .../identities/add_new_identity_screen/mod.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index db7301789..847ac2c11 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -668,6 +668,7 @@ impl AddNewIdentityScreen { }); row.col(|ui| { ui.vertical(|ui| { + let prev_purpose = *purpose; ComboBox::from_id_salt(format!("purpose_combo_{}", i)) .selected_text(format!("{:?}", purpose)) .show_ui(ui, |ui| { @@ -681,7 +682,37 @@ impl AddNewIdentityScreen { Purpose::TRANSFER, "TRANSFER", ); + ui.selectable_value( + purpose, + Purpose::ENCRYPTION, + "ENCRYPTION", + ); + ui.selectable_value( + purpose, + Purpose::DECRYPTION, + "DECRYPTION", + ); }); + // Auto-set security level when purpose changes + if *purpose != prev_purpose { + match *purpose { + Purpose::TRANSFER => { + *security_level = SecurityLevel::CRITICAL; + } + Purpose::ENCRYPTION | Purpose::DECRYPTION => { + *security_level = SecurityLevel::MEDIUM; + } + Purpose::AUTHENTICATION => { + if *security_level != SecurityLevel::CRITICAL + && *security_level != SecurityLevel::HIGH + && *security_level != SecurityLevel::MEDIUM + { + *security_level = SecurityLevel::CRITICAL; + } + } + _ => {} + } + } }); }); row.col(|ui| { @@ -710,6 +741,11 @@ impl AddNewIdentityScreen { if *purpose == Purpose::TRANSFER { *security_level = SecurityLevel::CRITICAL; ui.label("Locked to CRITICAL"); + } else if *purpose == Purpose::ENCRYPTION + || *purpose == Purpose::DECRYPTION + { + *security_level = SecurityLevel::MEDIUM; + ui.label("Locked to MEDIUM"); } else { ui.selectable_value( security_level, From 8a3beb0dd0d93de98b47b6fa3bf5ee1de205a607 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:33:13 -0600 Subject: [PATCH 6/9] fix: replace expect() on DocumentQuery creation with error propagation in query_tokens.rs --- src/backend_task/tokens/query_tokens.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend_task/tokens/query_tokens.rs b/src/backend_task/tokens/query_tokens.rs index 014c57400..dc4aa70cd 100644 --- a/src/backend_task/tokens/query_tokens.rs +++ b/src/backend_task/tokens/query_tokens.rs @@ -27,7 +27,7 @@ impl AppContext { // ── 1. fetch keyword → contractId docs ──────────────────────────────── let mut kw_query = DocumentQuery::new(self.keyword_search_contract.clone(), "contractKeywords") - .expect("create query"); + .map_err(|e| format!("Failed to create document query: {}", e))?; kw_query.limit = 100; kw_query.start = cursor.clone(); kw_query = kw_query.with_where(WhereClause { @@ -69,7 +69,7 @@ impl AppContext { // build a WHERE contractId == cid query let mut desc_query = DocumentQuery::new(self.keyword_search_contract.clone(), "shortDescription") - .expect("create desc query"); + .map_err(|e| format!("Failed to create document query: {}", e))?; desc_query.limit = 1; // only one per contract (schema‑unique) desc_query = desc_query.with_where(WhereClause { field: "contractId".into(), From 213b9b05b40e33b2d3e30851adcc822fd1795747 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 11 Feb 2026 22:33:35 -0600 Subject: [PATCH 7/9] fix: replace unwrap() on identity/key submission in token creator with validation --- src/ui/tokens/tokens_screen/token_creator.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ui/tokens/tokens_screen/token_creator.rs b/src/ui/tokens/tokens_screen/token_creator.rs index 3b4dc2fc8..e03bc6f84 100644 --- a/src/ui/tokens/tokens_screen/token_creator.rs +++ b/src/ui/tokens/tokens_screen/token_creator.rs @@ -1484,11 +1484,23 @@ impl TokensScreen { } }; + // Validate identity and key are selected + let (identity, signing_key) = + match (&self.selected_identity, &self.selected_key) { + (Some(id), Some(key)) => (id.clone(), key.clone()), + _ => { + self.token_creator_error_message = + Some("Please select an identity and signing key.".to_string()); + self.close_token_creator_confirmation_popup(); + return AppAction::None; + } + }; + // Now create your tasks let tasks = vec![ BackendTask::TokenTask(Box::new(TokenTask::RegisterTokenContract { - identity: self.selected_identity.clone().unwrap(), - signing_key: Box::new(self.selected_key.clone().unwrap()), + identity, + signing_key: Box::new(signing_key), token_names: args.token_names, contract_keywords: args.contract_keywords, From 12dbeb880685098197e7215347cdf869b5251cc2 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 16 Feb 2026 13:44:40 -0600 Subject: [PATCH 8/9] fix: replace unwrap_or_default with explicit error handling in transfer 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 --- src/ui/identities/transfer_screen.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 3c8545814..07b119b89 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -501,10 +501,13 @@ impl ScreenLike for TransferScreen { fn refresh(&mut self) { // Refresh the identity because there might be new keys - let identities = self - .app_context - .load_local_qualified_identities() - .unwrap_or_default(); + let identities = match self.app_context.load_local_qualified_identities() { + Ok(list) => list, + Err(e) => { + tracing::warn!("Failed to load identities during refresh: {}", e); + Vec::new() + } + }; if let Some(refreshed) = identities .iter() .find(|identity| identity.identity.id() == self.identity.identity.id()) From 111ff0e8d0d7eb94ec6b5aa3e503d394b7de99ea Mon Sep 17 00:00:00 2001 From: Pasta Lil Claw Date: Mon, 16 Feb 2026 22:18:28 -0600 Subject: [PATCH 9/9] fix: address all Lukasz review comments from PR #563 (#584) - 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 --- src/backend_task/identity/mod.rs | 33 +++++++++++++++++ src/ui/identities/identities_screen.rs | 20 ++++++++--- src/ui/tokens/claim_tokens_screen.rs | 15 ++++---- src/ui/tokens/destroy_frozen_funds_screen.rs | 36 ++++++++++--------- src/ui/tokens/freeze_tokens_screen.rs | 36 ++++++++++--------- src/ui/tokens/mint_tokens_screen.rs | 37 +++++++++++--------- src/ui/tokens/pause_tokens_screen.rs | 19 +++++----- src/ui/tokens/resume_tokens_screen.rs | 19 +++++----- src/ui/tokens/transfer_tokens_screen.rs | 33 +++++++++-------- src/ui/tokens/unfreeze_tokens_screen.rs | 36 ++++++++++--------- 10 files changed, 178 insertions(+), 106 deletions(-) diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index 554668e47..86199807f 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -188,6 +188,39 @@ impl IdentityKeys { keys_input.iter().enumerate() { let id = (i + 1) as KeyID; + + // Validate security level matches key purpose (defense-in-depth) + match purpose { + Purpose::TRANSFER => { + if *security_level != SecurityLevel::CRITICAL { + return Err(format!( + "Key {}: TRANSFER purpose requires CRITICAL security level, got {:?}", + id, security_level + )); + } + } + Purpose::ENCRYPTION | Purpose::DECRYPTION => { + if *security_level != SecurityLevel::MEDIUM { + return Err(format!( + "Key {}: {:?} purpose requires MEDIUM security level, got {:?}", + id, purpose, security_level + )); + } + } + Purpose::AUTHENTICATION => { + if *security_level != SecurityLevel::CRITICAL + && *security_level != SecurityLevel::HIGH + && *security_level != SecurityLevel::MEDIUM + { + return Err(format!( + "Key {}: AUTHENTICATION purpose requires CRITICAL, HIGH, or MEDIUM security level, got {:?}", + id, security_level + )); + } + } + _ => {} + } + let data = match key_type { KeyType::ECDSA_SECP256K1 => private_key.public_key(&secp).to_bytes().into(), KeyType::ECDSA_HASH160 => private_key diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index a1c561c74..7601499a2 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -931,15 +931,27 @@ impl IdentitiesScreen { if ui.add(yes_button).clicked() { let identity_id = identity_to_remove.identity.id(); - let mut lock = self.identities.lock().unwrap(); - lock.shift_remove(&identity_id); - if let Err(e) = self + match self .app_context .db .delete_local_qualified_identity(&identity_id, &self.app_context) { - tracing::warn!("Failed to delete identity from database: {}", e); + Ok(_) => { + let mut lock = self.identities.lock().unwrap(); + lock.shift_remove(&identity_id); + } + Err(e) => { + tracing::warn!( + "Failed to delete identity from database: {}", + e + ); + self.backend_message = Some(( + format!("Failed to remove identity: {}", e), + MessageType::Error, + Utc::now(), + )); + } } if let Some((voter_identity, _)) = diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 3dfa20487..3d3ca86fa 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -204,11 +204,14 @@ impl ClaimTokensScreen { Some(ConfirmationStatus::Confirmed) => { self.confirmation_dialog = None; - if self.selected_key.is_none() { - self.error_message = Some("No signing key selected".into()); - self.status = ClaimTokensStatus::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 = ClaimTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -223,7 +226,7 @@ impl ClaimTokensScreen { token_position: self.identity_token_basic_info.token_position, actor_identity: self.identity.clone(), distribution_type, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: self.public_note.clone(), })), BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs b/src/ui/tokens/destroy_frozen_funds_screen.rs index 32496ae36..a7beb05b0 100644 --- a/src/ui/tokens/destroy_frozen_funds_screen.rs +++ b/src/ui/tokens/destroy_frozen_funds_screen.rs @@ -260,25 +260,29 @@ impl DestroyFrozenFundsScreen { } 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()); - 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 = DestroyFrozenFundsStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + }; - let maybe_frozen_id = Identifier::from_string_try_encodings( + let frozen_id = match Identifier::from_string_try_encodings( &self.frozen_identity_id, &[ dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, ], - ); - if maybe_frozen_id.is_err() { - self.error_message = Some("Invalid frozen identity format".into()); - self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); - return AppAction::None; - } - let frozen_id = maybe_frozen_id.unwrap(); + ) { + Ok(id) => id, + Err(_) => { + self.error_message = Some("Invalid frozen identity format".into()); + self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); + return AppAction::None; + } + }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -288,12 +292,12 @@ impl DestroyFrozenFundsScreen { let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - let group_info = if self.group_action_id.is_some() { + let group_info = if let Some(action_id) = self.group_action_id { self.group.as_ref().map(|(pos, _)| { GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( GroupStateTransitionInfo { group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), + action_id, action_is_proposer: false, }, ) @@ -309,7 +313,7 @@ impl DestroyFrozenFundsScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/freeze_tokens_screen.rs b/src/ui/tokens/freeze_tokens_screen.rs index 43b26ec96..8f1d25cf3 100644 --- a/src/ui/tokens/freeze_tokens_screen.rs +++ b/src/ui/tokens/freeze_tokens_screen.rs @@ -250,26 +250,30 @@ impl FreezeTokensScreen { /// Handle confirmation OK action 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()); - 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; + } + }; // Validate user input - let parsed = Identifier::from_string_try_encodings( + let freeze_id = match Identifier::from_string_try_encodings( &self.freeze_identity_id, &[ dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, ], - ); - 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(); + ) { + 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; + } + }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -280,12 +284,12 @@ impl FreezeTokensScreen { // Grab the data contract for this token from the app context let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - let group_info = if self.group_action_id.is_some() { + let group_info = if let Some(action_id) = self.group_action_id { self.group.as_ref().map(|(pos, _)| { GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( GroupStateTransitionInfo { group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), + action_id, action_is_proposer: false, }, ) @@ -301,7 +305,7 @@ impl FreezeTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/mint_tokens_screen.rs b/src/ui/tokens/mint_tokens_screen.rs index 2a57d6553..27a07691d 100644 --- a/src/ui/tokens/mint_tokens_screen.rs +++ b/src/ui/tokens/mint_tokens_screen.rs @@ -278,11 +278,14 @@ impl MintTokensScreen { } 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()); - 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 = MintTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + }; if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); @@ -290,21 +293,21 @@ impl MintTokensScreen { return AppAction::None; } - let parsed_receiver_id = Identifier::from_string_try_encodings( + let receiver_id = match Identifier::from_string_try_encodings( &self.recipient_identity_id, &[ dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, ], - ); - - if parsed_receiver_id.is_err() { - self.status = MintTokensStatus::ErrorMessage("Invalid receiver".into()); - self.error_message = Some("Invalid receiver".into()); - return AppAction::None; - } + ) { + Ok(id) => id, + Err(_) => { + self.status = MintTokensStatus::ErrorMessage("Invalid receiver".into()); + self.error_message = Some("Invalid receiver".into()); + return AppAction::None; + } + }; - let receiver_id = parsed_receiver_id.unwrap(); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") @@ -313,12 +316,12 @@ impl MintTokensScreen { let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - let group_info = if self.group_action_id.is_some() { + let group_info = if let Some(action_id) = self.group_action_id { self.group.as_ref().map(|(pos, _)| { GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( GroupStateTransitionInfo { group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), + action_id, action_is_proposer: false, }, ) @@ -333,7 +336,7 @@ impl MintTokensScreen { sending_identity: self.identity_token_info.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/pause_tokens_screen.rs b/src/ui/tokens/pause_tokens_screen.rs index ffb498130..4b92895c4 100644 --- a/src/ui/tokens/pause_tokens_screen.rs +++ b/src/ui/tokens/pause_tokens_screen.rs @@ -208,11 +208,14 @@ impl PauseTokensScreen { Some(ConfirmationStatus::Confirmed) => { self.confirmation_dialog = None; - if self.selected_key.is_none() { - self.error_message = Some("No signing key selected".into()); - self.status = PauseTokensStatus::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 = PauseTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -224,12 +227,12 @@ impl PauseTokensScreen { let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - let group_info = if self.group_action_id.is_some() { + let group_info = if let Some(action_id) = self.group_action_id { self.group.as_ref().map(|(pos, _)| { GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( GroupStateTransitionInfo { group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), + action_id, action_is_proposer: false, }, ) @@ -244,7 +247,7 @@ impl PauseTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/resume_tokens_screen.rs b/src/ui/tokens/resume_tokens_screen.rs index d53273408..7d9777064 100644 --- a/src/ui/tokens/resume_tokens_screen.rs +++ b/src/ui/tokens/resume_tokens_screen.rs @@ -208,11 +208,14 @@ impl ResumeTokensScreen { Some(ConfirmationStatus::Confirmed) => { self.confirmation_dialog = None; - if self.selected_key.is_none() { - self.error_message = Some("No signing key selected".into()); - self.status = ResumeTokensStatus::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 = ResumeTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -224,12 +227,12 @@ impl ResumeTokensScreen { let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - let group_info = if self.group_action_id.is_some() { + let group_info = if let Some(action_id) = self.group_action_id { self.group.as_ref().map(|(pos, _)| { GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( GroupStateTransitionInfo { group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), + action_id, action_is_proposer: false, }, ) @@ -244,7 +247,7 @@ impl ResumeTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: if self.group_action_id.is_some() { None } else { diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index c3bdd3e87..d84b0838b 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -193,11 +193,14 @@ impl TransferTokensScreen { } fn confirmation_ok(&mut self) -> AppAction { - if self.selected_key.is_none() { - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage("No signing key selected".into()); - return AppAction::None; - } + let signing_key = match self.selected_key.clone() { + Some(key) => key, + None => { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("No signing key selected".into()); + return AppAction::None; + } + }; if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { self.transfer_tokens_status = @@ -205,21 +208,21 @@ impl TransferTokensScreen { return AppAction::None; } - let parsed_receiver_id = Identifier::from_string_try_encodings( + let receiver_id = match Identifier::from_string_try_encodings( &self.receiver_identity_id, &[ dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, ], - ); - - if parsed_receiver_id.is_err() { - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage("Invalid receiver".into()); - return AppAction::None; - } + ) { + Ok(id) => id, + Err(_) => { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("Invalid receiver".into()); + return AppAction::None; + } + }; - let receiver_id = parsed_receiver_id.unwrap(); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") @@ -240,7 +243,7 @@ impl TransferTokensScreen { amount: self.amount.clone().unwrap_or(Amount::new(0, 0)).value(), data_contract, token_position: self.identity_token_balance.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: self.public_note.clone(), }, ))) diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs b/src/ui/tokens/unfreeze_tokens_screen.rs index 859dffb1f..98f274df3 100644 --- a/src/ui/tokens/unfreeze_tokens_screen.rs +++ b/src/ui/tokens/unfreeze_tokens_screen.rs @@ -252,26 +252,30 @@ impl UnfreezeTokensScreen { } 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()); - 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 = UnfreezeTokensStatus::ErrorMessage("No key selected".into()); + return AppAction::None; + } + }; // Validate user input - let parsed = Identifier::from_string_try_encodings( + let unfreeze_id = match Identifier::from_string_try_encodings( &self.unfreeze_identity_id, &[ dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); - return AppAction::None; - } - let unfreeze_id = parsed.unwrap(); + ) { + Ok(id) => id, + Err(_) => { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); + return AppAction::None; + } + }; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -282,12 +286,12 @@ impl UnfreezeTokensScreen { // Grab the data contract for this token from the app context let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - let group_info = if self.group_action_id.is_some() { + let group_info = if let Some(action_id) = self.group_action_id { self.group.as_ref().map(|(pos, _)| { GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( GroupStateTransitionInfo { group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), + action_id, action_is_proposer: false, }, ) @@ -304,7 +308,7 @@ impl UnfreezeTokensScreen { actor_identity: self.identity.clone(), data_contract, token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().unwrap(), + signing_key, public_note: if self.group_action_id.is_some() { None } else {