diff --git a/Cargo.toml b/Cargo.toml index d9a4724..7d86a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ kaspa-bip32 = { path = "../rusty-kaspa/wallet/bip32" } kaspa-cli = { path = "../rusty-kaspa/cli" } kaspa-consensus-core = { path = "../rusty-kaspa/consensus/core" } kaspa-core = { path = "../rusty-kaspa/core" } -kaspa-metrics = { path = "../rusty-kaspa/metrics/metrics" } +kaspa-metrics-core = { path = "../rusty-kaspa/metrics/core" } kaspa-notify = { path = "../rusty-kaspa/notify" } kaspa-rpc-core = { path = "../rusty-kaspa/rpc/core" } kaspa-rpc-service = { path = "../rusty-kaspa/rpc/service" } @@ -78,7 +78,7 @@ kaspad = { path = "../rusty-kaspa/kaspad" } # kaspa-cli = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } # kaspa-consensus-core = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } # kaspa-core = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } -# kaspa-metrics = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } +# kaspa-metrics-core = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } # kaspa-notify = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } # kaspa-rpc-core = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } # kaspa-rpc-service = { git = "https://github.com/kaspanet/rusty-kaspa.git", branch = "master" } diff --git a/app/index.html b/app/index.html index b58e05b..c0cdab7 100644 --- a/app/index.html +++ b/app/index.html @@ -126,7 +126,7 @@ - - - - - - - diff --git a/core/src/imports.rs b/core/src/imports.rs index ec2cba3..c21b62f 100644 --- a/core/src/imports.rs +++ b/core/src/imports.rs @@ -62,6 +62,7 @@ pub use egui_plot::{PlotPoint, PlotPoints}; pub use crate::collection::Collection; pub use crate::core::Core; +pub use crate::device::{Device, Orientation}; pub use crate::egui::*; pub use crate::error::Error; pub use crate::events::{ApplicationEventsChannel, Events}; @@ -75,7 +76,7 @@ pub use crate::primitives::{ DagBlock, Transaction, TransactionCollection, }; pub use crate::result::Result; -pub use crate::runtime::{runtime, spawn, spawn_with_result, Device, Payload, Runtime, Service}; +pub use crate::runtime::{runtime, spawn, spawn_with_result, Payload, Runtime, Service}; pub use crate::settings::{ KaspadNodeKind, NetworkInterfaceConfig, NetworkInterfaceKind, NodeSettings, PluginSettings, PluginSettingsMap, RpcConfig, Settings, UserInterfaceSettings, diff --git a/core/src/lib.rs b/core/src/lib.rs index 539d903..10472b3 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,11 +8,13 @@ pub use core::Core; pub mod adaptor; pub mod app; pub mod collection; +pub mod device; pub mod egui; pub mod error; pub mod events; pub mod fonts; pub mod imports; +pub mod market; pub mod menu; pub mod mobile; pub mod modules; diff --git a/core/src/market.rs b/core/src/market.rs new file mode 100644 index 0000000..e2ea251 --- /dev/null +++ b/core/src/market.rs @@ -0,0 +1,28 @@ +use crate::imports::*; + +#[derive(Default, Debug)] +pub struct MarketData { + pub price: Option, + pub market_cap: Option, + pub volume: Option, + pub change: Option, +} + +pub type MarketDataMap = AHashMap; + +#[derive(Default, Debug)] +pub struct Ohlc {} + +pub type OhlcMap = AHashMap; + +#[derive(Default, Debug)] +pub struct Market { + pub price: Option>, + pub ohlc: Option>, +} + +#[derive(Clone, Debug)] +pub enum MarketUpdate { + Price(Arc), + Ohlc(Arc), +} diff --git a/core/src/menu.rs b/core/src/menu.rs index 200e902..de4b834 100644 --- a/core/src/menu.rs +++ b/core/src/menu.rs @@ -1,3 +1,5 @@ +use egui_phosphor::thin::TRANSLATE; + use crate::imports::*; pub struct Menu<'core> { @@ -20,93 +22,31 @@ impl<'core> Menu<'core> { egui::menu::bar(ui, |ui| { ui.columns(2, |cols| { cols[0].horizontal(|ui| { - ui.menu_button("File", |ui| { - #[cfg(not(target_arch = "wasm32"))] - if ui.button("Quit").clicked() { - ui.ctx().send_viewport_cmd(ViewportCommand::Close) - } + if self.core.settings.developer.enable && self.core.debug { + self.render_debug(ui); ui.separator(); - ui.label(" ~ Debug Modules ~"); - ui.label(" "); - - let (tests, mut modules): (Vec<_>, Vec<_>) = self - .core - .modules() - .values() - .cloned() - .partition(|module| module.name().starts_with('~')); - - tests.into_iter().for_each(|module| { - if ui.button(module.name()).clicked() { - self.core.select_with_type_id(module.type_id()); - ui.close_menu(); - } - }); - - ui.label(" "); - - modules.sort_by(|a, b| a.name().partial_cmp(b.name()).unwrap()); - modules.into_iter().for_each(|module| { - if ui.button(module.name()).clicked() { - self.core.select_with_type_id(module.type_id()); - ui.close_menu(); - } - }); - }); - - ui.separator(); - if ui.button("Overview").clicked() { - self.select::(); - } - ui.separator(); - if ui.button("Wallet").clicked() { - if self.core.state().is_open() { - self.select::(); - } else { - self.select::(); - } - } - - ui.separator(); - if ui.button("Metrics").clicked() { - self.select::(); - } - - ui.separator(); - if ui.button("Block DAG").clicked() { - self.select::(); - } - - #[cfg(not(target_arch = "wasm32"))] - { - ui.separator(); - if ui.button("Node").clicked() { - self.select::(); - } - } - - ui.separator(); - - if ui.button("Settings").clicked() { - self.select::(); } - #[cfg(not(target_arch = "wasm32"))] - { + if self.core.device().single_pane() { + // ui.menu_button(format!("{} Kaspa NG", LIST), |ui| { + ui.menu_button("Kaspa NG", |ui| { + self.render_menu(ui); + }); + } else { + self.render_menu(ui); ui.separator(); - if ui.button("Logs").clicked() { - self.select::(); - } } - - ui.separator(); }); cols[1].with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let dictionary = i18n::dictionary(); - // use egui_phosphor::light::TRANSLATE; + let lang_menu = if self.core.device().orientation() == Orientation::Portrait { + RichText::new(TRANSLATE).size(18.) + } else { + RichText::new(format!("{} ⏷", dictionary.current_title())) + }; #[allow(clippy::useless_format)] - ui.menu_button(format!("{} ⏷", dictionary.current_title()), |ui| { + ui.menu_button(lang_menu, |ui| { // ui.menu_button(RichText::new(format!("{TRANSLATE} ⏷")).size(18.), |ui| { dictionary .enabled_languages() @@ -175,6 +115,7 @@ impl<'core> Menu<'core> { ui.label("Theme Style"); let current_theme_style_name = theme_style().name(); + ui.menu_button(format!("{} ⏷", current_theme_style_name), |ui| { theme_styles().keys().for_each(|name| { if name.as_str() != current_theme_style_name @@ -237,4 +178,93 @@ impl<'core> Menu<'core> { }); }); } + + pub fn render_menu(&mut self, ui: &mut Ui) { + if ui.button("Overview").clicked() { + self.select::(); + ui.close_menu(); + } + ui.separator(); + if ui.button("Wallet").clicked() { + if self.core.state().is_open() { + self.select::(); + } else { + self.select::(); + } + ui.close_menu(); + } + + ui.separator(); + if ui.button("Metrics").clicked() { + self.select::(); + ui.close_menu(); + } + + ui.separator(); + if ui.button("Block DAG").clicked() { + self.select::(); + ui.close_menu(); + } + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + if ui.button("Node").clicked() { + self.select::(); + ui.close_menu(); + } + } + + ui.separator(); + + if ui.button("Settings").clicked() { + self.select::(); + ui.close_menu(); + } + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + if ui.button("Logs").clicked() { + self.select::(); + ui.close_menu(); + } + } + } + + pub fn render_debug(&mut self, ui: &mut Ui) { + ui.menu_button("Debug", |ui| { + #[cfg(not(target_arch = "wasm32"))] + if ui.button("Quit").clicked() { + ui.ctx().send_viewport_cmd(ViewportCommand::Close) + } + ui.separator(); + ui.label(" ~ Debug Modules ~"); + ui.label(" "); + + let (tests, mut modules): (Vec<_>, Vec<_>) = self + .core + .modules() + .values() + .cloned() + .partition(|module| module.name().starts_with('~')); + + tests.into_iter().for_each(|module| { + if ui.button(module.name()).clicked() { + self.core.select_with_type_id(module.type_id()); + ui.close_menu(); + } + }); + + ui.label(" "); + + modules.sort_by(|a, b| a.name().partial_cmp(b.name()).unwrap()); + modules.into_iter().for_each(|module| { + if ui.button(module.name()).clicked() { + self.core.select_with_type_id(module.type_id()); + ui.close_menu(); + } + }); + }); + } } diff --git a/core/src/mobile.rs b/core/src/mobile.rs index 5c66d90..f230a65 100644 --- a/core/src/mobile.rs +++ b/core/src/mobile.rs @@ -43,7 +43,7 @@ impl<'core> MobileMenu<'core> { pub fn render_closed(&mut self, ui: &mut Ui) { let handlers = vec![Handler::new( - LOCK_KEY_OPEN, + FINGERPRINT, "OPEN", Box::new(|core, _ui| { core.select::(); @@ -98,7 +98,9 @@ impl<'core> MobileMenu<'core> { HOUSE_SIMPLE, "HOME", Box::new(|core, _ui| { - core.get_mut::().select(None); + let device = core.device().clone(); + core.get_mut::() + .select(None, device); core.select::(); }), ), diff --git a/core/src/modules/account_create.rs b/core/src/modules/account_create.rs index bcbaa87..19d2911 100644 --- a/core/src/modules/account_create.rs +++ b/core/src/modules/account_create.rs @@ -239,7 +239,7 @@ impl ModuleT for AccountCreate { &mut this.focus, Focus::WalletSecret, |ui, text| { - ui.label(egui::RichText::new("Enter your wallet secret").size(12.).raised()); + ui.label(RichText::new("Enter your wallet secret").size(12.).raised()); ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) @@ -252,9 +252,8 @@ impl ModuleT for AccountCreate { .build(ui); }) .with_footer(|this,ui| { - let size = theme_style().large_button_size; let enabled = !this.context.wallet_secret.is_empty(); - if ui.add_enabled(enabled, egui::Button::new("Continue").min_size(size)).clicked() { + if ui.large_button_enabled(enabled,"Continue").clicked() { *submit.borrow_mut() = true; } }) @@ -287,7 +286,7 @@ impl ModuleT for AccountCreate { &mut this.focus, Focus::PaymentSecret, |ui, text| { - ui.label(egui::RichText::new("Enter your BIP39 passphrase").size(12.).raised()); + ui.label(RichText::new("Enter your BIP39 passphrase").size(12.).raised()); ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) @@ -401,8 +400,8 @@ impl ModuleT for AccountCreate { .with_header(move |this,ui| { ui.label(" "); ui.label(" "); - ui.label(egui::RichText::new("Error creating account").color(egui::Color32::from_rgb(255, 120, 120))); - ui.label(egui::RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new("Error creating account").color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); if ui.add_sized(theme_style().panel_editor_size, egui::Button::new("Restart")).clicked() { this.state = State::Start; @@ -440,7 +439,7 @@ impl ModuleT for AccountCreate { // ui.columns(6, |cols| { // for col in 0..chunk.len() { - // cols[col].label(egui::RichText::new(chunk[col]).family(FontFamily::Monospace).size(14.).color(egui::Color32::WHITE)); + // cols[col].label(RichText::new(chunk[col]).family(FontFamily::Monospace).size(14.).color(egui::Color32::WHITE)); // } // }) // }); diff --git a/core/src/modules/account_manager/address.rs b/core/src/modules/account_manager/address.rs new file mode 100644 index 0000000..089fc63 --- /dev/null +++ b/core/src/modules/account_manager/address.rs @@ -0,0 +1,30 @@ +use crate::imports::*; +use super::*; + +pub struct AddressPane<'context> { + #[allow(dead_code)] + context : &'context ManagerContext, +} + +impl<'context> AddressPane<'context> { + pub fn new(context : &'context ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, _core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + use egui_phosphor::light::CLIPBOARD_TEXT; + let address = format_address(rc.context.address(), Some(8)); + if ui.add(Label::new(format!("Address: {address} {CLIPBOARD_TEXT}")).sense(Sense::click())) + // .on_hover_ui_at_pointer(|ui|{ + // ui.vertical(|ui|{ + // ui.add(Label::new(format!("{}", context.address().to_string()))); + // ui.add_space(16.); + // ui.label("Click to copy address to clipboard".to_string()); + // }); + // }) + .clicked() { + ui.output_mut(|o| o.copied_text = rc.context.address().to_string()); + runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()) + } + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/balance.rs b/core/src/modules/account_manager/balance.rs new file mode 100644 index 0000000..ed32a58 --- /dev/null +++ b/core/src/modules/account_manager/balance.rs @@ -0,0 +1,88 @@ +use crate::imports::*; +use super::*; + +pub struct BalancePane<'context> { + context : &'context ManagerContext, +} + +impl<'context> BalancePane<'context> { + + pub fn new(context : &'context ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + + + // let theme = theme(); + let RenderContext { account, network_type, .. } = rc; + + ui.add_space(10.); + + if let Some(balance) = account.balance() { + + if !core.state().is_synced() { + ui.label( + s2kws_layout_job(balance.mature, network_type, theme_color().balance_syncing_color,FontId::proportional(28.)) + ); + ui.label(RichText::new(i18n("The balance may be out of date during node sync")).size(12.).color(theme_color().balance_syncing_color)); + return; + } else { + ui.label( + s2kws_layout_job(balance.mature, network_type, theme_color().balance_color,FontId::proportional(28.)) + ); + } + + if let Some(price_list) = core.market.price.as_ref() { + for (symbol, data) in price_list.iter() { + if let Some(price) = data.price { + let text = format!("{:.8} {}", sompi_to_kaspa(balance.mature) * price, symbol.to_uppercase()); + ui.label(RichText::new(text).font(FontId::proportional(16.))); + } + } + } + + if balance.pending != 0 { + ui.label(format!( + "Pending: {}", + sompi_to_kaspa_string_with_suffix( + balance.pending, + network_type + ) + )); + } + if balance.outgoing != 0 { + ui.label(format!( + "Sending: {}", + sompi_to_kaspa_string_with_suffix( + balance.outgoing, + network_type + ) + )); + } + + ui.add_space(10.); + + let suffix = if balance.pending_utxo_count != 0 && balance.stasis_utxo_count != 0 { + format!(" ({} pending, {} processing)", balance.pending_utxo_count, balance.stasis_utxo_count) + } else if balance.pending_utxo_count != 0 { + format!(" ({} pending)", balance.pending_utxo_count) + } else if balance.stasis_utxo_count != 0 { + format!(" ({} processing)", balance.stasis_utxo_count) + } else { + "".to_string() + }; + + if self.context.transaction_kind.is_none() { + ui.label(format!( + "UTXOs: {}{suffix}", + balance.mature_utxo_count.separated_string(), + )); + } + } else { + ui.label("Balance: N/A"); + } + + + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/destination.rs b/core/src/modules/account_manager/destination.rs new file mode 100644 index 0000000..08cdc2d --- /dev/null +++ b/core/src/modules/account_manager/destination.rs @@ -0,0 +1,62 @@ +use crate::imports::*; +use super::*; + +pub struct Destination<'context> { + context : &'context mut ManagerContext, +} + +impl<'context> Destination<'context> { + pub fn new(context : &'context mut ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, _core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + let RenderContext { network_type, .. } = rc; + + TextEditor::new( + &mut self.context.destination_address_string, + // None, + &mut self.context.focus, + Focus::Address, + |ui, text| { + ui.add_space(8.); + ui.label(RichText::new("Enter destination address").size(12.).raised()); + ui.add_sized(Overview::editor_size(ui), TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|address| { + match Address::try_from(address) { + Ok(address) => { + let address_network_type = NetworkType::try_from(address.prefix).expect("prefix to network type"); + if address_network_type != *network_type { + self.context.address_status = AddressStatus::NetworkMismatch(address_network_type); + } else { + self.context.address_status = AddressStatus::Valid; + } + } + Err(err) => { + self.context.address_status = AddressStatus::Invalid(err.to_string()); + } + } + }) + .submit(|_, focus|{ + // *focus = Some(Focus::Amount); + focus.next(Focus::Amount); + }) + .build(ui); + + match &self.context.address_status { + AddressStatus::Valid => {}, + AddressStatus::None => {}, + AddressStatus::NetworkMismatch(address_network_type) => { + ui.label(format!("This address if for the different\nnetwork ({address_network_type})")); + }, + AddressStatus::Invalid(err) => { + ui.label(format!("Please enter a valid address\n{err}")); + } + } + + + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/estimation.rs b/core/src/modules/account_manager/estimation.rs deleted file mode 100644 index e69de29..0000000 diff --git a/core/src/modules/account_manager/estimator.rs b/core/src/modules/account_manager/estimator.rs new file mode 100644 index 0000000..34261b6 --- /dev/null +++ b/core/src/modules/account_manager/estimator.rs @@ -0,0 +1,180 @@ +use crate::imports::*; +use super::*; + +pub struct Estimator<'context> { + context: &'context mut ManagerContext +} + +impl<'context> Estimator<'context> { + pub fn new(context: &'context mut ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, core : &mut Core, ui: &mut Ui, rc : &RenderContext<'_>) -> bool { + + + use egui_phosphor::light::{CHECK, X}; + + let RenderContext { network_type, .. } = rc; + + let mut request_estimate = self.context.request_estimate.take().unwrap_or_default(); + + match self.context.transaction_kind.as_ref().unwrap() { + TransactionKind::Send => { + Destination::new(self.context).render(core, ui, rc); + // self.render_address_input(core, ui, rc); + } + TransactionKind::Transfer => { + Transfer::new(self.context).render(core, ui, rc); + // self.render_transfer_account_selector(core, ui, rc); + } + } + + let response = TextEditor::new( + &mut self.context.send_amount_text, + &mut self.context.focus, + 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.add_sized(Overview::editor_size(ui), TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|_| { + request_estimate = true; + }) + .build(ui); + + if response.text_edit_submit(ui) { + if self.context.enable_priority_fees { + self.context.focus.next(Focus::Fees); + } else if self.update_user_args() { + self.context.action = Action::Sending; + self.context.focus.next(Focus::WalletSecret); + } + } + + ui.add_space(8.); + if ui + .checkbox(&mut self.context.enable_priority_fees,i18n("Include Priority Fees")) + // .on_hover_text_at_pointer(i18n("Add priority fees to ensure faster confirmation.\nUseful only if the network is congested.")) + .changed() { + if self.context.enable_priority_fees { + self.context.focus.next(Focus::Fees); + } else { + self.context.focus.next(Focus::Amount); + } + } + + if self.context.enable_priority_fees { + TextEditor::new( + &mut self.context.priority_fees_text, + &mut self.context.focus, + Focus::Fees, + |ui, text| { + ui.add_space(8.); + ui.label(RichText::new("Enter priority fees").size(12.).raised()); + ui.add_sized(Overview::editor_size(ui), TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|_| { + request_estimate = true; + }) + .submit(|_,_|{ + self.context.action = Action::Sending; + }) + .build(ui); + } + + ui.add_space(8.); + let ready_to_send = match &*self.context.estimate.lock().unwrap() { + EstimatorStatus::GeneratorSummary(estimate) => { + if let Some(final_transaction_amount) = estimate.final_transaction_amount { + ui.label(format!("Final Amount: {}", sompi_to_kaspa_string_with_suffix(final_transaction_amount + estimate.aggregated_fees, network_type))); + } + let fee_title = if self.context.priority_fees_sompi != 0 { + "Network and Priority Fees:" + } else { + "Network Fees:" + }; + ui.label(format!("{} {}", fee_title, sompi_to_kaspa_string_with_suffix(estimate.aggregated_fees, network_type))); + ui.label(format!("Transactions: {} UTXOs: {}", estimate.number_of_generated_transactions, estimate.aggregated_utxos)); + + self.context.address_status == AddressStatus::Valid || (self.context.transaction_kind == Some(TransactionKind::Transfer) && self.context.transfer_to_account.is_some()) + } + EstimatorStatus::Error(error) => { + ui.label(RichText::new(error.to_string()).color(theme_color().error_color)); + false + } + EstimatorStatus::None => { + ui.label("Please enter KAS amount to send"); + false + } + }; + ui.add_space(8.); + + ui.horizontal(|ui| { + ui.vertical_centered(|ui|{ + ui.horizontal(|ui| { + CenterLayoutBuilder::new() + .add_enabled(ready_to_send, Button::new(format!("{CHECK} Send")).min_size(theme_style().medium_button_size()), |this: &mut Estimator<'_>| { + this.context.action = Action::Sending; + this.context.focus.next(Focus::WalletSecret); + }) + .add(Button::new(format!("{X} Cancel")).min_size(theme_style().medium_button_size()), |this| { + this.context.reset_send_state(); + }) + .build(ui, self) + }); + }); + + }); + + self.update_user_args() + && request_estimate + && matches!(self.context.action,Action::Estimating) + + } + + + + fn update_user_args(&mut self) -> bool { + let mut valid = true; + + match try_kaspa_str_to_sompi(self.context.send_amount_text.as_str()) { + Ok(Some(sompi)) => { + self.context.send_amount_sompi = sompi; + } + Ok(None) => { + self.user_error("Please enter an amount".to_string()); + valid = false; + } + Err(err) => { + self.user_error(format!("Invalid amount: {err}")); + valid = false; + } + } + + match try_kaspa_str_to_sompi(self.context.priority_fees_text.as_str()) { + Ok(Some(sompi)) => { + self.context.priority_fees_sompi = sompi; + } + Ok(None) => { + self.context.priority_fees_sompi = 0; + } + Err(err) => { + self.user_error(format!("Invalid fee amount: {err}")); + valid = false; + } + } + + valid + } + + fn user_error(&self, error : impl Into) { + *self.context.estimate.lock().unwrap() = EstimatorStatus::Error(error.into()); + } + +} \ No newline at end of file diff --git a/core/src/modules/account_manager/mod.rs b/core/src/modules/account_manager/mod.rs index ff627a6..92f6524 100644 --- a/core/src/modules/account_manager/mod.rs +++ b/core/src/modules/account_manager/mod.rs @@ -6,25 +6,35 @@ use kaspa_wallet_core::tx::{GeneratorSummary, PaymentOutput, Fees}; use kaspa_wallet_core::api::*; use crate::primitives::descriptors::*; -mod overview; -mod transactions; +mod address; +mod balance; +mod destination; mod details; -mod utxo; +mod estimator; mod menus; -mod transfer; -mod send; -mod estimation; +mod network; +mod overview; +mod processor; +mod qr; mod secret; +mod transactions; +mod transfer; +mod utxo; -use overview::*; -use transactions::*; +use address::*; +use balance::*; +use destination::*; use details::*; -use utxo::*; +use estimator::*; use menus::*; -use transfer::*; -use send::*; -use estimation::*; +use network::*; +use overview::*; +use processor::*; +use qr::*; use secret::*; +use transactions::*; +use transfer::*; +use utxo::*; #[allow(dead_code)] @@ -232,13 +242,13 @@ impl AccountManager { self.context.request_estimate = Some(true); } - pub fn select(&mut self, account: Option) { + pub fn select(&mut self, account: Option, device : Device) { if let Some(account) = account { self.state = AccountManagerState::Overview { account: account.clone(), }; - if runtime().device().is_portrait() { + if device.orientation() == Orientation::Portrait { self.section = AccountManagerSection::Overview; } else { self.section = AccountManagerSection::Transactions; @@ -279,7 +289,7 @@ impl AccountManager { ui.label("Please create an account"); }).render(ui); } else if account_collection.len() == 1 { - self.select(Some(account_collection.first().unwrap().clone())); + self.select(Some(account_collection.first().unwrap().clone()), core.device().clone()); } else { Panel::new(self) .with_caption("Select Account") @@ -307,8 +317,8 @@ impl AccountManager { account_collection.iter().for_each(|account_select| { if ui.account_selector_button(account_select, &network_type, false).clicked() { - this.select(Some(account_select.clone())); - if runtime().device().is_single_pane() { + this.select(Some(account_select.clone()), core.device().clone()); + if core.device().single_pane() { this.section = AccountManagerSection::Overview; } else { this.section = AccountManagerSection::Transactions; @@ -325,7 +335,7 @@ impl AccountManager { AccountManagerState::Overview { account } => { let rc = RenderContext::new(&account, network_type, current_daa_score)?; - if runtime().device().is_single_pane() { + if core.device().single_pane() { self.render_singular_layout(core,ui,&rc, self.section); } else { if self.section == AccountManagerSection::Overview { @@ -348,6 +358,21 @@ impl AccountManager { AccountMenu::new().render(core,ui,self,rc, screen_rect_height * 0.8); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ToolsMenu::new().render(core,ui,self, rc, screen_rect_height * 0.8); + + ui.separator(); + + if ui.add(Label::new("UTXOs").sense(Sense::click())).clicked() { + self.section = AccountManagerSection::UtxoManager; + } + ui.separator(); + if ui.add(Label::new("Details").sense(Sense::click())).clicked() { + self.section = AccountManagerSection::Details; + } + ui.separator(); + if ui.add(Label::new("Transactions").sense(Sense::click())).clicked() { + self.section = AccountManagerSection::Transactions; + } + }); }); } @@ -377,26 +402,6 @@ impl AccountManager { ui.style_mut().text_styles = core.default_style.text_styles.clone(); // --- - egui::menu::bar(ui, |ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { - - ui.add_space(32.); - - if ui.button("UTXOs").clicked() { - self.section = AccountManagerSection::UtxoManager; - } - ui.separator(); - if ui.button("Details").clicked() { - self.section = AccountManagerSection::Details; - } - ui.separator(); - if ui.button("Transactions").clicked() { - self.section = AccountManagerSection::Transactions; - } - }); - }); - ui.separator(); - match section { AccountManagerSection::Overview => { Overview::new(&mut self.context).render(core,ui,rc); diff --git a/core/src/modules/account_manager/network.rs b/core/src/modules/account_manager/network.rs new file mode 100644 index 0000000..b3a7114 --- /dev/null +++ b/core/src/modules/account_manager/network.rs @@ -0,0 +1,53 @@ +use crate::imports::*; +use super::*; + +pub struct NetworkState<'context> { + pub context: &'context ManagerContext, +} + +impl<'context> NetworkState<'context> { + pub fn new(context: &'context ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, core: &mut Core, ui: &mut Ui, _rc: &RenderContext<'_>) { + + use egui_phosphor::light::{CLOUD_SLASH,CLOUD_ARROW_DOWN}; + + const ICON_SPACING: f32 = 24.0; + ui.vertical_centered(|ui|{ + // ui.add_space(16.); + if !core.state().is_connected() { + ui.add_space(ICON_SPACING); + ui.label( + RichText::new(CLOUD_SLASH) + .size(theme_style().icon_size_large) + .color(theme_color().icon_color_default) + ); + ui.add_space(ICON_SPACING); + + ui.label("You are currently not connected to the Kaspa node."); + } else if !core.state().is_synced() { + + ui.add_space(ICON_SPACING); + ui.label( + RichText::new(CLOUD_ARROW_DOWN) + .size(theme_style().icon_size_large) + .color(theme_color().icon_color_default) + ); + ui.add_space(ICON_SPACING); + + ui.label("The node is currently syncing with the Kaspa p2p network."); + ui.add_space(16.); + ui.label("Please wait for the node to sync or connect to a remote node."); + } + ui.add_space(16.); + ui.label("You can configure remote connection in Settings"); + ui.add_space(16.); + if ui.large_button("Go to Settings").clicked() { + core.select::(); + } + }); + + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/overview.rs b/core/src/modules/account_manager/overview.rs index 1da34b5..babcb71 100644 --- a/core/src/modules/account_manager/overview.rs +++ b/core/src/modules/account_manager/overview.rs @@ -3,12 +3,15 @@ use super::*; pub struct Overview<'manager> { context : &'manager mut ManagerContext, - editor_size : Vec2, } impl<'manager> Overview<'manager> { pub fn new(context : &'manager mut ManagerContext) -> Self { - Self { context, editor_size : Vec2::INFINITY } + Self { context } + } + + pub fn editor_size(ui : &Ui) -> Vec2 { + Vec2::new(ui.available_width() * 0.75, 32.) } pub fn render(&mut self, core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { @@ -16,7 +19,6 @@ impl<'manager> Overview<'manager> { core.apply_mobile_style(ui); - ui.separator(); ui.add_space(8.); egui::ScrollArea::vertical() @@ -24,22 +26,20 @@ impl<'manager> Overview<'manager> { .auto_shrink([false; 2]) .show(ui, |ui| { - self.editor_size = Vec2::new(ui.available_width() * 0.75, 32.); - ui.vertical_centered(|ui| { - self.render_address(core, ui, rc); - - self.render_balance(core, ui, rc); + AddressPane::new(self.context).render(core, ui, rc); + BalancePane::new(self.context).render(core, ui, rc); if !core.state().is_synced() || !core.state().is_connected() { - self.render_network_state(core,ui); + NetworkState::new(self.context).render(core, ui, rc); return; } match self.context.action.clone() { Action::Sending | Action::Estimating | Action::Processing => { - self.render_send_ui(core, ui, rc); + Processor::new(self.context).render(core, ui, rc); + // self.render_send_ui(core, ui, rc); } Action::Error(error) => { ui.vertical_centered(|ui|{ @@ -61,7 +61,7 @@ impl<'manager> Overview<'manager> { } Action::None => { - self.render_qr(core, ui, rc); + Qr::render(ui, rc); ui.vertical_centered(|ui|{ @@ -92,661 +92,9 @@ impl<'manager> Overview<'manager> { }); }); } - } }); }); } - fn render_network_state(&mut self, core : &mut Core, ui: &mut Ui) { - use egui_phosphor::light::{CLOUD_SLASH,CLOUD_ARROW_DOWN}; - - const ICON_SPACING: f32 = 24.0; - ui.vertical_centered(|ui|{ - // ui.add_space(16.); - if !core.state().is_connected() { - ui.add_space(ICON_SPACING); - ui.label( - RichText::new(CLOUD_SLASH) - .size(theme_style().icon_size_large) - .color(theme_color().icon_color_default) - ); - ui.add_space(ICON_SPACING); - - ui.label("You are currently not connected to the Kaspa node."); - } else if !core.state().is_synced() { - - ui.add_space(ICON_SPACING); - ui.label( - RichText::new(CLOUD_ARROW_DOWN) - .size(theme_style().icon_size_large) - .color(theme_color().icon_color_default) - ); - ui.add_space(ICON_SPACING); - - ui.label("The node is currently syncing with the Kaspa p2p network."); - ui.add_space(16.); - ui.label("Please wait for the node to sync or connect to a remote node."); - } - ui.add_space(32.); - ui.label("You can configure a remote connection in Settings"); - ui.add_space(16.); - if ui.large_button("Go to Settings").clicked() { - core.select::(); - } - }); - - - } - - fn render_address(&mut self, _core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { - use egui_phosphor::light::CLIPBOARD_TEXT; - let address = format_address(rc.context.address(), Some(8)); - if ui.add(Label::new(format!("Address: {address} {CLIPBOARD_TEXT}")).sense(Sense::click())) - // .on_hover_ui_at_pointer(|ui|{ - // ui.vertical(|ui|{ - // ui.add(Label::new(format!("{}", context.address().to_string()))); - // ui.add_space(16.); - // ui.label("Click to copy address to clipboard".to_string()); - // }); - // }) - .clicked() { - ui.output_mut(|o| o.copied_text = rc.context.address().to_string()); - runtime().notify(UserNotification::info(format!("{CLIPBOARD_TEXT} {}", i18n("Copied to clipboard"))).short()) - } - } - - fn render_balance(&mut self, core: &mut Core, ui : &mut Ui, rc: &RenderContext<'_>) { - - // let theme = theme(); - let RenderContext { account, network_type, .. } = rc; - - ui.add_space(10.); - - if let Some(balance) = account.balance() { - - if !core.state().is_synced() { - ui.label( - s2kws_layout_job(balance.mature, network_type, theme_color().balance_syncing_color,FontId::proportional(28.)) - ); - ui.label(RichText::new(i18n("The balance may be out of date during node sync")).size(12.).color(theme_color().balance_syncing_color)); - return; - } else { - ui.label( - s2kws_layout_job(balance.mature, network_type, theme_color().balance_color,FontId::proportional(28.)) - ); - } - - if balance.pending != 0 { - ui.label(format!( - "Pending: {}", - sompi_to_kaspa_string_with_suffix( - balance.pending, - network_type - ) - )); - } - if balance.outgoing != 0 { - ui.label(format!( - "Sending: {}", - sompi_to_kaspa_string_with_suffix( - balance.outgoing, - network_type - ) - )); - } - - ui.add_space(10.); - - let suffix = if balance.pending_utxo_count != 0 && balance.stasis_utxo_count != 0 { - format!(" ({} pending, {} processing)", balance.pending_utxo_count, balance.stasis_utxo_count) - } else if balance.pending_utxo_count != 0 { - format!(" ({} pending)", balance.pending_utxo_count) - } else if balance.stasis_utxo_count != 0 { - format!(" ({} processing)", balance.stasis_utxo_count) - } else { - "".to_string() - }; - - if self.context.transaction_kind.is_none() { - ui.label(format!( - "UTXOs: {}{suffix}", - balance.mature_utxo_count.separated_string(), - )); - } - } else { - ui.label("Balance: N/A"); - } - - - - } - - fn render_qr(&mut self, _core: &mut Core, ui : &mut Ui, rc: &RenderContext<'_>) { - let RenderContext { context, .. } = rc; - - // let scale = if self.context.action == Action::None { 1. } else { 0.35 }; - ui.add( - egui::Image::new(ImageSource::Bytes { uri : Cow::Owned(context.uri()), bytes: context.qr() }) - .fit_to_original_size(1.0) - .texture_options(TextureOptions::NEAREST) - ); - } - - fn render_transfer_account_selector(&mut self, core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) { - let RenderContext { network_type, .. } = rc; - - let default_account = core.account_collection().as_ref().and_then(|collection|{ - if collection.len() <= 1 { - unreachable!("expecting least 2 accounts"); - } - if collection.len() == 2 { - collection.list().iter().find(|account|account.id() != rc.account.id()).cloned() - } else { - None - } - }); - - if let Some(account) = default_account { - self.context.transfer_to_account = Some(account.clone()); - ui.label(format!("Transferring funds to: {}", account.name_or_id())); - ui.label(format!("Destination balance: {}", sompi_to_kaspa_string_with_suffix(account.balance().map(|balance|balance.mature).unwrap_or(0), network_type))); - } else { - - if self.context.transfer_to_account.as_ref().map(|account|account.id() == rc.account.id()).unwrap_or_default() { - self.context.transfer_to_account = None; - self.context.transfer_to_account.take(); - } - - let transfer_to_account = self.context.transfer_to_account.clone(); - - - PopupPanel::new(ui, "transfer_selector_popup",|ui|{ - let response = ui.vertical_centered(|ui| { - if let Some(account) = transfer_to_account { - let response = ui.add(Label::new(format!("Transferring funds to: {} ⏷", account.name_or_id())).sense(Sense::click())); - ui.label(format!("Destination balance: {}", sompi_to_kaspa_string_with_suffix(account.balance().map(|balance|balance.mature).unwrap_or(0), network_type))); - response - } else { - if self.context.send_amount_text.is_not_empty() { - ui.add(Label::new(RichText::new("Please select destination account ⏷").color(theme_color().warning_color)).sense(Sense::click())) - } else { - ui.add(Label::new(RichText::new("Please select destination account ⏷")).sense(Sense::click())) - } - } - }); - - response.inner - }, |ui, _| { - - egui::ScrollArea::vertical() - .id_source("transfer_selector_popup_scroll") - .auto_shrink([true; 2]) - .show(ui, |ui| { - - if let Some(account_collection) = core.account_collection() { - account_collection.iter().for_each(|account| { - if account.id() == rc.account.id() { - return; - } - - if ui.account_selector_button(account, network_type, false).clicked() { - self.context.transfer_to_account = Some(account.clone()); - } - }); - } - - }); - - }) - .with_min_width(240.) - .with_close_on_interaction(true) - .build(ui); - } - } - - fn render_address_input(&mut self, _core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) { - let RenderContext { network_type, .. } = rc; - - - TextEditor::new( - &mut self.context.destination_address_string, - // None, - &mut self.context.focus, - Focus::Address, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter destination address").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .vertical_align(Align::Center)) - }, - ) - .change(|address| { - match Address::try_from(address) { - Ok(address) => { - let address_network_type = NetworkType::try_from(address.prefix).expect("prefix to network type"); - if address_network_type != *network_type { - self.context.address_status = AddressStatus::NetworkMismatch(address_network_type); - } else { - self.context.address_status = AddressStatus::Valid; - } - } - Err(err) => { - self.context.address_status = AddressStatus::Invalid(err.to_string()); - } - } - }) - .submit(|_, focus|{ - // *focus = Some(Focus::Amount); - focus.next(Focus::Amount); - }) - .build(ui); - - match &self.context.address_status { - AddressStatus::Valid => {}, - AddressStatus::None => {}, - AddressStatus::NetworkMismatch(address_network_type) => { - ui.label(format!("This address if for the different\nnetwork ({address_network_type})")); - }, - AddressStatus::Invalid(err) => { - ui.label(format!("Please enter a valid address\n{err}")); - } - } - - - - - } - - fn render_estimation_ui(&mut self, core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) -> bool { - use egui_phosphor::light::{CHECK, X}; - - let RenderContext { network_type, .. } = rc; - - let mut request_estimate = self.context.request_estimate.take().unwrap_or_default(); - - match self.context.transaction_kind.as_ref().unwrap() { - TransactionKind::Send => { - self.render_address_input(core, ui, rc); - } - TransactionKind::Transfer => { - self.render_transfer_account_selector(core, ui, rc); - } - } - - let response = TextEditor::new( - &mut self.context.send_amount_text, - &mut self.context.focus, - Focus::Amount, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new(format!("Enter {} amount to send", kaspa_suffix(network_type))).size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .vertical_align(Align::Center)) - }, - ) - .change(|_| { - request_estimate = true; - }) - .build(ui); - - if response.text_edit_submit(ui) { - if self.context.enable_priority_fees { - self.context.focus.next(Focus::Fees); - } else if self.update_user_args() { - self.context.action = Action::Sending; - self.context.focus.next(Focus::WalletSecret); - } - } - - ui.add_space(8.); - if ui - .checkbox(&mut self.context.enable_priority_fees,i18n("Include Priority Fees")) - // .on_hover_text_at_pointer(i18n("Add priority fees to ensure faster confirmation.\nUseful only if the network is congested.")) - .changed() { - if self.context.enable_priority_fees { - self.context.focus.next(Focus::Fees); - } else { - self.context.focus.next(Focus::Amount); - } - } - - if self.context.enable_priority_fees { - TextEditor::new( - &mut self.context.priority_fees_text, - &mut self.context.focus, - Focus::Fees, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter priority fees").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .vertical_align(Align::Center)) - }, - ) - .change(|_| { - request_estimate = true; - }) - .submit(|_,_|{ - self.context.action = Action::Sending; - }) - .build(ui); - } - - ui.add_space(8.); - let ready_to_send = match &*self.context.estimate.lock().unwrap() { - EstimatorStatus::GeneratorSummary(estimate) => { - if let Some(final_transaction_amount) = estimate.final_transaction_amount { - ui.label(format!("Final Amount: {}", sompi_to_kaspa_string_with_suffix(final_transaction_amount + estimate.aggregated_fees, network_type))); - } - let fee_title = if self.context.priority_fees_sompi != 0 { - "Network and Priority Fees:" - } else { - "Network Fees:" - }; - ui.label(format!("{} {}", fee_title, sompi_to_kaspa_string_with_suffix(estimate.aggregated_fees, network_type))); - ui.label(format!("Transactions: {} UTXOs: {}", estimate.number_of_generated_transactions, estimate.aggregated_utxos)); - - self.context.address_status == AddressStatus::Valid || (self.context.transaction_kind == Some(TransactionKind::Transfer) && self.context.transfer_to_account.is_some()) - } - EstimatorStatus::Error(error) => { - ui.label(RichText::new(error.to_string()).color(theme_color().error_color)); - false - } - EstimatorStatus::None => { - ui.label("Please enter KAS amount to send"); - false - } - }; - ui.add_space(8.); - - ui.horizontal(|ui| { - ui.vertical_centered(|ui|{ - ui.horizontal(|ui| { - CenterLayoutBuilder::new() - .add_enabled(ready_to_send, Button::new(format!("{CHECK} Send")).min_size(theme_style().medium_button_size()), |this: &mut Overview<'_>| { - this.context.action = Action::Sending; - this.context.focus.next(Focus::WalletSecret); - }) - .add(Button::new(format!("{X} Cancel")).min_size(theme_style().medium_button_size()), |this| { - this.context.reset_send_state(); - }) - .build(ui, self) - }); - }); - - }); - - request_estimate - } - - fn render_passphrase_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) -> bool { - use egui_phosphor::light::{CHECK, X}; - - let RenderContext { account, .. } = rc; - - let requires_payment_passphrase = account.requires_bip39_passphrase(); - let mut proceed_with_send = false; - - let response = TextEditor::new( - &mut self.context.wallet_secret, - &mut self.context.focus, - Focus::WalletSecret, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter wallet password").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .password(true) - .vertical_align(Align::Center)) - }, - ) - .build(ui); - - if response.text_edit_submit(ui) { - if account.requires_bip39_passphrase() { - self.context.focus.next(Focus::PaymentSecret); - } else if !self.context.wallet_secret.is_empty() { - proceed_with_send = true; - } - } - - if requires_payment_passphrase { - let response = TextEditor::new( - &mut self.context.payment_secret, - &mut self.context.focus, - Focus::PaymentSecret, - |ui, text| { - ui.add_space(8.); - ui.label(egui::RichText::new("Enter bip39 passphrase").size(12.).raised()); - ui.add_sized(self.editor_size, TextEdit::singleline(text) - .password(true) - .vertical_align(Align::Center)) - }, - ) - .build(ui); - - if response.text_edit_submit(ui) && !self.context.wallet_secret.is_empty() && !self.context.payment_secret.is_empty() { - proceed_with_send = true; - } - - } - - let is_ready_to_send = !(self.context.wallet_secret.is_empty() || requires_payment_passphrase && self.context.payment_secret.is_empty()); - - ui.add_space(8.); - CenterLayoutBuilder::new() - .add_enabled(is_ready_to_send, Button::new(format!("{CHECK} Submit")).min_size(theme_style().medium_button_size()), |_this: &mut Overview<'_>| { - proceed_with_send = true; - }) - .add(Button::new(format!("{X} Cancel")).min_size(theme_style().medium_button_size()), |this| { - this.context.action = Action::Estimating; - this.context.focus.next(Focus::Amount); - }) - .build(ui,self); - - - - proceed_with_send - } - - fn render_send_ui(&mut self, core: &mut Core, ui: &mut egui::Ui, rc: &RenderContext<'_>) { - - let RenderContext { account, network_type, .. } = rc; - - ui.add_space(8.); - match self.context.transaction_kind.as_ref().unwrap() { - TransactionKind::Send => { - ui.label("Sending funds"); - ui.add_space(8.); - } - TransactionKind::Transfer => { - // ui.label("Transferring funds"); - } - } - // ui.label("Sending funds"); - - - let send_result = Payload::>::new("send_result"); - - - match &self.context.action { - Action::Estimating => { - - let request_estimate = self.render_estimation_ui(core, ui, rc); - - if request_estimate && self.update_user_args() { - - let priority_fees_sompi = if self.context.enable_priority_fees { - self.context.priority_fees_sompi - } else { 0 }; - - let address = match network_type { - NetworkType::Testnet => Address::try_from("kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya").unwrap(), - NetworkType::Mainnet => Address::try_from("kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e").unwrap(), - _ => panic!("Unsupported network"), - }; - - let account_id = account.id(); - let payment_output = PaymentOutput { - address, - amount: self.context.send_amount_sompi, - }; - - let status = self.context.estimate.clone(); - spawn(async move { - let request = AccountsEstimateRequest { - task_id: None, - account_id, - destination: payment_output.into(), - priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), - payload: None, - }; - - match runtime().wallet().accounts_estimate_call(request).await { - Ok(response) => { - *status.lock().unwrap() = EstimatorStatus::GeneratorSummary(response.generator_summary); - } - Err(error) => { - *status.lock().unwrap() = EstimatorStatus::Error(error.to_string()); - } - } - - runtime().egui_ctx().request_repaint(); - Ok(()) - }); - } - - } - - Action::Sending => { - - let proceed_with_send = self.render_passphrase_ui(core, ui, rc); - - if proceed_with_send { - - if self.context.destination_address_string.is_not_empty() && self.context.transfer_to_account.is_some() { - unreachable!("expecting only one of destination address or transfer to account"); - } - - let priority_fees_sompi = if self.context.enable_priority_fees { - self.context.priority_fees_sompi - } else { 0 }; - - let wallet_secret = Secret::try_from(self.context.wallet_secret.clone()).expect("expecting wallet secret"); - let payment_secret = account.requires_bip39_passphrase().then_some(Secret::try_from(self.context.payment_secret.clone()).expect("expecting payment secret")); - - match self.context.transaction_kind.unwrap() { - TransactionKind::Send => { - - let address = Address::try_from(self.context.destination_address_string.as_str()).expect("invalid address"); - let account_id = account.id(); - let payment_output = PaymentOutput { - address, - amount: self.context.send_amount_sompi, - }; - - spawn_with_result(&send_result, async move { - let request = AccountsSendRequest { - account_id, - destination: payment_output.into(), - wallet_secret, - payment_secret, - priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), - payload: None, - }; - - let generator_summary = runtime().wallet().accounts_send_call(request).await?.generator_summary; - runtime().request_repaint(); - Ok(generator_summary) - }); - - } - - TransactionKind::Transfer => { - let destination_account_id = self.context.transfer_to_account.as_ref().expect("transfer destination account").id(); - let source_account_id = account.id(); - let transfer_amount_sompi = self.context.send_amount_sompi; - - spawn_with_result(&send_result, async move { - let request = AccountsTransferRequest { - source_account_id, - destination_account_id, - wallet_secret, - payment_secret, - priority_fee_sompi: Some(Fees::SenderPaysAll(priority_fees_sompi)), - transfer_amount_sompi, - }; - - let generator_summary = runtime().wallet().accounts_transfer_call(request).await?.generator_summary; - runtime().request_repaint(); - Ok(generator_summary) - }); - } - } - - self.context.action = Action::Processing; - } - - } - Action::Processing => { - ui.add_space(16.); - ui.add(egui::Spinner::new().size(92.)); - - if let Some(result) = send_result.take() { - match result { - Ok(_) => { - self.context.reset_send_state(); - self.context.action = Action::None; - } - Err(error) => { - println!(); - println!("transaction error: {error}"); - println!(); - self.context.reset_send_state(); - self.context.action = Action::Error(Arc::new(error)); - } - } - } - } - _ => { } - } - - } - - fn update_user_args(&mut self) -> bool { - let mut valid = true; - - match try_kaspa_str_to_sompi(self.context.send_amount_text.as_str()) { - Ok(Some(sompi)) => { - self.context.send_amount_sompi = sompi; - } - Ok(None) => { - self.user_error("Please enter an amount".to_string()); - valid = false; - } - Err(err) => { - self.user_error(format!("Invalid amount: {err}")); - valid = false; - } - } - - match try_kaspa_str_to_sompi(self.context.priority_fees_text.as_str()) { - Ok(Some(sompi)) => { - self.context.priority_fees_sompi = sompi; - } - Ok(None) => { - self.context.priority_fees_sompi = 0; - } - Err(err) => { - self.user_error(format!("Invalid fee amount: {err}")); - valid = false; - } - } - - valid - } - - fn user_error(&self, error : impl Into) { - *self.context.estimate.lock().unwrap() = EstimatorStatus::Error(error.into()); - } - } \ No newline at end of file diff --git a/core/src/modules/account_manager/processor.rs b/core/src/modules/account_manager/processor.rs new file mode 100644 index 0000000..aa3f3ad --- /dev/null +++ b/core/src/modules/account_manager/processor.rs @@ -0,0 +1,173 @@ +use crate::imports::*; +use super::*; + +pub struct Processor<'context> { + context: &'context mut ManagerContext, +} + +impl<'context> Processor<'context> { + pub fn new(context: &'context mut ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, core : &mut Core, ui: &mut Ui, rc : &RenderContext<'_>) { + + let RenderContext { account, network_type, .. } = rc; + + ui.add_space(8.); + match self.context.transaction_kind.as_ref().unwrap() { + TransactionKind::Send => { + ui.label("Sending funds"); + ui.add_space(8.); + } + TransactionKind::Transfer => { + // ui.label("Transferring funds"); + } + } + + let send_result = Payload::>::new("send_result"); + + match &self.context.action { + Action::Estimating => { + + let request_estimate = Estimator::new(self.context).render(core, ui, rc); + + if request_estimate { + + let priority_fees_sompi = if self.context.enable_priority_fees { + self.context.priority_fees_sompi + } else { 0 }; + + let address = match network_type { + NetworkType::Testnet => Address::try_from("kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya").unwrap(), + NetworkType::Mainnet => Address::try_from("kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkx9awp4e").unwrap(), + _ => panic!("Unsupported network"), + }; + + let account_id = account.id(); + let payment_output = PaymentOutput { + address, + amount: self.context.send_amount_sompi, + }; + + let status = self.context.estimate.clone(); + spawn(async move { + let request = AccountsEstimateRequest { + task_id: None, + account_id, + destination: payment_output.into(), + priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), + payload: None, + }; + + match runtime().wallet().accounts_estimate_call(request).await { + Ok(response) => { + *status.lock().unwrap() = EstimatorStatus::GeneratorSummary(response.generator_summary); + } + Err(error) => { + *status.lock().unwrap() = EstimatorStatus::Error(error.to_string()); + } + } + + runtime().egui_ctx().request_repaint(); + Ok(()) + }); + } + + } + + Action::Sending => { + + let proceed_with_send = WalletSecret::new(self.context).render(ui, rc); + + if proceed_with_send { + + if self.context.destination_address_string.is_not_empty() && self.context.transfer_to_account.is_some() { + unreachable!("expecting only one of destination address or transfer to account"); + } + + let priority_fees_sompi = if self.context.enable_priority_fees { + self.context.priority_fees_sompi + } else { 0 }; + + let wallet_secret = Secret::try_from(self.context.wallet_secret.clone()).expect("expecting wallet secret"); + let payment_secret = account.requires_bip39_passphrase().then_some(Secret::try_from(self.context.payment_secret.clone()).expect("expecting payment secret")); + + match self.context.transaction_kind.unwrap() { + TransactionKind::Send => { + + let address = Address::try_from(self.context.destination_address_string.as_str()).expect("invalid address"); + let account_id = account.id(); + let payment_output = PaymentOutput { + address, + amount: self.context.send_amount_sompi, + }; + + spawn_with_result(&send_result, async move { + let request = AccountsSendRequest { + account_id, + destination: payment_output.into(), + wallet_secret, + payment_secret, + priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), + payload: None, + }; + + let generator_summary = runtime().wallet().accounts_send_call(request).await?.generator_summary; + runtime().request_repaint(); + Ok(generator_summary) + }); + + } + + TransactionKind::Transfer => { + let destination_account_id = self.context.transfer_to_account.as_ref().expect("transfer destination account").id(); + let source_account_id = account.id(); + let transfer_amount_sompi = self.context.send_amount_sompi; + + spawn_with_result(&send_result, async move { + let request = AccountsTransferRequest { + source_account_id, + destination_account_id, + wallet_secret, + payment_secret, + priority_fee_sompi: Some(Fees::SenderPaysAll(priority_fees_sompi)), + transfer_amount_sompi, + }; + + let generator_summary = runtime().wallet().accounts_transfer_call(request).await?.generator_summary; + runtime().request_repaint(); + Ok(generator_summary) + }); + } + } + + self.context.action = Action::Processing; + } + + } + Action::Processing => { + ui.add_space(16.); + ui.add(egui::Spinner::new().size(92.)); + + if let Some(result) = send_result.take() { + match result { + Ok(_) => { + self.context.reset_send_state(); + self.context.action = Action::None; + } + Err(error) => { + println!(); + println!("transaction error: {error}"); + println!(); + self.context.reset_send_state(); + self.context.action = Action::Error(Arc::new(error)); + } + } + } + } + _ => { } + } + + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/qr.rs b/core/src/modules/account_manager/qr.rs new file mode 100644 index 0000000..1b6e58e --- /dev/null +++ b/core/src/modules/account_manager/qr.rs @@ -0,0 +1,16 @@ +use crate::imports::*; +use super::*; + +pub struct Qr { } + +impl Qr { + pub fn render(ui : &mut Ui, rc : &RenderContext<'_>) { + let RenderContext { context, .. } = rc; + + ui.add( + Image::new(ImageSource::Bytes { uri : Cow::Owned(context.uri()), bytes: context.qr() }) + .fit_to_original_size(1.0) + .texture_options(TextureOptions::NEAREST) + ); + } +} \ No newline at end of file diff --git a/core/src/modules/account_manager/secret.rs b/core/src/modules/account_manager/secret.rs index 4424226..76dfbaf 100644 --- a/core/src/modules/account_manager/secret.rs +++ b/core/src/modules/account_manager/secret.rs @@ -1,21 +1,84 @@ use crate::imports::*; use super::*; -pub struct AccountSecret<'render> { - core : &'render mut Core, - manager : &'render AccountManager, +pub struct WalletSecret<'context> { + context : &'context mut ManagerContext, } -impl<'render> AccountSecret<'render> { +impl<'context> WalletSecret<'context> { - pub fn new(core : &'render mut Core, manager : &'render AccountManager) -> Self { - Self { core, manager } + pub fn new(context : &'context mut ManagerContext) -> Self { + Self { context } } - pub fn render(&mut self, ui : &mut Ui, rc : &RenderContext<'_>) { + pub fn render(&mut self, ui : &mut Ui, rc : &RenderContext<'_>) -> bool { + use egui_phosphor::light::{CHECK, X}; + let RenderContext { account, .. } = rc; + + let requires_payment_passphrase = account.requires_bip39_passphrase(); + let mut proceed_with_send = false; + + let response = TextEditor::new( + &mut self.context.wallet_secret, + &mut self.context.focus, + Focus::WalletSecret, + |ui, text| { + ui.add_space(8.); + ui.label(RichText::new("Enter wallet password").size(12.).raised()); + ui.add_sized(Overview::editor_size(ui), TextEdit::singleline(text) + .password(true) + .vertical_align(Align::Center)) + }, + ) + .build(ui); + + if response.text_edit_submit(ui) { + if account.requires_bip39_passphrase() { + self.context.focus.next(Focus::PaymentSecret); + } else if !self.context.wallet_secret.is_empty() { + proceed_with_send = true; + } + } + + if requires_payment_passphrase { + let response = TextEditor::new( + &mut self.context.payment_secret, + &mut self.context.focus, + Focus::PaymentSecret, + |ui, text| { + ui.add_space(8.); + ui.label(RichText::new("Enter bip39 passphrase").size(12.).raised()); + ui.add_sized(Overview::editor_size(ui), TextEdit::singleline(text) + .password(true) + .vertical_align(Align::Center)) + }, + ) + .build(ui); + + if response.text_edit_submit(ui) && !self.context.wallet_secret.is_empty() && !self.context.payment_secret.is_empty() { + proceed_with_send = true; + } + + } + + let is_ready_to_send = !(self.context.wallet_secret.is_empty() || requires_payment_passphrase && self.context.payment_secret.is_empty()); + + ui.add_space(8.); + CenterLayoutBuilder::new() + .add_enabled(is_ready_to_send, Button::new(format!("{CHECK} Submit")).min_size(theme_style().medium_button_size()), |_this: &mut WalletSecret<'_>| { + proceed_with_send = true; + }) + .add(Button::new(format!("{X} Cancel")).min_size(theme_style().medium_button_size()), |this| { + this.context.action = Action::Estimating; + this.context.focus.next(Focus::Amount); + }) + .build(ui,self); + + + + proceed_with_send } - } diff --git a/core/src/modules/account_manager/send.rs b/core/src/modules/account_manager/send.rs deleted file mode 100644 index e69de29..0000000 diff --git a/core/src/modules/account_manager/transfer.rs b/core/src/modules/account_manager/transfer.rs index e69de29..0f8abea 100644 --- a/core/src/modules/account_manager/transfer.rs +++ b/core/src/modules/account_manager/transfer.rs @@ -0,0 +1,84 @@ +use crate::imports::*; +use super::*; + +pub struct Transfer<'context> { + context : &'context mut ManagerContext, +} + +impl<'context> Transfer<'context> { + pub fn new(context : &'context mut ManagerContext) -> Self { + Self { context } + } + + pub fn render(&mut self, core: &mut Core, ui : &mut Ui, rc : &RenderContext<'_>) { + + let RenderContext { network_type, .. } = rc; + + let default_account = core.account_collection().as_ref().and_then(|collection|{ + if collection.len() <= 1 { + unreachable!("expecting least 2 accounts"); + } + if collection.len() == 2 { + collection.list().iter().find(|account|account.id() != rc.account.id()).cloned() + } else { + None + } + }); + + if let Some(account) = default_account { + self.context.transfer_to_account = Some(account.clone()); + ui.label(format!("Transferring funds to: {}", account.name_or_id())); + ui.label(format!("Destination balance: {}", sompi_to_kaspa_string_with_suffix(account.balance().map(|balance|balance.mature).unwrap_or(0), network_type))); + } else { + + if self.context.transfer_to_account.as_ref().map(|account|account.id() == rc.account.id()).unwrap_or_default() { + self.context.transfer_to_account = None; + self.context.transfer_to_account.take(); + } + + let transfer_to_account = self.context.transfer_to_account.clone(); + + + PopupPanel::new(ui, "transfer_selector_popup",|ui|{ + let response = ui.vertical_centered(|ui| { + if let Some(account) = transfer_to_account { + let response = ui.add(Label::new(format!("Transferring funds to: {} ⏷", account.name_or_id())).sense(Sense::click())); + ui.label(format!("Destination balance: {}", sompi_to_kaspa_string_with_suffix(account.balance().map(|balance|balance.mature).unwrap_or(0), network_type))); + response + } else if self.context.send_amount_text.is_not_empty() { + ui.add(Label::new(RichText::new("Please select destination account ⏷").color(theme_color().warning_color)).sense(Sense::click())) + } else { + ui.add(Label::new(RichText::new("Please select destination account ⏷")).sense(Sense::click())) + } + }); + + response.inner + }, |ui, _| { + + egui::ScrollArea::vertical() + .id_source("transfer_selector_popup_scroll") + .auto_shrink([true; 2]) + .show(ui, |ui| { + + if let Some(account_collection) = core.account_collection() { + account_collection.iter().for_each(|account| { + if account.id() == rc.account.id() { + return; + } + + if ui.account_selector_button(account, network_type, false).clicked() { + self.context.transfer_to_account = Some(account.clone()); + } + }); + } + + }); + + }) + .with_min_width(240.) + .with_close_on_interaction(true) + .build(ui); + } + + } +} \ No newline at end of file diff --git a/core/src/modules/export.rs b/core/src/modules/export.rs index a0d39cd..c3ddc81 100644 --- a/core/src/modules/export.rs +++ b/core/src/modules/export.rs @@ -249,7 +249,7 @@ impl ModuleT for Export { if let Some(err) = error { ui.label( - egui::RichText::new(err.to_string()) + RichText::new(err.to_string()) .color(egui::Color32::from_rgb(255, 120, 120)), ); ui.label(" "); @@ -279,7 +279,7 @@ impl ModuleT for Export { &mut this.context.focus, Focus::WalletSecret, |ui, text| { - ui.label(egui::RichText::new("Enter your wallet secret").size(12.).raised()); + ui.label(RichText::new("Enter your wallet secret").size(12.).raised()); ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) diff --git a/core/src/modules/import.rs b/core/src/modules/import.rs index 6c032dd..40b144c 100644 --- a/core/src/modules/import.rs +++ b/core/src/modules/import.rs @@ -72,10 +72,7 @@ impl ModuleT for Import { // ui.label(this.mnemonic.last().unwrap_or(&String::new())); this.mnemonic.iter().for_each(|word| { ui.label(" "); - - ui.label(egui::RichText::new(word).family(FontFamily::Monospace).size(14.).color(egui::Color32::WHITE)); - - + ui.label(RichText::new(word).family(FontFamily::Monospace).size(14.).color(egui::Color32::WHITE)); }); }); // ui.label(" "); @@ -155,14 +152,12 @@ impl ModuleT for Import { if let Some(message) = message { ui.label(" "); - // ui.label(format!("Error: {}",message)); - ui.label( - egui::RichText::new("Error unlocking wallet") + RichText::new("Error unlocking wallet") .color(egui::Color32::from_rgb(255, 120, 120)), ); ui.label( - egui::RichText::new(message) + RichText::new(message) .color(egui::Color32::from_rgb(255, 120, 120)), ); diff --git a/core/src/modules/metrics.rs b/core/src/modules/metrics.rs index d14dc4d..a32d526 100644 --- a/core/src/modules/metrics.rs +++ b/core/src/modules/metrics.rs @@ -1,7 +1,7 @@ use crate::imports::*; use crate::runtime::services::metrics_monitor::MAX_METRICS_SAMPLES; use egui_extras::{StripBuilder, Size}; -use kaspa_metrics::{Metric,MetricGroup, MetricsSnapshot}; +use kaspa_metrics_core::{Metric,MetricGroup, MetricsSnapshot}; use chrono::DateTime; use egui_plot::{ Legend, @@ -152,7 +152,9 @@ impl ModuleT for Metrics { format_duration(v as u64) }) ); - ui.label("Duration:"); + if core.device().orientation() == Orientation::Portrait { + ui.label("Duration:"); + } }); }); @@ -183,16 +185,21 @@ impl ModuleT for Metrics { .id_source("node_metrics") .auto_shrink([false; 2]) .show(ui, |ui| { - let view_width = ui.available_width() - 32.; let graph_height = core.settings.user_interface.metrics.graph_height as f32; - let graph_width = view_width / core.settings.user_interface.metrics.graph_columns as f32; + + let (columns, graph_width) = if core.device().orientation() == Orientation::Portrait { + (1,view_width) + } else { + (core.settings.user_interface.metrics.graph_columns, view_width / core.settings.user_interface.metrics.graph_columns as f32) + }; + let mut metric_iter = Metric::list().into_iter().filter(|metric| !core.settings.user_interface.metrics.disabled.contains(metric)); let mut draw = true; while draw { ui.horizontal(|ui| { - for _ in 0..core.settings.user_interface.metrics.graph_columns { + for _ in 0..columns { if let Some(metric) = metric_iter.next() { let range_from = core.settings.user_interface.metrics.graph_range_from; let range_to = core.settings.user_interface.metrics.graph_range_to; diff --git a/core/src/modules/mod.rs b/core/src/modules/mod.rs index aa6f59d..87e770e 100644 --- a/core/src/modules/mod.rs +++ b/core/src/modules/mod.rs @@ -19,6 +19,7 @@ kaspa_ng_macros::register_modules!( settings, testing, wallet_create, + wallet_secret, wallet_open, welcome, ] diff --git a/core/src/modules/node.rs b/core/src/modules/node.rs index 3311f5f..761d55d 100644 --- a/core/src/modules/node.rs +++ b/core/src/modules/node.rs @@ -104,9 +104,7 @@ fn render_peer(ui : &mut Ui, peer: &RpcPeerInfo) { Grid::new("peer_info_grid") .num_columns(2) - .spacing([40.0,4.0]) - .min_col_width(120.0) - // .striped(true) + .spacing([16.0,4.0]) .show(ui, |ui| { ui.label(i18n("User Agent")); diff --git a/core/src/modules/overview.rs b/core/src/modules/overview.rs index cf62c01..ddb722a 100644 --- a/core/src/modules/overview.rs +++ b/core/src/modules/overview.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use egui::load::Bytes; -use kaspa_metrics::{Metric,MetricGroup}; +use kaspa_metrics_core::{Metric,MetricGroup}; use egui_plot::{ Legend, Line, @@ -39,24 +39,27 @@ impl ModuleT for Overview { ) { let width = ui.available_width(); - SidePanel::left("overview_left").exact_width(width/2.).resizable(false).show_separator_line(true).show_inside(ui, |ui| { - // ui.label("Kaspa NG"); - egui::ScrollArea::vertical() - .id_source("overview_metrics") - .auto_shrink([false; 2]) - .show(ui, |ui| { - self.render_stats(core,ui); - }); - }); - - SidePanel::right("overview_right") - .exact_width(width/2.) - .resizable(false) - .show_separator_line(false) - .show_inside(ui, |ui| { - self.render_details(core, ui); + if core.device().single_pane() { + self.render_details(core, ui); + } else { + SidePanel::left("overview_left").exact_width(width/2.).resizable(false).show_separator_line(true).show_inside(ui, |ui| { + egui::ScrollArea::vertical() + .id_source("overview_metrics") + .auto_shrink([false; 2]) + .show(ui, |ui| { + self.render_stats(core,ui); + }); }); + SidePanel::right("overview_right") + .exact_width(width/2.) + .resizable(false) + .show_separator_line(false) + .show_inside(ui, |ui| { + self.render_details(core, ui); + }); + } + } } @@ -106,7 +109,15 @@ impl Overview { CollapsingHeader::new(i18n("Market")) .default_open(true) .show(ui, |ui| { - ui.label("TODO"); + + if let Some(price_list) = core.market.price.as_ref() { + for (symbol, data) in price_list.iter() { + if let Some(price) = data.price { + ui.label(RichText::new(format!("{} {} ",price,symbol.to_uppercase())));//.font(FontId::proportional(14.))); + } + } + } + }); CollapsingHeader::new(i18n("Resources")) @@ -239,45 +250,42 @@ impl Overview { .default_open(false) .show(ui, |ui| { ui.vertical(|ui|{ + ui.set_width(ui.available_width() - 48.); ui.label("Special thanks Kaspa developers and the following community members:"); - // ui.horizontal(|ui|{ - ui.horizontal_wrapped(|ui|{ - ui.set_width(ui.available_width() - 64.); - let mut nicks = [ - "0xAndrei", - "142673", - "Bape", - "Bubblegum Lightning", - "coderofstuff", - "CryptoK", - "Dhayse", - "Elertan", - "Gennady Gorin", - "hashdag", - "Helix", - "jablonx", - "jwj", - "KaffinPX", - "lAmeR", - "matoo", - "msutton", - "n15a", - "Rhubarbarian", - "shaideshe", - "someone235", - "supertypo", - "The AllFather", - "Tim", - "tmrlvi", - "Wolfie", - ]; - nicks.sort(); - nicks.into_iter().for_each(|nick| { - ui.label(format!("@{nick}")); - }); - }); - // ui.add_space(32.); - // }); + ui.horizontal_wrapped(|ui|{ + let nicks = [ + "0xAndrei", + "142673", + "Bape", + "Bubblegum Lightning", + "coderofstuff", + "CryptoK", + "Dhayse", + "elertan0", + "elichai2", + "Gennady Gorin", + "hashdag", + "Helix", + "jablonx", + "jwj", + "KaffinPX", + "lAmeR", + "matoo", + "msutton", + "n15a", + "Rhubarbarian", + "shaideshe", + "someone235", + "supertypo", + "The AllFather", + "Tim", + "tmrlvi", + "Wolfie", + ]; + + let text = nicks.into_iter().map(|nick|format!("@{nick} ")).collect::>().join(" "); + ui.label(text); + }); }); }); @@ -292,15 +300,6 @@ impl Overview { } }); }); - - - - - - - - - } fn render_graphs(&mut self, core: &mut Core, ui : &mut Ui) { @@ -394,8 +393,8 @@ impl Overview { }); let text = format!("{} {}", i18n(metric.title().1).to_uppercase(), metric.format(value, true, true)); - let rich_text_top = egui::RichText::new(&text).size(10.).color(theme_color().raised_text_color); - let rich_text_back = egui::RichText::new(text).size(10.).color(theme_color().raised_text_shadow); + let rich_text_top = RichText::new(&text).size(10.).color(theme_color().raised_text_color); + let rich_text_back = RichText::new(text).size(10.).color(theme_color().raised_text_shadow); let label_top = Label::new(rich_text_top).wrap(false); let label_back = Label::new(rich_text_back).wrap(false); let mut rect_top = plot_result.response.rect; diff --git a/core/src/modules/private_key_create.rs b/core/src/modules/private_key_create.rs index f731c3d..55a1e7b 100644 --- a/core/src/modules/private_key_create.rs +++ b/core/src/modules/private_key_create.rs @@ -218,7 +218,7 @@ impl ModuleT for PrivateKeyCreate { be able to use mnemonic to recover your wallet!"); }) .with_body(|this,ui| { - ui.label(egui::RichText::new("ENTER YOUR PAYMENT PASSWORD").size(12.).raised()); + ui.label(RichText::new("ENTER YOUR PAYMENT PASSWORD").size(12.).raised()); ui.add_sized( size, TextEdit::singleline(&mut this.args.payment_secret) @@ -227,7 +227,7 @@ impl ModuleT for PrivateKeyCreate { ); ui.label(" "); - ui.label(egui::RichText::new("VERIFY YOUR PAYMENT PASSWORD").size(12.).raised()); + ui.label(RichText::new("VERIFY YOUR PAYMENT PASSWORD").size(12.).raised()); ui.add_sized( size, @@ -238,7 +238,7 @@ impl ModuleT for PrivateKeyCreate { if this.args.payment_secret_confirm.is_not_empty() && this.args.payment_secret != this.args.payment_secret_confirm { ui.label(" "); - ui.label(egui::RichText::new("Passwords do not match").color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new("Passwords do not match").color(egui::Color32::from_rgb(255, 120, 120))); ui.label(" "); } else { ui.label(" "); @@ -357,8 +357,8 @@ impl ModuleT for PrivateKeyCreate { .with_header(move |this,ui| { ui.label(" "); ui.label(" "); - ui.label(egui::RichText::new("Error creating account").color(egui::Color32::from_rgb(255, 120, 120))); - ui.label(egui::RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new("Error creating account").color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); if ui.add_sized(size, egui::Button::new("Restart")).clicked() { this.state = State::Start; @@ -396,7 +396,7 @@ impl ModuleT for PrivateKeyCreate { // ui.columns(6, |cols| { // for col in 0..chunk.len() { - // cols[col].label(egui::RichText::new(chunk[col]).family(FontFamily::Monospace).size(14.).color(egui::Color32::WHITE)); + // cols[col].label(RichText::new(chunk[col]).family(FontFamily::Monospace).size(14.).color(egui::Color32::WHITE)); // } // }) // }); @@ -451,7 +451,8 @@ impl ModuleT for PrivateKeyCreate { core.account_collection.as_mut().unwrap().push_unchecked(account.clone()); core.select::(); - core.get_mut::().select(Some(account)); + let device = core.device().clone(); + core.get_mut::().select(Some(account), device); } }) .render(ui); diff --git a/core/src/modules/scanner.rs b/core/src/modules/scanner.rs index 6c7bd21..b7cf7cc 100644 --- a/core/src/modules/scanner.rs +++ b/core/src/modules/scanner.rs @@ -5,6 +5,7 @@ use kaspa_wallet_core::runtime::Wallet; #[derive(Clone)] pub enum State { Select, + Settings { account : Account }, WalletSecret { account : Account }, Spawn { account : Account }, Status, @@ -38,11 +39,20 @@ impl Status { #[derive(Default)] struct ScannerContext { + transfer_funds : bool, wallet_secret: String, status : Arc>, abortable : Abortable, } +impl Zeroize for ScannerContext { + fn zeroize(&mut self) { + self.wallet_secret.zeroize(); + self.status = Arc::new(Mutex::new(Status::default())); + self.abortable.reset(); + } +} + pub struct Scanner { #[allow(dead_code)] runtime: Runtime, @@ -89,10 +99,13 @@ impl ModuleT for Scanner { State::Select => { - let mut close = false; + let back = Rc::new(RefCell::new(false)); Panel::new(self) .with_caption("Scanner") + .with_back(|_this| { + *back.borrow_mut() = true; + }) .with_close_enabled(false, |_|{ }) .with_header(|_this,ui| { @@ -113,7 +126,7 @@ impl ModuleT for Scanner { ui.add_space(16.); if ui.large_button("Close").clicked() { - close = true; + *back.borrow_mut() = true; } return; @@ -128,7 +141,7 @@ impl ModuleT for Scanner { ui.add_space(16.); if ui.large_button("Close").clicked() { - close = true; + *back.borrow_mut() = true; } return; @@ -143,7 +156,7 @@ impl ModuleT for Scanner { ui.add_space(16.); if ui.large_button("Close").clicked() { - close = true; + *back.borrow_mut() = true; } return; @@ -159,20 +172,49 @@ impl ModuleT for Scanner { } if ui.account_selector_button(selectable_account, &network_type, false).clicked() { - this.state = State::WalletSecret { + this.state = State::Settings { account: selectable_account.clone(), }; - this.focus.next(Focus::WalletSecret); } } } }).render(ui); - if close { - core.select::(); + if *back.borrow() { + if core.has_stack() { + core.back(); + } else { + core.select::(); + } } } + State::Settings { account } => { + + Panel::new(self) + .with_caption("Settings") + .with_back(|this| { + this.state = State::Select; + }) + .with_header(|_ctx,_ui| { + // ui.label("Please enter the wallet secret"); + }) + .with_body(|this,ui| { + + ui.checkbox(&mut this.context.transfer_funds, "Transfer funds during scan"); + + ui.label(""); + ui.label("This option will transfer any discovered funds to the first change address of this account."); + + }) + .with_footer(|this,ui| { + if ui.large_button("Continue").clicked() { + this.state = State::WalletSecret { account }; + this.focus.next(Focus::WalletSecret) + } + }) + .render(ui); + } State::WalletSecret { account } => { let submit = Rc::new(RefCell::new(false)); @@ -193,7 +235,7 @@ impl ModuleT for Scanner { &mut this.focus, Focus::WalletSecret, |ui, text| { - ui.label(egui::RichText::new("Enter your wallet secret").size(12.).raised()); + ui.label(RichText::new("Enter your wallet secret").size(12.).raised()); ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) .vertical_align(Align::Center) .password(true)) @@ -206,9 +248,8 @@ impl ModuleT for Scanner { .build(ui); }) .with_footer(|this,ui| { - let size = theme_style().large_button_size; let enabled = !this.context.wallet_secret.is_empty(); - if ui.add_enabled(enabled, egui::Button::new("Continue").min_size(size)).clicked() { + if ui.large_button_enabled(enabled,"Continue").clicked() { *submit.borrow_mut() = true; } }) @@ -226,6 +267,7 @@ impl ModuleT for Scanner { let status = self.context.status.clone(); let abortable = self.context.abortable.clone(); let wallet_secret = Secret::from(self.context.wallet_secret.as_str()); + let transfer_funds = self.context.transfer_funds; self.context.wallet_secret.zeroize(); spawn(async move { @@ -239,7 +281,7 @@ impl ModuleT for Scanner { 0, usize::MAX, 64, - false, + transfer_funds, &abortable, Some(Arc::new(move |index,utxo_count, balance, txid|{ if let Some(txid) = txid { @@ -288,7 +330,7 @@ impl ModuleT for Scanner { ui.label(format!("Scanning address derivation {}...", index.separated_string())); ui.label(format!("Located {} UTXOs", utxo_count.separated_string())); ui.add_space(16.); - ui.label(egui::RichText::new("BALANCE").size(12.).raised()); + ui.label(RichText::new("BALANCE").size(12.).raised()); ui.label( s2kws_layout_job(*balance, &network_type, theme_color().balance_color,FontId::proportional(28.)) ); @@ -326,15 +368,16 @@ impl ModuleT for Scanner { ui.label(format!("Total addresses scanned: {}", index.separated_string())); ui.label(format!("Located {} UTXOs", utxo_count.separated_string())); ui.add_space(16.); - ui.label(egui::RichText::new("BALANCE").size(12.).raised()); + ui.label(RichText::new("BALANCE").size(12.).raised()); ui.label( s2kws_layout_job(*balance, &network_type, theme_color().balance_color,FontId::proportional(28.)) ); } }) - .with_footer(|_this,ui| { + .with_footer(|this,ui| { if ui.large_button("Close").clicked() { + this.context.zeroize(); core.select::(); } }) diff --git a/core/src/modules/settings/mod.rs b/core/src/modules/settings/mod.rs index 808dffa..74b6af1 100644 --- a/core/src/modules/settings/mod.rs +++ b/core/src/modules/settings/mod.rs @@ -1,7 +1,5 @@ -use clap::error::ErrorKind as ClapErrorKind; -use kaspad_lib::args::Args; -use crate::{imports::*, runtime::services::kaspa::Config}; +use crate::imports::*; pub struct Settings { #[allow(dead_code)] @@ -155,6 +153,10 @@ impl Settings { #[cfg(not(target_arch = "wasm32"))] if core.settings.developer.enable_custom_daemon_args() && core.settings.node.node_kind.is_config_capable() { + use kaspad_lib::args::Args; + use clap::error::ErrorKind as ClapErrorKind; + use crate::runtime::services::kaspa::Config; + ui.add_space(4.); ui.checkbox(&mut self.settings.node.kaspad_daemon_args_enable, i18n("Enable custom daemon arguments")); ui.add_space(4.); @@ -183,13 +185,13 @@ impl Settings { match Args::parse(args.iter()) { Ok(_) => { }, Err(err) => { + if matches!(err.kind(), ClapErrorKind::DisplayHelp | ClapErrorKind::DisplayVersion) { ui.label( RichText::new("--help and --version are not allowed") .color(theme_color().warning_color), ); } else { - println!("err: {:?}", err); let help = err.to_string(); let lines = help.split('\n').collect::>(); let text = if let Some(idx) = lines.iter().position(|line| line.starts_with("For more info") || line.starts_with("Usage:")) { @@ -334,71 +336,42 @@ impl Settings { ui.separator(); } } - CollapsingHeader::new("Plugins") + + CollapsingHeader::new("Centralized Services") .default_open(true) .show(ui, |ui| { - // let enable_plugins = self.settings.enable_plugins; - // if ui.checkbox(&mut self.settings.enable_plugins, i18n("Enable Plugins")).changed() { - // if self.settings.enable_plugins { - // self.runtime.plugin_manager_service().start_plugins(&self.settings).await.unwrap(); - // } else { - // self.runtime.plugin_manager_service().terminate_plugins(); - // } - // } - - if self.settings.enable_plugins { - - let plugins = runtime().plugin_manager_service().plugins(); - for plugin in plugins.iter() { - let plugin_name = plugin.name(); - let mut plugin_enabled = runtime().plugin_manager_service().is_enabled(plugin); - ui.collapsable(plugin_name, true, |ui,state|{ - if ui.add(Label::new(plugin.name()).sense(Sense::click())).clicked() { - *state = !*state; - } - ui.add_space(8.); - if ui.checkbox(&mut plugin_enabled,"Enable").changed() { - runtime().plugin_manager_service().enable(plugin, plugin_enabled); - } - }, |ui|{ - ui.vertical(|ui| { - // ui.set_max_width(340.); - - // ui.separator(); - plugin.render(ui); - // ui.label(plugin_name); - // ui.add(Separator::default().horizontal().); - - }); - - }); - // CollapsingHeader::new(plugin_name) - // .default_open(true) - // .show(ui, |ui| { - // }); - - // ui.horizontal(|ui|{ - // ui.checkbox(&mut runtime().plugin_manager_service().plugin_settings_mut().get_mut(plugin_name).unwrap().enabled, plugin_name); - // ui.label(plugin.description()); - // }); - } - } + CollapsingHeader::new("Market Monitor") + .default_open(true) + .show(ui, |ui| { + let mut v = false; + if ui.checkbox(&mut v, i18n("Enable Market Monitor")).changed() { + } + }); + #[cfg(not(target_arch = "wasm32"))] + CollapsingHeader::new("Check for Updates") + .default_open(true) + .show(ui, |ui| { + let mut v = false; + if ui.checkbox(&mut v, i18n("Check for Software Updates via GitHub")).changed() { + } + + }); }); - // ---------------------------- + CollapsingHeader::new("Advanced") .default_open(false) .show(ui, |ui| { ui.vertical(|ui|{ - // ui.set_max_width(340.); ui.checkbox(&mut self.settings.developer.enable, i18n("Developer Mode")); ui.label("Developer mode enables advanced and experimental features"); }); ui.vertical(|ui|{ if self.settings.developer.enable { + #[cfg(not(target_arch = "wasm32"))] ui.checkbox( &mut self.settings.developer.enable_experimental_features, i18n("Enable experimental features") @@ -406,11 +379,12 @@ impl Settings { 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") + i18n("Allow custom daemon arguments") ).on_hover_text_at_pointer( - i18n("Enables custom arguments for the Rusty Kaspa daemon") + i18n("Allow custom arguments for the Rusty Kaspa daemon") ); ui.checkbox( @@ -420,6 +394,7 @@ impl Settings { i18n("Removes security restrictions, allows for single-letter passwords") ); + #[cfg(not(target_arch = "wasm32"))] ui.checkbox( &mut self.settings.developer.enable_screen_capture, i18n("Screen capture") @@ -429,8 +404,6 @@ impl Settings { } }); - // ui.separator(); - if self.settings.developer != core.settings.developer { ui.add_space(16.); if let Some(response) = ui.confirm_medium_apply_cancel(Align::Max) { @@ -485,7 +458,7 @@ impl Settings { }); - // if ui.button("Test Toast").clicked() { + // if ui.button("Test Toast").clicked() { // self.runtime.try_send(Events::Notify { // notification : UserNotification::info("Test Toast") // }).unwrap(); diff --git a/core/src/modules/testing.rs b/core/src/modules/testing.rs index 49ba9d8..2d12f80 100644 --- a/core/src/modules/testing.rs +++ b/core/src/modules/testing.rs @@ -158,12 +158,12 @@ impl Testing { // self.text("icon 2 clicked"); // } - // let icon = CompositeIcon::new(egui::RichText::new(egui_phosphor::bold::UMBRELLA).size(100.0).color(Color32::RED)).text("Hello").padding(Some((10.0, 10.0).into())); + // let icon = CompositeIcon::new(RichText::new(egui_phosphor::bold::UMBRELLA).size(100.0).color(Color32::RED)).text("Hello").padding(Some((10.0, 10.0).into())); // if ui.add(icon).clicked(){ // self.text("icon 3 clicked"); // } - // let icon = CompositeIcon::new(egui::RichText::new(egui_phosphor::bold::UMBRELLA)).text("Hello").sense(Sense::hover()); + // let icon = CompositeIcon::new(RichText::new(egui_phosphor::bold::UMBRELLA)).text("Hello").sense(Sense::hover()); // if ui.add(icon).clicked(){ // self.text("icon 3 clicked"); // } @@ -253,11 +253,11 @@ impl Testing { // }); // ui.vertical(|ui| { // // ui.set_width(width-theme.error_icon_size.outer_width()); - // // ui.label(egui::RichText::new("Error unlocking wallet").color(egui::Color32::from_rgb(255, 120, 120))); + // // ui.label(RichText::new("Error unlocking wallet").color(egui::Color32::from_rgb(255, 120, 120))); // }); // }); ui.label( - egui::RichText::new(err.to_string()) + RichText::new(err.to_string()) .color(egui::Color32::from_rgb(255, 120, 120)), ); ui.label(" "); diff --git a/core/src/modules/wallet_create.rs b/core/src/modules/wallet_create.rs index 92b6335..c5f8993 100644 --- a/core/src/modules/wallet_create.rs +++ b/core/src/modules/wallet_create.rs @@ -3,9 +3,8 @@ use crate::imports::*; use kaspa_wallet_core::api::WalletCreateResponse; use kaspa_wallet_core::runtime::{AccountKind, AccountCreateArgs, PrvKeyDataCreateArgs, WalletCreateArgs}; use slug::slugify; -use passwords::analyzer; -use passwords::scorer; use kaspa_bip32::WordCount; +use crate::utils::{secret_score, secret_score_to_text}; #[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] enum Focus { @@ -75,6 +74,20 @@ struct Context { // mnemonic: Vec, } +impl Zeroize for Context { + fn zeroize(&mut self) { + self.wallet_name.zeroize(); + self.wallet_filename.zeroize(); + self.account_name.zeroize(); + self.phishing_hint.zeroize(); + self.wallet_secret.zeroize(); + self.wallet_secret_confirm.zeroize(); + self.payment_secret.zeroize(); + self.payment_secret_confirm.zeroize(); + // self.mnemonic_presenter_context.zeroize(); + } +} + pub struct WalletCreate { #[allow(dead_code)] runtime: Runtime, @@ -496,7 +509,6 @@ impl ModuleT for WalletCreate { if change { - // if ((this.context.wallet_secret.is_not_empty() || this.context.wallet_secret_confirm.is_not_empty())) { let wallet_secret = this .context .wallet_secret @@ -507,18 +519,18 @@ impl ModuleT for WalletCreate { .is_not_empty() .then_some(this.context.wallet_secret_confirm.clone()) ); - this.context.wallet_secret_score = wallet_secret.map(password_score); //Some(password_score(&this.context.wallet_secret)); + this.context.wallet_secret_score = wallet_secret.map(secret_score); //Some(password_score(&this.context.wallet_secret)); } if let Some(score) = this.context.wallet_secret_score { ui.label(""); - ui.label(format!("Password score: {}",score_to_text(score))); + ui.label(format!("Secret score: {}",secret_score_to_text(score))); if score < 80.0 { ui.label(""); - ui.label(RichText::new(i18n("Password is too weak")).color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new(i18n("Secret is too weak")).color(error_color())); if !core.settings.developer.disable_password_restrictions() { submit = false; - ui.label(RichText::new(i18n("Please create a stronger password")).color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new(i18n("Please create a stronger password")).color(error_color())); } } } @@ -526,7 +538,7 @@ impl ModuleT for WalletCreate { if this.context.wallet_secret_confirm.is_not_empty() && this.context.wallet_secret != this.context.wallet_secret_confirm { ui.label(" "); - ui.label(egui::RichText::new(i18n("Passwords do not match")).color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new(i18n("Passwords do not match")).color(error_color())); ui.label(" "); submit = false; } else { @@ -634,12 +646,12 @@ impl ModuleT for WalletCreate { .is_not_empty() .then_some(this.context.wallet_secret_confirm.clone()) ); - this.context.payment_secret_score = payment_secret.map(password_score); + this.context.payment_secret_score = payment_secret.map(secret_score); } if let Some(score) = this.context.payment_secret_score { ui.label(""); - ui.label(score_to_text(score)); + ui.label(secret_score_to_text(score)); if score < 80.0 { ui.label(""); ui.label(RichText::new(i18n("Passphrase is too weak")).color(egui::Color32::from_rgb(255, 120, 120))); @@ -749,9 +761,12 @@ impl ModuleT for WalletCreate { if let Some(result) = wallet_create_result.take() { match result { Ok((mnemonic,account)) => { + self.context.zeroize(); + println!("Wallet created successfully"); self.state = State::PresentMnemonic(mnemonic); - core.get_mut::().select(Some(account.into())); + let device = core.device().clone(); + core.get_mut::().select(Some(account.into()), device); } Err(err) => { println!("Wallet creation error: {}", err); @@ -769,8 +784,8 @@ impl ModuleT for WalletCreate { .with_header(move |this,ui| { ui.label(" "); ui.label(" "); - ui.label(egui::RichText::new("Error creating a wallet").color(egui::Color32::from_rgb(255, 120, 120))); - ui.label(egui::RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new("Error creating a wallet").color(egui::Color32::from_rgb(255, 120, 120))); + ui.label(RichText::new(err.to_string()).color(egui::Color32::from_rgb(255, 120, 120))); ui.label(" "); ui.label(" "); @@ -849,29 +864,3 @@ impl ModuleT for WalletCreate { } } - -fn password_score(password : impl AsRef) -> f64 { - scorer::score(&analyzer::analyze(password)) -} - -fn score_to_text(value: f64) -> String { - if (0.0..=20.0).contains(&value) { - return String::from(i18n("Very dangerous (may be cracked within few seconds)")); - } else if value > 20.0 && value <= 40.0 { - return String::from(i18n("Dangerous")); - } else if value > 40.0 && value <= 60.0 { - return String::from(i18n("Very weak")); - } else if value > 60.0 && value <= 80.0 { - return String::from(i18n("Weak")); - } else if value > 80.0 && value <= 90.0 { - return String::from(i18n("Good")); - } else if value > 90.0 && value <= 95.0 { - return String::from(i18n("Strong")); - } else if value > 95.0 && value <= 99.0 { - return String::from(i18n("Very strong")); - } else if value > 99.0 && value <= 100.0 { - return String::from(i18n("Invulnerable")); - } else { - return String::from("Value is outside the defined range"); - } -} \ No newline at end of file diff --git a/core/src/modules/wallet_open.rs b/core/src/modules/wallet_open.rs index e7572b0..550c3a7 100644 --- a/core/src/modules/wallet_open.rs +++ b/core/src/modules/wallet_open.rs @@ -15,7 +15,6 @@ pub struct WalletOpen { wallet_secret: String, pub state: State, pub message: Option, - // selected_wallet: Option, } impl WalletOpen { @@ -25,7 +24,6 @@ impl WalletOpen { wallet_secret: String::new(), state: State::Select, message: None, - // selected_wallet: None, } } @@ -33,10 +31,6 @@ impl WalletOpen { self.state = State::Unlock { wallet_descriptor, error : None}; } - // pub fn lock(&mut self) { - // // Go to unlock page - // self.state = State::Unlock(None); - // } } impl ModuleT for WalletOpen { @@ -72,7 +66,6 @@ impl ModuleT for WalletOpen { .with_body(|this, ui| { for wallet_descriptor in core.borrow_mut().wallet_list.clone().into_iter() { if ui.add_sized(theme_style().large_button_size(), CompositeButton::image_and_text( - // Composite::Icon(egui_phosphor::thin::LOCK_KEY_OPEN), Composite::icon(egui_phosphor::thin::FINGERPRINT_SIMPLE), wallet_descriptor.title.as_deref().unwrap_or("NO NAME"), wallet_descriptor.filename.clone(), @@ -113,7 +106,7 @@ impl ModuleT for WalletOpen { if let Some(err) = error { ui.label( - egui::RichText::new(err.to_string()) + RichText::new(err.to_string()) .color(egui::Color32::from_rgb(255, 120, 120)), ); ui.label(" "); diff --git a/core/src/modules/wallet_secret.rs b/core/src/modules/wallet_secret.rs new file mode 100644 index 0000000..56ffc69 --- /dev/null +++ b/core/src/modules/wallet_secret.rs @@ -0,0 +1,365 @@ +use egui_phosphor::thin::SEAL_WARNING; + +use crate::imports::*; +use crate::utils::{secret_score, render_secret_score_text}; + +#[derive(Clone)] +pub enum State { + Start, + WalletSecret, + Processing, + Error { error : Arc }, + Finish, +} + +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] +enum Focus { + #[default] + None, + OldWalletSecret, + NewWalletSecret, + NewWalletSecretConfirm, +} + +#[derive(Default)] +struct WalletSecretContext { + old_wallet_secret: String, + new_wallet_secret: String, + new_wallet_secret_confirm: String, + show_secrets: bool, + new_wallet_secret_score: Option, +} + +impl Zeroize for WalletSecretContext { + fn zeroize(&mut self) { + self.old_wallet_secret.zeroize(); + self.new_wallet_secret.zeroize(); + self.new_wallet_secret_confirm.zeroize(); + self.show_secrets = false; + self.new_wallet_secret_score = None; + } +} + +pub struct WalletSecret { + #[allow(dead_code)] + runtime: Runtime, + context: WalletSecretContext, + state: State, + focus: FocusManager, +} + +impl Zeroize for WalletSecret { + fn zeroize(&mut self) { + self.context.zeroize(); + self.state = State::Start; + self.focus.next(Focus::None); + } +} + +impl WalletSecret { + pub fn new(runtime: Runtime) -> Self { + Self { + runtime, + context: WalletSecretContext::default(), + state: State::Start, + focus: FocusManager::default(), + } + } +} + +impl ModuleT for WalletSecret { + + fn style(&self) -> ModuleStyle { + ModuleStyle::Mobile + } + + fn modal(&self) -> bool { + true + } + + fn render( + &mut self, + core: &mut Core, + _ctx: &egui::Context, + _frame: &mut eframe::Frame, + ui: &mut egui::Ui, + ) { + let secret_change_result = Payload::>::new("secret_change_result"); + + match self.state.clone() { + State::Start => { + let back = Rc::new(RefCell::new(false)); + if !core.state().is_open() { + Panel::new(self) + .with_caption("Change Wallet Secret") + .with_back(|_this| { + *back.borrow_mut() = true; + }) + .with_header(|_ctx,_ui| { + }) + .with_body(|_this,ui| { + ui.add_space(16.); + ui.label( + RichText::new(SEAL_WARNING) + .size(theme_style().icon_size_large) + .color(theme_color().error_color) + ); + ui.add_space(16.); + ui.label("This feature requires an open wallet"); + }) + .with_footer(|_,ui| { + if ui.large_button("Close").clicked() { + *back.borrow_mut() = true; + } + }) + .render(ui); + + if *back.borrow() { + core.back(); + } + + } else { + self.state = State::WalletSecret; + } + } + State::WalletSecret => { + + + let back = Rc::new(RefCell::new(false)); + let mut submit = false; + let mut allow = true; + // let mut back = false; + + Panel::new(self) + .with_caption("Change Wallet Secret") + .with_back(|_this| { + *back.borrow_mut() = true; + }) + .with_close_enabled(false, |_|{ + // *back.borrow_mut() = true + }) + .with_header(|_ctx,_ui| { + }) + .with_body(|this,ui| { + TextEditor::new( + &mut this.context.old_wallet_secret, + &mut this.focus, + Focus::OldWalletSecret, + |ui, text| { + ui.label(RichText::new("Enter your current wallet secret").size(12.).raised()); + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) + .vertical_align(Align::Center) + .password(!this.context.show_secrets)) + }, + ).submit(|text,focus| { + if !text.is_empty() { + focus.next(Focus::NewWalletSecret) + } + }) + .build(ui); + + ui.add_space(32.); + + let mut change = false; + + TextEditor::new( + &mut this.context.new_wallet_secret, + &mut this.focus, + Focus::NewWalletSecret, + |ui, text| { + ui.label(RichText::new("Enter new wallet secret").size(12.).raised()); + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) + .vertical_align(Align::Center) + .password(!this.context.show_secrets)) + }, + ) + .change(|_|{ + change = true; + }) + .submit(|text,focus| { + if !text.is_empty() { + focus.next(Focus::NewWalletSecretConfirm) + } + }) + .build(ui); + + ui.add_space(8.); + TextEditor::new( + &mut this.context.new_wallet_secret_confirm, + &mut this.focus, + Focus::NewWalletSecretConfirm, + |ui, text| { + ui.label(RichText::new("Validate new wallet secret").size(12.).raised()); + ui.add_sized(theme_style().panel_editor_size, TextEdit::singleline(text) + .vertical_align(Align::Center) + .password(!this.context.show_secrets)) + }, + ) + .change(|_|{ + change = true; + }) + .submit(|text,focus| { + if !text.is_empty() { + focus.next(Focus::None) + } + }) + .build(ui); + + ui.add_space(24.); + + ui.checkbox(&mut this.context.show_secrets, "Show secrets in clear text"); + ui.add_space(16.); + + if change { + let wallet_secret = this + .context + .new_wallet_secret + .is_not_empty() + .then_some(this.context.new_wallet_secret.clone()) + .or(this.context + .new_wallet_secret_confirm + .is_not_empty() + .then_some(this.context.new_wallet_secret_confirm.clone()) + ); + this.context.new_wallet_secret_score = wallet_secret.map(secret_score); //Some(password_score(&this.context.wallet_secret)); + } + + if let Some(score) = this.context.new_wallet_secret_score { + ui.label(""); + render_secret_score_text(ui, "Secret score:", score); + if score < 80.0 && !core.settings.developer.disable_password_restrictions() { + allow = false; + ui.label(RichText::new(i18n("Please enter a stronger secret")).color(error_color())); + } + ui.label(""); + } else if this.context.new_wallet_secret_confirm.is_not_empty() && this.context.new_wallet_secret != this.context.new_wallet_secret_confirm { + ui.label(" "); + ui.label(RichText::new(i18n("Secrets do not match")).color(error_color())); + ui.label(" "); + allow = false; + } else { + ui.label(" "); + } + + }) + .with_footer(|this,ui| { + let enabled = this.context.old_wallet_secret.is_not_empty() && + this.context.new_wallet_secret.is_not_empty() && + this.context.new_wallet_secret == this.context.new_wallet_secret_confirm; + + if ui.large_button_enabled(enabled,"Change Secret").clicked() { + submit = true; + } + }) + .render(ui); + + if *back.borrow() { + self.zeroize(); + core.back(); + } else if submit { + self.state = State::Processing; + self.focus.next(Focus::None); + } + + } + State::Processing => { + + Panel::new(self) + .with_caption("Change Wallet Secret") + .with_close_enabled(false, |_|{ + }) + .with_header(|_ctx,ui| { + ui.label("Processing..."); + }) + .with_body(|_this,ui| { + + + ui.add_space(64.); + ui.add(egui::Spinner::new().size(92.)); + + }) + .render(ui); + + if !secret_change_result.is_pending() { + let old_wallet_secret = Secret::from(self.context.old_wallet_secret.as_str()); + let new_wallet_secret = Secret::from(self.context.new_wallet_secret.as_str()); + let wallet = self.runtime.wallet().clone(); + spawn_with_result(&secret_change_result, async move { + wallet.wallet_change_secret(old_wallet_secret, new_wallet_secret).await?; + Ok(()) + }); + } + + if let Some(result) = secret_change_result.take() { + match result { + Ok(()) => { + self.state = State::Finish; + self.context.zeroize(); + } + Err(err) => { + self.state = State::Error { error : Arc::new(err) }; + } + } + } + } + + State::Error { error } => { + + Panel::new(self) + .with_caption("Change Wallet Secret") + .with_header(|_ctx,_ui| { + }) + .with_body(|_this,ui| { + + ui.label( + RichText::new(SEAL_WARNING) + .size(theme_style().icon_size_large) + .color(theme_color().error_color) + ); + ui.add_space(8.); + ui.colored_label(error_color(), format!("{error}")); + + }) + .with_footer(|this,ui| { + if ui.large_button("Retry").clicked() { + this.state = State::WalletSecret; + this.focus.next(Focus::NewWalletSecret); + } + if ui.large_button("Close").clicked() { + this.zeroize(); + if core.has_stack() { + core.back(); + } else { + core.select::(); + } + } + }) + .render(ui); + } + + State::Finish => { + + Panel::new(self) + .with_caption("Change Wallet Secret") + .with_header(|_ctx,_ui| { + }) + .with_body(|_this,ui| { + + ui.label("The wallet secret has been changed successfully."); + + }) + .with_footer(|this,ui| { + if ui.large_button("Close").clicked() { + this.zeroize(); + if core.has_stack() { + core.back(); + } else { + core.select::(); + } + } + }) + .render(ui); + } + } + } +} diff --git a/core/src/notifications.rs b/core/src/notifications.rs index 3cea48e..ae1c266 100644 --- a/core/src/notifications.rs +++ b/core/src/notifications.rs @@ -49,7 +49,9 @@ impl UserNotification { } pub fn error(text: impl Into) -> Self { - Self::new(UserNotifyKind::Error, text) + let text = text.into(); + // println!("error: {}", text); + Self::new(UserNotifyKind::Error, text).duration(Duration::from_millis(5000)) } pub fn success(text: impl Into) -> Self { @@ -60,6 +62,11 @@ impl UserNotification { Self::new(UserNotifyKind::Basic, text) } + pub fn duration(mut self, duration: Duration) -> Self { + self.duration = Some(duration); + self + } + pub fn short(mut self) -> Self { self.duration = Some(Duration::from_millis(1500)); self diff --git a/core/src/primitives/transaction.rs b/core/src/primitives/transaction.rs index 23d5719..32c2144 100644 --- a/core/src/primitives/transaction.rs +++ b/core/src/primitives/transaction.rs @@ -1,23 +1,27 @@ use crate::imports::*; +use egui_phosphor::light::*; use kaspa_consensus_core::tx::{TransactionInput, TransactionOutpoint, TransactionOutput}; use kaspa_wallet_core::storage::{ transaction::{TransactionData, UtxoRecord}, - TransactionType, + TransactionKind, }; pub trait AsColor { fn as_color(&self) -> Color32; } -impl AsColor for TransactionType { +impl AsColor for TransactionKind { fn as_color(&self) -> Color32 { match self { - TransactionType::Incoming => theme_color().transaction_incoming, - TransactionType::Outgoing => theme_color().transaction_outgoing, - TransactionType::External => theme_color().transaction_external, - TransactionType::Reorg => theme_color().transaction_reorg, - TransactionType::Batch => theme_color().transaction_batch, - TransactionType::Stasis => theme_color().transaction_stasis, + TransactionKind::Incoming => theme_color().transaction_incoming, + TransactionKind::Outgoing => theme_color().transaction_outgoing, + TransactionKind::External => theme_color().transaction_external, + TransactionKind::Reorg => theme_color().transaction_reorg, + TransactionKind::Batch => theme_color().transaction_batch, + TransactionKind::Stasis => theme_color().transaction_stasis, + TransactionKind::TransferIncoming => theme_color().transaction_transfer_incoming, + TransactionKind::TransferOutgoing => theme_color().transaction_transfer_outgoing, + TransactionKind::Change => theme_color().transaction_change, } } } @@ -126,8 +130,7 @@ impl Transaction { _include_utxos: bool, largest: Option, ) { - let ppp = ui.ctx().pixels_per_point(); - let width = ui.available_width() / ppp; + let width = ui.available_width() / ui.ctx().pixels_per_point(); let Context { record, maturity } = &*self.context(); @@ -147,39 +150,30 @@ impl Transaction { let default_color = theme_color().default_color; let strong_color = theme_color().strong_color; - let font_id_header = FontId::monospace(15.0); - let font_id_content = FontId::monospace(15.0); - let icon_font_id = FontId::proportional(18.0); + let content_font = FontId::monospace(15.0); + let icon_font = FontId::proportional(18.0); - let header = LayoutJobBuilderSettings::new(width, 8.0, Some(font_id_content.clone())); - let content = LayoutJobBuilderSettings::new(width, 8.0, Some(font_id_content.clone())); + let header = LayoutJobBuilderSettings::new(width, 8.0, Some(content_font.clone())); + let content = LayoutJobBuilderSettings::new(width, 8.0, Some(content_font.clone())); + + let is_transfer = record.is_transfer(); match record.transaction_data() { - TransactionData::Reorg { - utxo_entries, - aggregate_input_value, - } - | TransactionData::Stasis { - utxo_entries, - aggregate_input_value, - } - | TransactionData::Incoming { - utxo_entries, - aggregate_input_value, - } - | TransactionData::External { - utxo_entries, - aggregate_input_value, - } => { - let aggregate_input_value = ps2k(*aggregate_input_value); - let mut job = LayoutJobBuilder::new(width, 8.0, Some(font_id_header.clone())) - .with_icon_font(icon_font_id); - job = job.icon( - egui_phosphor::light::ARROW_SQUARE_RIGHT, - TransactionType::Incoming.as_color(), - ); + TransactionData::Reorg { utxo_entries, .. } + | TransactionData::Stasis { utxo_entries, .. } + | TransactionData::Incoming { utxo_entries, .. } + | TransactionData::TransferIncoming { utxo_entries, .. } + | TransactionData::External { utxo_entries, .. } => { + let value = ps2k(record.value()); + + let mut job = ljb(&header).with_icon_font(icon_font); + if is_transfer { + job = job.icon(DOTS_THREE_CIRCLE, TransactionKind::Incoming.as_color()); + } else { + job = job.icon(ARROW_CIRCLE_RIGHT, TransactionKind::Incoming.as_color()); + } - if maturity.unwrap_or(false) { + if !maturity.unwrap_or(false) { let maturity_progress = current_daa_score.and_then(|current_daa_score| { record .maturity_progress(current_daa_score) @@ -193,12 +187,12 @@ impl Transaction { job = job .text(timestamp.as_str(), default_color) - .text(&aggregate_input_value, TransactionType::Incoming.as_color()); + .text(&value, TransactionKind::Incoming.as_color()); // ui.LayoutJobBuilder::new(width,8.0(&transaction_id, false, |ui,state| { // ui.horizontal( |ui| { - // let icon = RichText::new(egui_phosphor::light::ARROW_SQUARE_RIGHT).color(TransactionType::Incoming.as_color()); + // let icon = RichText::new(egui_phosphor::light::ARROW_SQUARE_RIGHT).color(TransactionKind::Incoming.as_color()); // if ui.add(Label::new(icon).sense(Sense::click())).clicked() { // *state = !*state; // } @@ -230,60 +224,61 @@ impl Transaction { // }); // }, |ui| { - CollapsingHeader::new(job) + let mut collapsing_header = CollapsingHeader::new(job) .id_source(&transaction_id) .icon(paint_header_icon) - .default_open(false) - .show(ui, |ui| { - ljb(&content) - .padded(15, "Transaction id:", default_color) - .text(&transaction_id, default_color) - .label(ui); + .default_open(false); - ljb(&content) - .padded(15, "Received at:", default_color) - .text(&format!("{} DAA", block_daa_score), default_color) - .label(ui); + if !maturity.unwrap_or(true) { + collapsing_header = collapsing_header.icon(|ui, _rect, response| { + Spinner::new().paint_at(ui, response.rect.expand(4.)); + }); + } + + collapsing_header.show(ui, |ui| { + ljb(&content) + .padded(15, "Transaction id:", default_color) + .text(&transaction_id, default_color) + .label(ui); - utxo_entries.iter().for_each(|utxo_entry| { - let UtxoRecord { - index: _, - address, - amount, - script_public_key, - is_coinbase, - } = utxo_entry; - let address = address - .as_ref() - .map(|addr| addr.to_string()) - .unwrap_or_else(|| "n/a".to_string()); - - ljb(&content).text(&address, default_color).label(ui); - - if *is_coinbase { - ljb(&content) - .text( - &format!("{} - Coinbase UTXO", s2k(*amount)), - default_color, - ) - .label(ui); - } else { - ljb(&content) - .text( - &format!("{} - Standard UTXO", s2k(*amount)), - default_color, - ) - .label(ui); - } + ljb(&content) + .padded(15, "Received at:", default_color) + .text(&format!("{} DAA", block_daa_score), default_color) + .label(ui); + + utxo_entries.iter().for_each(|utxo_entry| { + let UtxoRecord { + index: _, + address, + amount, + script_public_key, + is_coinbase, + } = utxo_entry; + let address = address + .as_ref() + .map(|addr| addr.to_string()) + .unwrap_or_else(|| "n/a".to_string()); + ljb(&content).text(&address, default_color).label(ui); + + if *is_coinbase { + ljb(&content) + .text(&format!("{} - Coinbase UTXO", s2k(*amount)), default_color) + .label(ui); + } else { ljb(&content) - .text( - &format!("Script: {}", script_public_key.script_as_hex()), - default_color, - ) + .text(&format!("{} - Standard UTXO", s2k(*amount)), default_color) .label(ui); - }); + } + + ljb(&content) + .text( + &format!("Script: {}", script_public_key.script_as_hex()), + default_color, + ) + .label(ui); }); + }); } TransactionData::Outgoing { fees, @@ -293,20 +288,29 @@ impl Transaction { change_value, accepted_daa_score, .. + } + | TransactionData::TransferOutgoing { + fees, + aggregate_input_value, + transaction, + payment_value, + change_value, + accepted_daa_score, + .. } => { let job = if let Some(payment_value) = payment_value { - let mut job = ljb(&header).with_icon_font(icon_font_id); + let mut job = ljb(&header).with_icon_font(icon_font); - job = job - .icon( - egui_phosphor::light::ARROW_SQUARE_LEFT, - TransactionType::Outgoing.as_color(), - ) - .text(timestamp.as_str(), default_color) - .text( - &ps2k(*payment_value + *fees), - TransactionType::Outgoing.as_color(), - ); + if is_transfer { + job = job.icon(DOTS_THREE_CIRCLE, TransactionKind::Outgoing.as_color()); + } else { + job = job.icon(ARROW_CIRCLE_LEFT, TransactionKind::Outgoing.as_color()); + } + + job = job.text(timestamp.as_str(), default_color).text( + &ps2k(*payment_value + *fees), + TransactionKind::Outgoing.as_color(), + ); if !maturity.unwrap_or(true) { job = job.text("Submitting...", strong_color); @@ -320,7 +324,7 @@ impl Transaction { .text("Fees:", default_color) .text( &sompi_to_kaspa_string(*fees), - TransactionType::Outgoing.as_color(), + TransactionKind::Outgoing.as_color(), ) .text("Change:", default_color) .text(&sompi_to_kaspa_string(*change_value), strong_color) @@ -329,7 +333,7 @@ impl Transaction { // ui.collapsable(&transaction_id, false, |ui,state| { // ui.horizontal( |ui| { - // let icon = RichText::new(egui_phosphor::light::ARROW_SQUARE_LEFT).color(TransactionType::Outgoing.as_color()); + // let icon = RichText::new(egui_phosphor::light::ARROW_SQUARE_LEFT).color(TransactionKind::Outgoing.as_color()); // if ui.add(Label::new(icon).sense(Sense::click())).clicked() { // *state = !*state; // } @@ -365,7 +369,7 @@ impl Transaction { // // .text("Fees:", default_color) // // .text( // // &sompi_to_kaspa_string(*fees), - // // TransactionType::Outgoing.as_color(), + // // TransactionKind::Outgoing.as_color(), // // ) // // .text("Change:", default_color) // // .text(&sompi_to_kaspa_string(*change_value), strong_color) @@ -391,8 +395,6 @@ impl Transaction { }); } collapsing_header.show(ui, |ui| { - let width = ui.available_width() - 64.0; - ljb(&content) .text( &format!("{}: {}", "Transaction id", transaction_id), @@ -418,13 +420,13 @@ impl Transaction { if let Some(payment_value) = payment_value { ljb(&content) .padded(15, "Amount:", default_color) - .text(&ps2k(*payment_value), TransactionType::Outgoing.as_color()) + .text(&ps2k(*payment_value), TransactionKind::Outgoing.as_color()) .label(ui); } ljb(&content) - .padded(14, "Fees:", default_color) - .text(&ps2k(*fees), TransactionType::Outgoing.as_color()) + .padded(15, "Fees:", default_color) + .text(&ps2k(*fees), TransactionKind::Outgoing.as_color()) .label(ui); ljb(&content) @@ -434,12 +436,12 @@ impl Transaction { ljb(&content) .padded(15, "Change:", default_color) - .text(&ps2k(*change_value), TransactionType::Incoming.as_color()) + .text(&ps2k(*change_value), TransactionKind::Incoming.as_color()) .label(ui); ljb(&content) .text( - &format!("{} UTXO inputs", transaction.inputs.len()), + &format!("UTXO inputs ({})", transaction.inputs.len()), default_color, ) .label(ui); @@ -460,19 +462,19 @@ impl Transaction { .text( &format!( " {sequence:>2}: {}:{index} SigOps: {sig_op_count}", - transaction_id.to_string() + transaction_id ), default_color, ) .label(ui); } - let text = LayoutJobBuilder::new(width, 16.0, Some(font_id_content.clone())) + ljb(&content) .text( - &format!("{} UTXO outputs:", transaction.outputs.len()), + &format!("UTXO outputs ({})", transaction.outputs.len()), default_color, - ); - ui.label(text); + ) + .label(ui); for output in transaction.outputs.iter() { let TransactionOutput { @@ -493,6 +495,43 @@ impl Transaction { } }); } + TransactionData::Batch { fees, .. } => { + let aggregate_input_value = record.aggregate_input_value(); + let mut job = ljb(&header).with_icon_font(icon_font); + job = job.icon(CIRCLES_FOUR, TransactionKind::Batch.as_color()); + + job = job.text(timestamp.as_str(), default_color).text( + &ps2k(aggregate_input_value), + TransactionKind::Batch.as_color(), + ); + + let mut collapsing_header = CollapsingHeader::new(job) + .id_source(&transaction_id) + .icon(paint_header_icon) + .default_open(false); + + if !maturity.unwrap_or(true) { + collapsing_header = collapsing_header.icon(|ui, _rect, response| { + Spinner::new().paint_at(ui, response.rect.expand(4.)); + }); + } + + collapsing_header.show(ui, |ui| { + ljb(&content) + .text("Sweep:", default_color) + .text(&sompi_to_kaspa_string(aggregate_input_value), strong_color) + .label(ui); + + ljb(&content) + .text("Fees:", default_color) + .text( + &sompi_to_kaspa_string(*fees), + TransactionKind::Outgoing.as_color(), + ) + .label(ui); + }); + } + TransactionData::Change { .. } => {} } } } diff --git a/core/src/runtime/device.rs b/core/src/runtime/device.rs deleted file mode 100644 index 637abce..0000000 --- a/core/src/runtime/device.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::imports::*; - -#[derive(Default)] -struct Inner { - pub is_portrait: bool, - pub is_mobile: bool, -} - -#[derive(Default)] -pub struct Device { - inner: Arc>, -} - -impl Device { - pub fn new() -> Self { - Self { - inner: Arc::new(Mutex::new(Inner { - is_portrait: false, - is_mobile: false, - })), - } - } - - fn inner(&self) -> MutexGuard<'_, Inner> { - self.inner.lock().unwrap() - } - - pub fn is_portrait(&self) -> bool { - self.inner().is_portrait - } - - pub fn is_mobile(&self) -> bool { - self.inner().is_mobile - } - - pub fn toggle_portrait(&self) { - let mut inner = self.inner(); - inner.is_portrait = !inner.is_portrait; - } - - pub fn toggle_mobile(&self) { - let mut inner = self.inner(); - inner.is_mobile = !inner.is_mobile; - } - - pub fn is_single_pane(&self) -> bool { - let inner = self.inner(); - inner.is_mobile || inner.is_portrait - } -} diff --git a/core/src/runtime/mod.rs b/core/src/runtime/mod.rs index 73050fc..7768889 100644 --- a/core/src/runtime/mod.rs +++ b/core/src/runtime/mod.rs @@ -10,12 +10,9 @@ cfg_if! { } pub mod channel; -pub mod device; pub mod payload; -pub mod plugins; pub mod services; pub mod system; -pub use device::Device; pub use payload::Payload; pub use services::Service; use services::*; @@ -29,13 +26,13 @@ pub struct Inner { peer_monitor_service: Arc, metrics_service: Arc, block_dag_monitor_service: Arc, - plugin_manager_service: Arc, + market_monitor_service: Arc, + update_monitor_service: Arc, application_events: ApplicationEventsChannel, egui_ctx: egui::Context, is_running: Arc, start_time: Instant, system: Option, - device: Device, } /// Runtime is a core component of the Kaspa NG application responsible for @@ -62,7 +59,11 @@ impl Runtime { application_events.clone(), settings, )); - let plugin_manager_service = Arc::new(PluginManagerService::new( + let market_monitor_service = Arc::new(MarketMonitorService::new( + application_events.clone(), + settings, + )); + let update_monitor_service = Arc::new(UpdateMonitorService::new( application_events.clone(), settings, )); @@ -73,7 +74,8 @@ impl Runtime { peer_monitor_service.clone(), metrics_service.clone(), block_dag_monitor_service.clone(), - plugin_manager_service.clone(), + market_monitor_service.clone(), + update_monitor_service.clone(), ]); let runtime = Self { @@ -83,15 +85,14 @@ impl Runtime { repaint_service, kaspa, peer_monitor_service, - plugin_manager_service, + market_monitor_service, + update_monitor_service, metrics_service, block_dag_monitor_service, egui_ctx: egui_ctx.clone(), is_running: Arc::new(AtomicBool::new(false)), start_time: Instant::now(), - system: Some(system), - device: Device::default(), }), }; @@ -108,10 +109,6 @@ impl Runtime { &self.inner.system } - pub fn device(&self) -> &Device { - &self.inner.device - } - pub fn start_services(&self) { let services = self.services(); for service in services { @@ -191,8 +188,12 @@ impl Runtime { &self.inner.block_dag_monitor_service } - pub fn plugin_manager_service(&self) -> &Arc { - &self.inner.plugin_manager_service + pub fn market_monitor_service(&self) -> &Arc { + &self.inner.market_monitor_service + } + + pub fn update_monitor_service(&self) -> &Arc { + &self.inner.update_monitor_service } /// Returns the reference to the application events channel. @@ -231,7 +232,6 @@ impl Runtime { .send(Events::Error(Box::new(err.to_string()))) .await .unwrap(); - // println!("spawned task error: {:?}", err); } }); } diff --git a/core/src/runtime/plugins/mod.rs b/core/src/runtime/plugins/mod.rs deleted file mode 100644 index 0c0286a..0000000 --- a/core/src/runtime/plugins/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::imports::*; - -pub mod market_monitor; -pub use market_monitor::MarketMonitorPlugin; - -#[async_trait] -pub trait Plugin: Sync + Send { - /// Short identifier of the plugin (used for storage of options in the application settings) - fn ident(&self) -> &'static str; - - /// Human-readable name of the plugin - fn name(&self) -> &'static str; - - /// User interface rendering of the plugin within the settings panel - fn render(&self, ui: &mut Ui); - - fn store(&self) -> Result>; - fn load(&self, data: serde_json::Value) -> Result<()>; - - /// Indicates if a plugin is currently enabled. The plugin - /// will not be started if it is not enabled. - // fn is_enabled(&self) -> bool; - - /// Called when the plugin needs to be started - async fn start(self: Arc) -> Result<()>; - - /// Signal the plugin termination (post a shutdown request) - fn terminate(self: Arc); - - /// Block until the plugin is terminated - async fn join(self: Arc) -> Result<()>; - - /// Called when Kaspa RPC API is available (connection to the node is established successfully) - async fn attach_rpc(self: Arc, _rpc_api: &Arc) -> Result<()> { - Ok(()) - } - - /// Called when Kaspa RPC API is no longer available (node is disconnected) - async fn detach_rpc(self: Arc) -> Result<()> { - Ok(()) - } -} diff --git a/core/src/runtime/plugins/market_monitor/coingecko.rs b/core/src/runtime/services/market_monitor/coingecko.rs similarity index 69% rename from core/src/runtime/plugins/market_monitor/coingecko.rs rename to core/src/runtime/services/market_monitor/coingecko.rs index e0a96a8..84b5d92 100644 --- a/core/src/runtime/plugins/market_monitor/coingecko.rs +++ b/core/src/runtime/services/market_monitor/coingecko.rs @@ -24,9 +24,9 @@ use workflow_http::get_json; // "name": "01coin" // }, -#[derive(Default, Serialize, Deserialize)] +#[derive(Default, Debug, Serialize, Deserialize)] struct CoinGeckoSimplePrice { - kaspa: Option>, + kaspa: Option>, } impl CoinGeckoSimplePrice { @@ -42,9 +42,9 @@ impl CoinGeckoSimplePrice { } } -impl From for MarketPriceMap { +impl From for MarketDataMap { fn from(data: CoinGeckoSimplePrice) -> Self { - let mut prices = HashMap::new(); + let mut prices = AHashMap::new(); if let Some(kaspa) = data.kaspa { prices = group_by_currency_prefix(&kaspa); } @@ -58,24 +58,30 @@ pub async fn fetch_available_currencies() -> Result { Ok(available_currencies) } -pub async fn fetch_market_price_list(currencies: &[&str]) -> Result { +pub async fn fetch_market_price_list(currencies: &[&str]) -> Result { let market_data = CoinGeckoSimplePrice::get(currencies).await?; + // println!("market_data: {:?}", market_data); + Ok(market_data.into()) } -fn group_by_currency_prefix(data: &HashMap) -> MarketPriceMap { - let mut grouped_data: MarketPriceMap = HashMap::new(); +fn group_by_currency_prefix(data: &AHashMap) -> MarketDataMap { + let mut grouped_data: MarketDataMap = AHashMap::new(); for (coin, info) in data.iter() { - let parts: Vec<&str> = coin.split('_').collect(); - let currency_prefix = parts[0].to_lowercase(); - let suffix = parts.last().map(|suffix| suffix.to_lowercase()); + let mut parts: Vec<&str> = coin.split('_').collect(); + if parts.is_empty() { + continue; + } + let currency_prefix = parts.remove(0).to_lowercase(); + let suffix = parts.join("_"); let existing_data = grouped_data.entry(currency_prefix.clone()).or_default(); - match suffix.as_deref() { - None => existing_data.price = Some(*info), - Some("market_cap") => existing_data.market_cap = Some(*info), - Some("24h_vol") => existing_data.volume = Some(*info), - Some("24h_change") => existing_data.change = Some(*info), + + match suffix.as_str() { + "" => existing_data.price = Some(*info), + "market_cap" => existing_data.market_cap = Some(*info), + "24h_vol" => existing_data.volume = Some(*info), + "24h_change" => existing_data.change = Some(*info), _ => (), } } diff --git a/core/src/runtime/plugins/market_monitor/coinmarketcap.rs b/core/src/runtime/services/market_monitor/coinmarketcap.rs similarity index 86% rename from core/src/runtime/plugins/market_monitor/coinmarketcap.rs rename to core/src/runtime/services/market_monitor/coinmarketcap.rs index 843fcf3..bbae97a 100644 --- a/core/src/runtime/plugins/market_monitor/coinmarketcap.rs +++ b/core/src/runtime/services/market_monitor/coinmarketcap.rs @@ -3,7 +3,7 @@ use workflow_http::get_json; #[derive(Default, Serialize, Deserialize)] struct CoinGeckoSimplePrice { - kaspa: Option>, + kaspa: Option>, } impl CoinGeckoSimplePrice { @@ -19,9 +19,9 @@ impl CoinGeckoSimplePrice { } } -impl From for MarketPriceMap { +impl From for MarketDataMap { fn from(data: CoinGeckoSimplePrice) -> Self { - let mut prices = HashMap::new(); + let mut prices = AHashMap::new(); if let Some(kaspa) = data.kaspa { prices = group_by_currency_prefix(&kaspa); } @@ -35,13 +35,13 @@ pub async fn fetch_available_currencies() -> Result { Ok(available_currencies) } -pub async fn fetch_market_price_list(currencies: &[&str]) -> Result { +pub async fn fetch_market_price_list(currencies: &[&str]) -> Result { let market_data = CoinGeckoSimplePrice::get(currencies).await?; Ok(market_data.into()) } -fn group_by_currency_prefix(data: &HashMap) -> MarketPriceMap { - let mut grouped_data: MarketPriceMap = HashMap::new(); +fn group_by_currency_prefix(data: &AHashMap) -> MarketDataMap { + let mut grouped_data: MarketDataMap = AHashMap::new(); for (coin, info) in data.iter() { let parts: Vec<&str> = coin.split('_').collect(); diff --git a/core/src/runtime/plugins/market_monitor/mod.rs b/core/src/runtime/services/market_monitor/mod.rs similarity index 67% rename from core/src/runtime/plugins/market_monitor/mod.rs rename to core/src/runtime/services/market_monitor/mod.rs index 4978b11..09a0d4c 100644 --- a/core/src/runtime/plugins/market_monitor/mod.rs +++ b/core/src/runtime/services/market_monitor/mod.rs @@ -1,11 +1,10 @@ use crate::imports::*; -use crate::runtime::plugins::Plugin; -// use workflow_http::get_json; +use crate::market::*; mod coingecko; mod coinmarketcap; -pub const POLLING_INTERVAL_SECONDS: usize = 60; +pub const POLLING_INTERVAL_SECONDS: u64 = 60; #[derive(Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -24,7 +23,7 @@ impl MarketDataProvider { } } - async fn fetch_market_price_list(&self, currencies: &[&str]) -> Result { + async fn fetch_market_price_list(&self, currencies: &[&str]) -> Result { match self { Self::CoinGecko => coingecko::fetch_market_price_list(currencies).await, Self::CoinMarketCap => coinmarketcap::fetch_market_price_list(currencies).await, @@ -55,16 +54,6 @@ struct MarketMonitorSettings { // } // } -#[derive(Default, Debug)] -pub struct MarketPrice { - pub price: Option, - pub market_cap: Option, - pub volume: Option, - pub change: Option, -} - -pub type MarketPriceMap = HashMap; - // struct MarketPriceList { // pub prices: HashMap, // } @@ -84,7 +73,7 @@ pub enum MarketMonitorEvents { Exit, } -pub struct MarketMonitorPlugin { +pub struct MarketMonitorService { pub application_events: ApplicationEventsChannel, pub plugin_events: Channel, pub task_ctl: Channel<()>, @@ -92,11 +81,16 @@ pub struct MarketMonitorPlugin { pub currencies: Mutex>>, pub provider: Mutex, pub available_currencies: Mutex>>, - pub market_price_list: Mutex>>, + pub market_price_list: Mutex>>, } -impl MarketMonitorPlugin { - pub fn new(application_events: ApplicationEventsChannel) -> Self { +impl MarketMonitorService { + pub fn new(application_events: ApplicationEventsChannel, _settings: &Settings) -> Self { + let currencies = ["usd", "btc"] + .into_iter() + .map(String::from) + .collect::>(); + Self { application_events, plugin_events: Channel::unbounded(), @@ -104,7 +98,8 @@ impl MarketMonitorPlugin { is_enabled: AtomicBool::new(false), provider: Mutex::new(MarketDataProvider::default()), // ------ - currencies: Mutex::new(Some(vec!["usd".to_string()])), + // currencies: Mutex::new(Some(vec!["usd".to_string()])), + currencies: Mutex::new(Some(currencies)), // ------ // currencies: Mutex::new(None), available_currencies: Mutex::new(None), @@ -136,11 +131,18 @@ impl MarketMonitorPlugin { if let Ok(market_price_list) = self.provider().fetch_market_price_list(¤cies).await { - println!("market price list: {:?}", market_price_list); - self.market_price_list - .lock() - .unwrap() - .replace(Arc::new(market_price_list)); + // println!("market price list: {:?}", market_price_list); + // self.market_price_list + // .lock() + // .unwrap() + // .replace(Arc::new(market_price_list)); + + self.application_events + .sender + .try_send(Events::Market(MarketUpdate::Price(Arc::new( + market_price_list, + )))) + .unwrap(); // println!("market_data: {:?}", market_data); } } @@ -149,48 +151,47 @@ impl MarketMonitorPlugin { } #[async_trait] -impl Plugin for MarketMonitorPlugin { - fn ident(&self) -> &'static str { - "market-monitor" - } +impl Service for MarketMonitorService { + // fn ident(&self) -> &'static str { + // "market-monitor" + // } fn name(&self) -> &'static str { - "Market Monitor" - } - - fn load(&self, settings: serde_json::Value) -> Result<()> { - let MarketMonitorSettings { - enabled, - provider, - currencies, - } = serde_json::from_value(settings)?; - self.is_enabled.store(enabled, Ordering::SeqCst); - self.currencies.lock().unwrap().replace(currencies); - *self.provider.lock().unwrap() = provider; - - Ok(()) - } - - fn store(&self) -> Result> { - let settings = MarketMonitorSettings { - enabled: self.is_enabled.load(Ordering::SeqCst), - provider: self.provider.lock().unwrap().clone(), - currencies: self.currencies.lock().unwrap().clone().unwrap_or_default(), - }; - - Ok(Some(serde_json::to_value(settings)?)) + "market-monitor" } - async fn start(self: Arc) -> Result<()> { + // fn load(&self, settings: serde_json::Value) -> Result<()> { + // let MarketMonitorSettings { + // enabled, + // provider, + // currencies, + // } = serde_json::from_value(settings)?; + // self.is_enabled.store(enabled, Ordering::SeqCst); + // self.currencies.lock().unwrap().replace(currencies); + // *self.provider.lock().unwrap() = provider; + + // Ok(()) + // } + + // fn store(&self) -> Result> { + // let settings = MarketMonitorSettings { + // enabled: self.is_enabled.load(Ordering::SeqCst), + // provider: self.provider.lock().unwrap().clone(), + // currencies: self.currencies.lock().unwrap().clone().unwrap_or_default(), + // }; + + // Ok(Some(serde_json::to_value(settings)?)) + // } + + 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(1)); + let interval = interval(Duration::from_secs(POLLING_INTERVAL_SECONDS)); pin_mut!(interval); loop { select! { _ = interval.next().fuse() => { - println!("Updating market price list..."); this.update_market_price_list().await?; }, @@ -225,9 +226,9 @@ impl Plugin for MarketMonitorPlugin { Ok(()) } - fn render(&self, ui: &mut Ui) { - ui.label("Market Monitor"); + // fn render(&self, ui: &mut Ui) { + // ui.label("Market Monitor"); - ui.label("TODO - Add Market Monitor Settings"); - } + // ui.label("TODO - Add Market Monitor Settings"); + // } } diff --git a/core/src/runtime/services/metrics_monitor.rs b/core/src/runtime/services/metrics_monitor.rs index 2204ddc..73c733b 100644 --- a/core/src/runtime/services/metrics_monitor.rs +++ b/core/src/runtime/services/metrics_monitor.rs @@ -1,7 +1,7 @@ use crate::imports::*; use crate::runtime::Service; pub use futures::{future::FutureExt, select, Future}; -use kaspa_metrics::{Metric, Metrics, MetricsSnapshot}; +use kaspa_metrics_core::{Metric, Metrics, MetricsSnapshot}; #[allow(unused_imports)] use kaspa_wallet_core::rpc::{NotificationMode, Rpc, RpcCtl, WrpcEncoding}; diff --git a/core/src/runtime/services/mod.rs b/core/src/runtime/services/mod.rs index 9791989..a49a22b 100644 --- a/core/src/runtime/services/mod.rs +++ b/core/src/runtime/services/mod.rs @@ -15,8 +15,11 @@ pub use metrics_monitor::MetricsService; pub mod blockdag_monitor; pub use blockdag_monitor::BlockDagMonitorService; -pub mod plugin_manager; -pub use plugin_manager::PluginManagerService; +pub mod market_monitor; +pub use market_monitor::MarketMonitorService; + +pub mod update_monitor; +pub use update_monitor::UpdateMonitorService; /// Service is a core component of the Kaspa NG application responsible for /// running application services and communication between these services. diff --git a/core/src/runtime/services/update_monitor.rs b/core/src/runtime/services/update_monitor.rs new file mode 100644 index 0000000..d6013af --- /dev/null +++ b/core/src/runtime/services/update_monitor.rs @@ -0,0 +1,117 @@ +use crate::imports::*; + +// TODO - move to settings +pub const UPDATE_POLLING_INTERVAL_SECONDS: u64 = 60 * 60 * 12; + +pub enum UpdateMonitorEvents { + Enable, + Disable, + Exit, +} + +pub struct UpdateMonitorService { + pub application_events: ApplicationEventsChannel, + pub service_events: Channel, + pub task_ctl: Channel<()>, + pub rpc_api: Mutex>>, + pub is_enabled: Arc, +} + +impl UpdateMonitorService { + pub fn new(application_events: ApplicationEventsChannel, _settings: &Settings) -> Self { + Self { + application_events, + service_events: Channel::unbounded(), + task_ctl: Channel::oneshot(), + rpc_api: Mutex::new(None), + is_enabled: Arc::new(AtomicBool::new(false)), + } + } + + pub fn rpc_api(&self) -> Option> { + self.rpc_api.lock().unwrap().clone() + } + + pub fn enable(&self) { + self.service_events + .sender + .try_send(UpdateMonitorEvents::Enable) + .unwrap(); + } + + pub fn disable(&self) { + self.service_events + .sender + .try_send(UpdateMonitorEvents::Disable) + .unwrap(); + } +} + +#[async_trait] +impl Service for UpdateMonitorService { + fn name(&self) -> &'static str { + "peer-monitor" + } + + async fn attach_rpc(self: Arc, rpc_api: &Arc) -> Result<()> { + self.rpc_api.lock().unwrap().replace(rpc_api.clone()); + Ok(()) + } + + async fn detach_rpc(self: Arc) -> Result<()> { + self.rpc_api.lock().unwrap().take(); + + Ok(()) + } + + 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(UPDATE_POLLING_INTERVAL_SECONDS)); + pin_mut!(interval); + + loop { + select! { + _ = interval.next().fuse() => { + if !self.is_enabled.load(Ordering::Relaxed) { + continue; + } + + }, + msg = this.as_ref().service_events.receiver.recv().fuse() => { + if let Ok(event) = msg { + match event { + UpdateMonitorEvents::Enable => { + self.is_enabled.store(true, Ordering::Relaxed); + } + UpdateMonitorEvents::Disable => { + self.is_enabled.store(false, Ordering::Relaxed); + } + UpdateMonitorEvents::Exit => { + break; + } + } + } else { + break; + } + } + } + } + + this.task_ctl.send(()).await.unwrap(); + Ok(()) + } + + fn terminate(self: Arc) { + self.service_events + .sender + .try_send(UpdateMonitorEvents::Exit) + .unwrap(); + } + + async fn join(self: Arc) -> Result<()> { + self.task_ctl.recv().await.unwrap(); + Ok(()) + } +} diff --git a/core/src/runtime/system.rs b/core/src/runtime/system.rs index acee622..5402500 100644 --- a/core/src/runtime/system.rs +++ b/core/src/runtime/system.rs @@ -46,7 +46,7 @@ cfg_if! { } pub fn render(&self, ui: &mut Ui) { - use kaspa_metrics::data::as_data_size; + use kaspa_metrics_core::data::as_data_size; CollapsingHeader::new(i18n("System")) .default_open(true) diff --git a/core/src/settings.rs b/core/src/settings.rs index fc5afc2..c6d9190 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -1,5 +1,5 @@ use crate::imports::*; -use kaspa_metrics::Metric; +use kaspa_metrics_core::Metric; use kaspa_utils::networking::ContextualNetAddress; use kaspa_wallet_core::storage::local::storage::Storage; use kaspa_wrpc_client::WrpcEncoding; diff --git a/core/src/status.rs b/core/src/status.rs index 307a73b..9571070 100644 --- a/core/src/status.rs +++ b/core/src/status.rs @@ -1,6 +1,6 @@ use crate::imports::*; use crate::sync::SyncStatus; -use kaspa_metrics::MetricsSnapshot; +use kaspa_metrics_core::MetricsSnapshot; enum ConnectionStatus { Connected { @@ -37,8 +37,8 @@ impl<'core> Status<'core> { self.core.module() } - fn device(&self) -> &Device { - runtime().device() + fn device(&mut self) -> &Device { + self.core.device() } fn metrics(&self) -> &Option> { @@ -136,7 +136,7 @@ impl<'core> Status<'core> { } KaspadNodeKind::Remote => { ui.label( - RichText::new(egui_phosphor::light::TREE_STRUCTURE) + RichText::new(egui_phosphor::light::CLOUD_X) .size(status_icon_size) .color(theme_color().error_color), ); @@ -186,7 +186,7 @@ impl<'core> Status<'core> { } } - if !self.device().is_single_pane() { + if !self.device().single_pane() { module.status_bar(self.core, ui); } } @@ -219,14 +219,16 @@ impl<'core> Status<'core> { // }); // }); - if !self.device().is_single_pane() { + if !self.device().mobile() { ui.separator(); self.render_peers(ui, peers); if let Some(current_daa_score) = current_daa_score { ui.separator(); ui.label(format!("DAA {}", current_daa_score.separated_string())); } + } + if !self.device().single_pane() { module.status_bar(self.core, ui); } } @@ -244,7 +246,7 @@ impl<'core> Status<'core> { ui.separator(); self.render_network_selector(ui); - if !self.device().is_single_pane() { + if !self.device().single_pane() { ui.separator(); self.render_peers(ui, peers); if let Some(status) = sync_status.as_ref() { diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index 7ff5650..8749eb6 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -18,6 +18,8 @@ mod image; pub use image::*; mod version; pub use version::*; +mod secret; +pub use secret::*; #[macro_export] macro_rules! spawn { diff --git a/core/src/utils/secret.rs b/core/src/utils/secret.rs new file mode 100644 index 0000000..ced9acb --- /dev/null +++ b/core/src/utils/secret.rs @@ -0,0 +1,43 @@ +use crate::imports::*; +use passwords::analyzer; +use passwords::scorer; + +pub fn secret_score(password: impl AsRef) -> f64 { + scorer::score(&analyzer::analyze(password)) +} + +pub fn secret_score_to_text(score: f64) -> String { + if (0.0..=20.0).contains(&score) { + return String::from(i18n("Very dangerous (may be cracked within few seconds)")); + } else if score > 20.0 && score <= 40.0 { + return String::from(i18n("Dangerous")); + } else if score > 40.0 && score <= 60.0 { + return String::from(i18n("Very weak")); + } else if score > 60.0 && score <= 80.0 { + return String::from(i18n("Weak")); + } else if score > 80.0 && score <= 90.0 { + return String::from(i18n("Good")); + } else if score > 90.0 && score <= 95.0 { + return String::from(i18n("Strong")); + } else if score > 95.0 && score <= 99.0 { + return String::from(i18n("Very strong")); + } else if score > 99.0 && score <= 100.0 { + return String::from(i18n("Invulnerable")); + } else { + return String::from("Value is outside the defined range"); + } +} + +pub fn render_secret_score_text(ui: &mut Ui, prefix: impl Into, score: f64) { + let text = format!("{}: {}", prefix.into(), secret_score_to_text(score)); + + let color = if score < 80.0 { + error_color() + } else if score < 90.0 { + warning_color() + } else { + theme_color().strong_color + }; + + ui.colored_label(color, text); +} diff --git a/resources/i18n/i18n.json b/resources/i18n/i18n.json index 77c01ad..c07a641 100644 --- a/resources/i18n/i18n.json +++ b/resources/i18n/i18n.json @@ -11,16 +11,16 @@ "languages": { "pt": "Português", "hi": "Hindi", - "fi": "Finnish", + "nl": "Dutch", "vi": "Vietnamese", "fil": "Filipino", + "pa": "Panjabi", "fa": "Farsi", + "fi": "Finnish", "lt": "Lithuanian", "sv": "Swedish", - "pa": "Panjabi", - "nl": "Dutch", - "uk": "Ukrainian", "es": "Español", + "uk": "Ukrainian", "af": "Afrikaans", "et": "Esti", "en": "English", @@ -59,16 +59,16 @@ "translations": { "pt": {}, "hi": {}, - "fi": {}, + "nl": {}, "vi": {}, "fil": {}, + "pa": {}, "fa": {}, + "fi": {}, "lt": {}, "sv": {}, - "es": {}, - "pa": {}, - "nl": {}, "uk": {}, + "es": {}, "af": {}, "et": {}, "en": { @@ -107,6 +107,7 @@ "Enable custom daemon arguments": "Enable custom daemon arguments", "Storage Read/s": "Storage Read/s", "Rusty Kaspa Daemon Path:": "Rusty Kaspa Daemon Path:", + "gRPC Rx/s": "gRPC Rx/s", "Connections": "Connections", "Allows you to specify custom arguments for the Rusty Kaspa daemon": "Allows you to specify custom arguments for the Rusty Kaspa daemon", "Copied to clipboard": "Copied to clipboard", @@ -142,6 +143,7 @@ "All": "All", "Json Connection Attempts": "Json Connection Attempts", "Tools ⏷": "Tools ⏷", + "p2p Rx/s": "p2p Rx/s", "Redistributables": "Redistributables", "Connects to a Remote Rusty Kaspa Node via wRPC.": "Connects to a Remote Rusty Kaspa Node via wRPC.", "Difficulty": "Difficulty", @@ -152,33 +154,42 @@ "Password is too weak": "Password is too weak", "Handles": "Handles", "Please create a stronger password": "Please create a stronger password", + "Enable Market Monitor": "Enable Market Monitor", "Very dangerous (may be cracked within few seconds)": "Very dangerous (may be cracked within few seconds)", "Account:": "Account:", "Good": "Good", + "Secret is too weak": "Secret is too weak", "DAA": "DAA", - "Signature Type": "Signature Type", + "Network": "Network", "Time Offset:": "Time Offset:", "Net Rx/s": "Net Rx/s", - "Network": "Network", + "Signature Type": "Signature Type", + "gRPC Tx": "gRPC Tx", "Dependencies": "Dependencies", "Active p2p Peers": "Active p2p Peers", "Disable Password Score Restrictions": "Disable Password Score Restrictions", "Chain Blocks": "Chain Blocks", "GitHub Release Page for": "GitHub Release Page for", + "Check for Software Updates via GitHub": "Check for Software Updates via GitHub", + "p2p Tx": "p2p Tx", + "p2p Rx": "p2p Rx", "The balance may be out of date during node sync": "The balance may be out of date during node sync", "Parent levels": "Parent levels", "Continue": "Continue", "WASM SDK for JavaScript and TypeScript": "WASM SDK for JavaScript and TypeScript", "Kaspa p2p Node": "Kaspa p2p Node", "Enable experimental features": "Enable experimental features", + "p2p Tx/s": "p2p Tx/s", "gRPC User Tx": "gRPC User Tx", "NPM Modules for NodeJS": "NPM Modules for NodeJS", "wRPC Encoding:": "wRPC Encoding:", "License Information": "License Information", "Storage Write": "Storage Write", + "gRPC Rx": "gRPC Rx", "Processed Bodies": "Processed Bodies", "Stor Write": "Stor Write", "Processed Mass Counts": "Processed Mass Counts", + "Allow custom arguments for the Rusty Kaspa daemon": "Allow custom arguments for the Rusty Kaspa daemon", "User Agent": "User Agent", "bye!": "bye!", "The node is spawned as a child daemon process (recommended).": "The node is spawned as a child daemon process (recommended).", @@ -233,12 +244,14 @@ "Rusty Kaspa on GitHub": "Rusty Kaspa on GitHub", "Ping:": "Ping:", "Enable UPnP": "Enable UPnP", + "No peers": "No peers", "Bodies": "Bodies", "Network Difficulty": "Network Difficulty", "IBD:": "IBD:", "Key Perf.": "Key Perf.", "Processed Dependencies": "Processed Dependencies", "Bandwidth": "Bandwidth", + "gRPC Tx/s": "gRPC Tx/s", "Total Rx": "Total Rx", "Rust Wallet SDK": "Rust Wallet SDK", "Enables features currently in development": "Enables features currently in development",