diff --git a/src/spv/manager.rs b/src/spv/manager.rs index 7c63774c4..d06e6ede8 100644 --- a/src/spv/manager.rs +++ b/src/spv/manager.rs @@ -1160,14 +1160,16 @@ impl SpvManager { *guard = SpvStatus::Error; drop(guard); // Maintain lock ordering: status → release → last_error } - // TODO: truncate error string to ~512 chars to prevent - // unbounded memory from adversarial peer errors (CWE-400). - let msg = format!("Sync manager {} failed: {}", manager, error); + + // Truncate error before formatting to avoid + // large transient allocations from adversarial peers. + let limit = error.floor_char_boundary(100); + let msg = format!("Sync manager {} failed: {}", manager, &error[..limit]); if let Ok(mut err_guard) = last_error.write() { if err_guard.is_none() { *err_guard = Some(msg); } else { - tracing::warn!("SPV last_error already set, ignoring subsequent: {}", msg); + tracing::warn!(%manager, error, "SPV last_error already set, ignoring subsequent: {}", msg); } } } diff --git a/src/ui/components/message_banner.rs b/src/ui/components/message_banner.rs index eee83fc9a..c6caa7d4f 100644 --- a/src/ui/components/message_banner.rs +++ b/src/ui/components/message_banner.rs @@ -730,6 +730,9 @@ fn icon_for_type(message_type: MessageType) -> &'static str { pub trait ResultBannerExt { /// If `Err`, displays a global error banner with the error's `Display` text. /// Returns `self` unchanged — this is a side-effect-only method. + /// + /// INTENTIONAL(SEC-007): Raw `Display` text is shown directly. Callers must + /// ensure error types have user-friendly Display implementations. fn or_show_error(self, ctx: &egui::Context) -> Self; } diff --git a/src/ui/contracts_documents/document_action_screen.rs b/src/ui/contracts_documents/document_action_screen.rs index a5c3990d5..0eb5ece0e 100644 --- a/src/ui/contracts_documents/document_action_screen.rs +++ b/src/ui/contracts_documents/document_action_screen.rs @@ -15,7 +15,7 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock_popup::{ WalletUnlockPopup, WalletUnlockResult, try_open_wallet_no_password, wallet_needs_unlock, }; -use crate::ui::components::{BannerHandle, MessageBanner, ResultBannerExt}; +use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt, ResultBannerExt}; use crate::ui::helpers::{ TransactionType, add_contract_doc_type_chooser_with_filtering, add_key_chooser_with_doc_type, show_success_screen_with_info, @@ -90,7 +90,7 @@ pub struct DocumentActionScreen { pub action_type: DocumentActionType, // Common fields - pub backend_message: Option, + no_documents_found: bool, pub selected_identity: Option, selected_identity_string: String, pub selected_key: Option, @@ -163,7 +163,7 @@ impl DocumentActionScreen { Self { app_context, action_type, - backend_message: None, + no_documents_found: false, selected_identity, selected_identity_string, selected_key: None, @@ -190,19 +190,15 @@ impl DocumentActionScreen { } fn set_fetching_banner(&mut self, ctx: &egui::Context, text: &str) { - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } + self.refresh_banner.take_and_clear(); let handle = MessageBanner::set_global(ctx, text, crate::ui::MessageType::Info); handle.with_elapsed(); self.refresh_banner = Some(handle); } fn reset_screen(&mut self) { - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } - self.backend_message = None; + self.refresh_banner.take_and_clear(); + self.no_documents_found = false; self.selected_identity = None; self.selected_identity_string = String::new(); self.selected_key = None; @@ -228,6 +224,12 @@ impl DocumentActionScreen { ui.heading("1. Select a contract and document type:"); ui.add_space(10.0); + let prev_contract_id = self.selected_contract.as_ref().map(|c| c.contract.id()); + let prev_doc_type = self + .selected_document_type + .as_ref() + .map(|d| d.name().to_owned()); + add_contract_doc_type_chooser_with_filtering( ui, &mut self.contract_search, @@ -235,6 +237,19 @@ impl DocumentActionScreen { &mut self.selected_contract, &mut self.selected_document_type, ); + + let contract_changed = + prev_contract_id != self.selected_contract.as_ref().map(|c| c.contract.id()); + let doc_type_changed = prev_doc_type + != self + .selected_document_type + .as_ref() + .map(|d| d.name().to_owned()); + if contract_changed || doc_type_changed { + self.no_documents_found = false; + self.fetched_documents.clear(); + } + ui.add_space(10.0); } @@ -265,6 +280,8 @@ impl DocumentActionScreen { // Handle identity change - auto-select key and update wallet if response.changed() { + self.no_documents_found = false; + self.fetched_documents.clear(); if let Some(identity) = &self.selected_identity { // Auto-select a suitable key for document actions // Note: MASTER keys cannot be used for document operations, @@ -467,9 +484,7 @@ impl DocumentActionScreen { } } - if let Some(backend_message) = &self.backend_message - && backend_message.contains("No owned documents found") - { + if self.no_documents_found { ui.add_space(10.0); ui.label("No owned documents found."); } @@ -520,7 +535,11 @@ impl DocumentActionScreen { ))); } } else { - self.backend_message = Some("Invalid Document ID format".to_string()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Invalid Document ID format", + crate::ui::MessageType::Error, + ); } } @@ -576,7 +595,11 @@ impl DocumentActionScreen { ))); } } else { - self.backend_message = Some("Invalid Document ID format".to_string()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Invalid Document ID format", + crate::ui::MessageType::Error, + ); } } }); @@ -910,7 +933,6 @@ impl DocumentActionScreen { .min_size(egui::vec2(100.0, 30.0)); if ui.add(button).clicked() && self.can_broadcast() { - self.backend_message = None; let task = self.create_document_action(); if task != BackendTask::None { self.broadcast_status = BroadcastStatus::Broadcasting; @@ -972,7 +994,11 @@ impl DocumentActionScreen { ))) } Err(e) => { - self.backend_message = Some(format!("Failed to build document: {}", e)); + MessageBanner::set_global( + self.app_context.egui_ctx(), + format!("Failed to build document: {}", e), + crate::ui::MessageType::Error, + ); BackendTask::None } } @@ -1067,7 +1093,11 @@ impl DocumentActionScreen { ))) } Err(e) => { - self.backend_message = Some(format!("Failed to build updated document: {}", e)); + MessageBanner::set_global( + self.app_context.egui_ctx(), + format!("Failed to build updated document: {}", e), + crate::ui::MessageType::Error, + ); BackendTask::None } } @@ -1602,23 +1632,20 @@ impl ScreenLike for DocumentActionScreen { // Backend messages are handled via display_message } - fn display_message(&mut self, message: &str, message_type: crate::ui::MessageType) { + fn display_message(&mut self, _message: &str, message_type: crate::ui::MessageType) { if matches!( message_type, crate::ui::MessageType::Error | crate::ui::MessageType::Warning - ) && let Some(handle) = self.refresh_banner.take() - { - handle.clear(); + ) { + self.refresh_banner.take_and_clear(); } - self.backend_message = Some(message.to_string()); + // Banner display is handled globally by AppState; this is only for side-effects. self.broadcast_status = BroadcastStatus::NotBroadcasted; } fn display_task_result(&mut self, result: crate::ui::BackendTaskSuccessResult) { // Clear the progress banner on any completed task - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } + self.refresh_banner.take_and_clear(); match result { BackendTaskSuccessResult::BroadcastedDocument(_) => { self.broadcast_status = BroadcastStatus::Broadcasted; @@ -1687,28 +1714,34 @@ impl ScreenLike for DocumentActionScreen { self.fetched_price = Some(price); } Ok(None) => { - self.backend_message = - Some("Document has no price set".to_string()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Document has no price set", + crate::ui::MessageType::Error, + ); self.fetched_price = None; } Err(_) => { - self.backend_message = - Some("Failed to get document price".to_string()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Failed to get document price", + crate::ui::MessageType::Error, + ); self.fetched_price = None; } } } else { - self.backend_message = Some("No document found".to_string()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "No document found", + crate::ui::MessageType::Error, + ); self.fetched_price = None; } } DocumentActionType::Delete => { // For delete, store the fetched documents - if documents.is_empty() { - self.backend_message = Some("No owned documents found".to_string()); - } else { - self.backend_message = None; - } + self.no_documents_found = documents.is_empty(); self.fetched_documents = documents; } _ => {} @@ -1766,7 +1799,12 @@ impl DocumentActionScreen { if let Some(wallet) = &self.wallet { if !self.wallet_open_attempted { if let Err(e) = try_open_wallet_no_password(wallet) { - self.backend_message = Some(e); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Unable to open wallet. Please unlock it and try again.", + crate::ui::MessageType::Error, + ) + .with_details(e); } self.wallet_open_attempted = true; } @@ -1790,26 +1828,6 @@ impl DocumentActionScreen { _ => self.render_action_specific_inputs(ui), }; - if let Some(ref msg) = self.backend_message { - ui.add_space(10.0); - let error_color = DashColors::error_color(ui.visuals().dark_mode); - let msg = msg.clone(); - Frame::new() - .fill(error_color.gamma_multiply(0.1)) - .inner_margin(Margin::symmetric(10, 8)) - .corner_radius(5.0) - .stroke(egui::Stroke::new(1.0, error_color)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(&msg).color(error_color)); - ui.add_space(10.0); - if ui.small_button("Dismiss").clicked() { - self.backend_message = None; - } - }); - }); - } - action }) .inner diff --git a/src/ui/contracts_documents/register_contract_screen.rs b/src/ui/contracts_documents/register_contract_screen.rs index f0e9e0afe..2fc5fc8ca 100644 --- a/src/ui/contracts_documents/register_contract_screen.rs +++ b/src/ui/contracts_documents/register_contract_screen.rs @@ -13,7 +13,7 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock_popup::{ WalletUnlockPopup, WalletUnlockResult, try_open_wallet_no_password, wallet_needs_unlock, }; -use crate::ui::components::{BannerHandle, MessageBanner, ResultBannerExt}; +use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt, ResultBannerExt}; use crate::ui::helpers::{TransactionType, add_key_chooser}; use crate::ui::identities::get_selected_wallet; use crate::ui::theme::DashColors; @@ -54,7 +54,6 @@ pub struct RegisterDataContractScreen { pub selected_wallet: Option>>, wallet_open_attempted: bool, wallet_unlock_popup: WalletUnlockPopup, - error_message: Option, completed_fee_result: Option, refresh_banner: Option, } @@ -110,7 +109,6 @@ impl RegisterDataContractScreen { selected_wallet, wallet_open_attempted: false, wallet_unlock_popup: WalletUnlockPopup::new(), - error_message: None, completed_fee_result: None, refresh_banner: None, } @@ -285,9 +283,7 @@ impl RegisterDataContractScreen { && let ContractTask::RegisterDataContract(_, _, _, _) = **contract_task { self.broadcast_status = BroadcastStatus::Broadcasting; - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } + self.refresh_banner.take_and_clear(); let handle = MessageBanner::set_global(ui.ctx(), "Broadcasting contract...", MessageType::Info); handle.with_elapsed(); @@ -331,14 +327,11 @@ impl RegisterDataContractScreen { impl ScreenLike for RegisterDataContractScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { - if matches!(message_type, MessageType::Error | MessageType::Warning) - && let Some(handle) = self.refresh_banner.take() - { - handle.clear(); + if matches!(message_type, MessageType::Error | MessageType::Warning) { + self.refresh_banner.take_and_clear(); } if message_type == MessageType::Error { if message.contains("proof error logged, contract inserted into the database") { - self.error_message = Some(message.to_string()); self.broadcast_status = BroadcastStatus::Done; } else { self.broadcast_status = BroadcastStatus::BroadcastError(message.to_string()); @@ -352,9 +345,7 @@ impl ScreenLike for RegisterDataContractScreen { self.broadcast_status = BroadcastStatus::Broadcasting; } BackendTaskSuccessResult::RegisteredContract(fee_result) => { - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } + self.refresh_banner.take_and_clear(); self.completed_fee_result = Some(fee_result); self.broadcast_status = BroadcastStatus::Done; } @@ -515,9 +506,8 @@ impl ScreenLike for RegisterDataContractScreen { // Render wallet unlock if needed if let Some(wallet) = &self.selected_wallet { if !self.wallet_open_attempted { - if let Err(e) = try_open_wallet_no_password(wallet) { - self.error_message = Some(e); - } + let _ = try_open_wallet_no_password(wallet) + .or_show_error(ui.ctx()); self.wallet_open_attempted = true; } if wallet_needs_unlock(wallet) { diff --git a/src/ui/contracts_documents/update_contract_screen.rs b/src/ui/contracts_documents/update_contract_screen.rs index 716b6cd1c..933c3066f 100644 --- a/src/ui/contracts_documents/update_contract_screen.rs +++ b/src/ui/contracts_documents/update_contract_screen.rs @@ -14,7 +14,7 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock_popup::{ WalletUnlockPopup, WalletUnlockResult, try_open_wallet_no_password, wallet_needs_unlock, }; -use crate::ui::components::{BannerHandle, MessageBanner, ResultBannerExt}; +use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt, ResultBannerExt}; use crate::ui::helpers::{TransactionType, add_key_chooser}; use crate::ui::identities::get_selected_wallet; use crate::ui::theme::DashColors; @@ -59,7 +59,6 @@ pub struct UpdateDataContractScreen { pub selected_wallet: Option>>, wallet_open_attempted: bool, wallet_unlock_popup: WalletUnlockPopup, - error_message: Option, completed_fee_result: Option, refresh_banner: Option, } @@ -120,7 +119,6 @@ impl UpdateDataContractScreen { selected_wallet, wallet_open_attempted: false, wallet_unlock_popup: WalletUnlockPopup::new(), - error_message: None, completed_fee_result: None, refresh_banner: None, } @@ -294,9 +292,7 @@ impl UpdateDataContractScreen { && let ContractTask::UpdateDataContract(_, _, _) = **contract_task { self.broadcast_status = BroadcastStatus::FetchingNonce; - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } + self.refresh_banner.take_and_clear(); let handle = MessageBanner::set_global( ui.ctx(), "Fetching identity contract nonce...", @@ -342,14 +338,11 @@ impl UpdateDataContractScreen { impl ScreenLike for UpdateDataContractScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { - if matches!(message_type, MessageType::Error | MessageType::Warning) - && let Some(handle) = self.refresh_banner.take() - { - handle.clear(); + if matches!(message_type, MessageType::Error | MessageType::Warning) { + self.refresh_banner.take_and_clear(); } if message_type == MessageType::Error { if message.contains("proof error logged, contract inserted into the database") { - self.error_message = Some(message.to_string()); self.broadcast_status = BroadcastStatus::Done; } else { self.broadcast_status = BroadcastStatus::BroadcastError(message.to_string()); @@ -367,9 +360,7 @@ impl ScreenLike for UpdateDataContractScreen { } } BackendTaskSuccessResult::UpdatedContract(fee_result) => { - if let Some(handle) = self.refresh_banner.take() { - handle.clear(); - } + self.refresh_banner.take_and_clear(); self.completed_fee_result = Some(fee_result); self.broadcast_status = BroadcastStatus::Done; } @@ -528,9 +519,7 @@ impl ScreenLike for UpdateDataContractScreen { // Render the wallet unlock if needed if let Some(wallet) = &self.selected_wallet { if !self.wallet_open_attempted { - if let Err(e) = try_open_wallet_no_password(wallet) { - self.error_message = Some(e); - } + let _ = try_open_wallet_no_password(wallet).or_show_error(ui.ctx()); self.wallet_open_attempted = true; } if wallet_needs_unlock(wallet) { diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index e96e36dac..a2d02e802 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -8,7 +8,7 @@ use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_ use crate::ui::helpers::{TransactionType, add_key_chooser}; use std::collections::HashSet; use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; @@ -32,6 +32,7 @@ use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; use crate::model::wallet::Wallet; use crate::ui::theme::DashColors; use crate::ui::{MessageType, Screen, ScreenLike}; +use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt}; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock_popup::{wallet_needs_unlock, try_open_wallet_no_password, WalletUnlockPopup, WalletUnlockResult}; use crate::ui::identities::get_selected_wallet; @@ -43,8 +44,8 @@ use super::tokens_screen::IdentityTokenBasicInfo; #[derive(PartialEq)] pub enum ClaimTokensStatus { NotStarted, - WaitingForResult(u64), - ErrorMessage(String), + WaitingForResult, + Error, Complete, } @@ -58,7 +59,7 @@ pub struct ClaimTokensScreen { token_configuration: TokenConfiguration, distribution_type: Option, status: ClaimTokensStatus, - error_message: Option, + refresh_banner: Option, pub app_context: Arc, confirmation_dialog: Option, selected_wallet: Option>>, @@ -132,7 +133,7 @@ impl ClaimTokensScreen { token_configuration, distribution_type, status: ClaimTokensStatus::NotStarted, - error_message: None, + refresh_banner: None, app_context: app_context.clone(), confirmation_dialog: None, selected_wallet, @@ -196,7 +197,12 @@ impl ClaimTokensScreen { fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { let Some(identity) = self.identity.clone() else { - self.status = ClaimTokensStatus::ErrorMessage("Identity not available".to_string()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Identity not available", + MessageType::Error, + ); + self.status = ClaimTokensStatus::Error; return AppAction::None; }; let distribution_type = self @@ -217,17 +223,25 @@ impl ClaimTokensScreen { 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()); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "No signing key selected", + MessageType::Error, + ); + self.status = ClaimTokensStatus::Error; return AppAction::None; } }; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - self.status = ClaimTokensStatus::WaitingForResult(now); + self.status = ClaimTokensStatus::WaitingForResult; + self.refresh_banner.take_and_clear(); + let handle = MessageBanner::set_global( + self.app_context.egui_ctx(), + "Claiming tokens...", + MessageType::Info, + ); + handle.with_elapsed(); + self.refresh_banner = Some(handle); AppAction::BackendTasks( vec![ @@ -263,14 +277,16 @@ impl ClaimTokensScreen { } impl ScreenLike for ClaimTokensScreen { - fn display_message(&mut self, message: &str, message_type: MessageType) { - if let MessageType::Error = message_type { - self.status = ClaimTokensStatus::ErrorMessage(message.to_string()); - self.error_message = Some(message.to_string()); + fn display_message(&mut self, _message: &str, message_type: MessageType) { + // Banner display is handled globally by AppState; this is only for side-effects. + if matches!(message_type, MessageType::Error | MessageType::Warning) { + self.refresh_banner.take_and_clear(); + self.status = ClaimTokensStatus::Error; } } fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + self.refresh_banner.take_and_clear(); if let BackendTaskSuccessResult::ClaimedTokens(fee_result) = backend_task_success_result { self.completed_fee_result = Some(fee_result); self.status = ClaimTokensStatus::Complete; @@ -384,7 +400,12 @@ impl ScreenLike for ClaimTokensScreen { if let Some(wallet) = &self.selected_wallet { if !self.wallet_open_attempted { if let Err(e) = try_open_wallet_no_password(wallet) { - self.error_message = Some(e); + MessageBanner::set_global( + self.app_context.egui_ctx(), + "Unable to open wallet. Please unlock it and try again.", + MessageType::Error, + ) + .with_details(e); } self.wallet_open_attempted = true; } @@ -574,9 +595,12 @@ impl ScreenLike for ClaimTokensScreen { if ui.add(button).clicked() { if self.distribution_type.is_none() { - self.status = ClaimTokensStatus::ErrorMessage( - "Please select a distribution type.".to_string(), + MessageBanner::set_global( + ctx, + "Please select a distribution type.", + MessageType::Error, ); + self.status = ClaimTokensStatus::Error; return; } else if self.confirmation_dialog.is_none() { self.confirmation_dialog = Some(ConfirmationDialog::new( @@ -591,39 +615,8 @@ impl ScreenLike for ClaimTokensScreen { action |= self.show_confirmation_popup(ui); } - ui.add_space(10.0); - match &self.status { - ClaimTokensStatus::NotStarted => {} - ClaimTokensStatus::WaitingForResult(start_time) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - let elapsed = now - start_time; - ui.label(format!("Claiming... elapsed: {}s", elapsed)); - } - ClaimTokensStatus::ErrorMessage(msg) => { - let error_color = DashColors::ERROR; - let msg = msg.clone(); - Frame::new() - .fill(error_color.gamma_multiply(0.1)) - .inner_margin(Margin::symmetric(10, 8)) - .corner_radius(5.0) - .stroke(egui::Stroke::new(1.0, error_color)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - RichText::new(format!("Error: {}", msg)).color(error_color), - ); - ui.add_space(10.0); - if ui.small_button("Dismiss").clicked() { - self.status = ClaimTokensStatus::NotStarted; - } - }); - }); - } - ClaimTokensStatus::Complete => {} - } + // Status display is handled by the global MessageBanner + // (progress with elapsed timer, errors, etc.) } }); diff --git a/src/ui/tokens/tokens_screen/keyword_search.rs b/src/ui/tokens/tokens_screen/keyword_search.rs index 5a1109b09..59e402e5a 100644 --- a/src/ui/tokens/tokens_screen/keyword_search.rs +++ b/src/ui/tokens/tokens_screen/keyword_search.rs @@ -3,7 +3,7 @@ use crate::backend_task::BackendTask; use crate::backend_task::contract::ContractTask; use crate::backend_task::tokens::TokenTask; use crate::ui::MessageType; -use crate::ui::components::MessageBanner; +use crate::ui::components::{MessageBanner, OptionBannerExt}; use crate::ui::theme::DashColors; use crate::ui::tokens::tokens_screen::{ ContractDescriptionInfo, ContractSearchStatus, TokensScreen, @@ -61,9 +61,7 @@ impl TokensScreen { // Clear old results, set status self.search_results.lock().unwrap().clear(); self.contract_search_status = ContractSearchStatus::WaitingForResult; - if let Some(h) = self.operation_banner.take() { - h.clear(); - } + self.operation_banner.take_and_clear(); let handle = MessageBanner::set_global( ui.ctx(), "Searching contracts...", diff --git a/src/ui/tokens/tokens_screen/mod.rs b/src/ui/tokens/tokens_screen/mod.rs index 4e79905a0..0330998ce 100644 --- a/src/ui/tokens/tokens_screen/mod.rs +++ b/src/ui/tokens/tokens_screen/mod.rs @@ -2504,9 +2504,7 @@ impl TokensScreen { // Set adding status self.adding_token_start_time = Some(Utc::now()); self.adding_token_name = Some(token_info.token_name.clone()); - if let Some(h) = self.operation_banner.take() { - h.clear(); - } + self.operation_banner.take_and_clear(); let handle = MessageBanner::set_global( self.app_context.egui_ctx(), "Adding token...", @@ -2534,9 +2532,7 @@ impl TokensScreen { if let Some(next_cursor) = self.next_cursors.last().cloned() { // set status self.contract_search_status = ContractSearchStatus::WaitingForResult; - if let Some(h) = self.operation_banner.take() { - h.clear(); - } + self.operation_banner.take_and_clear(); let handle = MessageBanner::set_global( self.app_context.egui_ctx(), "Searching contracts...", @@ -2566,9 +2562,7 @@ impl TokensScreen { // Move to (page - 1) self.search_current_page -= 1; self.contract_search_status = ContractSearchStatus::WaitingForResult; - if let Some(h) = self.operation_banner.take() { - h.clear(); - } + self.operation_banner.take_and_clear(); let handle = MessageBanner::set_global( self.app_context.egui_ctx(), "Searching contracts...", @@ -2977,9 +2971,7 @@ impl ScreenLike for TokensScreen { if matches!(token_task.as_ref(), TokenTask::QueryMyTokenBalances) => { self.refreshing_status = RefreshingStatus::Refreshing; - if let Some(h) = self.operation_banner.take() { - h.clear(); - } + self.operation_banner.take_and_clear(); let handle = MessageBanner::set_global(ctx, "Refreshing tokens...", MessageType::Info); handle.with_elapsed(); @@ -3096,9 +3088,7 @@ impl ScreenLike for TokensScreen { fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { // Clear any active operation banner - if let Some(h) = self.operation_banner.take() { - h.clear(); - } + self.operation_banner.take_and_clear(); match backend_task_success_result { BackendTaskSuccessResult::DescriptionsByKeyword(descriptions, next_cursor) => { diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 6ff955525..0227e97c4 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -11,13 +11,13 @@ use crate::context::connection_status::spv_phase_summary; use crate::model::amount::Amount; use crate::model::wallet::{Wallet, WalletSeedHash, WalletTransaction}; use crate::spv::{CoreBackendMode, SpvStatus}; -use crate::ui::components::MessageBanner; use crate::ui::components::component_trait::Component; use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock_popup::{WalletUnlockPopup, WalletUnlockResult}; +use crate::ui::components::{BannerHandle, MessageBanner, OptionBannerExt}; use crate::ui::helpers::copy_text_to_clipboard; use crate::ui::theme::DashColors; use crate::ui::wallets::account_summary::{ @@ -100,6 +100,8 @@ pub struct WalletsBalancesScreen { pending_refresh_mode: RefreshMode, /// Whether we should search for asset locks after wallet is unlocked pending_asset_lock_search_after_unlock: bool, + /// Banner handle for asset lock search progress + asset_lock_search_banner: Option, /// Current page for single key wallet UTXO pagination (0-indexed) utxo_page: usize, /// Selected refresh mode (only shown in dev mode) @@ -196,6 +198,7 @@ impl WalletsBalancesScreen { pending_refresh_after_unlock: false, pending_refresh_mode: RefreshMode::default(), pending_asset_lock_search_after_unlock: false, + asset_lock_search_banner: None, utxo_page: 0, refresh_mode: RefreshMode::default(), platform_sync_info, @@ -1742,11 +1745,14 @@ impl ScreenLike for WalletsBalancesScreen { if self.pending_asset_lock_search_after_unlock { self.pending_asset_lock_search_after_unlock = false; if let Some(wallet_arc) = self.selected_wallet.clone() { - MessageBanner::set_global( + self.asset_lock_search_banner.take_and_clear(); + let handle = MessageBanner::set_global( ctx, "Searching for unused asset locks...", MessageType::Info, ); + handle.with_elapsed(); + self.asset_lock_search_banner = Some(handle); action |= AppAction::BackendTask(BackendTask::CoreTask( CoreTask::RecoverAssetLocks(wallet_arc), )); @@ -1916,11 +1922,14 @@ impl ScreenLike for WalletsBalancesScreen { action = AppAction::None; } else { // Wallet is unlocked - proceed with search - MessageBanner::set_global( + self.asset_lock_search_banner.take_and_clear(); + let handle = MessageBanner::set_global( ctx, "Searching for unused asset locks...", MessageType::Info, ); + handle.with_elapsed(); + self.asset_lock_search_banner = Some(handle); action = AppAction::BackendTask(BackendTask::CoreTask( CoreTask::RecoverAssetLocks(wallet_arc), )); @@ -1937,6 +1946,7 @@ impl ScreenLike for WalletsBalancesScreen { // Banner display is handled globally by AppState; this is only for side-effects. if matches!(message_type, MessageType::Error | MessageType::Warning) { self.refreshing = false; + self.asset_lock_search_banner.take_and_clear(); // If the fund platform dialog is processing, show error in the dialog instead if self.fund_platform_dialog.is_processing { @@ -1981,6 +1991,7 @@ impl ScreenLike for WalletsBalancesScreen { recovered_count, total_amount, } => { + self.asset_lock_search_banner.take_and_clear(); let msg = if recovered_count == 0 { "No additional unused asset locks found".to_string() } else {