diff --git a/src/spv/manager.rs b/src/spv/manager.rs index 7b192b54f..b3d380128 100644 --- a/src/spv/manager.rs +++ b/src/spv/manager.rs @@ -3,7 +3,9 @@ use crate::config::NetworkConfig; use crate::utils::tasks::TaskManager; use dash_sdk::dash_spv::network::MultiPeerNetworkManager; use dash_sdk::dash_spv::storage::DiskStorageManager; -use dash_sdk::dash_spv::types::{DetailedSyncProgress, SpvEvent, SyncProgress, ValidationMode}; +use dash_sdk::dash_spv::types::{ + DetailedSyncProgress, SpvEvent, SyncProgress, SyncStage, ValidationMode, +}; use dash_sdk::dash_spv::{ClientConfig, DashSpvClient}; use dash_sdk::dpp::dashcore::Network; use dash_sdk::dpp::key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; @@ -103,6 +105,9 @@ pub struct SpvManager { status: Arc>, last_error: Arc>>, started_at: Arc>>, + sync_progress_state: Arc>>, + detailed_progress_state: Arc>>, + progress_updated_at: Arc>>, // mapping DET wallet seed_hash -> SPV wallet identifier (if created) det_wallets: Arc>>, // signal channel to trigger external reconcile on wallet-related events @@ -132,6 +137,9 @@ impl SpvManager { status: Arc::new(RwLock::new(SpvStatus::Idle)), last_error: Arc::new(RwLock::new(None)), started_at: Arc::new(RwLock::new(None)), + sync_progress_state: Arc::new(RwLock::new(None)), + detailed_progress_state: Arc::new(RwLock::new(None)), + progress_updated_at: Arc::new(RwLock::new(None)), det_wallets: Arc::new(RwLock::new(std::collections::BTreeMap::new())), reconcile_tx: Mutex::new(None), stop_token: Mutex::new(None), @@ -142,7 +150,7 @@ impl SpvManager { /// Async status method for getting full details including progress pub async fn status_async(&self) -> SpvStatusSnapshot { - let client_guard = self.client.read().await; + let _client_guard = self.client.read().await; let status = *self.status.read().expect("SPV status lock poisoned"); let last_error = self .last_error @@ -153,15 +161,21 @@ impl SpvManager { .started_at .read() .expect("SPV started_at lock poisoned"); - - // Get progress directly from the client if available - let (sync_progress, detailed_progress) = if let Some(_client) = client_guard.as_ref() { - // Note: These would need to be exposed by dash-spv's DashSpvClient - // For now, we'll track them separately until dash-spv exposes them - (None, None) - } else { - (None, None) - }; + let sync_progress = self + .sync_progress_state + .read() + .expect("SPV sync_progress lock poisoned") + .clone(); + let detailed_progress = self + .detailed_progress_state + .read() + .expect("SPV detailed_progress lock poisoned") + .clone(); + let last_updated = (*self + .progress_updated_at + .read() + .expect("SPV progress_updated lock poisoned")) + .or(Some(SystemTime::now())); SpvStatusSnapshot { status, @@ -169,7 +183,7 @@ impl SpvManager { detailed_progress, last_error, started_at, - last_updated: Some(SystemTime::now()), + last_updated, } } @@ -185,14 +199,29 @@ impl SpvManager { .started_at .read() .expect("SPV started_at lock poisoned"); + let sync_progress = self + .sync_progress_state + .read() + .expect("SPV sync_progress lock poisoned") + .clone(); + let detailed_progress = self + .detailed_progress_state + .read() + .expect("SPV detailed_progress lock poisoned") + .clone(); + let last_updated = (*self + .progress_updated_at + .read() + .expect("SPV progress_updated lock poisoned")) + .or(Some(SystemTime::now())); SpvStatusSnapshot { status, - sync_progress: None, - detailed_progress: None, + sync_progress, + detailed_progress, last_error, started_at, - last_updated: Some(SystemTime::now()), + last_updated, } } @@ -217,6 +246,18 @@ impl SpvManager { .started_at .write() .expect("SPV started_at lock poisoned") = Some(SystemTime::now()); + *self + .sync_progress_state + .write() + .expect("SPV sync_progress lock poisoned") = None; + *self + .detailed_progress_state + .write() + .expect("SPV detailed_progress lock poisoned") = None; + *self + .progress_updated_at + .write() + .expect("SPV progress_updated lock poisoned") = None; let stop_token = CancellationToken::new(); *self @@ -485,9 +526,24 @@ impl SpvManager { // Sync to tip match client.sync_to_tip().await { Ok(progress) => { - tracing::info!("Initial sync complete: {:?}", progress); + tracing::info!("Initial sync progress snapshot: {:?}", progress); + { + let mut stored_sync = self + .sync_progress_state + .write() + .expect("SPV sync_progress lock poisoned"); + *stored_sync = Some(progress.clone()); + } + { + let mut updated_at = self + .progress_updated_at + .write() + .expect("SPV progress_updated lock poisoned"); + *updated_at = Some(SystemTime::now()); + } + // Stay in Syncing mode until detailed progress reports completion. *self.status.write().expect("SPV status lock poisoned") = - SpvStatus::Running; + SpvStatus::Syncing; } Err(err) => { tracing::error!("Initial sync failed: {}", err); @@ -557,6 +613,10 @@ impl SpvManager { mut progress_rx: tokio::sync::mpsc::UnboundedReceiver, ) { let status = Arc::clone(&self.status); + let last_error = Arc::clone(&self.last_error); + let sync_progress_state = Arc::clone(&self.sync_progress_state); + let detailed_progress_state = Arc::clone(&self.detailed_progress_state); + let progress_updated_at = Arc::clone(&self.progress_updated_at); let cancel = self.subtasks.cancellation_token.clone(); self.subtasks.spawn_sync(async move { @@ -569,14 +629,49 @@ impl SpvManager { msg = progress_rx.recv() => { match msg { Some(detailed) => { + { + let mut stored_detailed = detailed_progress_state + .write() + .expect("SPV detailed_progress lock poisoned"); + *stored_detailed = Some(detailed.clone()); + } + { + let mut stored_sync = sync_progress_state + .write() + .expect("SPV sync_progress lock poisoned"); + *stored_sync = Some(detailed.sync_progress.clone()); + } + { + let mut updated_at = progress_updated_at + .write() + .expect("SPV progress_updated lock poisoned"); + *updated_at = Some(detailed.last_update_time); + } + if last_update.elapsed() >= min_interval { - // Update status based on progress - if detailed.percentage >= 100.0 || detailed.sync_progress.header_height >= detailed.peer_best_height { - *status.write().expect("SPV status lock poisoned") = SpvStatus::Running; - } else { - let current = *status.read().expect("SPV status lock poisoned"); - if matches!(current, SpvStatus::Starting | SpvStatus::Idle | SpvStatus::Stopped) { - *status.write().expect("SPV status lock poisoned") = SpvStatus::Syncing; + // Update status based on progress stage and completeness + let mut status_guard = status + .write() + .expect("SPV status lock poisoned"); + let current = *status_guard; + match &detailed.sync_stage { + SyncStage::Complete => { + *status_guard = SpvStatus::Running; + } + SyncStage::Failed(message) => { + *status_guard = SpvStatus::Error; + let mut err_guard = last_error + .write() + .expect("SPV last_error lock poisoned"); + *err_guard = Some(format!("SPV sync failed: {message}")); + } + _ => { + if !matches!( + current, + SpvStatus::Stopping | SpvStatus::Stopped | SpvStatus::Error + ) { + *status_guard = SpvStatus::Syncing; + } } } last_update = std::time::Instant::now(); @@ -634,12 +729,18 @@ impl SpvManager { >, String, > { + let start_height = { + let guard = self.wallet.read().await; + if guard.wallet_count() == 0 { + u32::MAX + } else { + 0 + } + }; let mut config = ClientConfig::new(self.network) .with_storage_path(self.data_dir.clone()) .with_validation_mode(ValidationMode::Full) - // Start from the latest built-in checkpoint instead of genesis - // (effective only when storage is empty / first initialization) - .with_start_height(u32::MAX); + .with_start_height(start_height); // Pin peers when running against local nodes to avoid random peers. if self.network == Network::Devnet || self.network == Network::Regtest { diff --git a/src/ui/network_chooser_screen.rs b/src/ui/network_chooser_screen.rs index ddef52944..1a1098bff 100644 --- a/src/ui/network_chooser_screen.rs +++ b/src/ui/network_chooser_screen.rs @@ -40,6 +40,7 @@ pub struct NetworkChooserScreen { theme_preference: ThemeMode, should_reset_collapsing_states: bool, backend_modes: HashMap, + filter_headers_stage_start: Option, } impl NetworkChooserScreen { @@ -121,6 +122,7 @@ impl NetworkChooserScreen { theme_preference, should_reset_collapsing_states: true, // Start with collapsed state backend_modes, + filter_headers_stage_start: None, } } @@ -773,7 +775,25 @@ impl NetworkChooserScreen { app_action } - fn render_spv_sync_progress(&self, ui: &mut Ui, snapshot: &SpvStatusSnapshot) { + fn render_spv_sync_progress(&mut self, ui: &mut Ui, snapshot: &SpvStatusSnapshot) { + if let Some(detailed) = &snapshot.detailed_progress { + match detailed.sync_stage { + SyncStage::DownloadingFilterHeaders { current, target } => { + let baseline = current.min(target); + if let Some(existing) = self.filter_headers_stage_start { + self.filter_headers_stage_start = Some(existing.min(target)); + } else { + self.filter_headers_stage_start = Some(baseline); + } + } + _ => { + self.filter_headers_stage_start = None; + } + } + } else { + self.filter_headers_stage_start = None; + } + let dark_mode = ui.ctx().style().visuals.dark_mode; // Raw sync status display @@ -806,31 +826,53 @@ impl NetworkChooserScreen { } // Prefer detailed header progress when available - if let Some(detailed) = &snapshot.detailed_progress { + if snapshot.detailed_progress.is_some() { // Add separator between status and progress bars ui.separator(); ui.separator(); ui.end_row(); - // Use detailed percentage for headers bar + // Headers progress ui.label( egui::RichText::new("Headers:") .color(DashColors::text_secondary(dark_mode)), ); - let overall_progress = (detailed.calculate_percentage() / 100.0) as f32; - ui.add(egui::ProgressBar::new(overall_progress).show_percentage()); + let headers_progress = self.calculate_headers_progress(snapshot); + ui.add(egui::ProgressBar::new(headers_progress).show_percentage()); + ui.end_row(); + + // Validating headers progress (formerly masternode lists) + ui.label( + egui::RichText::new("Validating Headers:") + .color(DashColors::text_secondary(dark_mode)), + ); + let validating_progress = + self.calculate_validating_headers_progress(snapshot); + ui.add(egui::ProgressBar::new(validating_progress).show_percentage()); + ui.end_row(); + + // Filter headers progress + ui.label( + egui::RichText::new("Filter Headers:") + .color(DashColors::text_secondary(dark_mode)), + ); + let filter_headers_progress = + self.calculate_filter_headers_progress(snapshot); + ui.add( + egui::ProgressBar::new(filter_headers_progress).show_percentage(), + ); ui.end_row(); - // Masternode Lists progress bar (estimate based on sync stage) + // Filters progress ui.label( - egui::RichText::new("Masternode Lists:") + egui::RichText::new("Filters:") .color(DashColors::text_secondary(dark_mode)), ); - let mn_progress = self.calculate_mn_progress(snapshot); - ui.add(egui::ProgressBar::new(mn_progress).show_percentage()); + let filters_progress = self.calculate_filters_progress(snapshot); + ui.add(egui::ProgressBar::new(filters_progress).show_percentage()); ui.end_row(); - // Blocks/Filters progress bar + // Blocks progress bar ui.label( egui::RichText::new("Blocks:") .color(DashColors::text_secondary(dark_mode)), @@ -838,28 +880,13 @@ impl NetworkChooserScreen { let blocks_progress = self.calculate_blocks_progress(snapshot); ui.add(egui::ProgressBar::new(blocks_progress).show_percentage()); ui.end_row(); - - // Peers (if we have a snapshot with counts) - if let Some(progress) = &snapshot.sync_progress - && progress.peer_count > 0 - { - ui.label( - egui::RichText::new("Peers:") - .color(DashColors::text_secondary(dark_mode)), - ); - ui.label(format!("{}", progress.peer_count)); - ui.end_row(); - } } else if let Some(ev) = &snapshot.sync_progress { // Event-driven progress (updates most frequently) ui.label( egui::RichText::new("Synced:") .color(DashColors::text_secondary(dark_mode)), ); - ui.label(format!( - "Headers: {} / {}", - ev.header_height, ev.filter_header_height - )); + ui.label(format!("Headers height: {}", ev.header_height)); ui.end_row(); // Add separator between stats and progress bars @@ -868,44 +895,49 @@ impl NetworkChooserScreen { ui.end_row(); // Progress bars for different components - // Headers progress bar (from events) + let headers_progress = self.calculate_headers_progress(snapshot); ui.label( egui::RichText::new("Headers:") .color(DashColors::text_secondary(dark_mode)), ); - let headers_progress = self.calculate_headers_progress(snapshot); ui.add(egui::ProgressBar::new(headers_progress).show_percentage()); ui.end_row(); - // Masternode Lists progress bar (estimate based on sync stage) + let validating_progress = + self.calculate_validating_headers_progress(snapshot); ui.label( - egui::RichText::new("Masternode Lists:") + egui::RichText::new("Validating Headers:") .color(DashColors::text_secondary(dark_mode)), ); - let mn_progress = self.calculate_mn_progress(snapshot); - ui.add(egui::ProgressBar::new(mn_progress).show_percentage()); + ui.add(egui::ProgressBar::new(validating_progress).show_percentage()); ui.end_row(); - // Blocks/Filters progress bar + let filter_headers_progress = + self.calculate_filter_headers_progress(snapshot); ui.label( - egui::RichText::new("Blocks:") + egui::RichText::new("Filter Headers:") .color(DashColors::text_secondary(dark_mode)), ); + ui.add( + egui::ProgressBar::new(filter_headers_progress).show_percentage(), + ); + ui.end_row(); + + let filters_progress = self.calculate_filters_progress(snapshot); + ui.label( + egui::RichText::new("Filters:") + .color(DashColors::text_secondary(dark_mode)), + ); + ui.add(egui::ProgressBar::new(filters_progress).show_percentage()); + ui.end_row(); + let blocks_progress = self.calculate_blocks_progress(snapshot); + ui.label( + egui::RichText::new("Blocks:") + .color(DashColors::text_secondary(dark_mode)), + ); ui.add(egui::ProgressBar::new(blocks_progress).show_percentage()); ui.end_row(); - - // Peers (if we also have a snapshot with counts) - if let Some(progress) = &snapshot.sync_progress - && progress.peer_count > 0 - { - ui.label( - egui::RichText::new("Peers:") - .color(DashColors::text_secondary(dark_mode)), - ); - ui.label(format!("{}", progress.peer_count)); - ui.end_row(); - } } }); }); @@ -913,48 +945,124 @@ impl NetworkChooserScreen { fn calculate_headers_progress(&self, snapshot: &SpvStatusSnapshot) -> f32 { if let Some(detailed) = &snapshot.detailed_progress { - // If we have detailed progress, use it match &detailed.sync_stage { SyncStage::DownloadingHeaders { start, end } => { + // Respect restored checkpoints: show progress relative to the download window. if end > start { - ((detailed.sync_progress.header_height - start) as f32 - / (end - start) as f32) - .min(1.0) + let window = (end - start) as f32; + let current = detailed.sync_progress.header_height; + let clamped = current.clamp(*start, *end) - start; + (clamped as f32 / window).clamp(0.0, 1.0) } else { 0.0 } } - SyncStage::ValidatingHeaders { .. } | SyncStage::StoringHeaders { .. } => 0.8, // Headers mostly done - SyncStage::Complete => 1.0, + SyncStage::ValidatingHeaders { .. } + | SyncStage::StoringHeaders { .. } + | SyncStage::DownloadingFilterHeaders { .. } + | SyncStage::DownloadingFilters { .. } + | SyncStage::DownloadingBlocks { .. } + | SyncStage::Complete => 1.0, + SyncStage::Failed(_) => 0.0, _ => 0.0, } - } else if let Some(ev) = &snapshot.sync_progress { - // Estimate based on filter progress - if ev.filter_header_height > 0 && ev.header_height > 0 { - (ev.filter_header_height as f32 / ev.header_height as f32).clamp(0.0, 1.0) - } else { + } else if let Some(progress) = &snapshot.sync_progress { + if progress.header_height == 0 { 0.0 + } else { + // Without detailed context fall back to comparing against masternode progress + (progress.masternode_height as f32 / progress.header_height as f32).clamp(0.0, 1.0) } } else { 0.0 } } - fn calculate_mn_progress(&self, snapshot: &SpvStatusSnapshot) -> f32 { - // Since the current SPV doesn't directly report MN list progress, - // we estimate based on the sync stage + fn calculate_filter_headers_progress(&self, snapshot: &SpvStatusSnapshot) -> f32 { + if let Some(detailed) = &snapshot.detailed_progress { + if detailed.peer_best_height == 0 { + return 0.0; + } + match &detailed.sync_stage { + SyncStage::DownloadingFilterHeaders { current, target } => { + let current = *current; + let target = *target; + if target == 0 { + return 0.0; + } + + let start = self + .filter_headers_stage_start + .unwrap_or(current) + .min(target); + let span = target.saturating_sub(start); + if span == 0 { + if current >= target { 1.0 } else { 0.0 } + } else { + let progress = current.saturating_sub(start); + (progress as f32 / span as f32).clamp(0.0, 1.0) + } + } + SyncStage::DownloadingFilters { .. } + | SyncStage::DownloadingBlocks { .. } + | SyncStage::Complete => (detailed.sync_progress.filter_header_height as f32 + / detailed.peer_best_height as f32) + .clamp(0.0, 1.0), + SyncStage::Failed(_) => 0.0, + _ => 0.0, + } + } else { + 0.0 + } + } + + fn calculate_filters_progress(&self, snapshot: &SpvStatusSnapshot) -> f32 { + if let Some(detailed) = &snapshot.detailed_progress { + match &detailed.sync_stage { + SyncStage::DownloadingFilters { completed, total } => { + if *total == 0 { + 0.0 + } else { + (*completed as f32 / *total as f32).clamp(0.0, 1.0) + } + } + SyncStage::DownloadingBlocks { .. } | SyncStage::Complete => 1.0, + SyncStage::Failed(_) => 0.0, + _ => 0.0, + } + } else { + 0.0 + } + } + + fn calculate_validating_headers_progress(&self, snapshot: &SpvStatusSnapshot) -> f32 { if snapshot.status == SpvStatus::Running { - 1.0 // Fully synced means MN lists are done - } else if let Some(progress) = &snapshot.sync_progress { - if progress.filter_header_height > 0 && progress.header_height > 0 { - // If we have both headers and filters, MN lists are likely in progress or done - if progress.filter_header_height >= progress.header_height { - 1.0 - } else { - (progress.filter_header_height as f32 / progress.header_height as f32).min(1.0) + return 1.0; + } + + if let Some(detailed) = &snapshot.detailed_progress { + match &detailed.sync_stage { + SyncStage::ValidatingHeaders { .. } | SyncStage::StoringHeaders { .. } => { + if detailed.peer_best_height == 0 { + 0.0 + } else { + let best_height = detailed.peer_best_height as f32; + let validated = detailed.sync_progress.masternode_height as f32; + (validated / best_height).clamp(0.0, 1.0) + } } - } else { + SyncStage::DownloadingFilterHeaders { .. } + | SyncStage::DownloadingFilters { .. } + | SyncStage::DownloadingBlocks { .. } + | SyncStage::Complete => 1.0, + SyncStage::Failed(_) => 0.0, + _ => 0.0, + } + } else if let Some(progress) = &snapshot.sync_progress { + if progress.header_height == 0 { 0.0 + } else { + (progress.masternode_height as f32 / progress.header_height as f32).clamp(0.0, 1.0) } } else { 0.0 @@ -963,13 +1071,25 @@ impl NetworkChooserScreen { fn calculate_blocks_progress(&self, snapshot: &SpvStatusSnapshot) -> f32 { if snapshot.status == SpvStatus::Running { - 1.0 // Fully synced - } else if let Some(progress) = &snapshot.sync_progress { - // Blocks progress is roughly filters progress - if progress.header_height > 0 && progress.filter_header_height > 0 { - (progress.filter_header_height as f32 / progress.header_height as f32).min(1.0) - } else { - 0.0 + return 1.0; + } + + if let Some(detailed) = &snapshot.detailed_progress { + match &detailed.sync_stage { + SyncStage::DownloadingBlocks { .. } => { + if detailed.peer_best_height == 0 { + 0.0 + } else { + let processed_height = detailed + .sync_progress + .last_synced_filter_height + .unwrap_or(0); + (processed_height as f32 / detailed.peer_best_height as f32).clamp(0.0, 1.0) + } + } + SyncStage::Complete => 1.0, + SyncStage::Failed(_) => 0.0, + _ => 0.0, } } else { 0.0 @@ -1014,22 +1134,6 @@ impl NetworkChooserScreen { return Some(Self::format_detailed_progress(progress)); } - if let Some(ev) = snapshot.sync_progress.as_ref() { - return Some(format!( - "Sync: {} / {} ({:.1}%)", - ev.header_height, - ev.filter_header_height, - (ev.filter_header_height as f32 / ev.header_height.max(1) as f32 * 100.0) - )); - } - - if let Some(progress) = snapshot.sync_progress.as_ref() { - return Some(format!( - "Headers: {} | Filters: {} | Peers: {}", - progress.header_height, progress.filter_header_height, progress.peer_count - )); - } - snapshot.last_error.clone() } @@ -1037,19 +1141,28 @@ impl NetworkChooserScreen { let mut message = match &progress.sync_stage { SyncStage::Connecting => "Connecting to peers".to_string(), SyncStage::QueryingPeerHeight => "Querying peer heights".to_string(), - SyncStage::DownloadingHeaders { start, end } => { - format!("Headers: {start} / {end}") + SyncStage::DownloadingHeaders { .. } => { + format!( + "Headers: {} / {}", + progress.sync_progress.header_height, progress.peer_best_height, + ) } SyncStage::ValidatingHeaders { batch_size } => { - format!("Validating headers (batch {batch_size})") + format!( + "Validating headers (batch {batch_size}) | Height {}", + progress.sync_progress.masternode_height + ) } SyncStage::StoringHeaders { batch_size } => { - format!("Storing headers (batch {batch_size})") + format!( + "Storing headers (batch {batch_size}) | Height {}", + progress.sync_progress.header_height + ) } SyncStage::Complete => "Sync complete".to_string(), SyncStage::Failed(reason) => format!("Failed: {reason}"), SyncStage::DownloadingFilterHeaders { current, target } => { - format!("FilterHeaders: {current} / {target}") + format!("Filter headers: {current} / {target}") } SyncStage::DownloadingFilters { completed, total } => { format!("Filters: {completed} / {total}") @@ -1063,12 +1176,6 @@ impl NetworkChooserScreen { message = format!("{message} | Peers: {}", progress.sync_progress.peer_count); } - if let Some(eta) = progress.calculate_eta() - && eta.as_secs() > 0 - { - message = format!("{message} | ETA: {}s", eta.as_secs()); - } - message } }