From a9af914de7eac54b16f5d3b70285cfd2f862d94f Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Tue, 16 Jan 2024 05:58:58 +0200 Subject: [PATCH] storage management --- Cargo.toml | 4 +- core/Cargo.toml | 2 + core/src/core.rs | 22 +- core/src/egui/extensions.rs | 5 + core/src/events.rs | 2 + core/src/imports.rs | 3 +- core/src/lib.rs | 1 + core/src/modules/account_manager/estimator.rs | 17 +- core/src/modules/overview.rs | 3 + core/src/modules/settings/mod.rs | 128 ++++----- core/src/runtime/mod.rs | 19 ++ core/src/runtime/services/kaspa/mod.rs | 67 +++-- .../runtime/services/market_monitor/mod.rs | 2 +- core/src/runtime/services/peer_monitor.rs | 2 +- core/src/runtime/services/repaint_service.rs | 2 +- core/src/runtime/services/update_monitor.rs | 2 +- core/src/runtime/system.rs | 2 +- core/src/settings.rs | 17 +- core/src/status.rs | 7 +- core/src/storage.rs | 251 ++++++++++++++++++ core/src/sync.rs | 44 ++- resources/i18n/i18n.json | 20 +- 22 files changed, 480 insertions(+), 142 deletions(-) create mode 100644 core/src/storage.rs diff --git a/Cargo.toml b/Cargo.toml index e06099e..066444a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,8 +133,8 @@ cfg-if = "1.0.0" chrome-sys = "0.1.0" # chrome-sys = {path = "../chrome-sys"} chrono = "0.4.31" -convert_case = "0.6.0" clap = { version = "4.4.7", features = ["derive", "string", "cargo"] } +convert_case = "0.6.0" ctrlc = { version = "3.2", features = ["termination"] } derivative = "2.2.0" downcast = "0.11.0" @@ -147,6 +147,7 @@ js-sys = "0.3.64" log = "0.4.20" nix = "0.27.1" num_cpus = "1.15.0" +open = "5.0.1" pad = "0.1.6" passwords = "3.1.16" qrcode = "0.12.0" @@ -163,6 +164,7 @@ sysinfo = "0.29.10" thiserror = "1.0.50" tokio = { version = "1", features = ["sync", "rt-multi-thread", "process"] } toml = "0.8.8" +walkdir = "2.4.0" wasm-bindgen = "0.2.87" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3.64", features = ['Window'] } diff --git a/core/Cargo.toml b/core/Cargo.toml index 121d6d9..793f174 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -62,6 +62,7 @@ slug.workspace = true smallvec.workspace = true thiserror.workspace = true toml.workspace = true +walkdir.workspace = true wasm-bindgen.workspace = true xxhash-rust.workspace = true zeroize.workspace = true @@ -104,6 +105,7 @@ kaspa-rpc-service.workspace = true kaspa-wrpc-server.workspace = true kaspad.workspace = true num_cpus.workspace = true +open.workspace = true rlimit.workspace = true sysinfo.workspace = true tokio.workspace = true diff --git a/core/src/core.rs b/core/src/core.rs index ef96b72..5ea40c1 100644 --- a/core/src/core.rs +++ b/core/src/core.rs @@ -54,6 +54,7 @@ pub struct Core { callback_map: CallbackMap, pub network_pressure: NetworkPressure, notifications: Notifications, + pub storage: Storage, } impl Core { @@ -192,6 +193,7 @@ impl Core { callback_map: CallbackMap::default(), network_pressure: NetworkPressure::default(), notifications: Notifications::default(), + storage: Storage::default(), }; modules.values().for_each(|module| { @@ -202,9 +204,18 @@ impl Core { this.wallet_update_list(); - #[cfg(target_arch = "wasm32")] - { - this.register_visibility_handler(); + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + this.register_visibility_handler(); + } else { + let storage = this.storage.clone(); + spawn(async move { + loop { + storage.update(None); + task::sleep(Duration::from_secs(60)).await; + } + }); + } } this @@ -616,6 +627,11 @@ impl Core { _frame: &mut eframe::Frame, ) -> Result<()> { match event { + Events::UpdateStorage(_options) => { + #[cfg(not(target_arch = "wasm32"))] + self.storage + .update(Some(_options.with_network(self.settings.node.network))); + } Events::VisibilityChange(state) => match state { VisibilityState::Visible => { self.module.clone().show(self); diff --git a/core/src/egui/extensions.rs b/core/src/egui/extensions.rs index 1b9351f..63ba0f4 100644 --- a/core/src/egui/extensions.rs +++ b/core/src/egui/extensions.rs @@ -34,6 +34,7 @@ pub trait UiExtension { ) -> Option; fn confirm_medium_apply_cancel(&mut self, align: Align) -> Option; fn confirm_medium_cancel(&mut self, align: Align) -> Option; + fn sized_separator(&mut self, size: Vec2) -> Response; } impl UiExtension for Ui { @@ -103,6 +104,10 @@ impl UiExtension for Ui { format!("{} {}", egui_phosphor::light::X, i18n("Cancel")), ) } + + fn sized_separator(&mut self, size: Vec2) -> Response { + self.add_sized(size, Separator::default()) + } } pub struct LayoutJobBuilderSettings { diff --git a/core/src/events.rs b/core/src/events.rs index d25dfb3..2f67bdf 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -1,5 +1,6 @@ use crate::imports::*; use crate::market::*; +use crate::storage::StorageUpdateOptions; use crate::utils::Release; use kaspa_metrics_core::MetricsSnapshot; use kaspa_wallet_core::{events as kaspa, storage::PrvKeyDataInfo}; @@ -8,6 +9,7 @@ pub type ApplicationEventsChannel = crate::runtime::channel::Channel; #[derive(Clone)] pub enum Events { + UpdateStorage(StorageUpdateOptions), VisibilityChange(VisibilityState), VersionUpdate(Release), ThemeChange, diff --git a/core/src/imports.rs b/core/src/imports.rs index eafb175..1e17c04 100644 --- a/core/src/imports.rs +++ b/core/src/imports.rs @@ -39,7 +39,7 @@ pub use workflow_core::abortable::Abortable; pub use workflow_core::channel::{oneshot, Channel, Receiver, Sender}; pub use workflow_core::enums::Describe; pub use workflow_core::extensions::is_not_empty::*; -pub use workflow_core::task::interval; +pub use workflow_core::task; pub use workflow_core::time::{unixtime_as_millis_f64, Instant}; pub use workflow_dom::utils::*; pub use workflow_http as http; @@ -85,5 +85,6 @@ pub use crate::settings::{ }; pub use crate::state::State; pub use crate::status::Status; +pub use crate::storage::{Storage, StorageUpdateOptions}; pub use crate::utils::spawn; pub use crate::utils::*; diff --git a/core/src/lib.rs b/core/src/lib.rs index 49097ef..cf4e8da 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -29,6 +29,7 @@ pub mod servers; pub mod settings; pub mod state; pub mod status; +pub mod storage; pub mod sync; pub mod utils; diff --git a/core/src/modules/account_manager/estimator.rs b/core/src/modules/account_manager/estimator.rs index b9a063f..0df0cbf 100644 --- a/core/src/modules/account_manager/estimator.rs +++ b/core/src/modules/account_manager/estimator.rs @@ -36,7 +36,7 @@ impl<'context> Estimator<'context> { Focus::Amount, |ui, text| { ui.add_space(8.); - ui.label(RichText::new(format!("Enter {} amount to send", kaspa_suffix(network_type))).size(12.).raised()); + ui.label(RichText::new(format!("{} {} {}", i18n("Enter"), kaspa_suffix(network_type), i18n("amount to send"))).size(12.).raised()); ui.add_sized(Overview::editor_size(ui), TextEdit::singleline(text) .vertical_align(Align::Center)) }, @@ -57,10 +57,15 @@ impl<'context> Estimator<'context> { // TODO - improve the logic if core.network_pressure.is_high() { - ui.label(format!("The network is currently experiencing high load (~{}% of its capacity). \ - It is recommended that you add a priority fee of at least {:0.3} {} \ - to ensure faster transaction acceptance.", - core.network_pressure.capacity(), 0.001, kaspa_suffix(network_type))); + ui.label(format!("{}: {}% {} {} {:0.3} {} {}", + i18n("The network is currently experiencing high load"), + core.network_pressure.capacity(), + i18n("of its capacity."), + i18n("It is recommended that you add a priority fee of at least"), + 0.001, + kaspa_suffix(network_type), + i18n("to ensure faster transaction acceptance."), + )); } ui.add_space(8.); @@ -117,7 +122,7 @@ impl<'context> Estimator<'context> { false } EstimatorStatus::None => { - ui.label(i18n("Please enter KAS amount to send")); + ui.label(format!("{} {} {}", i18n("Please enter"), kaspa_suffix(network_type), i18n("amount to send"))); false } }; diff --git a/core/src/modules/overview.rs b/core/src/modules/overview.rs index a1bd466..89378b6 100644 --- a/core/src/modules/overview.rs +++ b/core/src/modules/overview.rs @@ -249,6 +249,9 @@ impl Overview { if let Some(system) = runtime().system() { system.render(ui); } + + #[cfg(not(target_arch = "wasm32"))] + core.storage.render(ui); CollapsingHeader::new(i18n("License Information")) .default_open(false) diff --git a/core/src/modules/settings/mod.rs b/core/src/modules/settings/mod.rs index 08d4497..8478faf 100644 --- a/core/src/modules/settings/mod.rs +++ b/core/src/modules/settings/mod.rs @@ -1,4 +1,3 @@ - use crate::imports::*; use crate::servers::render_public_server_selector; @@ -117,11 +116,9 @@ impl ModuleT for Settings { } fn style(&self) -> ModuleStyle { - // ModuleStyle::Large ModuleStyle::Default } - fn render( &mut self, core: &mut Core, @@ -135,11 +132,15 @@ impl ModuleT for Settings { self.render_settings(core,ui); }); } -} -impl Settings { + fn deactivate(&mut self, _core: &mut Core) { + #[cfg(not(target_arch = "wasm32"))] + _core.storage.clear_settings(); + } +} +impl Settings { fn render_node_settings( &mut self, @@ -197,23 +198,19 @@ impl Settings { #[cfg(not(target_arch = "wasm32"))] KaspadNodeKind::ExternalAsDaemon => { - // let binary_path = self.settings.node.kaspad_daemon_binary.clone(); - ui.horizontal(|ui|{ ui.label(i18n("Rusty Kaspa Daemon Path:")); ui.add(TextEdit::singleline(&mut self.settings.node.kaspad_daemon_binary)); }); - // if binary_path != self.settings.node.kaspad_daemon_binary { - let path = std::path::PathBuf::from(&self.settings.node.kaspad_daemon_binary); - if path.exists() && !path.is_file() { - ui.label( - RichText::new(format!("Rusty Kaspa Daemon not found at '{path}'", path = self.settings.node.kaspad_daemon_binary)) - .color(theme_color().error_color), - ); - node_settings_error = Some("Rusty Kaspa Daemon not found"); - } - // } + let path = std::path::PathBuf::from(&self.settings.node.kaspad_daemon_binary); + if path.exists() && !path.is_file() { + ui.label( + RichText::new(format!("Rusty Kaspa Daemon not found at '{path}'", path = self.settings.node.kaspad_daemon_binary)) + .color(theme_color().error_color), + ); + node_settings_error = Some("Rusty Kaspa Daemon not found"); + } }, _ => { } } @@ -298,15 +295,12 @@ impl Settings { }); - if !self.grpc_network_interface.is_valid() { node_settings_error = Some(i18n("Invalid gRPC network interface configuration")); } else { self.settings.node.grpc_network_interface = self.grpc_network_interface.as_ref().try_into().unwrap(); //NetworkInterfaceConfig::try_from(&self.grpc_network_interface).unwrap(); } - // ui.add_space(4.); - if self.settings.node.node_kind == KaspadNodeKind::Remote { node_settings_error = Self::render_remote_settings(core, ui, &mut self.settings.node); } @@ -395,11 +389,8 @@ impl Settings { ui.separator(); } } - - } - fn render_settings( &mut self, core: &mut Core, @@ -434,6 +425,9 @@ impl Settings { }); }); + #[cfg(not(target_arch = "wasm32"))] + core.storage.clone().render_settings(core, ui); + CollapsingHeader::new(i18n("Advanced")) .default_open(false) .show(ui, |ui| { @@ -451,45 +445,43 @@ impl Settings { if self.settings.developer.enable { ui.indent("developer_mode_settings", |ui | { - // ui.vertical(|ui|{ - #[cfg(not(target_arch = "wasm32"))] - ui.checkbox( - &mut self.settings.developer.enable_experimental_features, - i18n("Enable experimental features") - ).on_hover_text_at_pointer( - i18n("Enables features currently in development") - ); - - #[cfg(not(target_arch = "wasm32"))] - ui.checkbox( - &mut self.settings.developer.enable_custom_daemon_args, - i18n("Enable custom daemon arguments") - ).on_hover_text_at_pointer( - i18n("Allow custom arguments for the Rusty Kaspa daemon") - ); - - ui.checkbox( - &mut self.settings.developer.disable_password_restrictions, - i18n("Disable password score restrictions") - ).on_hover_text_at_pointer( - i18n("Removes security restrictions, allows for single-letter passwords") - ); - - ui.checkbox( - &mut self.settings.developer.market_monitor_on_testnet, - i18n("Show balances in alternate currencies for testnet coins") - ).on_hover_text_at_pointer( - i18n("Shows balances in alternate currencies (BTC, USD) when using testnet coins as if you are on mainnet") - ); + #[cfg(not(target_arch = "wasm32"))] + ui.checkbox( + &mut self.settings.developer.enable_experimental_features, + i18n("Enable experimental features") + ).on_hover_text_at_pointer( + i18n("Enables features currently in development") + ); + + #[cfg(not(target_arch = "wasm32"))] + ui.checkbox( + &mut self.settings.developer.enable_custom_daemon_args, + i18n("Enable custom daemon arguments") + ).on_hover_text_at_pointer( + i18n("Allow custom arguments for the Rusty Kaspa daemon") + ); + + ui.checkbox( + &mut self.settings.developer.disable_password_restrictions, + i18n("Disable password score restrictions") + ).on_hover_text_at_pointer( + i18n("Removes security restrictions, allows for single-letter passwords") + ); + + ui.checkbox( + &mut self.settings.developer.market_monitor_on_testnet, + i18n("Show balances in alternate currencies for testnet coins") + ).on_hover_text_at_pointer( + i18n("Shows balances in alternate currencies (BTC, USD) when using testnet coins as if you are on mainnet") + ); - #[cfg(not(target_arch = "wasm32"))] - ui.checkbox( - &mut self.settings.developer.enable_screen_capture, - i18n("Enable screen capture") - ).on_hover_text_at_pointer( - i18n("Allows you to take screenshots from within the application") - ); - // }); + #[cfg(not(target_arch = "wasm32"))] + ui.checkbox( + &mut self.settings.developer.enable_screen_capture, + i18n("Enable screen capture") + ).on_hover_text_at_pointer( + i18n("Allows you to take screenshots from within the application") + ); }); } @@ -542,21 +534,7 @@ impl Settings { } ui.separator(); } - - - }); - - // if ui.button("Test Toast").clicked() { - // self.runtime.try_send(Events::Notify { - // notification : UserNotification::info("Test Toast") - // }).unwrap(); - // } - // ui.add_space(32.); - // if ui.button("Test Panic").clicked() { - // panic!("Testing panic..."); - // } - } } diff --git a/core/src/runtime/mod.rs b/core/src/runtime/mod.rs index 23bc932..6bbd039 100644 --- a/core/src/runtime/mod.rs +++ b/core/src/runtime/mod.rs @@ -214,6 +214,15 @@ impl Runtime { Ok(()) } + /// Update storage size + pub fn update_storage(&self, options: StorageUpdateOptions) { + self.inner + .application_events + .sender + .try_send(Events::UpdateStorage(options)) + .ok(); + } + pub fn notify(&self, user_notification: UserNotification) { self.inner .application_events @@ -222,6 +231,16 @@ impl Runtime { .ok(); } + pub fn error(&self, text: impl Into) { + self.inner + .application_events + .sender + .try_send(Events::Notify { + user_notification: UserNotification::error(text), + }) + .ok(); + } + pub fn toast(&self, user_notification: UserNotification) { self.inner .application_events diff --git a/core/src/runtime/services/kaspa/mod.rs b/core/src/runtime/services/kaspa/mod.rs index decef0a..0fb069d 100644 --- a/core/src/runtime/services/kaspa/mod.rs +++ b/core/src/runtime/services/kaspa/mod.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use crate::imports::*; use crate::runtime::Service; pub use futures::{future::FutureExt, select, Future}; @@ -363,6 +361,17 @@ impl KaspaService { Ok(()) } + + #[cfg(not(target_arch = "wasm32"))] + fn update_storage(&self) { + const STORAGE_UPDATE_DELAY: Duration = Duration::from_millis(3000); + + let options = StorageUpdateOptions::default() + .if_not_present() + .with_delay(STORAGE_UPDATE_DELAY); + + runtime().update_storage(options); + } } #[async_trait] @@ -372,8 +381,7 @@ impl Service for KaspaService { } async fn spawn(self: Arc) -> Result<()> { - let this = self.clone(); - let wallet_events = this.wallet.multiplexer().channel(); + let wallet_events = self.wallet.multiplexer().channel(); let _application_events_sender = self.application_events.sender.clone(); loop { @@ -387,22 +395,22 @@ impl Service for KaspaService { CoreWallet::DAAScoreChange{ .. } => { } CoreWallet::Connect { .. } => { - this.connect_all_services().await?; + self.connect_all_services().await?; } CoreWallet::Disconnect { .. } => { - this.disconnect_all_services().await?; + self.disconnect_all_services().await?; } _ => { // println!("wallet event: {:?}", event); } } - this.application_events.sender.send(crate::events::Events::Wallet{event}).await.unwrap(); + self.application_events.sender.send(crate::events::Events::Wallet{event}).await.unwrap(); } else { break; } } - msg = this.as_ref().service_events.receiver.recv().fuse() => { + msg = self.as_ref().service_events.receiver.recv().fuse() => { if let Ok(event) = msg { @@ -411,20 +419,20 @@ impl Service for KaspaService { #[cfg(not(target_arch = "wasm32"))] KaspadServiceEvents::Stdout { line } => { - if !this.wallet().utxo_processor().is_synced() { - this.wallet().utxo_processor().sync_proc().handle_stdout(&line).await?; + if !self.wallet().utxo_processor().is_synced() { + self.wallet().utxo_processor().sync_proc().handle_stdout(&line).await?; } - this.update_logs(line).await; + self.update_logs(line).await; } #[cfg(not(target_arch = "wasm32"))] KaspadServiceEvents::StartInternalInProc { config, network } => { - this.stop_all_services().await?; + self.stop_all_services().await?; let kaspad = Arc::new(inproc::InProc::default()); - this.retain(kaspad.clone()); + self.retain(kaspad.clone()); // this.kaspad.lock().unwrap().replace(kaspad.clone()); kaspad.clone().start(config).await.unwrap(); @@ -433,16 +441,17 @@ impl Service for KaspaService { let rpc_ctl = RpcCtl::new(); let rpc = Rpc::new(rpc_api, rpc_ctl.clone()); - this.start_all_services(rpc, network).await?; - this.connect_rpc_client().await?; + self.start_all_services(rpc, network).await?; + self.connect_rpc_client().await?; + self.update_storage(); }, #[cfg(not(target_arch = "wasm32"))] KaspadServiceEvents::StartInternalAsDaemon { config, network } => { self.stop_all_services().await?; - let kaspad = Arc::new(daemon::Daemon::new(None, &this.service_events)); - this.retain(kaspad.clone()); + let kaspad = Arc::new(daemon::Daemon::new(None, &self.service_events)); + self.retain(kaspad.clone()); kaspad.clone().start(config).await.unwrap(); let rpc_config = RpcConfig::Wrpc { @@ -451,15 +460,17 @@ impl Service for KaspaService { }; let rpc = Self::create_rpc_client(&rpc_config, network).expect("Kaspad Service - unable to create wRPC client"); - this.start_all_services(rpc, network).await?; - this.connect_rpc_client().await?; + self.start_all_services(rpc, network).await?; + self.connect_rpc_client().await?; + + self.update_storage(); }, #[cfg(not(target_arch = "wasm32"))] KaspadServiceEvents::StartExternalAsDaemon { path, config, network } => { self.stop_all_services().await?; - let kaspad = Arc::new(daemon::Daemon::new(Some(path), &this.service_events)); - this.retain(kaspad.clone()); + let kaspad = Arc::new(daemon::Daemon::new(Some(path), &self.service_events)); + self.retain(kaspad.clone()); kaspad.clone().start(config).await.unwrap(); @@ -469,15 +480,17 @@ impl Service for KaspaService { }; let rpc = Self::create_rpc_client(&rpc_config, network).expect("Kaspad Service - unable to create wRPC client"); - this.start_all_services(rpc, network).await?; - this.connect_rpc_client().await?; + self.start_all_services(rpc, network).await?; + self.connect_rpc_client().await?; + + self.update_storage(); }, KaspadServiceEvents::StartRemoteConnection { rpc_config, network } => { self.stop_all_services().await?; let rpc = Self::create_rpc_client(&rpc_config, network).expect("Kaspad Service - unable to create wRPC client"); - this.start_all_services(rpc, network).await?; - this.connect_rpc_client().await?; + self.start_all_services(rpc, network).await?; + self.connect_rpc_client().await?; }, KaspadServiceEvents::Disable { network } => { @@ -505,8 +518,8 @@ impl Service for KaspaService { } } - this.stop_all_services().await?; - this.task_ctl.send(()).await.unwrap(); + self.stop_all_services().await?; + self.task_ctl.send(()).await.unwrap(); Ok(()) } diff --git a/core/src/runtime/services/market_monitor/mod.rs b/core/src/runtime/services/market_monitor/mod.rs index 5ba5c42..250a9ec 100644 --- a/core/src/runtime/services/market_monitor/mod.rs +++ b/core/src/runtime/services/market_monitor/mod.rs @@ -140,7 +140,7 @@ impl Service for MarketMonitorService { async fn spawn(self: Arc) -> Result<()> { let this = self.clone(); let _application_events_sender = self.application_events.sender.clone(); - let interval = interval(Duration::from_secs(POLLING_INTERVAL_SECONDS)); + let interval = task::interval(Duration::from_secs(POLLING_INTERVAL_SECONDS)); pin_mut!(interval); loop { diff --git a/core/src/runtime/services/peer_monitor.rs b/core/src/runtime/services/peer_monitor.rs index 7b8b32a..0c6ce4b 100644 --- a/core/src/runtime/services/peer_monitor.rs +++ b/core/src/runtime/services/peer_monitor.rs @@ -75,7 +75,7 @@ impl Service for PeerMonitorService { let this = self.clone(); let _application_events_sender = self.application_events.sender.clone(); - let interval = interval(Duration::from_secs(PEER_POLLING_INTERVAL_SECONDS)); + let interval = task::interval(Duration::from_secs(PEER_POLLING_INTERVAL_SECONDS)); pin_mut!(interval); loop { diff --git a/core/src/runtime/services/repaint_service.rs b/core/src/runtime/services/repaint_service.rs index 35c99aa..77fe726 100644 --- a/core/src/runtime/services/repaint_service.rs +++ b/core/src/runtime/services/repaint_service.rs @@ -47,7 +47,7 @@ impl Service for RepaintService { async fn spawn(self: Arc) -> Result<()> { let _application_events_sender = self.application_events.sender.clone(); - let interval = interval(Duration::from_millis(REPAINT_INTERVAL_MILLIS)); + let interval = task::interval(Duration::from_millis(REPAINT_INTERVAL_MILLIS)); pin_mut!(interval); loop { diff --git a/core/src/runtime/services/update_monitor.rs b/core/src/runtime/services/update_monitor.rs index 5e1d8b8..8bea742 100644 --- a/core/src/runtime/services/update_monitor.rs +++ b/core/src/runtime/services/update_monitor.rs @@ -50,7 +50,7 @@ impl Service for UpdateMonitorService { let this = self.clone(); let _application_events_sender = self.application_events.sender.clone(); - let interval = interval(Duration::from_secs(UPDATE_POLLING_INTERVAL_SECONDS)); + let interval = task::interval(Duration::from_secs(UPDATE_POLLING_INTERVAL_SECONDS)); pin_mut!(interval); loop { diff --git a/core/src/runtime/system.rs b/core/src/runtime/system.rs index 5402500..746ab68 100644 --- a/core/src/runtime/system.rs +++ b/core/src/runtime/system.rs @@ -63,7 +63,7 @@ cfg_if! { ui.label(format!("{} CPU cores {freq}", cpu_physical_core_count)); } ui.label(format!("{} RAM", as_data_size(self.total_memory as f64, false))); - ui.label(format!("File Descriptors: {}", self.fd_limit.separated_string())); + ui.label(format!("Handles: {}", self.fd_limit.separated_string())); }); } } diff --git a/core/src/settings.rs b/core/src/settings.rs index 530f762..f3fb264 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -283,14 +283,6 @@ impl std::fmt::Display for NodeMemoryScale { } } -const GIGABYTE: u64 = 1024 * 1024 * 1024; -const MEMORY_8GB: u64 = 8 * GIGABYTE; -const MEMORY_16GB: u64 = 16 * GIGABYTE; -const MEMORY_32GB: u64 = 32 * GIGABYTE; -const MEMORY_64GB: u64 = 64 * GIGABYTE; -const MEMORY_96GB: u64 = 96 * GIGABYTE; -const MEMORY_128GB: u64 = 128 * GIGABYTE; - impl NodeMemoryScale { pub fn describe(&self) -> &str { match self { @@ -303,6 +295,15 @@ impl NodeMemoryScale { pub fn get(&self) -> f64 { cfg_if! { if #[cfg(not(target_arch = "wasm32"))] { + + const GIGABYTE: u64 = 1024 * 1024 * 1024; + const MEMORY_8GB: u64 = 8 * GIGABYTE; + const MEMORY_16GB: u64 = 16 * GIGABYTE; + const MEMORY_32GB: u64 = 32 * GIGABYTE; + const MEMORY_64GB: u64 = 64 * GIGABYTE; + const MEMORY_96GB: u64 = 96 * GIGABYTE; + const MEMORY_128GB: u64 = 128 * GIGABYTE; + let total_memory = runtime().system().as_ref().map(|system|system.total_memory).unwrap_or(MEMORY_16GB); let target_memory = if total_memory <= MEMORY_8GB { diff --git a/core/src/status.rs b/core/src/status.rs index dd999cc..65ba6cd 100644 --- a/core/src/status.rs +++ b/core/src/status.rs @@ -103,7 +103,12 @@ impl<'core> Status<'core> { if let Some(peers) = peers { if peers != 0 { - ui.label(format!("{peers} {}", i18n("peers"))); + let text = if peers > 1 { + i18n("peers") + } else { + i18n("peer") + }; + ui.label(format!("{peers} {text}")); } else { ui.label( RichText::new(egui_phosphor::light::CLOUD_SLASH) diff --git a/core/src/storage.rs b/core/src/storage.rs new file mode 100644 index 0000000..d82ef09 --- /dev/null +++ b/core/src/storage.rs @@ -0,0 +1,251 @@ +use crate::imports::*; + +#[derive(PartialEq, Eq)] +pub struct StorageFolder { + pub path: PathBuf, + pub network: Network, + pub name: String, + pub folder_size: u64, + pub folder_size_string: String, + pub confirm_deletion: bool, +} + +impl Ord for StorageFolder { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for StorageFolder { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Default, Clone)] +pub struct StorageUpdateOptions { + pub update_if_not_present: bool, + pub network: Option, + pub delay: Option, +} + +impl StorageUpdateOptions { + pub fn if_not_present(mut self) -> Self { + self.update_if_not_present = true; + self + } + + pub fn with_network(mut self, network: Network) -> Self { + self.network = Some(network); + self + } + + pub fn with_delay(mut self, delay: Duration) -> Self { + self.delay = Some(delay); + self + } +} + +#[derive(Default, Clone)] +pub struct Storage { + pub folders: Arc>>, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Storage { + pub fn update(&self, options: Option) { + let options = options.unwrap_or_default(); + + if options.update_if_not_present { + if let Some(network) = options.network { + if self.has_network(network) { + return; + } + } + } + + let this = self.clone(); + spawn(async move { + if let Some(delay) = options.delay { + task::sleep(delay).await; + } + + let rusty_kaspa_app_dir = kaspad_lib::daemon::get_app_dir(); + let paths = std::fs::read_dir(rusty_kaspa_app_dir).unwrap(); + for path in paths { + let path = path?.path(); + if std::fs::metadata(&path)?.is_dir() { + if let Some(folder) = path.clone().file_name().and_then(|path| path.to_str()) { + if !folder.starts_with('.') { + if let Some(network) = folder + .strip_prefix("kaspa-") + .and_then(|folder| folder.parse::().ok()) + { + let mut folder_size = 0; + for entry in walkdir::WalkDir::new(&path).into_iter().flatten() { + folder_size += entry + .metadata() + .map(|metadata| metadata.len()) + .unwrap_or_default(); + } + + this.update_folder_size(network, folder_size, path); + } + } + } + } + } + + runtime().request_repaint(); + + Ok(()) + }); + } + + fn update_folder_size(&self, network: Network, folder_size: u64, path: PathBuf) { + use kaspa_metrics_core::data::as_data_size; + let folder_size_string = as_data_size(folder_size as f64, true); + + let mut folders = self.folders.lock().unwrap(); + if let Some(folder) = folders.iter_mut().find(|folder| folder.network == network) { + folder.folder_size = folder_size; + folder.folder_size_string = folder_size_string; + } else { + folders.push(StorageFolder { + name: network.to_string().to_uppercase(), + path, + network, + folder_size, + folder_size_string, + confirm_deletion: false, + }); + + folders.sort(); + } + } + + pub fn has_network(&self, network: Network) -> bool { + self.folders + .lock() + .unwrap() + .iter() + .any(|folder| folder.network == network) + } + + pub fn folder(&self, network: Network) -> Option { + self.folders + .lock() + .unwrap() + .iter() + .find(|folder| folder.network == network) + .map(|folder| folder.path.clone()) + } + + pub fn remove(&self, network: Network) { + let this = self.clone(); + spawn(async move { + if let Some(path) = this.folder(network) { + if path.exists() { + println!("Removing storage folder: {:?}", path.display()); + if let Err(e) = std::fs::remove_dir_all(&path) { + println!("Error removing storage folder: {:?}", e); + runtime().error(format!("Error removing storage folder: {:?}", e)); + } + println!("Storage folder removed: {:?}", path.display()); + this.update(None); + } else { + runtime().error(format!("Folder not found: {}", path.display())); + } + } + Ok(()) + }); + } + + pub fn render(&self, ui: &mut Ui) { + let folders = self.folders.lock().unwrap(); + if !folders.is_empty() { + ui.vertical_centered(|ui| { + CollapsingHeader::new(i18n("Storage")) + .default_open(true) + .show(ui, |ui| { + ui.vertical(|ui| { + for folder in folders.iter() { + let StorageFolder { + network, + folder_size_string, + .. + } = folder; + ui.label(format!( + "{}: {folder_size_string}", + network.to_string().to_uppercase() + )); + } + }); + }); + }); + } + } + + pub fn clear_settings(&self) { + let mut folders = self.folders.lock().unwrap(); + for folder in folders.iter_mut() { + folder.confirm_deletion = false; + } + } + + pub fn render_settings(&self, core: &mut Core, ui: &mut Ui) { + let mut folders = self.folders.lock().unwrap(); + if !folders.is_empty() { + ui.vertical_centered(|ui| { + CollapsingHeader::new(i18n("Storage")) + .default_open(false) + .show(ui, |ui| { + ui.vertical(|ui| { + for folder in folders.iter_mut() { + let StorageFolder { network, folder_size_string, path, confirm_deletion, .. } = folder; + + CollapsingHeader::new(format!("{}: {folder_size_string}", network.to_string().to_uppercase())) + .default_open(false) + .show(ui, |ui| { + let is_running = core.settings.node.network == *network && core.settings.node.node_kind.is_local(); + + ui.horizontal(|ui|{ + if ui.medium_button(i18n("Open Folder")).clicked() { + if let Err(err) = open::that(path) { + runtime().error(format!("Error opening folder: {:?}", err)); + } + } + if ui.medium_button_enabled(!is_running && !*confirm_deletion, i18n("Delete Database & Logs")).clicked() { + *confirm_deletion = true; + } + }); + + if is_running { + ui.label(i18n("Cannot delete data folder while the node is running")); + ui.label(i18n("Please set node to 'Disabled' to delete the data folder")); + } + + if *confirm_deletion { + ui.add_sized(vec2(260.,4.), Separator::default()); + ui.colored_label(theme_color().alert_color, i18n("Please Confirm Deletion")); + if let Some(response) = ui.confirm_medium_apply_cancel(Align::Min) { + match response { + Confirm::Ack => { + *confirm_deletion = false; + self.remove(*network); + }, + Confirm::Nack => { + *confirm_deletion = false; + } + } + } + ui.add_sized(vec2(260.,4.), Separator::default()); + } + }); + } + }); + }); + }); + } + } +} diff --git a/core/src/sync.rs b/core/src/sync.rs index 987765e..947f089 100644 --- a/core/src/sync.rs +++ b/core/src/sync.rs @@ -20,27 +20,39 @@ impl SyncStatus { if level == 0 { SyncStatus { stage: Some(1), - caption: "Syncing Proof...".to_string(), + caption: i18n("Syncing Cryptographic Proof...").to_string(), ..Default::default() } } else { SyncStatus { stage: Some(1), - caption: format!("Syncing Proof {}", level.separated_string()), + caption: format!( + "{} {}", + i18n("Syncing Cryptographic Proof..."), + level.separated_string() + ), ..Default::default() } } } SyncState::Headers { headers, progress } => SyncStatus { stage: Some(2), - caption: format!("Syncing Headers... {}", headers.separated_string()), + caption: format!( + "{} {}", + i18n("Syncing Headers..."), + headers.separated_string() + ), progress_bar_percentage: Some(progress as f32 / 100_f32), progress_bar_text: Some(format!("{}%", progress)), ..Default::default() }, SyncState::Blocks { blocks, progress } => SyncStatus { stage: Some(3), - caption: format!("Syncing DAG Blocks... {}", blocks.separated_string()), + caption: format!( + "{} {}", + i18n("Syncing DAG Blocks..."), + blocks.separated_string() + ), // caption: "Syncing DAG Blocks...".to_string(), progress_bar_percentage: Some(progress as f32 / 100_f32), progress_bar_text: Some(format!("{}%", progress)), @@ -51,7 +63,11 @@ impl SyncStatus { SyncStatus { stage: Some(4), - caption: format!("Syncing DAG Trust... {}", processed.separated_string()), + caption: format!( + "{} {}", + i18n("Syncing DAG Trust..."), + processed.separated_string() + ), // caption: "Syncing DAG Trust...".to_string(), progress_bar_percentage: Some(progress as f32 / 100_f32), progress_bar_text: Some(format!("{}%", progress)), @@ -61,21 +77,25 @@ impl SyncStatus { } SyncState::UtxoSync { total, .. } => SyncStatus { stage: Some(5), - caption: format!("Syncing UTXO entries... {}", total.separated_string()), + caption: format!( + "{} {}", + i18n("Syncing UTXO entries..."), + total.separated_string() + ), // caption: "Syncing UTXO entries...".to_string(), // progress_bar_text: Some(total.separated_string()), ..Default::default() }, SyncState::UtxoResync => SyncStatus { - caption: "Syncing...".to_string(), + caption: i18n("Syncing...").to_string(), ..Default::default() }, SyncState::NotSynced => SyncStatus { - caption: "Syncing...".to_string(), + caption: i18n("Syncing...").to_string(), ..Default::default() }, SyncState::Synced { .. } => SyncStatus { - caption: "Ready...".to_string(), + caption: i18n("Ready...").to_string(), synced: true, ..Default::default() }, @@ -102,7 +122,11 @@ impl SyncStatus { pub fn render_text_state(&self, ui: &mut egui::Ui) { if let Some(stage) = self.stage { - ui.label(format!("Stage {stage} of {SYNC_STAGES}")); + ui.label(format!( + "{} {stage} {} {SYNC_STAGES}", + i18n("Stage"), + i18n("of") + )); ui.separator(); } ui.label(self.caption.as_str()); diff --git a/resources/i18n/i18n.json b/resources/i18n/i18n.json index 9fb8057..e3fa56c 100644 --- a/resources/i18n/i18n.json +++ b/resources/i18n/i18n.json @@ -14,13 +14,13 @@ "nl": "Dutch", "vi": "Vietnamese", "fil": "Filipino", + "lt": "Lithuanian", "pa": "Panjabi", "fa": "Farsi", + "sv": "Swedish", "fi": "Finnish", "es": "EspaƱol", - "lt": "Lithuanian", "uk": "Ukrainian", - "sv": "Swedish", "af": "Afrikaans", "et": "Esti", "en": "English", @@ -62,13 +62,13 @@ "nl": {}, "vi": {}, "fil": {}, + "lt": {}, "pa": {}, "fa": {}, "sv": {}, "fi": {}, - "es": {}, - "lt": {}, "uk": {}, + "es": {}, "af": {}, "et": {}, "en": { @@ -97,6 +97,7 @@ "Track in the background": "Track in the background", "Enable custom daemon arguments": "Enable custom daemon arguments", "Local": "Local", + "Block DAG": "Block DAG", "Connections": "Connections", "Levels": "Levels", "Enter the amount": "Enter the amount", @@ -104,6 +105,7 @@ "Conservative": "Conservative", "Unlocking": "Unlocking", "Please configure your Kaspa NG settings": "Please configure your Kaspa NG settings", + "Please set node to 'Disabled' to delete the data folder": "Please set node to 'Disabled' to delete the data folder", "Please enter KAS amount to send": "Please enter KAS amount to send", "Unlock": "Unlock", "DAA Offset": "DAA Offset", @@ -149,6 +151,7 @@ "Market Monitor": "Market Monitor", "Unlock Wallet": "Unlock Wallet", "Wallet Encryption Password": "Wallet Encryption Password", + "Delete Database & Logs": "Delete Database & Logs", "Please note that this is an alpha release. Until this message is removed, avoid using this software with mainnet funds.": "Please note that this is an alpha release. Until this message is removed, avoid using this software with mainnet funds.", "You are currently not connected to the Kaspa node.": "You are currently not connected to the Kaspa node.", "Dependencies": "Dependencies", @@ -166,6 +169,7 @@ "p2p Tx/s": "p2p Tx/s", "wRPC Encoding:": "wRPC Encoding:", "License Information": "License Information", + "Syncing Headers...": "Syncing Headers...", "Storage Write": "Storage Write", "User Interface": "User Interface", "Please enter an amount": "Please enter an amount", @@ -241,12 +245,14 @@ "wRPC JSON Tx": "wRPC JSON Tx", "Virtual DAA Score": "Virtual DAA Score", "Wallet:": "Wallet:", + "Stage": "Stage", + "of": "of", "Double click on the graph to re-center...": "Double click on the graph to re-center...", "Stor Read": "Stor Read", "Database Blocks": "Database Blocks", - "Headers": "Headers", "Use all available system memory": "Use all available system memory", "Address:": "Address:", + "Headers": "Headers", "Custom": "Custom", "Mainnet (Main Kaspa network)": "Mainnet (Main Kaspa network)", "Developer mode enables advanced and experimental features": "Developer mode enables advanced and experimental features", @@ -264,6 +270,7 @@ "Type": "Type", "Filename:": "Filename:", "Please wait...": "Please wait...", + "Open Folder": "Open Folder", "DAA Range": "DAA Range", "Volume": "Volume", "You can create multiple wallets, but only one wallet can be open at a time.": "You can create multiple wallets, but only one wallet can be open at a time.", @@ -276,6 +283,7 @@ "Reset Settings": "Reset Settings", "Copied to clipboard": "Copied to clipboard", "CONNECTED": "CONNECTED", + "Syncing DAG Blocks...": "Syncing DAG Blocks...", "Testnet-10 (1 BPS)": "Testnet-10 (1 BPS)", "Storage Write/s": "Storage Write/s", "Wallet Name": "Wallet Name", @@ -284,6 +292,7 @@ "Please specify the private key type for the new wallet": "Please specify the private key type for the new wallet", "Notifications": "Notifications", "Public p2p Nodes for": "Public p2p Nodes for", + "Syncing...": "Syncing...", "Disables node connectivity (Offline Mode).": "Disables node connectivity (Offline Mode).", "Total Tx/s": "Total Tx/s", "Threshold": "Threshold", @@ -321,6 +330,7 @@ "Send": "Send", "Theme Color:": "Theme Color:", "Click to try an another server...": "Click to try an another server...", + "Please Confirm Deletion": "Please Confirm Deletion", "Theme Style": "Theme Style", "Show password": "Show password", "Balance: N/A": "Balance: N/A",