From 9bb8637537f895d511a53a656dfe3c18cf6abd65 Mon Sep 17 00:00:00 2001 From: Anton Yemelyanov Date: Thu, 23 Nov 2023 06:41:04 +0200 Subject: [PATCH] wip - send interface --- README.md | 15 +- core/src/egui/extensions.rs | 106 ++++++ core/src/egui/theme.rs | 2 + core/src/modules/account_manager.rs | 552 +++++++++++++++++----------- core/src/modules/block_dag.rs | 2 +- core/src/modules/overview.rs | 318 ++++++++++------ core/src/primitives/descriptors.rs | 9 +- core/src/utils/mod.rs | 51 +++ 8 files changed, 716 insertions(+), 339 deletions(-) diff --git a/README.md b/README.md index 097ab18..34004ee 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,17 @@ Access via [https://localhost:8080](https://localhost:8080) This project currently supports Chrome browser extension target, but this component of the project is under heavy development and is not ready for use. ``` ./build-chrome -``` \ No newline at end of file +``` + +
+ Windows x64 +Windows build instructions +
+
+ Linux +Linux build instructions +
+
+ Mac OS +Mac OS build instructions +
diff --git a/core/src/egui/extensions.rs b/core/src/egui/extensions.rs index 36c666b..ff3b2d2 100644 --- a/core/src/egui/extensions.rs +++ b/core/src/egui/extensions.rs @@ -163,3 +163,109 @@ impl From for WidgetText { builder.job.into() } } + +pub trait HyperlinkExtension { + fn hyperlink_to_tab(&mut self, text: impl Into, url: impl Into) + -> Response; + fn hyperlink_url_to_tab(&mut self, url: impl Into) -> Response; +} + +impl HyperlinkExtension for Ui { + fn hyperlink_to_tab( + &mut self, + text: impl Into, + url: impl Into, + ) -> Response { + let url = url.into(); + Hyperlink::from_label_and_url(text, url) + .open_in_new_tab(true) + .ui(self) + } + fn hyperlink_url_to_tab(&mut self, url: impl Into) -> Response { + let url = url.into(); + Hyperlink::from_label_and_url(url.clone(), url) + .open_in_new_tab(true) + .ui(self) + } +} + +type TextEditorCreateFn<'editor> = Box Response + 'editor>; +type TextEditorChangeFn<'editor> = Box; +type TextEditorSubmitFn<'editor, Focus> = Box; + +pub struct TextEditor<'editor, Focus> +where + Focus: PartialEq + Copy, +{ + user_text: &'editor mut String, + focus_mut: &'editor mut Focus, + focus_value: Focus, + editor_create_fn: TextEditorCreateFn<'editor>, + editor_change_fn: Option>, + editor_submit_fn: Option>, +} + +impl<'editor, Focus> TextEditor<'editor, Focus> +where + Focus: PartialEq + Copy, +{ + pub fn new( + user_text: &'editor mut String, + focus_mut_ref: &'editor mut Focus, + focus_value: Focus, + editor_create_fn: impl FnOnce(&mut Ui, &mut String) -> Response + 'editor, + ) -> Self { + Self { + user_text, + focus_mut: focus_mut_ref, + focus_value, + editor_create_fn: Box::new(editor_create_fn), + editor_change_fn: None, + editor_submit_fn: None, + } + } + + pub fn change(mut self, change: impl FnOnce(&str) + 'editor) -> Self { + self.editor_change_fn = Some(Box::new(change)); + self + } + + pub fn submit(mut self, submit: impl FnOnce(&str, &mut Focus) + 'editor) -> Self { + self.editor_submit_fn = Some(Box::new(submit)); + self + } + + pub fn build(self, ui: &mut Ui) -> Response { + let TextEditor { + user_text, + focus_mut, + focus_value, + editor_create_fn, + editor_change_fn, + editor_submit_fn, + } = self; + + let mut editor_text = user_text.clone(); + let response = editor_create_fn(ui, &mut editor_text); + + if response.gained_focus() { + *focus_mut = focus_value; + } else if *focus_mut == focus_value && !response.has_focus() { + response.request_focus(); + }; + + if *user_text != editor_text { + *user_text = editor_text; + if let Some(editor_change_fn) = editor_change_fn { + editor_change_fn(user_text.as_str()); + } + } else if response.text_edit_submit(ui) { + *user_text = editor_text; + if let Some(editor_submit_fn) = editor_submit_fn { + editor_submit_fn(user_text.as_str(), focus_mut); + } + } + + response + } +} diff --git a/core/src/egui/theme.rs b/core/src/egui/theme.rs index db9e517..43272e2 100644 --- a/core/src/egui/theme.rs +++ b/core/src/egui/theme.rs @@ -6,6 +6,7 @@ pub struct Theme { pub kaspa_color: Color32, pub hyperlink_color: Color32, pub node_data_color: Color32, + pub balance_color: Color32, pub panel_icon_size: IconSize, pub panel_margin_size: f32, pub error_icon_size: IconSize, @@ -43,6 +44,7 @@ impl Default for Theme { hyperlink_color: Color32::from_rgb(141, 184, 178), // node_data_color : Color32::from_rgb(217, 233,230), node_data_color: Color32::WHITE, + balance_color: Color32::WHITE, // node_data_color : Color32::from_rgb(151, 209, 198), // panel_icon_size : IconSize::new(26.,36.), panel_icon_size: IconSize::new(Vec2::splat(26.)).with_padding(Vec2::new(6., 0.)), diff --git a/core/src/modules/account_manager.rs b/core/src/modules/account_manager.rs index 4cd96d0..c772217 100644 --- a/core/src/modules/account_manager.rs +++ b/core/src/modules/account_manager.rs @@ -20,18 +20,22 @@ enum Details { UtxoSelector } -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq)] enum Action { None, - Sending, Estimating, + Sending, } -impl Action { - // fn is_none(&self) -> bool { - // matches!(self, Action::None) - // } +#[derive(Clone, Copy, Eq, PartialEq)] +enum Focus { + None, + Address, + Amount, + Fees, +} +impl Action { fn is_sending(&self) -> bool { matches!(self, Action::Sending | Action::Estimating) } @@ -45,6 +49,24 @@ enum Estimate { Error(String), } +// impl Estimate { +// fn is_ok(&self) -> bool { +// matches!(self, Estimate::GeneratorSummary(_)) +// } + +// fn error(&mut self, error : impl Into) { +// *self = Estimate::Error(error.into()); +// } +// } + +#[derive(Clone, Eq, PartialEq)] +enum AddressStatus { + Valid, + None, + NetworkMismatch(NetworkType), + Invalid(String), +} + pub struct AccountManager { #[allow(dead_code)] runtime: Runtime, @@ -55,10 +77,14 @@ pub struct AccountManager { destination_address_string : String, send_amount_text: String, send_amount_sompi : u64, + enable_priority_fees : bool, + priority_fees_text : String, + priority_fees_sompi : u64, send_info: Option, - // running_estimate : bool, estimate : Arc>, + address_status : AddressStatus, action : Action, + focus : Focus, wallet_secret : String, payment_secret : String, } @@ -73,12 +99,16 @@ impl AccountManager { destination_address_string : String::new(), send_amount_text: String::new(), send_amount_sompi : 0, + enable_priority_fees : false, + priority_fees_text : String::new(), + priority_fees_sompi : 0, send_info : None, estimate : Arc::new(Mutex::new(Estimate::None)), + address_status : AddressStatus::None, action : Action::None, + focus : Focus::None, wallet_secret : String::new(), payment_secret : String::new(), - // running_estimate : false, } } @@ -95,8 +125,10 @@ impl ModuleT for AccountManager { _frame: &mut eframe::Frame, ui: &mut egui::Ui, ) { + use egui_phosphor::light::{ARROW_CIRCLE_UP,QR_CODE}; + + let theme = theme(); - // let wallet_state = core.state(); let network_type = if let Some(network_id) = core.state().network_id() { network_id.network_type() } else { @@ -119,7 +151,6 @@ impl ModuleT for AccountManager { ui.heading("Select Account"); ui.separator(); - // for account in account_collection.iter() { account_collection.iter().for_each(|account| { if ui .button(format!("Select {}", account.name_or_id())) @@ -143,18 +174,18 @@ impl ModuleT for AccountManager { ui.horizontal(|ui| { - ui.heading("Wallet"); + ui.heading("Kaspa Wallet"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label(format!("Account: {}", account.name_or_id())); }); }); + SidePanel::left("account_manager_left").exact_width(width/2.).resizable(false).show_separator_line(true).show_inside(ui, |ui| { + ui.separator(); + ui.add_space(8.); - - SidePanel::left("account_manager_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]) @@ -162,130 +193,36 @@ impl ModuleT for AccountManager { ui.vertical_centered(|ui| { - - // ui.label("This is the overview page"); let context = if let Some(context) = account.context() { context } else { ui.label("Account is missing context"); return; }; - ui.separator(); - // ui.label(" "); - ui.add_space(8.); - - ui.horizontal(|ui|{ - - let address = format_address(context.address(), Some(8)); - // ui.label(format!("Address: {}", context.address())); - ui.label(format!("Address: {address}")); - if ui.button(RichText::new(egui_phosphor::light::CLIPBOARD_TEXT)).clicked() { - ui.output_mut(|o| o.copied_text = context.address().to_string()); - } - }); - - - // let network_type = if let Some(network_id) = wallet_state.network_id() { - // network_id.network_type() - // } else { - // ui.label("Network is not selected"); - // return; - // }; - - - - - // let balance = account.balance(); - if let Some(balance) = account.balance() { - // ui.label("Balance"); - ui.heading( - RichText::new(sompi_to_kaspa_string_with_suffix(balance.mature, &network_type)).font(FontId::proportional(24.)) - ); - 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 - ) - )); - } - } else { - ui.label("Balance: N/A"); - } - - if let Some((mature_utxo_size, pending_utxo_size)) = - account.utxo_sizes() - { - if pending_utxo_size == 0 { - ui.label(format!( - "UTXOs: {}", - mature_utxo_size, - )); - } else { - ui.label(format!( - "UTXOs: {} ({} pending)", - mature_utxo_size, pending_utxo_size - )); - } - } else { - ui.label("No UTXOs"); - } - - ui.add( - egui::Image::new(ImageSource::Bytes { uri : Cow::Borrowed("bytes://qr.svg"), bytes: context.qr() }) - .fit_to_original_size(1.) - .texture_options(TextureOptions::NEAREST) - // .shrink_to_fit() - ); - // }); - - // ui.separator(); - - - // ----------------------------------------------------------------- - // ----------------------------------------------------------------- - // ----------------------------------------------------------------- + self.render_balance(core, ui, &account, &context, network_type); if self.action.is_sending() { self.render_send_ui(core, ui, &account, &context, network_type); } else { - ui.vertical_centered(|ui|{ + + self.render_qr(core, ui, &context); + ui.vertical_centered(|ui|{ ui.horizontal(|ui| { - if ui.medium_button(format!("{} Send", egui_phosphor::light::ARROW_CIRCLE_UP)).clicked() { - self.action = Action::Estimating; - // self.state = State::Send { - // account: account.clone(), - // }; - } - if ui.medium_button(format!("{} Request", egui_phosphor::light::QR_CODE)).clicked() { - // self.state = State::Receive { - // account: account.clone(), - // }; - } + CenterLayoutBuilder::new() + .add(Button::new(format!("{} Send", ARROW_CIRCLE_UP)).min_size(theme.medium_button_size()), || { + self.action = Action::Estimating; + }) + .add(Button::new(format!("{} Request", QR_CODE)).min_size(theme.medium_button_size()), || {}) + .build(ui); }); - }); } }); - // ----------------------------------------------------------------- - // ----------------------------------------------------------------- - // ----------------------------------------------------------------- }); }); - SidePanel::right("account_manager_right") .exact_width(width/2.) @@ -294,6 +231,10 @@ impl ModuleT for AccountManager { .show_inside(ui, |ui| { ui.separator(); + // --- + 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| { @@ -310,13 +251,10 @@ impl ModuleT for AccountManager { if ui.button("Transactions").clicked() { self.details = Details::Transactions; } - - }); }); ui.separator(); - match self.details { Details::Transactions => { self.render_transactions(ui, core, &account, network_type, current_daa_score); @@ -328,9 +266,7 @@ impl ModuleT for AccountManager { self.render_utxo_selector(ui, core, &account); } } - }); - } State::Send { account: _ } => {} @@ -355,9 +291,7 @@ impl AccountManager { transaction.render(ui, network_type, current_daa_score, true, Some(total)); }); } - }); - } fn render_account_details(&mut self, ui: &mut Ui, _core : &mut Core, account : &Account) { @@ -373,75 +307,216 @@ impl AccountManager { ui.label("Unknown descriptor type"); } } - }); - } fn render_utxo_selector(&mut self, ui: &mut Ui, _core : &mut Core, _account : &Account) { egui::ScrollArea::vertical().auto_shrink([false,false]).show(ui, |ui| { - ui.label("UTXO Selection"); - }); } - fn render_send_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, account : &Account, _context : &Arc, network_type: NetworkType) { + fn render_balance(&mut self, _core: &mut Core, ui : &mut Ui, account : &Account, context : &Context, network_type : NetworkType) { + + let theme = theme(); + + use egui_phosphor::light::CLIPBOARD_TEXT; + let address = format_address(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 = context.address().to_string()); + } + ui.add_space(10.); + + if let Some(balance) = account.balance() { + ui.heading( + RichText::new(sompi_to_kaspa_string_with_suffix(balance.mature, &network_type)).font(FontId::proportional(28.)).color(theme.balance_color) + ); + + 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 + ) + )); + } + } else { + ui.label("Balance: N/A"); + } + ui.add_space(10.); - let size = egui::Vec2::new(300_f32, 32_f32); - let mut proceed_with_estimate = false; + if let Some((mature_utxo_size, pending_utxo_size)) = + account.utxo_sizes() + { + if pending_utxo_size == 0 { + ui.label(format!( + "UTXOs: {}", + mature_utxo_size, + )); + } else { + ui.label(format!( + "UTXOs: {} ({} pending)", + mature_utxo_size, pending_utxo_size + )); + } + } else { + ui.label("No UTXOs"); + } + + } - let mut destination_address_string = self.destination_address_string.clone(); - ui.label(egui::RichText::new("Enter address").size(12.).raised()); + fn render_qr(&mut self, _core: &mut Core, ui : &mut Ui, context : &Context) { - // TODO - address processing... - let _response = ui.add_sized( - size, - TextEdit::singleline(&mut destination_address_string) - // .hint_text("Payment password...") - .vertical_align(Align::Center), + let scale = if self.action == Action::None { 1. } else { 0.35 }; + ui.add( + egui::Image::new(ImageSource::Bytes { uri : Cow::Borrowed("bytes://qr.svg"), bytes: context.qr() }) + .fit_to_original_size(scale) + .texture_options(TextureOptions::NEAREST) ); - if destination_address_string != self.destination_address_string { - self.destination_address_string = destination_address_string; - match try_user_string_to_address(self.destination_address_string.as_str(), &network_type) { - Ok(_address) => {}, + + } + + fn render_send_ui(&mut self, _core: &mut Core, ui: &mut egui::Ui, account : &Account, _context : &Arc, network_type: NetworkType) { + + let theme = theme(); + let size = egui::Vec2::new(300_f32, 32_f32); + let mut request_estimate = false; + + ui.add_space(8.); + ui.label("Sending funds"); + ui.add_space(8.); + + TextEditor::new( + &mut self.destination_address_string, + // None, + &mut self.focus, + Focus::Address, + |ui, text| { + ui.label(egui::RichText::new("Enter destination address").size(12.).raised()); + ui.add_sized(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.address_status = AddressStatus::NetworkMismatch(address_network_type); + } else { + self.address_status = AddressStatus::Valid; + } + } Err(err) => { - self.send_info = Some(err.to_string()); + self.address_status = AddressStatus::Invalid(err.to_string()); } } + }) + .submit(|_, focus|{ + *focus = Focus::Amount; + }) + .build(ui); + + match &self.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}")); + } } - ui.label(egui::RichText::new("Enter amount to send").size(12.).raised()); - let mut send_amount_text = self.send_amount_text.clone(); - let response = ui.add_sized( - size, - TextEdit::singleline(&mut send_amount_text) - // .hint_text("Payment password...") - .vertical_align(Align::Center), - ); - if response.text_edit_submit(ui) { - proceed_with_estimate = true; - } else if self.action == Action::Estimating { - response.request_focus(); + TextEditor::new( + &mut self.send_amount_text, + &mut self.focus, + Focus::Amount, + |ui, text| { + ui.label(egui::RichText::new("Enter KAS amount to send").size(12.).raised()); + ui.add_sized(size, TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|_| { + request_estimate = true; + }) + .submit(|_, focus|{ + if self.enable_priority_fees { + *focus = Focus::Fees; + } else { + self.action = Action::Sending; + } + }) + .build(ui); + + ui.checkbox(&mut self.enable_priority_fees,i18n("Include Priority Fees")); + + if self.enable_priority_fees { + TextEditor::new( + &mut self.priority_fees_text, + &mut self.focus, + Focus::Fees, + |ui, text| { + ui.label(egui::RichText::new("Enter priority fees").size(12.).raised()); + ui.add_sized(size, TextEdit::singleline(text) + .vertical_align(Align::Center)) + }, + ) + .change(|_| { + request_estimate = true; + }) + .submit(|_,_|{ + self.action = Action::Sending; + }) + .build(ui); } if let Some(send_info) = &self.send_info { ui.label(send_info); } - if send_amount_text != self.send_amount_text { - self.send_amount_text = send_amount_text; - match try_kaspa_str_to_sompi(self.send_amount_text.clone()) { - Ok(Some(send_amount_sompi)) => { + match self.action { + Action::Estimating => { + // if request_estimate { + // println!("request estimate: {}", request_estimate); + // } + + if request_estimate && self.update_estimate_args() { + self.send_info = None; - self.send_amount_sompi = send_amount_sompi; + // self.send_amount_sompi = send_amount_sompi; + let priority_fees_sompi = if self.enable_priority_fees { + self.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"), + }; - // - TODO - - let address = Address::try_from("kaspatest:qqz22l98sf8jun72rwh5rqe2tm8lhwtdxdmynrz4ypwak427qed5juktjt7ju").expect("Invalid address"); - // let address = Address::try_from(context.address()).expect("Invalid address"); let runtime = self.runtime.clone(); let account_id = account.id(); let payment_output = PaymentOutput { @@ -450,21 +525,22 @@ impl AccountManager { }; let estimate = self.estimate.clone(); - spawn(async move { let request = AccountsEstimateRequest { task_id: None, account_id, destination: payment_output.into(), - priority_fee_sompi: Fees::SenderPaysAll(0), + priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), payload: None, }; match runtime.wallet().accounts_estimate_call(request).await { Ok(response) => { + println!("estimate ok"); *estimate.lock().unwrap() = Estimate::GeneratorSummary(response.generator_summary); } Err(error) => { + println!("estimate error"); *estimate.lock().unwrap() = Estimate::Error(error.to_string()); } } @@ -472,57 +548,49 @@ impl AccountManager { runtime.egui_ctx().request_repaint(); Ok(()) }); + } - } - Ok(None) => { - self.send_info = None; - *self.estimate.lock().unwrap() = Estimate::None; - } - Err(_) => { - *self.estimate.lock().unwrap() = Estimate::None; - self.send_info = Some("Please enter amount".to_string()); - } - } - } - - match &*self.estimate.lock().unwrap() { - Estimate::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))); - } - ui.label(format!("Fees: {}", sompi_to_kaspa_string_with_suffix(estimate.aggregated_fees, &network_type))); - ui.label(format!("Transactions: {} UTXOs: {}", estimate.number_of_generated_transactions, estimate.aggregated_utxos)); - } - Estimate::Error(error) => { - ui.label(RichText::new(error.to_string()).color(theme().error_color)); - } - Estimate::None => { - ui.label("Please enter KAS amount to send"); - } - } - - - - match self.action { - Action::Estimating => { - - - - - ui.horizontal(|ui| { - if ui.medium_button_enabled(!self.send_amount_text.is_empty() && self.send_amount_sompi > 0,"Send").clicked() { - proceed_with_estimate = true; + let ready_to_send = match &*self.estimate.lock().unwrap() { + Estimate::GeneratorSummary(estimate) => { + println!("rendering 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))); + } + ui.label(format!("Fees: {}", 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.address_status == AddressStatus::Valid } - if proceed_with_estimate { - self.action = Action::Sending; + Estimate::Error(error) => { + ui.label(RichText::new(error.to_string()).color(theme.error_color)); + false } - - if ui.medium_button("Cancel").clicked() { - self.reset(); - // *self.estimate.lock().unwrap() = Estimate::None; - // self.send_amount_text = String::new(); - // self.action = Action::None; + Estimate::None => { + ui.label("Please enter KAS amount to send"); + false } + }; + + ui.horizontal(|ui| { + use egui_phosphor::light::{CHECK, X}; + ui.vertical_centered(|ui|{ + ui.horizontal(|ui| { + let mut reset = false; + CenterLayoutBuilder::new() + .add_enabled(ready_to_send, Button::new(format!("{CHECK} Send")).min_size(theme.medium_button_size()), || { + self.action = Action::Sending; + }) + .add(Button::new(format!("{X} Cancel")).min_size(theme.medium_button_size()), || { + reset = true; + }) + .build(ui); + + if reset { + self.reset(); + } + }); + }); + }); } @@ -552,8 +620,11 @@ impl AccountManager { if proceed_with_send { - // let address = Address::try_from(context.address()).expect("Invalid address"); - let address = Address::try_from("kaspatest:qqz22l98sf8jun72rwh5rqe2tm8lhwtdxdmynrz4ypwak427qed5juktjt7ju").expect("Invalid address"); + let priority_fees_sompi = if self.enable_priority_fees { + self.priority_fees_sompi + } else { 0 }; + + let address = Address::try_from(self.destination_address_string.as_str()).expect("Invalid address"); let runtime = self.runtime.clone(); let account_id = account.id(); let payment_output = PaymentOutput { @@ -561,7 +632,7 @@ impl AccountManager { amount: self.send_amount_sompi, }; let wallet_secret = Secret::try_from(self.wallet_secret.clone()).expect("Invalid secret"); - let payment_secret = None; // Secret::try_from(self.payment_secret.clone()).expect("Invalid secret"); + let payment_secret = None; spawn(async move { let request = AccountsSendRequest { @@ -570,7 +641,7 @@ impl AccountManager { destination: payment_output.into(), wallet_secret, payment_secret, - priority_fee_sompi: Fees::SenderPaysAll(0), + priority_fee_sompi: Fees::SenderPaysAll(priority_fees_sompi), payload: None, }; @@ -603,12 +674,53 @@ impl AccountManager { } + fn update_estimate_args(&mut self) -> bool { + let mut valid = true; + + match try_kaspa_str_to_sompi(self.send_amount_text.as_str()) { + Ok(Some(sompi)) => { + self.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.priority_fees_text.as_str()) { + Ok(Some(sompi)) => { + self.priority_fees_sompi = sompi; + } + Ok(None) => { + self.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.estimate.lock().unwrap() = Estimate::Error(error.into()); + } + fn reset(&mut self) { *self.estimate.lock().unwrap() = Estimate::None; - self.send_amount_text = String::new(); + self.address_status = AddressStatus::None; + self.destination_address_string = String::default(); + self.send_amount_text = String::default(); self.send_amount_sompi = 0; self.action = Action::None; + self.focus = Focus::None; self.wallet_secret.zeroize(); self.payment_secret.zeroize(); -} + } + } \ No newline at end of file diff --git a/core/src/modules/block_dag.rs b/core/src/modules/block_dag.rs index 4f3d54a..1469c43 100644 --- a/core/src/modules/block_dag.rs +++ b/core/src/modules/block_dag.rs @@ -77,7 +77,7 @@ impl ModuleT for BlockDag { ui.add( Slider::new(&mut self.settings.y_dist, 1.0..=100.0) .clamp_to_range(true) - .text(i18n("Y Distance")) + .text(i18n("Spread")) // .step_by(1.0) ); // ui.separator(); diff --git a/core/src/modules/overview.rs b/core/src/modules/overview.rs index 245e4c7..855fea5 100644 --- a/core/src/modules/overview.rs +++ b/core/src/modules/overview.rs @@ -37,14 +37,9 @@ impl ModuleT for Overview { _frame: &mut eframe::Frame, ui: &mut egui::Ui, ) { - + // let width = ui.available_width(); let screen_rect = ui.ctx().screen_rect(); - let logo_size = vec2(648., 994.,) * 0.25; - let left = screen_rect.width() - logo_size.x - 8.; - let top = 32.; - let logo_rect = Rect::from_min_size(Pos2::new(left, top), logo_size); - - let width = ui.available_width(); + let width = screen_rect.width(); SidePanel::left("overview_left").exact_width(width/2.).resizable(false).show_separator_line(true).show_inside(ui, |ui| { // ui.label("Kaspa NG"); @@ -52,24 +47,8 @@ impl ModuleT for Overview { .id_source("overview_metrics") .auto_shrink([false; 2]) .show(ui, |ui| { - - CollapsingHeader::new(i18n("Kaspa p2p Node")) - .default_open(true) - .show(ui, |ui| { - - if core.state().is_connected() { - self.render_graphs(core,ui); - } else { - ui.label(i18n("Not connected")); - } - }); - - if let Some(system) = runtime().system() { - system.render(ui); - } - - ui.add_space(48.); - }); + self.render_stats(core,ui); + }); }); SidePanel::right("overview_right") @@ -77,110 +56,221 @@ impl ModuleT for Overview { .resizable(false) .show_separator_line(false) .show_inside(ui, |ui| { + self.render_details(core, ui); + }); - Image::new(ImageSource::Bytes { uri : Cow::Borrowed("bytes://logo.svg"), bytes : Bytes::Static(crate::app::KASPA_NG_LOGO_SVG)}) - .maintain_aspect_ratio(true) - .max_size(logo_size) - .fit_to_exact_size(logo_size) - .shrink_to_fit() - .texture_options(TextureOptions::LINEAR) - .tint(Color32::from_f32(0.8)) - .paint_at(ui, logo_rect); - - egui::ScrollArea::vertical() - .id_source("overview_metrics") - .auto_shrink([false; 2]) - .show(ui, |ui| { - - CollapsingHeader::new(i18n("Market")) - .default_open(true) - .show(ui, |ui| { - ui.label("TODO"); - }); - CollapsingHeader::new(i18n("Resources")) - .default_open(true) - .show(ui, |ui| { - // egui::special_emojis - // use egui_phosphor::light::{DISCORD_LOGO,GITHUB_LOGO}; - Hyperlink::from_label_and_url( - format!("• {}",i18n("Kaspa NextGen on GitHub")), - "https://github.com/aspectron/kaspa-ng" - ).open_in_new_tab(true).ui(ui); - Hyperlink::from_label_and_url( - format!("• {}",i18n("Rusty Kaspa on GitHub")), - "https://github.com/kaspanet/rusty-kaspa", - ).open_in_new_tab(true).ui(ui); - Hyperlink::from_label_and_url( - format!("• {}",i18n("NPM Modules for NodeJS")), - "https://www.npmjs.com/package/kaspa", - ).open_in_new_tab(true).ui(ui); - Hyperlink::from_label_and_url( - format!("• {}",i18n("WASM SDK for JavaScript and TypeScript")), - "https://github.com/kaspanet/rusty-kaspa/wasm", - ).open_in_new_tab(true).ui(ui); - Hyperlink::from_label_and_url( - format!("• {}",i18n("Rust Wallet SDK")), - "https://docs.rs/kaspa-wallet-core/0.0.4/kaspa_wallet_core/", - ).open_in_new_tab(true).ui(ui); - Hyperlink::from_label_and_url( - format!("• {}",i18n("Kaspa Discord")), - "https://discord.com/invite/kS3SK5F36R", - ).open_in_new_tab(true).ui(ui); - }); + } +} - let version = env!("CARGO_PKG_VERSION"); - let download = |platform: &str| { format!("https://github.com/aspectron/kaspa-ng/releases/download/{}/kaspa-ng-{}-{}.zip", version, version, platform) }; - CollapsingHeader::new(i18n("Redistributables")) - .default_open(false) - .show(ui, |ui| { - ["windows-x64", "linux-gnu-amd64", "macos-arm64"].into_iter().for_each(|platform| { - Hyperlink::from_label_and_url( - format!("• kaspa-ng-{}-{}.zip", version, platform), - download(platform), - ).open_in_new_tab(true).ui(ui); - }); - }); +impl Overview { - CollapsingHeader::new(i18n("Music")) - .default_open(true) - .show(ui, |ui| { - ui.label("TODO"); - }); + fn render_stats(&mut self, core: &mut Core, ui : &mut Ui) { + CollapsingHeader::new(i18n("Kaspa p2p Node")) + .default_open(true) + .show(ui, |ui| { - CollapsingHeader::new(i18n("Build")) - .default_open(true) - .show(ui, |ui| { - ui.label(format!("Kaspa NG v{}-{} + Rusty Kaspa v{}", env!("CARGO_PKG_VERSION"),crate::app::GIT_DESCRIBE, kaspa_wallet_core::version())); - ui.label(format!("Timestamp: {}", crate::app::BUILD_TIMESTAMP)); - ui.label(format!("rustc {}-{} {} llvm {}", - crate::app::RUSTC_SEMVER, - crate::app::RUSTC_COMMIT_HASH.chars().take(8).collect::(), - crate::app::RUSTC_CHANNEL, - crate::app::RUSTC_LLVM_VERSION, - )); - ui.label(format!("architecture {}", - crate::app::CARGO_TARGET_TRIPLE - )); - ui.label("Codename: \"This is the way\""); - }); + if core.state().is_connected() { + self.render_graphs(core,ui); + } else { + ui.label(i18n("Not connected")); + } + }); + + if let Some(system) = runtime().system() { + system.render(ui); + } + ui.add_space(48.); + } + + fn render_details(&mut self, _core: &mut Core, ui : &mut Ui) { - CollapsingHeader::new(i18n("License Information")) - .default_open(true) - .show(ui, |ui| { - ui.label("TODO"); + let screen_rect = ui.ctx().screen_rect(); + let logo_size = vec2(648., 994.,) * 0.25; + let left = screen_rect.width() - logo_size.x - 8.; + let top = 32.; + let logo_rect = Rect::from_min_size(Pos2::new(left, top), logo_size); + + Image::new(ImageSource::Bytes { uri : Cow::Borrowed("bytes://logo.svg"), bytes : Bytes::Static(crate::app::KASPA_NG_LOGO_SVG)}) + .maintain_aspect_ratio(true) + .max_size(logo_size) + .fit_to_exact_size(logo_size) + .shrink_to_fit() + .texture_options(TextureOptions::LINEAR) + .tint(Color32::from_f32(0.8)) + .paint_at(ui, logo_rect); + + egui::ScrollArea::vertical() + .id_source("overview_metrics") + .auto_shrink([false; 2]) + .show(ui, |ui| { + + CollapsingHeader::new(i18n("Market")) + .default_open(true) + .show(ui, |ui| { + ui.label("TODO"); + }); + + CollapsingHeader::new(i18n("Resources")) + .default_open(true) + .show(ui, |ui| { + // egui::special_emojis + // use egui_phosphor::light::{DISCORD_LOGO,GITHUB_LOGO}; + ui.hyperlink_to_tab( + format!("• {}",i18n("Kaspa NextGen on GitHub")), + "https://github.com/aspectron/kaspa-ng" + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("Rusty Kaspa on GitHub")), + "https://github.com/kaspanet/rusty-kaspa", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("NPM Modules for NodeJS")), + "https://www.npmjs.com/package/kaspa", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("WASM SDK for JavaScript and TypeScript")), + "https://github.com/kaspanet/rusty-kaspa/wasm", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("Rust Wallet SDK")), + "https://docs.rs/kaspa-wallet-core/0.0.4/kaspa_wallet_core/", + ); + ui.hyperlink_to_tab( + format!("• {}",i18n("Kaspa Discord")), + "https://discord.com/invite/kS3SK5F36R", + ); + }); + + let version = env!("CARGO_PKG_VERSION"); + let download = |platform: &str| { format!("https://github.com/aspectron/kaspa-ng/releases/download/{}/kaspa-ng-{}-{}.zip", version, version, platform) }; + CollapsingHeader::new(i18n("Redistributables")) + .default_open(false) + .show(ui, |ui| { + ["windows-x64", "linux-gnu-amd64", "macos-arm64"].into_iter().for_each(|platform| { + Hyperlink::from_label_and_url( + format!("• kaspa-ng-{}-{}.zip", version, platform), + download(platform), + ).open_in_new_tab(true).ui(ui); + }); + }); + + CollapsingHeader::new(i18n("Music")) + .default_open(true) + .show(ui, |ui| { + ui.label("TODO"); + }); + + + CollapsingHeader::new(i18n("Build")) + .default_open(true) + .show(ui, |ui| { + ui.label(format!("Kaspa NG v{}-{} + Rusty Kaspa v{}", env!("CARGO_PKG_VERSION"),crate::app::GIT_DESCRIBE, kaspa_wallet_core::version())); + ui.label(format!("Timestamp: {}", crate::app::BUILD_TIMESTAMP)); + ui.label(format!("rustc {}-{} {} llvm {}", + crate::app::RUSTC_SEMVER, + crate::app::RUSTC_COMMIT_HASH.chars().take(8).collect::(), + crate::app::RUSTC_CHANNEL, + crate::app::RUSTC_LLVM_VERSION, + )); + ui.label(format!("architecture {}", + crate::app::CARGO_TARGET_TRIPLE + )); + ui.label("Codename: \"This is the way\""); + }); + + + CollapsingHeader::new(i18n("License Information")) + .default_open(false) + .show(ui, |ui| { + ui.vertical(|ui|{ + ui.label("Rusty Kaspa"); + ui.label("Copyright (c) 2023 Kaspa Developers"); + ui.label("License: ISC"); + ui.hyperlink_url_to_tab("https://github.com/kaspanet/rusty-kaspa"); + ui.label(""); + ui.label("Kaspa NG"); + ui.label("Copyright (c) 2023 ASPECTRON"); + ui.label("License: MIT or Apache 2.0"); + ui.hyperlink_url_to_tab("https://aspectron.com"); + ui.label(""); + ui.label("WORKFLOW-RS"); + ui.label("Copyright (c) 2023 ASPECTRON"); + ui.label("License: MIT"); + ui.hyperlink_url_to_tab("https://github.com/workflow-rs/workflow-rs"); + ui.label(""); + ui.label("EGUI"); + ui.label("Copyright (c) 2023 Rerun"); + ui.label("License: MIT or Apache 2.0"); + ui.hyperlink_url_to_tab("https://github.com/emilk/egui"); + ui.label(""); + ui.label("PHOSPHOR ICONS"); + ui.label("Copyright (c) 2023 "); + ui.label("License: MIT"); + ui.hyperlink_url_to_tab("https://phosphoricons.com/"); + ui.label(""); + ui.label("Graphics Design"); + ui.label("Copyright (c) 2023 Rhubarb Media"); + ui.label("License: CC BY 4.0"); + ui.hyperlink_url_to_tab("https://rhubarbmedia.ca/"); + ui.label(""); + }); + }); + + CollapsingHeader::new(i18n("Credits")) + .default_open(false) + .show(ui, |ui| { + ui.vertical(|ui|{ + ui.label("Special thanks to the following people:"); + ui.horizontal_wrapped(|ui|{ + let mut nicks = [ + "142673", + "Bubblegum Lightning", + "coderofstuff", + "CryptoK", + "Elertan", + "hashdag", + "jablonx", + "jwj", + "lAmeR", + "matoo", + "msutton", + "Rhubarbarian", + "shaideshe", + "someone235", + "supertypo", + "Tim", + "Wolfie", + "KaffinPX" + ]; + nicks.sort(); + nicks.into_iter().for_each(|nick| { + ui.label(format!("@{nick}")); }); + }); }); + }); + + CollapsingHeader::new(i18n("Donations")) + .default_open(true) + .show(ui, |ui| { + ui.label("Please support Kaspa NG development"); + ui.label("kaspatest:qqdr2mv4vkes6kvhgy8elsxhvzwde42629vnpcxe4f802346rnfkklrhz0x7x"); + }); + }); + + + + + + - }); } -} -impl Overview { fn render_graphs(&mut self, core: &mut Core, ui : &mut Ui) { // let mut metric_iter = [Metric::CpuUsage, diff --git a/core/src/primitives/descriptors.rs b/core/src/primitives/descriptors.rs index 757c0fc..31eea25 100644 --- a/core/src/primitives/descriptors.rs +++ b/core/src/primitives/descriptors.rs @@ -57,8 +57,8 @@ fn grid(ui: &mut Ui, id: &AccountId, add_contents: impl FnOnce(&mut Ui)) { .show(ui, |ui| { Grid::new("bip32_descriptor") .num_columns(2) - .spacing([40.0, 4.0]) - .min_col_width(120.0) + .spacing([20.0, 4.0]) + // .min_col_width(120.0) // .striped(true) .show(ui, |ui| { add_contents(ui); @@ -76,7 +76,10 @@ impl RenderDescriptor for Bip32 { let color = Color32::WHITE; ui.label(i18n("Account Name")); - ui.colored_label(color, self.account_name.as_ref().unwrap_or(&"".to_string())); + ui.colored_label( + color, + self.account_name.as_ref().unwrap_or(&"...".to_string()), + ); ui.end_row(); ui.label(i18n("Type")); ui.colored_label(color, "BIP-32 / Kaspa Core"); diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index ab85382..14881fd 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -81,3 +81,54 @@ pub fn icon_with_text(ui: &Ui, icon: &str, color: Color32, text: &str) -> Layout job } + +type Handler<'panel> = Box; + +#[derive(Default)] +pub struct CenterLayoutBuilder<'layout, W> +where + W: Widget, +{ + pub list: Vec<(bool, W, Handler<'layout>)>, +} + +impl<'layout, W> CenterLayoutBuilder<'layout, W> +where + W: Widget, +{ + pub fn new() -> Self { + Self { list: Vec::new() } + } + pub fn add(mut self, widget: W, handler: impl FnOnce() + 'layout) -> Self { + self.list.push((true, widget, Box::new(handler))); + self + } + pub fn add_enabled( + mut self, + enabled: bool, + widget: W, + handler: impl FnOnce() + 'layout, + ) -> Self { + self.list.push((enabled, widget, Box::new(handler))); + self + } + + pub fn build(self, ui: &mut Ui) { + let theme = theme(); + let button_size = theme.medium_button_size(); + let available_width = ui.available_width(); + let buttons_len = self.list.len() as f32; + let spacing = ui.spacing().item_spacing.x; + let total_width = buttons_len * button_size.x + spacing * (buttons_len - 1.0); + let margin = (available_width - total_width) * 0.5; + + ui.add_space(margin); + self.list + .into_iter() + .for_each(|(enabled, widget, handler)| { + if ui.add_enabled(enabled, widget).clicked() { + handler(); + } + }); + } +}