diff --git a/src/ui/tokens/tokens_screen/mod.rs b/src/ui/tokens/tokens_screen/mod.rs index 123ff9355..28a1e4dfb 100644 --- a/src/ui/tokens/tokens_screen/mod.rs +++ b/src/ui/tokens/tokens_screen/mod.rs @@ -2985,13 +2985,15 @@ impl ScreenLike for TokensScreen { #[cfg(test)] mod tests { use std::path::Path; + use std::sync::Once; use crate::app_dir::copy_env_file_if_not_exists; use crate::database::Database; use crate::model::qualified_identity::IdentityStatus; use crate::model::qualified_identity::encrypted_key_storage::KeyStorage; - use super::*; use dash_sdk::dpp::dashcore::Network; + use super::*; + use dash_sdk::dpp::dashcore::Network; use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; use dash_sdk::dpp::data_contract::associated_token::token_configuration_localization::accessors::v0::TokenConfigurationLocalizationV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_keeps_history_rules::TokenKeepsHistoryRules; @@ -3000,6 +3002,34 @@ mod tests { use dash_sdk::dpp::identifier::Identifier; use dash_sdk::platform::{DataContract, Identity}; + fn ensure_test_env() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + copy_env_file_if_not_exists(); // required by AppContext::new() + + // Ensure minimum required configs exist even if .env isn't loaded. + // Safety: tests set env vars once to ensure deterministic config. + // No other test mutates these values. + unsafe { + std::env::set_var("MAINNET_dapi_addresses", "http://127.0.0.1:1443"); + std::env::set_var("MAINNET_core_host", "127.0.0.1"); + std::env::set_var("MAINNET_core_rpc_port", "9998"); + std::env::set_var("MAINNET_core_rpc_user", "dashrpc"); + std::env::set_var("MAINNET_core_rpc_password", "password"); + std::env::set_var("MAINNET_insight_api_url", "http://127.0.0.1:3001"); + std::env::set_var("MAINNET_show_in_ui", "true"); + + std::env::set_var("LOCAL_dapi_addresses", "http://127.0.0.1:2443"); + std::env::set_var("LOCAL_core_host", "127.0.0.1"); + std::env::set_var("LOCAL_core_rpc_port", "20302"); + std::env::set_var("LOCAL_core_rpc_user", "dashmate"); + std::env::set_var("LOCAL_core_rpc_password", "password"); + std::env::set_var("LOCAL_insight_api_url", "http://127.0.0.1:3001"); + std::env::set_var("LOCAL_show_in_ui", "true"); + } + }); + } + impl ChangeControlRulesUI { /// Sets every field to some dummy/test value to ensure coverage in tests. pub fn set_all_fields_for_testing(&mut self) { @@ -3026,7 +3056,7 @@ mod tests { let db = Arc::new(Database::new(db_file_path).unwrap()); db.initialize(Path::new(&db_file_path)).unwrap(); - copy_env_file_if_not_exists(); // Required by AppContext::new() + ensure_test_env(); let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); @@ -3332,7 +3362,7 @@ mod tests { let db = Arc::new(Database::new(db_file_path).unwrap()); db.initialize(Path::new(&db_file_path)).unwrap(); - copy_env_file_if_not_exists(); // required by AppContext::new() + ensure_test_env(); let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); @@ -3452,7 +3482,7 @@ mod tests { let db = Arc::new(Database::new(db_file_path).unwrap()); db.initialize(Path::new(&db_file_path)).unwrap(); - copy_env_file_if_not_exists(); // required by AppContext::new() + ensure_test_env(); let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); diff --git a/src/ui/wallets/send_screen.rs b/src/ui/wallets/send_screen.rs index 909b8dfee..622b62a2b 100644 --- a/src/ui/wallets/send_screen.rs +++ b/src/ui/wallets/send_screen.rs @@ -440,7 +440,7 @@ impl WalletSendScreen { return estimate_platform_fee(fee_estimator, 1); } - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); if dest_type == AddressType::Core { let output_script = self .destination_address @@ -482,6 +482,17 @@ impl WalletSendScreen { self.send_status = SendStatus::NotStarted; } + fn now_epoch_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + } + + fn mark_sending(&mut self) { + self.send_status = SendStatus::WaitingForResult(Self::now_epoch_secs()); + } + fn format_dash(amount_duffs: u64) -> String { Amount::dash_from_duffs(amount_duffs).to_string() } @@ -503,7 +514,7 @@ impl WalletSendScreen { } /// Detect address type from the address string - fn detect_address_type(&self, address: &str) -> AddressType { + fn detect_address_type(address: &str) -> AddressType { let trimmed = address.trim(); if trimmed.is_empty() { return AddressType::Unknown; @@ -634,7 +645,7 @@ impl WalletSendScreen { /// Get description of transaction type based on source and destination fn get_transaction_type_description(&self) -> &'static str { - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); match (&self.selected_source, dest_type) { (Some(SourceSelection::CoreWallet), AddressType::Core) => "Core Transaction", (Some(SourceSelection::CoreWallet), AddressType::Platform) => "Fund Platform Address", @@ -666,7 +677,7 @@ impl WalletSendScreen { .ok_or("Please select a source")?; // Validate destination - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); if dest_type == AddressType::Unknown { return Err( "Invalid destination address. Use a Dash address (X.../y...) or Platform address (evo1.../tevo1...)" @@ -732,11 +743,7 @@ impl WalletSendScreen { amount_duffs, }; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::CoreTask( CoreTask::SendWalletPayment { @@ -778,11 +785,7 @@ impl WalletSendScreen { )); } - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::WalletTask( WalletTask::FundPlatformAddressFromWalletUtxos { @@ -911,11 +914,7 @@ impl WalletSendScreen { allocation.fee_payer_index ); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::WalletTask( WalletTask::TransferPlatformCredits { @@ -1032,11 +1031,7 @@ impl WalletSendScreen { allocation.fee_payer_index ); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::WalletTask( WalletTask::WithdrawFromPlatformAddress { @@ -1049,35 +1044,141 @@ impl WalletSendScreen { ))) } - fn render_unified_send(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - - // Wallet info - self.render_wallet_info(ui); - - // Wallet unlock if needed + fn render_unlock_gate(&mut self, ui: &mut Ui) -> bool { let wallet_is_open = self .selected_wallet .as_ref() .is_some_and(|w| w.read().map(|g| g.is_open()).unwrap_or(false)); - if !wallet_is_open && let Some(wallet) = &self.selected_wallet { - if let Err(e) = try_open_wallet_no_password(wallet) { - self.error_message = Some(e); + if wallet_is_open { + return true; + } + + let Some(wallet) = &self.selected_wallet else { + return true; + }; + + if let Err(e) = try_open_wallet_no_password(wallet) { + self.error_message = Some(e); + } + if wallet_needs_unlock(wallet) { + ui.add_space(10.0); + ui.colored_label( + egui::Color32::from_rgb(200, 150, 50), + "Wallet is locked. Please unlock to continue.", + ); + ui.add_space(8.0); + if ui.button("Unlock Wallet").clicked() { + self.wallet_unlock_popup.open(); } - if wallet_needs_unlock(wallet) { - ui.add_space(10.0); - ui.colored_label( - egui::Color32::from_rgb(200, 150, 50), - "Wallet is locked. Please unlock to continue.", - ); - ui.add_space(8.0); - if ui.button("Unlock Wallet").clicked() { - self.wallet_unlock_popup.open(); + ui.add_space(10.0); + return false; + } + + true + } + + fn format_elapsed_time(start_time: u64) -> String { + let elapsed_seconds = Self::now_epoch_secs().saturating_sub(start_time); + if elapsed_seconds < 60 { + format!( + "{} second{}", + elapsed_seconds, + if elapsed_seconds == 1 { "" } else { "s" } + ) + } else { + let minutes = elapsed_seconds / 60; + let seconds = elapsed_seconds % 60; + format!( + "{} minute{} {} second{}", + minutes, + if minutes == 1 { "" } else { "s" }, + seconds, + if seconds == 1 { "" } else { "s" } + ) + } + } + + fn render_send_status(&mut self, ui: &mut Ui, dark_mode: bool) -> Option { + match self.send_status.clone() { + SendStatus::Complete(message) => { + let mut action = AppAction::None; + ui.vertical_centered(|ui| { + ui.add_space(100.0); + ui.heading("🎉"); + ui.heading(&message); + ui.add_space(20.0); + + if ui.button("Send Another").clicked() { + self.reset_form(); + } + ui.add_space(8.0); + if ui.button("Back to Wallet").clicked() { + action = AppAction::PopScreenAndRefresh; + } + + ui.add_space(100.0); + }); + Some(action) + } + SendStatus::WaitingForResult(start_time) => { + ui.vertical_centered(|ui| { + ui.add_space(100.0); + ui.add(egui::Spinner::new().size(40.0)); + ui.add_space(20.0); + ui.heading("Sending..."); + ui.add_space(10.0); + ui.label( + RichText::new(format!( + "Time elapsed: {}", + Self::format_elapsed_time(start_time) + )) + .color(DashColors::text_secondary(dark_mode)), + ); + ui.add_space(100.0); + }); + Some(AppAction::None) + } + SendStatus::Error(error_msg) => { + let mut dismiss = false; + ui.horizontal(|ui| { + Frame::new() + .fill(Color32::from_rgb(255, 100, 100).gamma_multiply(0.1)) + .inner_margin(Margin::symmetric(10, 8)) + .corner_radius(5.0) + .stroke(egui::Stroke::new(1.0, Color32::from_rgb(255, 100, 100))) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label( + RichText::new(&error_msg) + .color(Color32::from_rgb(255, 100, 100)), + ); + ui.add_space(10.0); + if ui.small_button("Dismiss").clicked() { + dismiss = true; + } + }); + }); + }); + if dismiss { + self.send_status = SendStatus::NotStarted; } ui.add_space(10.0); - return AppAction::None; + None } + SendStatus::NotStarted => None, + } + } + + fn render_unified_send(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + + // Wallet info + self.render_wallet_info(ui); + + // Wallet unlock if needed + if !self.render_unlock_gate(ui) { + return AppAction::None; } ui.add_space(10.0); @@ -1258,7 +1359,7 @@ impl WalletSendScreen { fn render_destination_input(&mut self, ui: &mut Ui) { let dark_mode = ui.ctx().style().visuals.dark_mode; - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); ui.horizontal(|ui| { ui.label( @@ -1330,7 +1431,7 @@ impl WalletSendScreen { .ok() .map(|wallet| wallet.total_balance_duffs() * CREDITS_PER_DUFF) // duffs to credits }); - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); let hint = if dest_type == AddressType::Platform { let destination = PlatformAddress::from_bech32m_string(self.destination_address.trim()) @@ -1401,7 +1502,7 @@ impl WalletSendScreen { Some(SourceSelection::PlatformAddresses(_)) => AddressType::Platform, None => AddressType::Unknown, }; - let output_type = self.detect_address_type(&self.destination_address); + let output_type = Self::detect_address_type(&self.destination_address); let min_amount = self.min_output_amount(input_type, output_type); Frame::group(ui.style()) @@ -1446,7 +1547,7 @@ impl WalletSendScreen { } // Show subtract fee checkbox for Core wallet to Core address transactions - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); if matches!(self.selected_source, Some(SourceSelection::CoreWallet)) && dest_type == AddressType::Core { @@ -1580,7 +1681,7 @@ impl WalletSendScreen { .as_ref() .is_some_and(|w| w.read().map(|g| g.is_open()).unwrap_or(false)); - let dest_type = self.detect_address_type(&self.destination_address); + let dest_type = Self::detect_address_type(&self.destination_address); let has_destination = dest_type != AddressType::Unknown; let has_amount = self.amount.as_ref().map(|a| a.value() > 0).unwrap_or(false); let has_source = self.selected_source.is_some(); @@ -1634,28 +1735,8 @@ impl WalletSendScreen { self.render_wallet_info(ui); // Wallet unlock if needed - let wallet_is_open = self - .selected_wallet - .as_ref() - .is_some_and(|w| w.read().map(|g| g.is_open()).unwrap_or(false)); - - if !wallet_is_open && let Some(wallet) = &self.selected_wallet { - if let Err(e) = try_open_wallet_no_password(wallet) { - self.error_message = Some(e); - } - if wallet_needs_unlock(wallet) { - ui.add_space(10.0); - ui.colored_label( - egui::Color32::from_rgb(200, 150, 50), - "Wallet is locked. Please unlock to continue.", - ); - ui.add_space(8.0); - if ui.button("Unlock Wallet").clicked() { - self.wallet_unlock_popup.open(); - } - ui.add_space(10.0); - return AppAction::None; - } + if !self.render_unlock_gate(ui) { + return AppAction::None; } ui.add_space(10.0); @@ -1766,7 +1847,7 @@ impl WalletSendScreen { // ========== FEE STRATEGY SECTION ========== // Only show for platform source or platform outputs let has_platform_output = self.advanced_outputs.iter().any(|o| { - let addr_type = Self::detect_address_type_static(&o.address); + let addr_type = Self::detect_address_type(&o.address); addr_type == AddressType::Platform }); @@ -2078,7 +2159,7 @@ impl WalletSendScreen { let addr_types: Vec = self .advanced_outputs .iter() - .map(|o| Self::detect_address_type_static(&o.address)) + .map(|o| Self::detect_address_type(&o.address)) .collect(); for (idx, &addr_type) in addr_types.iter().enumerate() { @@ -2155,26 +2236,6 @@ impl WalletSendScreen { } } - /// Static version of detect_address_type that doesn't need self - fn detect_address_type_static(address: &str) -> AddressType { - let trimmed = address.trim(); - if trimmed.is_empty() { - return AddressType::Unknown; - } - - // Check for Platform address (Bech32m format) - if trimmed.starts_with("evo1") || trimmed.starts_with("tevo1") { - return AddressType::Platform; - } - - // Try to parse as Core address - if trimmed.parse::>().is_ok() { - return AddressType::Core; - } - - AddressType::Unknown - } - /// Render the send button for advanced mode fn render_advanced_send_button(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -2262,7 +2323,7 @@ impl WalletSendScreen { let output_types: Vec = self .advanced_outputs .iter() - .map(|o| Self::detect_address_type_static(&o.address)) + .map(|o| Self::detect_address_type(&o.address)) .collect(); let has_core_output = output_types.contains(&AddressType::Core); @@ -2356,11 +2417,7 @@ impl WalletSendScreen { )); } - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::CoreTask( CoreTask::SendWalletPayment { @@ -2423,11 +2480,7 @@ impl WalletSendScreen { PlatformFeeStrategy::ReduceFirstOutput | PlatformFeeStrategy::ReduceLastOutput ); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::WalletTask( WalletTask::FundPlatformAddressFromWalletUtxos { @@ -2483,11 +2536,7 @@ impl WalletSendScreen { .map(|(idx, _)| idx as u16) .unwrap_or(0); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::WalletTask( WalletTask::TransferPlatformCredits { @@ -2545,11 +2594,7 @@ impl WalletSendScreen { .map(|(idx, _)| idx as u16) .unwrap_or(0); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.send_status = SendStatus::WaitingForResult(now); + self.mark_sending(); Ok(AppAction::BackendTask(BackendTask::WalletTask( WalletTask::WithdrawFromPlatformAddress { @@ -2582,101 +2627,8 @@ impl ScreenLike for WalletSendScreen { let mut inner_action = AppAction::None; let dark_mode = ui.ctx().style().visuals.dark_mode; - // Handle different states - clone to avoid borrow issues - let current_status = self.send_status.clone(); - match current_status { - SendStatus::Complete(message) => { - // Show custom success screen - ui.vertical_centered(|ui| { - ui.add_space(100.0); - ui.heading("🎉"); - ui.heading(&message); - ui.add_space(20.0); - - if ui.button("Send Another").clicked() { - self.reset_form(); - } - ui.add_space(8.0); - if ui.button("Back to Wallet").clicked() { - inner_action = AppAction::PopScreenAndRefresh; - } - - ui.add_space(100.0); - }); - - return inner_action; - } - SendStatus::WaitingForResult(start_time) => { - // Show sending spinner - ui.vertical_centered(|ui| { - ui.add_space(100.0); - ui.add(egui::Spinner::new().size(40.0)); - ui.add_space(20.0); - ui.heading("Sending..."); - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let elapsed_seconds = now.saturating_sub(start_time); - - let display_time = if elapsed_seconds < 60 { - format!( - "{} second{}", - elapsed_seconds, - if elapsed_seconds == 1 { "" } else { "s" } - ) - } else { - let minutes = elapsed_seconds / 60; - let seconds = elapsed_seconds % 60; - format!( - "{} minute{} {} second{}", - minutes, - if minutes == 1 { "" } else { "s" }, - seconds, - if seconds == 1 { "" } else { "s" } - ) - }; - - ui.add_space(10.0); - ui.label( - RichText::new(format!("Time elapsed: {}", display_time)) - .color(DashColors::text_secondary(dark_mode)), - ); - ui.add_space(100.0); - }); - return inner_action; - } - SendStatus::Error(error_msg) => { - // Show error at the top - let mut dismiss = false; - ui.horizontal(|ui| { - Frame::new() - .fill(Color32::from_rgb(255, 100, 100).gamma_multiply(0.1)) - .inner_margin(Margin::symmetric(10, 8)) - .corner_radius(5.0) - .stroke(egui::Stroke::new(1.0, Color32::from_rgb(255, 100, 100))) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - RichText::new(&error_msg) - .color(Color32::from_rgb(255, 100, 100)), - ); - ui.add_space(10.0); - if ui.small_button("Dismiss").clicked() { - dismiss = true; - } - }); - }); - }); - if dismiss { - self.send_status = SendStatus::NotStarted; - } - ui.add_space(10.0); - } - SendStatus::NotStarted => { - // Normal flow - continue to render the form - } + if let Some(status_action) = self.render_send_status(ui, dark_mode) { + return status_action; } egui::ScrollArea::vertical() diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 3d8373fd3..62a756543 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -322,6 +322,49 @@ impl WalletsBalancesScreen { } } + fn persist_selected_wallet_hash(&self, hash: Option) { + if let Ok(mut guard) = self.app_context.selected_wallet_hash.lock() { + *guard = hash; + } + let _ = self + .app_context + .db + .update_selected_wallet_hash(hash.as_ref()); + } + + fn persist_selected_single_key_hash(&self, hash: Option<[u8; 32]>) { + if let Ok(mut guard) = self.app_context.selected_single_key_hash.lock() { + *guard = hash; + } + let _ = self + .app_context + .db + .update_selected_single_key_hash(hash.as_ref()); + } + + fn select_hd_wallet(&mut self, wallet: Arc>) { + self.selected_wallet = Some(wallet.clone()); + self.selected_single_key_wallet = None; + self.selected_account = None; + + if let Ok(hash) = wallet.read().map(|g| g.seed_hash()) { + self.persist_selected_wallet_hash(Some(hash)); + } + self.persist_selected_single_key_hash(None); + } + + fn select_single_key_wallet(&mut self, wallet: Arc>) { + self.selected_single_key_wallet = Some(wallet.clone()); + self.selected_wallet = None; + self.selected_account = None; + self.utxo_page = 0; + + if let Ok(hash) = wallet.read().map(|g| g.key_hash) { + self.persist_selected_single_key_hash(Some(hash)); + } + self.persist_selected_wallet_hash(None); + } + pub(crate) fn update_selected_wallet_for_network(&mut self) { // Check if HD wallet selection is still valid if let Some(wallet_arc) = &self.selected_wallet { @@ -537,58 +580,12 @@ impl WalletsBalancesScreen { if ui.selectable_label(is_selected, label).clicked() { match wallet_item { WalletItem::Hd(w) => { - self.selected_wallet = Some(w.clone()); - self.selected_single_key_wallet = None; - // Persist selection to AppContext and database - if let Ok(hash) = w.read().map(|g| g.seed_hash()) - && let Ok(mut guard) = - self.app_context.selected_wallet_hash.lock() - { - *guard = Some(hash); - // Save to database for persistence across restarts - let _ = self - .app_context - .db - .update_selected_wallet_hash(Some(&hash)); - } - if let Ok(mut guard) = - self.app_context.selected_single_key_hash.lock() - { - *guard = None; - let _ = self - .app_context - .db - .update_selected_single_key_hash(None); - } + self.select_hd_wallet(w.clone()); } WalletItem::SingleKey(w) => { - self.selected_single_key_wallet = Some(w.clone()); - self.selected_wallet = None; - self.utxo_page = 0; // Reset pagination - // Persist selection to AppContext and database - if let Ok(hash) = w.read().map(|g| g.key_hash) - && let Ok(mut guard) = - self.app_context.selected_single_key_hash.lock() - { - *guard = Some(hash); - // Save to database for persistence across restarts - let _ = self - .app_context - .db - .update_selected_single_key_hash(Some(&hash)); - } - if let Ok(mut guard) = - self.app_context.selected_wallet_hash.lock() - { - *guard = None; - let _ = self - .app_context - .db - .update_selected_wallet_hash(None); - } + self.select_single_key_wallet(w.clone()); } } - self.selected_account = None; } } }); @@ -698,12 +695,7 @@ impl WalletsBalancesScreen { } self.selected_single_key_wallet = None; // Clear persisted selection in AppContext and database - if let Ok(mut guard) = - self.app_context.selected_single_key_hash.lock() - { - *guard = None; - } - let _ = self.app_context.db.update_selected_single_key_hash(None); + self.persist_selected_single_key_hash(None); self.display_message("Wallet removed", MessageType::Success); } } @@ -953,26 +945,17 @@ impl WalletsBalancesScreen { let network = self.app_context.network; for data in &address_data { body.row(25.0, |mut row| { + let is_key_only = data.account_category.is_key_only(); + let is_platform_payment = + data.account_category == AccountCategory::PlatformPayment; + row.col(|ui| { ui.label(data.display_address(network)); }); row.col(|ui| { - // These address types are used for key derivation/proofs, not holding funds - let is_key_only_address = matches!( - data.account_category, - AccountCategory::IdentityRegistration - | AccountCategory::IdentityTopup - | AccountCategory::IdentityInvitation - | AccountCategory::IdentitySystem - | AccountCategory::ProviderVoting - | AccountCategory::ProviderOwner - | AccountCategory::ProviderOperator - | AccountCategory::ProviderPlatform - ); - - if is_key_only_address { + if is_key_only { ui.label("N/A"); - } else if data.account_category == AccountCategory::PlatformPayment { + } else if is_platform_payment { // Platform credits: convert from credits to DASH // Credits are in duffs * 1000, so divide by 1000 then by 1e8 let dash_balance = @@ -985,20 +968,7 @@ impl WalletsBalancesScreen { }); row.col(|ui| { // Key-only addresses and Platform addresses don't hold UTXOs - let no_utxos = matches!( - data.account_category, - AccountCategory::IdentityRegistration - | AccountCategory::IdentityTopup - | AccountCategory::IdentityInvitation - | AccountCategory::IdentitySystem - | AccountCategory::ProviderVoting - | AccountCategory::ProviderOwner - | AccountCategory::ProviderOperator - | AccountCategory::ProviderPlatform - | AccountCategory::PlatformPayment - ); - - if no_utxos { + if is_key_only || is_platform_payment { ui.label("N/A"); } else { ui.label(format!("{}", data.utxo_count)); @@ -1006,20 +976,7 @@ impl WalletsBalancesScreen { }); row.col(|ui| { // These address types don't track historical received amounts - let no_total_received = matches!( - data.account_category, - AccountCategory::IdentityRegistration - | AccountCategory::IdentityTopup - | AccountCategory::IdentityInvitation - | AccountCategory::IdentitySystem - | AccountCategory::ProviderVoting - | AccountCategory::ProviderOwner - | AccountCategory::ProviderOperator - | AccountCategory::ProviderPlatform - | AccountCategory::PlatformPayment - ); - - if no_total_received { + if is_key_only || is_platform_payment { ui.label("N/A"); } else { let dash_received = data.total_received as f64 * 1e-8; @@ -1181,14 +1138,7 @@ impl WalletsBalancesScreen { let new_hash = next_wallet .as_ref() .and_then(|w| w.read().ok().map(|g| g.seed_hash())); - if let Ok(mut guard) = self.app_context.selected_wallet_hash.lock() { - *guard = new_hash; - } - // Persist to database - let _ = self - .app_context - .db - .update_selected_wallet_hash(new_hash.as_ref()); + self.persist_selected_wallet_hash(new_hash); self.show_rename_dialog = false; self.rename_input.clear(); @@ -1436,6 +1386,10 @@ impl WalletsBalancesScreen { // Messages no longer auto-expire, they must be dismissed manually } + fn set_message(&mut self, message: String, message_type: MessageType) { + self.message = Some((message, message_type, Utc::now())); + } + fn format_dash(amount_duffs: u64) -> String { Amount::dash_from_duffs(amount_duffs).to_string() } @@ -1825,6 +1779,38 @@ impl WalletsBalancesScreen { action } + fn draw_modal_overlay(ctx: &Context, id: &str) { + let screen_rect = ctx.screen_rect(); + let painter = ctx.layer_painter(egui::LayerId::new( + egui::Order::Background, + egui::Id::new(id), + )); + painter.rect_filled( + screen_rect, + 0.0, + egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), + ); + } + + fn modal_frame(ctx: &Context) -> Frame { + Frame { + inner_margin: egui::Margin::same(20), + outer_margin: egui::Margin::same(0), + corner_radius: egui::CornerRadius::same(8), + shadow: egui::epaint::Shadow { + offset: [0, 8], + blur: 16, + spread: 0, + color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), + }, + fill: ctx.style().visuals.window_fill, + stroke: egui::Stroke::new( + 1.0, + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), + ), + } + } + fn render_send_dialog(&mut self, ctx: &Context) -> AppAction { if !self.send_dialog.is_open { return AppAction::None; @@ -1946,16 +1932,7 @@ impl WalletsBalancesScreen { // Draw dark overlay behind the dialog (only when open) if open { - let screen_rect = ctx.screen_rect(); - let painter = ctx.layer_painter(egui::LayerId::new( - egui::Order::Background, - egui::Id::new("receive_dialog_overlay"), - )); - painter.rect_filled( - screen_rect, - 0.0, - egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), - ); + Self::draw_modal_overlay(ctx, "receive_dialog_overlay"); } egui::Window::new("Receive") @@ -1963,22 +1940,7 @@ impl WalletsBalancesScreen { .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .open(&mut open) - .frame(egui::Frame { - inner_margin: egui::Margin::same(20), - outer_margin: egui::Margin::same(0), - corner_radius: egui::CornerRadius::same(8), - shadow: egui::epaint::Shadow { - offset: [0, 8], - blur: 16, - spread: 0, - color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), - }, - fill: ctx.style().visuals.window_fill, - stroke: egui::Stroke::new( - 1.0, - egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), - ), - }) + .frame(Self::modal_frame(ctx)) .show(ctx, |ui| { ui.set_min_width(350.0); ui.vertical_centered(|ui| { @@ -2326,38 +2288,14 @@ impl WalletsBalancesScreen { let dark_mode = ctx.style().visuals.dark_mode; // Draw dark overlay behind the popup - let screen_rect = ctx.screen_rect(); - let painter = ctx.layer_painter(egui::LayerId::new( - egui::Order::Background, - egui::Id::new("fund_platform_dialog_overlay"), - )); - painter.rect_filled( - screen_rect, - 0.0, - egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), - ); + Self::draw_modal_overlay(ctx, "fund_platform_dialog_overlay"); egui::Window::new("Fund Platform Address from Asset Lock") .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .open(&mut open) - .frame(egui::Frame { - inner_margin: egui::Margin::same(20), - outer_margin: egui::Margin::same(0), - corner_radius: egui::CornerRadius::same(8), - shadow: egui::epaint::Shadow { - offset: [0, 8], - blur: 16, - spread: 0, - color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), - }, - fill: ctx.style().visuals.window_fill, - stroke: egui::Stroke::new( - 1.0, - egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), - ), - }) + .frame(Self::modal_frame(ctx)) .show(ctx, |ui| { ui.set_min_width(400.0); @@ -2525,16 +2463,7 @@ impl WalletsBalancesScreen { // Draw dark overlay behind the dialog if open { - let screen_rect = ctx.screen_rect(); - let painter = ctx.layer_painter(egui::LayerId::new( - egui::Order::Background, - egui::Id::new("private_key_dialog_overlay"), - )); - painter.rect_filled( - screen_rect, - 0.0, - egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), - ); + Self::draw_modal_overlay(ctx, "private_key_dialog_overlay"); } egui::Window::new("Private Key") @@ -2542,22 +2471,7 @@ impl WalletsBalancesScreen { .resizable(false) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .open(&mut open) - .frame(egui::Frame { - inner_margin: egui::Margin::same(20), - outer_margin: egui::Margin::same(0), - corner_radius: egui::CornerRadius::same(8), - shadow: egui::epaint::Shadow { - offset: [0, 8], - blur: 16, - spread: 0, - color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), - }, - fill: ctx.style().visuals.window_fill, - stroke: egui::Stroke::new( - 1.0, - egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), - ), - }) + .frame(Self::modal_frame(ctx)) .show(ctx, |ui| { ui.set_min_width(400.0); ui.vertical_centered(|ui| { @@ -3181,6 +3095,19 @@ impl WalletsBalancesScreen { /// Creates the appropriate refresh action based on the current refresh mode fn create_refresh_action(&self, wallet_arc: &Arc>) -> AppAction { + self.create_refresh_action_for_mode(wallet_arc, self.refresh_mode) + } + + /// Creates the appropriate refresh action using the pending refresh mode + fn create_pending_refresh_action(&self, wallet_arc: &Arc>) -> AppAction { + self.create_refresh_action_for_mode(wallet_arc, self.pending_refresh_mode) + } + + fn create_refresh_action_for_mode( + &self, + wallet_arc: &Arc>, + mode: RefreshMode, + ) -> AppAction { use crate::backend_task::wallet::PlatformSyncMode; let seed_hash = wallet_arc @@ -3189,7 +3116,7 @@ impl WalletsBalancesScreen { .map(|w| w.seed_hash()) .unwrap_or_default(); - match self.refresh_mode { + match mode { RefreshMode::All => { // Default behavior: Core + Platform (Auto) AppAction::BackendTask(BackendTask::CoreTask(CoreTask::RefreshWalletInfo( @@ -3238,47 +3165,6 @@ impl WalletsBalancesScreen { } } } - - /// Creates the appropriate refresh action using the pending refresh mode - fn create_pending_refresh_action(&self, wallet_arc: &Arc>) -> AppAction { - use crate::backend_task::wallet::PlatformSyncMode; - - let seed_hash = wallet_arc - .read() - .ok() - .map(|w| w.seed_hash()) - .unwrap_or_default(); - - match self.pending_refresh_mode { - RefreshMode::All => AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::RefreshWalletInfo(wallet_arc.clone(), Some(PlatformSyncMode::Auto)), - )), - RefreshMode::CoreOnly => AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::RefreshWalletInfo(wallet_arc.clone(), None), - )), - RefreshMode::PlatformFull => AppAction::BackendTask(BackendTask::WalletTask( - crate::backend_task::wallet::WalletTask::FetchPlatformAddressBalances { - seed_hash, - sync_mode: PlatformSyncMode::ForceFull, - }, - )), - RefreshMode::PlatformTerminal => AppAction::BackendTask(BackendTask::WalletTask( - crate::backend_task::wallet::WalletTask::FetchPlatformAddressBalances { - seed_hash, - sync_mode: PlatformSyncMode::TerminalOnly, - }, - )), - RefreshMode::CoreAndPlatformFull => AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::RefreshWalletInfo(wallet_arc.clone(), Some(PlatformSyncMode::ForceFull)), - )), - RefreshMode::CoreAndPlatformTerminal => { - AppAction::BackendTask(BackendTask::CoreTask(CoreTask::RefreshWalletInfo( - wallet_arc.clone(), - Some(PlatformSyncMode::TerminalOnly), - ))) - } - } - } } impl ScreenLike for WalletsBalancesScreen { @@ -3756,7 +3642,7 @@ impl ScreenLike for WalletsBalancesScreen { return; } } - self.message = Some((message.to_string(), message_type, Utc::now())) + self.set_message(message.to_string(), message_type); } fn display_task_result( @@ -3767,17 +3653,15 @@ impl ScreenLike for WalletsBalancesScreen { crate::ui::BackendTaskSuccessResult::RefreshedWallet { warning } => { self.refreshing = false; if let Some(warn_msg) = warning { - self.message = Some(( + self.set_message( format!("Wallet refreshed with warning: {}", warn_msg), MessageType::Info, - Utc::now(), - )); + ); } else { - self.message = Some(( + self.set_message( "Successfully refreshed wallet".to_string(), MessageType::Success, - Utc::now(), - )); + ); } } crate::ui::BackendTaskSuccessResult::RecoveredAssetLocks { @@ -3873,11 +3757,10 @@ impl ScreenLike for WalletsBalancesScreen { wallet.set_platform_address_info(addr, balance, nonce); } } - self.message = Some(( + self.set_message( "Successfully synced Platform balances".to_string(), MessageType::Success, - Utc::now(), - )); + ); } crate::ui::BackendTaskSuccessResult::Message(msg) => { self.refreshing = false; @@ -3889,27 +3772,26 @@ impl ScreenLike for WalletsBalancesScreen { fn refresh_on_arrival(&mut self) { // Check if there's a pending wallet selection (e.g., from wallet creation/import) - if let Ok(mut pending) = self.app_context.pending_wallet_selection.lock() - && let Some(seed_hash) = pending.take() - && let Ok(wallets) = self.app_context.wallets.read() - && let Some(wallet) = wallets.get(&seed_hash) - { - self.selected_wallet = Some(wallet.clone()); - self.selected_single_key_wallet = None; // Clear SK selection - self.selected_account = None; - // Persist selection to AppContext and database - if let Ok(mut guard) = self.app_context.selected_wallet_hash.lock() { - *guard = Some(seed_hash); - } - if let Ok(mut guard) = self.app_context.selected_single_key_hash.lock() { - *guard = None; - } - let _ = self + let pending_seed_hash = self + .app_context + .pending_wallet_selection + .lock() + .ok() + .and_then(|mut pending| pending.take()); + + if let Some(seed_hash) = pending_seed_hash { + let selected_wallet = self .app_context - .db - .update_selected_wallet_hash(Some(&seed_hash)); - let _ = self.app_context.db.update_selected_single_key_hash(None); - return; + .wallets + .read() + .ok() + .and_then(|wallets| wallets.get(&seed_hash).cloned()); + + if let Some(wallet) = selected_wallet { + self.select_hd_wallet(wallet); + self.persist_selected_wallet_hash(Some(seed_hash)); + return; + } } // If no wallet of either type is selected but wallets exist, select the first HD wallet