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