diff --git a/Cargo.lock b/Cargo.lock index 80818d6d7..0664542e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1850,6 +1850,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "tz-rs", "which 8.0.0", "winres", "zeroize", @@ -7992,6 +7993,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "tz-rs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc6c929ffa10fb34f4a3c7e9a73620a83ef2e85e47f9ec3381b8289e6762f42" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/src/app.rs b/src/app.rs index cecc1a608..4bf5ba5f8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,6 +7,7 @@ use crate::backend_task::core::CoreItem; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::components::core_zmq_listener::{CoreZMQListener, ZMQMessage}; use crate::context::AppContext; +use crate::context::connection_status::ConnectionStatus; use crate::database::Database; use crate::logging::initialize_logger; use crate::model::settings::Settings; @@ -65,6 +66,7 @@ pub struct AppState { pub selected_main_screen: RootScreenType, pub screen_stack: Vec, pub chosen_network: Network, + pub connection_status: Arc, pub mainnet_app_context: Arc, pub testnet_app_context: Option>, pub devnet_app_context: Option>, @@ -182,11 +184,13 @@ impl AppState { let onboarding_completed = settings.onboarding_completed; let subtasks = Arc::new(TaskManager::new()); + let connection_status = Arc::new(ConnectionStatus::new()); let mainnet_app_context = match AppContext::new( Network::Dash, db.clone(), password_info.clone(), subtasks.clone(), + connection_status.clone(), ) { Some(context) => context, None => { @@ -201,18 +205,21 @@ impl AppState { db.clone(), password_info.clone(), subtasks.clone(), + connection_status.clone(), ); let devnet_app_context = AppContext::new( Network::Devnet, db.clone(), password_info.clone(), subtasks.clone(), + connection_status.clone(), ); let local_app_context = AppContext::new( Network::Regtest, db.clone(), password_info, subtasks.clone(), + connection_status.clone(), ); // load fonts @@ -604,6 +611,7 @@ impl AppState { selected_main_screen, screen_stack: vec![], chosen_network, + connection_status, mainnet_app_context, testnet_app_context, devnet_app_context, @@ -738,6 +746,9 @@ impl AppState { for screen in self.main_screens.values_mut() { screen.change_context(app_context.clone()) } + + self.connection_status + .reset(app_context.core_backend_mode()); } pub fn visible_screen_mut(&mut self) -> &mut Screen { @@ -789,14 +800,14 @@ impl App for AppState { // Apply Dash theme with user preference crate::ui::theme::apply_theme(ctx, self.theme_preference); - if let Ok(event) = self.current_app_context().rx_zmq_status.try_recv() - && let Ok(mut status) = self.current_app_context().zmq_connection_status.lock() - { - *status = event; - } + let active_context = self.current_app_context().clone(); // Poll the receiver for any new task results while let Ok(task_result) = self.task_result_receiver.try_recv() { + active_context + .connection_status() + .handle_task_result(&task_result, active_context.network); + // Handle the result on the main thread match task_result { TaskResult::Success(message) => { @@ -984,112 +995,120 @@ impl App for AppState { } // Show welcome screen if onboarding not completed - let action = if self.show_welcome_screen { - if let Some(welcome_screen) = &mut self.welcome_screen { - welcome_screen.ui(ctx) - } else { - AppAction::None - } + let mut actions = Vec::new(); + if self.show_welcome_screen + && let Some(welcome_screen) = &mut self.welcome_screen + { + actions.push(welcome_screen.ui(ctx)); } else { - self.visible_screen_mut().ui(ctx) + actions.push(self.visible_screen_mut().ui(ctx)); }; - match action { - AppAction::AddScreen(screen) => self.screen_stack.push(screen), - AppAction::None => {} - AppAction::Refresh => self.visible_screen_mut().refresh(), - AppAction::PopScreen => { - if !self.screen_stack.is_empty() { - self.screen_stack.pop(); + // Schedule connection status refresh + actions.push( + active_context + .connection_status() + .trigger_refresh(active_context.as_ref()), + ); + + for action in actions { + match action { + AppAction::None => {} + AppAction::AddScreen(screen) => self.screen_stack.push(screen), + AppAction::Refresh => self.visible_screen_mut().refresh(), + AppAction::PopScreen => { + if !self.screen_stack.is_empty() { + self.screen_stack.pop(); + } } - } - AppAction::PopScreenAndRefresh => { - if !self.screen_stack.is_empty() { - self.screen_stack.pop(); + AppAction::PopScreenAndRefresh => { + if !self.screen_stack.is_empty() { + self.screen_stack.pop(); + } + if let Some(screen) = self.screen_stack.last_mut() { + screen.refresh(); + } else { + self.active_root_screen_mut().refresh_on_arrival(); + } } - if let Some(screen) = self.screen_stack.last_mut() { - screen.refresh(); - } else { + AppAction::GoToMainScreen => { + self.screen_stack = vec![]; self.active_root_screen_mut().refresh_on_arrival(); } - } - AppAction::GoToMainScreen => { - self.screen_stack = vec![]; - self.active_root_screen_mut().refresh_on_arrival(); - } - AppAction::BackendTask(task) => { - self.handle_backend_task(task); - } - AppAction::BackendTasks(tasks, mode) => { - self.handle_backend_tasks(tasks, mode); - } - AppAction::SetMainScreen(root_screen_type) => { - self.selected_main_screen = root_screen_type; - self.active_root_screen_mut().refresh_on_arrival(); - self.current_app_context() - .update_settings(root_screen_type) - .ok(); - } - AppAction::SetMainScreenThenGoToMainScreen(root_screen_type) => { - self.selected_main_screen = root_screen_type; - self.active_root_screen_mut().refresh_on_arrival(); - self.current_app_context() - .update_settings(root_screen_type) - .ok(); - self.screen_stack = vec![]; - } - AppAction::SetMainScreenThenPopScreen(root_screen_type) => { - self.selected_main_screen = root_screen_type; - self.active_root_screen_mut().refresh_on_arrival(); - self.current_app_context() - .update_settings(root_screen_type) - .ok(); - if !self.screen_stack.is_empty() { - self.screen_stack.pop(); + AppAction::BackendTask(task) => { + self.handle_backend_task(task); } - } - AppAction::SwitchNetwork(network) => { - self.change_network(network); - self.current_app_context() - .update_settings(RootScreenType::RootScreenNetworkChooser) - .ok(); - } - AppAction::PopThenAddScreenToMainScreen(root_screen_type, screen) => { - self.screen_stack = vec![screen]; - self.selected_main_screen = root_screen_type; - self.active_root_screen_mut().refresh_on_arrival(); - self.current_app_context() - .update_settings(root_screen_type) - .ok(); - } - AppAction::Custom(_) => {} - AppAction::OnboardingComplete { - main_screen, - add_screen, - } => { - self.show_welcome_screen = false; - self.welcome_screen = None; - self.selected_main_screen = main_screen; - self.active_root_screen_mut().refresh_on_arrival(); - self.current_app_context().update_settings(main_screen).ok(); - // If there's an additional screen to push, create and push it - if let Some(screen_type) = add_screen { - let screen = screen_type.create_screen(self.current_app_context()); - self.screen_stack.push(screen); + AppAction::BackendTasks(tasks, mode) => { + self.handle_backend_tasks(tasks, mode); } - // Start SPV sync after onboarding completes (if auto-start is enabled and developer mode is on) - // TODO: SPV auto-start is gated behind developer mode while SPV is in development. - // Remove the is_developer_mode() check once SPV is production-ready. - let current_context = self.current_app_context(); - let auto_start_spv = current_context.db.get_auto_start_spv().unwrap_or(false); - if auto_start_spv - && current_context.is_developer_mode() - && current_context.core_backend_mode() == crate::spv::CoreBackendMode::Spv - { - if let Err(e) = current_context.start_spv() { - tracing::warn!("Failed to start SPV sync after onboarding: {}", e); - } else { - tracing::info!("SPV sync started after onboarding"); + AppAction::SetMainScreen(root_screen_type) => { + self.selected_main_screen = root_screen_type; + self.active_root_screen_mut().refresh_on_arrival(); + self.current_app_context() + .update_settings(root_screen_type) + .ok(); + } + AppAction::SetMainScreenThenGoToMainScreen(root_screen_type) => { + self.selected_main_screen = root_screen_type; + self.active_root_screen_mut().refresh_on_arrival(); + self.current_app_context() + .update_settings(root_screen_type) + .ok(); + self.screen_stack = vec![]; + } + AppAction::SetMainScreenThenPopScreen(root_screen_type) => { + self.selected_main_screen = root_screen_type; + self.active_root_screen_mut().refresh_on_arrival(); + self.current_app_context() + .update_settings(root_screen_type) + .ok(); + if !self.screen_stack.is_empty() { + self.screen_stack.pop(); + } + } + AppAction::SwitchNetwork(network) => { + self.change_network(network); + self.current_app_context() + .update_settings(RootScreenType::RootScreenNetworkChooser) + .ok(); + } + AppAction::PopThenAddScreenToMainScreen(root_screen_type, screen) => { + self.screen_stack = vec![screen]; + self.selected_main_screen = root_screen_type; + self.active_root_screen_mut().refresh_on_arrival(); + self.current_app_context() + .update_settings(root_screen_type) + .ok(); + } + AppAction::Custom(_) => {} + AppAction::OnboardingComplete { + main_screen, + add_screen, + } => { + self.show_welcome_screen = false; + self.welcome_screen = None; + self.selected_main_screen = main_screen; + self.active_root_screen_mut().refresh_on_arrival(); + self.current_app_context().update_settings(main_screen).ok(); + // If there's an additional screen to push, create and push it + if let Some(screen_type) = add_screen { + let screen = screen_type.create_screen(self.current_app_context()); + self.screen_stack.push(screen); + } + // Start SPV sync after onboarding completes (if auto-start is enabled and developer mode is on) + // TODO: SPV auto-start is gated behind developer mode while SPV is in development. + // Remove the is_developer_mode() check once SPV is production-ready. + let current_context = self.current_app_context(); + let auto_start_spv = current_context.db.get_auto_start_spv().unwrap_or(false); + if auto_start_spv + && current_context.is_developer_mode() + && current_context.core_backend_mode() == crate::spv::CoreBackendMode::Spv + { + if let Err(e) = current_context.start_spv() { + tracing::warn!("Failed to start SPV sync after onboarding: {}", e); + } else { + tracing::info!("SPV sync started after onboarding"); + } } } } diff --git a/src/context/connection_status.rs b/src/context/connection_status.rs new file mode 100644 index 000000000..703adc869 --- /dev/null +++ b/src/context/connection_status.rs @@ -0,0 +1,281 @@ +use crate::app::AppAction; +use crate::app::TaskResult; +use crate::backend_task::BackendTask; +use crate::backend_task::BackendTaskSuccessResult; +use crate::backend_task::core::{CoreItem, CoreTask}; +use crate::components::core_zmq_listener::ZMQConnectionEvent; +use crate::spv::{CoreBackendMode, SpvStatus}; +use dash_sdk::dpp::dashcore::{ChainLock, Network}; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::time::{Duration, Instant}; + +const REFRESH_CONNECTED: Duration = Duration::from_secs(10); +const REFRESH_DISCONNECTED: Duration = Duration::from_secs(2); +/// Tracks the connection status to currently active network, and provides helper methods +/// to determine overall connectivity status. +/// +/// Supports Dash Core and SPV. +#[derive(Debug)] +pub struct ConnectionStatus { + rpc_online: AtomicBool, + zmq_status: Mutex, + spv_status: AtomicU8, + backend_mode: AtomicU8, + disable_zmq: AtomicBool, + overall_connected: AtomicBool, + last_update: Mutex, +} + +impl ConnectionStatus { + pub fn new() -> Self { + Self { + rpc_online: AtomicBool::new(false), + zmq_status: Mutex::new(ZMQConnectionEvent::Disconnected), + spv_status: AtomicU8::new(SpvStatus::Idle as u8), + backend_mode: AtomicU8::new(CoreBackendMode::Rpc.as_u8()), + disable_zmq: AtomicBool::new(false), + overall_connected: AtomicBool::new(false), + last_update: Mutex::new(Instant::now()), + } + } + + /// Reset all connection state. Called when switching the active network + /// so the status reflects the new network from a clean slate. + /// + /// `backend_mode` should be the new network's current backend mode so that + /// `overall_connected()` and `tooltip_text()` read the correct mode immediately. + pub fn reset(&self, backend_mode: CoreBackendMode) { + self.rpc_online.store(false, Ordering::Relaxed); + if let Ok(mut status) = self.zmq_status.lock() { + *status = ZMQConnectionEvent::Disconnected; + } + self.spv_status + .store(SpvStatus::Idle as u8, Ordering::Relaxed); + self.backend_mode + .store(backend_mode.as_u8(), Ordering::Relaxed); + self.disable_zmq.store(false, Ordering::Relaxed); + self.overall_connected.store(false, Ordering::Relaxed); + // Set last_update to epoch so the next trigger_refresh fires immediately + if let Ok(mut last) = self.last_update.lock() { + *last = Instant::now() - REFRESH_CONNECTED; + } + } + + pub fn rpc_online(&self) -> bool { + self.rpc_online.load(Ordering::Relaxed) + } + + pub fn set_rpc_online(&self, online: bool) { + self.rpc_online.store(online, Ordering::Relaxed); + } + + pub fn zmq_connected(&self) -> bool { + self.zmq_status + .lock() + .map(|status| matches!(*status, ZMQConnectionEvent::Connected)) + .unwrap_or(false) + } + + pub fn set_zmq_status(&self, event: ZMQConnectionEvent) { + if let Ok(mut status) = self.zmq_status.lock() { + *status = event; + } + } + + pub fn spv_status(&self) -> SpvStatus { + SpvStatus::from(self.spv_status.load(Ordering::Relaxed)) + } + + pub fn set_spv_status(&self, status: SpvStatus) { + self.spv_status.store(status as u8, Ordering::Relaxed); + } + + pub fn backend_mode(&self) -> CoreBackendMode { + self.backend_mode.load(Ordering::Relaxed).into() + } + + pub fn set_backend_mode(&self, mode: CoreBackendMode) { + self.backend_mode.store(mode.as_u8(), Ordering::Relaxed); + } + + pub fn disable_zmq(&self) -> bool { + self.disable_zmq.load(Ordering::Relaxed) + } + + pub fn set_disable_zmq(&self, disable: bool) { + self.disable_zmq.store(disable, Ordering::Relaxed); + } + + pub fn spv_connected(status: SpvStatus) -> bool { + status.is_active() + } + + pub fn overall_connected(&self) -> bool { + self.overall_connected.load(Ordering::Relaxed) + } + + pub fn refresh_overall(&self) { + let backend_mode = self.backend_mode(); + let disable_zmq = self.disable_zmq(); + let spv_status = self.spv_status(); + let connected = match backend_mode { + CoreBackendMode::Rpc => self.rpc_online() && (disable_zmq || self.zmq_connected()), + CoreBackendMode::Spv => Self::spv_connected(spv_status), + }; + self.overall_connected.store(connected, Ordering::Relaxed); + } + + pub fn tooltip_text(&self) -> String { + let backend_mode = self.backend_mode(); + let disable_zmq = self.disable_zmq(); + let spv_status = self.spv_status(); + match backend_mode { + CoreBackendMode::Rpc => { + let rpc_status = if self.rpc_online() { + "RPC: Connected" + } else { + "RPC: Disconnected" + }; + let zmq_status = if disable_zmq { + "ZMQ: Disabled" + } else if self.zmq_connected() { + "ZMQ: Connected" + } else { + "ZMQ: Disconnected" + }; + + if self.overall_connected() { + format!("Connected to Dash Core Wallet\n{rpc_status}\n{zmq_status}") + } else if self.rpc_online() { + format!("Dash Core connection incomplete\n{rpc_status}\n{zmq_status}") + } else { + format!( + "Disconnected from Dash Core Wallet. Click to start it.\n{rpc_status}\n{zmq_status}" + ) + } + } + CoreBackendMode::Spv => { + let spv_label = format!("SPV: {:?}", spv_status); + if self.overall_connected() { + format!("SPV connected\n{spv_label}") + } else { + format!("SPV disconnected\n{spv_label}") + } + } + } + } + + pub fn update_from_chainlocks( + &self, + network: Network, + mainnet_chainlock: &Option, + testnet_chainlock: &Option, + devnet_chainlock: &Option, + local_chainlock: &Option, + ) { + let online = match network { + Network::Dash => mainnet_chainlock.is_some(), + Network::Testnet => testnet_chainlock.is_some(), + Network::Devnet => devnet_chainlock.is_some(), + Network::Regtest => local_chainlock.is_some(), + _ => false, + }; + self.set_rpc_online(online); + } + + pub fn handle_task_result(&self, task_result: &TaskResult, active_network: Network) { + match task_result { + TaskResult::Success(message) => match message.as_ref() { + BackendTaskSuccessResult::CoreItem(CoreItem::ChainLocks( + mainnet_chainlock, + testnet_chainlock, + devnet_chainlock, + local_chainlock, + )) => { + self.update_from_chainlocks( + active_network, + mainnet_chainlock, + testnet_chainlock, + devnet_chainlock, + local_chainlock, + ); + self.refresh_overall(); + } + BackendTaskSuccessResult::CoreItem(CoreItem::ChainLock(_, network)) => { + if *network == active_network { + self.set_rpc_online(true); + self.refresh_overall(); + } + } + _ => {} + }, + TaskResult::Error(message) => { + if message.contains( + "Failed to get best chain lock for mainnet, testnet, devnet, and local", + ) { + self.set_rpc_online(false); + self.refresh_overall(); + } + } + _ => {} + } + } + + pub fn trigger_refresh(&self, app_context: &crate::context::AppContext) -> AppAction { + // throttle updates to once every 2 seconds + let mut last_update = match self.last_update.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let now = Instant::now(); + let timeout = if self.overall_connected() { + REFRESH_CONNECTED + } else { + REFRESH_DISCONNECTED + }; + if now.duration_since(*last_update) < timeout { + return AppAction::None; + } + *last_update = now; + + self.refresh_zmq_and_spv(app_context); + AppAction::BackendTask(BackendTask::CoreTask(CoreTask::GetBestChainLocks)) + } + + fn refresh_zmq_and_spv(&self, app_context: &crate::context::AppContext) { + // Get current backend mode + let backend_mode = app_context.core_backend_mode(); + self.set_backend_mode(backend_mode); + + match backend_mode { + CoreBackendMode::Spv => { + // SPV status is updated elsewhere + let spv_status = app_context.spv_manager().status().status; + self.set_spv_status(spv_status); + } + CoreBackendMode::Rpc => { + // Update ZMQ status if there's a new event + let disable_zmq = app_context + .get_settings() + .ok() + .flatten() + .map(|s| s.disable_zmq) + .unwrap_or(false); + self.set_disable_zmq(disable_zmq); + + if let Ok(event) = app_context.rx_zmq_status.try_recv() { + self.set_zmq_status(event); + } + } + } + + self.refresh_overall(); + } +} + +impl Default for ConnectionStatus { + fn default() -> Self { + Self::new() + } +} diff --git a/src/context/mod.rs b/src/context/mod.rs index fd4e65120..a2e9000cd 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1,3 +1,4 @@ +pub mod connection_status; mod contract_token_db; mod identity_db; mod settings_db; @@ -19,6 +20,7 @@ use crate::model::wallet::{Wallet, WalletSeedHash}; use crate::sdk_wrapper::initialize_sdk; use crate::spv::{CoreBackendMode, SpvManager}; use crate::utils::tasks::TaskManager; +use connection_status::ConnectionStatus; use crossbeam_channel::{Receiver, Sender}; use dash_sdk::Sdk; use dash_sdk::dashcore_rpc::{Auth, Client}; @@ -59,7 +61,6 @@ pub struct AppContext { pub(crate) config: Arc>, pub(crate) rx_zmq_status: Receiver, pub(crate) sx_zmq_status: Sender, - pub(crate) zmq_connection_status: Mutex, pub(crate) dpns_contract: Arc, pub(crate) withdraws_contract: Arc, pub(crate) dashpay_contract: Arc, @@ -84,6 +85,8 @@ pub struct AppContext { pub(crate) subtasks: Arc, pub(crate) spv_manager: Arc, core_backend_mode: AtomicU8, + /// Tracks the connection status to currently active network + pub(crate) connection_status: Arc, /// Pending wallet selection - set after creating/importing a wallet /// so the wallet screen can auto-select the new wallet pub(crate) pending_wallet_selection: Mutex>, @@ -102,6 +105,7 @@ impl AppContext { db: Arc, password_info: Option, subtasks: Arc, + connection_status: Arc, ) -> Option> { let config = match Config::load() { Ok(config) => config, @@ -254,12 +258,12 @@ impl AppContext { single_key_wallets: RwLock::new(single_key_wallets), password_info, transactions_waiting_for_finality: Mutex::new(BTreeMap::new()), - zmq_connection_status: Mutex::new(ZMQConnectionEvent::Disconnected), animate, cached_settings: RwLock::new(None), subtasks, spv_manager, core_backend_mode: AtomicU8::new(saved_core_backend_mode), + connection_status, pending_wallet_selection: Mutex::new(None), selected_wallet_hash: Mutex::new(selected_wallet_hash), selected_single_key_hash: Mutex::new(selected_single_key_hash), @@ -333,6 +337,10 @@ impl AppContext { self.core_backend_mode.load(Ordering::Relaxed).into() } + pub fn connection_status(&self) -> &ConnectionStatus { + &self.connection_status + } + pub fn set_core_backend_mode(self: &Arc, mode: CoreBackendMode) { self.core_backend_mode .store(mode.as_u8(), Ordering::Relaxed); diff --git a/src/spv/manager.rs b/src/spv/manager.rs index ebad6b7ee..9d5395664 100644 --- a/src/spv/manager.rs +++ b/src/spv/manager.rs @@ -56,15 +56,16 @@ impl From for CoreBackendMode { /// High-level status of the SPV client runtime. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(u8)] pub enum SpvStatus { #[default] - Idle, - Starting, - Syncing, - Running, - Stopping, - Stopped, - Error, + Idle = 0, + Starting = 1, + Syncing = 2, + Running = 3, + Stopping = 4, + Stopped = 5, + Error = 6, } impl SpvStatus { @@ -76,6 +77,35 @@ impl SpvStatus { } } +impl std::fmt::Display for SpvStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SpvStatus::Idle => write!(f, "Idle"), + SpvStatus::Starting => write!(f, "Starting"), + SpvStatus::Syncing => write!(f, "Syncing"), + SpvStatus::Running => write!(f, "Running"), + SpvStatus::Stopping => write!(f, "Stopping"), + SpvStatus::Stopped => write!(f, "Stopped"), + SpvStatus::Error => write!(f, "Error"), + } + } +} + +impl From for SpvStatus { + fn from(value: u8) -> Self { + match value { + 0 => SpvStatus::Idle, + 1 => SpvStatus::Starting, + 2 => SpvStatus::Syncing, + 3 => SpvStatus::Running, + 4 => SpvStatus::Stopping, + 5 => SpvStatus::Stopped, + 6 => SpvStatus::Error, + _ => SpvStatus::Idle, + } + } +} + /// Snapshot of the SPV runtime state for UI consumption. /// Uses dash-spv's built-in progress types directly instead of duplicating. #[derive(Debug, Clone, Default)] diff --git a/src/ui/components/top_panel.rs b/src/ui/components/top_panel.rs index 4dfde7ad5..67a42c227 100644 --- a/src/ui/components/top_panel.rs +++ b/src/ui/components/top_panel.rs @@ -1,8 +1,8 @@ use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::BackendTask; use crate::backend_task::core::CoreTask; -use crate::components::core_zmq_listener::ZMQConnectionEvent; use crate::context::AppContext; +use crate::spv::CoreBackendMode; use crate::ui::ScreenType; use crate::ui::theme::{DashColors, Shadow, Shape}; use dash_sdk::dashcore_rpc::dashcore::Network; @@ -96,11 +96,9 @@ fn add_location_view(ui: &mut Ui, location: Vec<(&str, AppAction)>, dark_mode: b fn add_connection_indicator(ui: &mut Ui, app_context: &Arc) -> AppAction { let mut action = AppAction::None; - let connected = app_context - .zmq_connection_status - .lock() - .map(|status| matches!(*status, ZMQConnectionEvent::Connected)) - .unwrap_or(false); + let status = app_context.connection_status(); + let backend_mode = status.backend_mode(); + let connected = status.overall_connected(); // Get time for pulsating animation (only when connected) let pulse_scale = if connected { @@ -149,14 +147,13 @@ fn add_connection_indicator(ui: &mut Ui, app_context: &Arc) -> AppAc if connected { app_context.repaint_animation(ui.ctx()); } - let tip = if connected { - "Connected to Dash Core Wallet" - } else { - "Disconnected from Dash Core Wallet. Click to start it." - }; + let tip = status.tooltip_text(); let resp = resp.on_hover_text(tip); - if resp.clicked() && !connected { + if resp.clicked() + && backend_mode == CoreBackendMode::Rpc + && !status.rpc_online() + { let settings = app_context.get_settings().ok().flatten(); let (custom_path, overwrite) = settings diff --git a/src/ui/network_chooser_screen.rs b/src/ui/network_chooser_screen.rs index 527fafa98..573117051 100644 --- a/src/ui/network_chooser_screen.rs +++ b/src/ui/network_chooser_screen.rs @@ -1,9 +1,10 @@ use crate::app::AppAction; -use crate::backend_task::core::{CoreItem, CoreTask}; +use crate::backend_task::BackendTask; +use crate::backend_task::core::CoreTask; use crate::backend_task::system_task::SystemTask; -use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::config::Config; use crate::context::AppContext; +use crate::context::connection_status::ConnectionStatus; use crate::model::wallet::DerivationPathHelpers; use crate::spv::{CoreBackendMode, SpvStatus, SpvStatusSnapshot}; use crate::ui::components::component_trait::Component; @@ -43,10 +44,6 @@ pub struct NetworkChooserScreen { pub local_app_context: Option>, pub local_network_dashmate_password: String, pub current_network: Network, - pub mainnet_core_status_online: bool, - pub testnet_core_status_online: bool, - pub devnet_core_status_online: bool, - pub local_core_status_online: bool, pub recheck_time: Option, custom_dash_qt_path: Option, custom_dash_qt_error_message: Option, @@ -141,10 +138,6 @@ impl NetworkChooserScreen { local_app_context: local_app_context.cloned(), local_network_dashmate_password, current_network, - mainnet_core_status_online: false, - testnet_core_status_online: false, - devnet_core_status_online: false, - local_core_status_online: false, recheck_time: None, custom_dash_qt_path, custom_dash_qt_error_message: None, @@ -450,20 +443,23 @@ impl NetworkChooserScreen { .entry(self.current_network) .or_insert(CoreBackendMode::Rpc); - // Check connection status - let (is_connected, snapshot) = match current_backend_mode { - CoreBackendMode::Rpc => (self.check_network_status(self.current_network), None), - CoreBackendMode::Spv => { - let ctx = self.current_app_context(); - let snap = ctx.spv_manager().status(); - let connected = snap.status.is_active() || snap.status == SpvStatus::Running; - (connected, Some(snap)) - } + let ctx = self.current_app_context(); + let status = ctx.connection_status(); + let disable_zmq = status.disable_zmq(); + let rpc_online = status.rpc_online(); + let zmq_connected = status.zmq_connected(); + let spv_status = status.spv_status(); + let spv_connected = ConnectionStatus::spv_connected(spv_status); + let snapshot = if current_backend_mode == CoreBackendMode::Spv { + Some(ctx.spv_manager().status().clone()) + } else { + None }; + let overall_connected = status.overall_connected(); // Button on the left with status ui.horizontal(|ui| { - if is_connected { + if overall_connected { if current_backend_mode == CoreBackendMode::Spv { let disconnect_button = egui::Button::new( egui::RichText::new("Disconnect").color(DashColors::WHITE), @@ -510,13 +506,22 @@ impl NetworkChooserScreen { } } else { // For Core mode, just show status since it can switch networks freely - ui.colored_label(DashColors::DASH_BLUE, "✅ Connected"); + let label = if disable_zmq { + "✅ Connected (RPC, ZMQ disabled)" + } else { + "✅ Connected (RPC + ZMQ)" + }; + ui.colored_label(DashColors::DASH_BLUE, label); } } else { // Don't show Connect button for Local network in RPC mode // (there's no Dash-Qt to start for local/regtest) - let show_connect_button = !(self.current_network == Network::Regtest - && current_backend_mode == CoreBackendMode::Rpc); + let show_connect_button = match current_backend_mode { + CoreBackendMode::Spv => true, + CoreBackendMode::Rpc => { + !rpc_online && self.current_network != Network::Regtest + } + }; if show_connect_button { let connect_button = egui::Button::new( @@ -552,6 +557,7 @@ impl NetworkChooserScreen { } } } + } }); @@ -568,6 +574,82 @@ impl NetworkChooserScreen { self.render_spv_sync_progress(ui, snap); } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.vertical(|ui| { + if current_backend_mode == CoreBackendMode::Rpc && !self.developer_mode { + ui.horizontal(|ui| { + ui.label("Core RPC:"); + let rpc_color = if rpc_online { + DashColors::SUCCESS + } else { + DashColors::ERROR + }; + let rpc_label = if rpc_online { "Connected" } else { "Disconnected" }; + ui.colored_label(rpc_color, rpc_label); + + ui.label(","); + ui.label("ZMQ:"); + if disable_zmq { + ui.colored_label(DashColors::text_secondary(dark_mode), "Disabled"); + } else { + let zmq_color = if zmq_connected { + DashColors::SUCCESS + } else { + DashColors::ERROR + }; + let zmq_label = if zmq_connected { "Connected" } else { "Disconnected" }; + ui.colored_label(zmq_color, zmq_label); + } + }); + } + + if current_backend_mode == CoreBackendMode::Rpc && self.developer_mode { + ui.horizontal(|ui| { + ui.label("Dash Core RPC:"); + let color = if rpc_online { + DashColors::SUCCESS + } else { + DashColors::ERROR + }; + let label = if rpc_online { "Connected" } else { "Disconnected" }; + ui.colored_label(color, label); + }); + + ui.horizontal(|ui| { + ui.label("ZMQ:"); + if disable_zmq { + ui.colored_label( + DashColors::text_secondary(dark_mode), + "Disabled", + ); + } else { + let color = if zmq_connected { + DashColors::SUCCESS + } else { + DashColors::ERROR + }; + let label = if zmq_connected { "Connected" } else { "Disconnected" }; + ui.colored_label(color, label); + } + }); + } + + if current_backend_mode == CoreBackendMode::Spv { + ui.horizontal(|ui| { + ui.label("SPV:"); + let color = if spv_connected { + DashColors::SUCCESS + } else { + DashColors::ERROR + }; + ui.colored_label(color, spv_status.to_string()); + }); + } + }); }); // Advanced Settings section with clean dropdown @@ -1697,17 +1779,6 @@ impl NetworkChooserScreen { } } - /// Check if the network is working - fn check_network_status(&self, network: Network) -> bool { - match network { - Network::Dash => self.mainnet_core_status_online, - Network::Testnet => self.testnet_core_status_online, - Network::Devnet => self.devnet_core_status_online, - Network::Regtest => self.local_core_status_online, - _ => false, - } - } - fn any_rpc_backend(&self) -> bool { self.backend_modes .iter() @@ -1810,43 +1881,6 @@ impl ScreenLike for NetworkChooserScreen { } } - fn display_message(&mut self, message: &str, _message_type: super::MessageType) { - if message.contains("Failed to get best chain lock for mainnet, testnet, devnet, and local") - { - self.mainnet_core_status_online = false; - self.testnet_core_status_online = false; - self.devnet_core_status_online = false; - self.local_core_status_online = false; - } - } - - fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { - if let BackendTaskSuccessResult::CoreItem(CoreItem::ChainLocks( - mainnet_chainlock, - testnet_chainlock, - devnet_chainlock, - local_chainlock, - )) = backend_task_success_result - { - match mainnet_chainlock { - Some(_) => self.mainnet_core_status_online = true, - None => self.mainnet_core_status_online = false, - } - match testnet_chainlock { - Some(_) => self.testnet_core_status_online = true, - None => self.testnet_core_status_online = false, - } - match devnet_chainlock { - Some(_) => self.devnet_core_status_online = true, - None => self.devnet_core_status_online = false, - } - match local_chainlock { - Some(_) => self.local_core_status_online = true, - None => self.local_core_status_online = false, - } - } - } - fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = add_top_panel( ctx, diff --git a/src/ui/tokens/tokens_screen/mod.rs b/src/ui/tokens/tokens_screen/mod.rs index 5ca8ffcae..938579d95 100644 --- a/src/ui/tokens/tokens_screen/mod.rs +++ b/src/ui/tokens/tokens_screen/mod.rs @@ -3240,8 +3240,14 @@ mod tests { db.initialize(Path::new(&db_file_path)).unwrap(); ensure_test_env(); - let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) - .expect("Expected to create AppContext"); + let app_context = AppContext::new( + Network::Regtest, + db, + None, + Default::default(), + Default::default(), + ) + .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); // Identity selection @@ -3546,8 +3552,14 @@ mod tests { db.initialize(Path::new(&db_file_path)).unwrap(); ensure_test_env(); - let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) - .expect("Expected to create AppContext"); + let app_context = AppContext::new( + Network::Regtest, + db, + None, + Default::default(), + Default::default(), + ) + .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); // Identity selection @@ -3666,8 +3678,14 @@ mod tests { db.initialize(Path::new(&db_file_path)).unwrap(); ensure_test_env(); - let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) - .expect("Expected to create AppContext"); + let app_context = AppContext::new( + Network::Regtest, + db, + None, + Default::default(), + Default::default(), + ) + .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); // Identity selection