From c5c1d04e84365f00087bcb0c73f4e0adee3f4bc3 Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Fri, 13 Dec 2024 13:10:06 +0200 Subject: [PATCH 01/14] chore: bump version (#131) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a54340b6..42bbe4cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dash-evo-tool" -version = "0.6.0" +version = "0.7.0" license = "MIT" edition = "2021" default-run = "dash-evo-tool" From 3c9bf5e1d557e730ec884d9478ebac722159130d Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:34:03 +0700 Subject: [PATCH 02/14] fix: filter messages in vote scheduling screen (#132) --- src/ui/dpns_vote_scheduling_screen.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index bf3f80a0..df689742 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -160,7 +160,7 @@ impl ScheduleVoteScreen { if chosen_time > self.ending_time { self.message = Some(( MessageType::Error, - "Scheduled time is after contest end time.".to_string(), + "Error inserting scheduled votes: Scheduled time is after contest end time.".to_string(), )); return AppAction::None; } @@ -178,7 +178,10 @@ impl ScheduleVoteScreen { } if scheduled_votes.is_empty() { - self.message = Some((MessageType::Error, "No votes selected.".to_string())); + self.message = Some(( + MessageType::Error, + "Error inserting scheduled votes: No votes selected.".to_string(), + )); return AppAction::None; } @@ -271,10 +274,14 @@ impl ScreenLike for ScheduleVoteScreen { if let Some(message) = &self.message { match message.0 { MessageType::Error => { - ui.colored_label(Color32::DARK_RED, message.1.clone()); + if message.1.contains("Error inserting scheduled votes") { + ui.colored_label(Color32::DARK_RED, message.1.clone()); + } } MessageType::Success => { - action = self.show_success(ui); + if message.1.contains("Votes scheduled") { + action = self.show_success(ui); + } } MessageType::Info => { ui.colored_label(Color32::DARK_BLUE, message.1.clone()); From 9f46f13f98b344d18f333dc1674bd9252f179842 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:34:58 +0700 Subject: [PATCH 03/14] feat: clean up top up screen (#134) --- src/ui/components/wallet_unlock.rs | 4 +- .../by_using_unused_asset_lock.rs | 15 ++++- .../by_using_unused_balance.rs | 26 +++++++-- .../by_wallet_qr_code.rs | 2 - .../identities/top_up_identity_screen/mod.rs | 57 +++++++++++-------- 5 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/ui/components/wallet_unlock.rs b/src/ui/components/wallet_unlock.rs index 41efa18a..13d8b173 100644 --- a/src/ui/components/wallet_unlock.rs +++ b/src/ui/components/wallet_unlock.rs @@ -46,7 +46,7 @@ pub trait ScreenWithWalletUnlock { // Only render the unlock prompt if the wallet requires a password and is locked if wallet.uses_password && !wallet.is_open() { - ui.add_space(10.0); + ui.add_space(20.0); if let Some(alias) = &wallet.alias { ui.label(format!( "This wallet ({}) is locked. Please enter the password to unlock it:", @@ -56,6 +56,8 @@ pub trait ScreenWithWalletUnlock { ui.label("This wallet is locked. Please enter the password to unlock it:"); } + ui.add_space(5.0); + let mut unlocked = false; // Capture necessary values before the closure diff --git a/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs index edd41cfe..647d5c0b 100644 --- a/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs +++ b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs @@ -1,7 +1,7 @@ use crate::app::AppAction; use crate::ui::identities::add_new_identity_screen::FundingMethod; use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; -use egui::Ui; +use egui::{Color32, RichText, Ui}; impl TopUpIdentityScreen { fn render_choose_funding_asset_lock(&mut self, ui: &mut egui::Ui) { @@ -90,12 +90,23 @@ impl TopUpIdentityScreen { ); ui.add_space(10.0); self.render_choose_funding_asset_lock(ui); + ui.add_space(10.0); - if ui.button("Create Identity").clicked() { + // Top up button + let mut new_style = (**ui.style()).clone(); + new_style.spacing.button_padding = egui::vec2(10.0, 5.0); + ui.set_style(new_style); + let button = egui::Button::new(RichText::new("Top Up Identity").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { self.error_message = None; action |= self.top_up_identity_clicked(FundingMethod::UseUnusedAssetLock); } + ui.add_space(20.0); + match step { WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("Waiting for Platform acknowledgement"); diff --git a/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs index 0ad85ff4..89d80783 100644 --- a/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs +++ b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs @@ -1,7 +1,7 @@ use crate::app::AppAction; use crate::ui::identities::add_new_identity_screen::FundingMethod; use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; -use egui::Ui; +use egui::{Color32, RichText, Ui}; impl TopUpIdentityScreen { fn show_wallet_balance(&self, ui: &mut egui::Ui) { @@ -27,15 +27,17 @@ impl TopUpIdentityScreen { ) -> AppAction { let mut action = AppAction::None; - self.show_wallet_balance(ui); - - ui.add_space(10.0); - ui.heading(format!( "{}. How much of your wallet balance would you like to transfer?", step_number )); + ui.add_space(10.0); + + self.show_wallet_balance(ui); + + ui.add_space(10.0); + step_number += 1; self.top_up_funding_amount_input(ui); @@ -47,11 +49,23 @@ impl TopUpIdentityScreen { return action; }; - if ui.button("Top Up Identity").clicked() { + ui.add_space(10.0); + + // Top up button + let mut new_style = (**ui.style()).clone(); + new_style.spacing.button_padding = egui::vec2(10.0, 5.0); + ui.set_style(new_style); + let button = egui::Button::new(RichText::new("Top Up Identity").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { self.error_message = None; action = self.top_up_identity_clicked(FundingMethod::UseWalletBalance); } + ui.add_space(20.0); + match step { WalletFundedScreenStep::WaitingForAssetLock => { ui.heading("Waiting for Core Chain to produce proof of transfer of funds."); diff --git a/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs index dc8d8187..63936ef2 100644 --- a/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs @@ -114,8 +114,6 @@ impl TopUpIdentityScreen { return action; }; - ui.add_space(10.0); - ui.heading( format!( "{}. Select how much you would like to transfer?", diff --git a/src/ui/identities/top_up_identity_screen/mod.rs b/src/ui/identities/top_up_identity_screen/mod.rs index 16034143..b55a6cc5 100644 --- a/src/ui/identities/top_up_identity_screen/mod.rs +++ b/src/ui/identities/top_up_identity_screen/mod.rs @@ -82,10 +82,6 @@ impl TopUpIdentityScreen { .and_then(|wallet| wallet.read().ok()?.alias.clone()) .unwrap_or_else(|| "Select".to_string()); - ui.heading("1. Choose the wallet to use to top up this identity."); - - ui.add_space(10.0); - // Display the ComboBox for wallet selection ComboBox::from_label("Select Wallet") .selected_text(selected_wallet_alias) @@ -108,7 +104,6 @@ impl TopUpIdentityScreen { } } }); - ui.add_space(10.0); true } else if let Some(wallet) = wallets.values().next() { if self.wallet.is_none() { @@ -425,40 +420,54 @@ impl ScreenLike for TopUpIdentityScreen { action |= self.show_success(ui); return; } + ui.add_space(10.0); ui.heading("Follow these steps to top up your identity:"); ui.add_space(15.0); let mut step_number = 1; + ui.heading(format!("{}. Choose your funding method.", step_number).as_str()); + step_number += 1; + ui.add_space(10.0); - if self.render_wallet_selection(ui) { - // We had more than 1 wallet - step_number += 1; - } - - if self.wallet.is_none() { - return; - }; + self.render_funding_method(ui); - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + ui.add_space(20.0); + ui.separator(); + ui.add_space(20.0); - if needed_unlock && !just_unlocked { + // Extract the funding method from the RwLock to minimize borrow scope + let funding_method = self.funding_method.read().unwrap().clone(); + if funding_method == FundingMethod::NoSelection { return; } - ui.add_space(10.0); + if funding_method == FundingMethod::UseWalletBalance + || funding_method == FundingMethod::UseUnusedAssetLock + { + ui.heading(format!( + "{}. Choose the wallet to use to top up this identity.", + step_number + )); + step_number += 1; - ui.heading(format!("{}. Choose your funding method.", step_number).as_str()); - step_number += 1; + ui.add_space(10.0); - ui.add_space(10.0); - self.render_funding_method(ui); + self.render_wallet_selection(ui); - // Extract the funding method from the RwLock to minimize borrow scope - let funding_method = self.funding_method.read().unwrap().clone(); + if self.wallet.is_none() { + return; + }; - if funding_method == FundingMethod::NoSelection { - return; + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + + if needed_unlock && !just_unlocked { + return; + } + + ui.add_space(20.0); + ui.separator(); + ui.add_space(20.0); } match funding_method { From 81e02b291704c390a41fb9fb64b496c3ab9b3610 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:41:02 +0700 Subject: [PATCH 04/14] fix: max withdrawal amount (#133) --- src/ui/identities/withdraw_from_identity_screen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/identities/withdraw_from_identity_screen.rs b/src/ui/identities/withdraw_from_identity_screen.rs index b2e38954..22f5d716 100644 --- a/src/ui/identities/withdraw_from_identity_screen.rs +++ b/src/ui/identities/withdraw_from_identity_screen.rs @@ -105,7 +105,7 @@ impl WithdrawalScreen { ui.text_edit_singleline(&mut self.withdrawal_amount); if ui.button("Max").clicked() { - let expected_max_amount = self.max_amount.saturating_sub(5000000) as f64 * 1e-11; + let expected_max_amount = self.max_amount.saturating_sub(500000000) as f64 * 1e-11; // Use flooring and format the result with 4 decimal places let floored_amount = (expected_max_amount * 10_000.0).floor() / 10_000.0; From 5c15c16e6b3798c34ebad30144047aad7de6f135 Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Wed, 18 Dec 2024 15:44:41 +0200 Subject: [PATCH 05/14] fix: correct way to print version (#139) --- Cargo.toml | 1 + build.rs | 16 ++++++++++++++++ src/main.rs | 6 ++++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index 42bbe4cc..53032498 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ license = "MIT" edition = "2021" default-run = "dash-evo-tool" rust-version = "1.81" +build = "build.rs" [build] rustflags = [ diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..223fc1c5 --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + // Fetch the version from CARGO_PKG_VERSION + let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "??".to_string()); + + // Generate a Rust file with the version constant + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("version.rs"); + fs::write( + dest_path, + format!(r#"pub const VERSION: &str = "{}";"#, version), + ).expect("Failed to write version.rs"); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 59435766..744872f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,10 @@ mod model; mod sdk_wrapper; mod ui; +include!(concat!(env!("OUT_DIR"), "/version.rs")); + fn main() -> eframe::Result<()> { + println!("running v{}", VERSION); check_cpu_compatibility(); // Initialize the Tokio runtime let runtime = tokio::runtime::Builder::new_multi_thread() @@ -32,9 +35,8 @@ fn main() -> eframe::Result<()> { centered: true, // Center window on startup if not maximized ..Default::default() }; - let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "".to_string()); eframe::run_native( - &format!("Dash Evo Tool v{}", version), + &format!("Dash Evo Tool v{}", VERSION), native_options, Box::new(|_cc| Ok(Box::new(app::AppState::new()))), ) From 1ce3232867b0ae81ca1740df412d04c4d8816234 Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Wed, 18 Dec 2024 15:45:27 +0200 Subject: [PATCH 06/14] fix: store egui config in app dir (#140) --- src/main.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 744872f8..0c939c79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use std::env; - +use crate::app_dir::{app_user_data_dir_path, create_app_user_data_directory_if_not_exists}; use crate::cpu_compatibility::check_cpu_compatibility; mod app; @@ -19,6 +19,10 @@ mod ui; include!(concat!(env!("OUT_DIR"), "/version.rs")); fn main() -> eframe::Result<()> { + create_app_user_data_directory_if_not_exists() + .expect("Failed to create app user_data directory"); + let app_data_dir = app_user_data_dir_path() + .expect("Failed to get app user_data directory path"); println!("running v{}", VERSION); check_cpu_compatibility(); // Initialize the Tokio runtime @@ -33,6 +37,7 @@ fn main() -> eframe::Result<()> { let native_options = eframe::NativeOptions { persist_window: true, // Persist window size and position centered: true, // Center window on startup if not maximized + persistence_path: Some(app_data_dir.join("app.ron")), ..Default::default() }; eframe::run_native( From 4eb61e9bd196b4793233b5bb3fd6cf7b067bb315 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:59:35 +0700 Subject: [PATCH 07/14] feat: reorg screen files and folders (#137) --- src/app.rs | 8 +++--- src/backend_task/core/refresh_wallet_info.rs | 2 +- .../dpns_subscreen_chooser_panel.rs | 2 +- .../{ => dpns}/dpns_contested_names_screen.rs | 4 +-- .../{ => dpns}/dpns_vote_scheduling_screen.rs | 4 +-- src/ui/dpns/mod.rs | 2 ++ src/ui/identities/identities_screen.rs | 2 +- src/ui/identities/mod.rs | 1 + src/ui/{ => identities}/transfers/mod.rs | 4 +-- src/ui/mod.rs | 26 +++++++++---------- src/ui/network_chooser_screen.rs | 2 +- src/ui/{tool_screens => tools}/mod.rs | 0 .../proof_log_screen.rs | 0 .../transition_visualizer_screen.rs | 0 .../add_new_wallet_screen.rs | 2 +- .../import_wallet_screen.rs | 2 +- src/ui/{wallet => wallets}/mod.rs | 0 .../{wallet => wallets}/wallets_screen/mod.rs | 0 18 files changed, 31 insertions(+), 30 deletions(-) rename src/ui/{ => dpns}/dpns_contested_names_screen.rs (99%) rename src/ui/{ => dpns}/dpns_vote_scheduling_screen.rs (99%) create mode 100644 src/ui/dpns/mod.rs rename src/ui/{ => identities}/transfers/mod.rs (99%) rename src/ui/{tool_screens => tools}/mod.rs (100%) rename src/ui/{tool_screens => tools}/proof_log_screen.rs (100%) rename src/ui/{tool_screens => tools}/transition_visualizer_screen.rs (100%) rename src/ui/{wallet => wallets}/add_new_wallet_screen.rs (99%) rename src/ui/{wallet => wallets}/import_wallet_screen.rs (99%) rename src/ui/{wallet => wallets}/mod.rs (100%) rename src/ui/{wallet => wallets}/wallets_screen/mod.rs (100%) diff --git a/src/app.rs b/src/app.rs index 11851932..be15b30e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,14 +10,14 @@ use crate::context::AppContext; use crate::database::Database; use crate::logging::initialize_logger; use crate::ui::document_query_screen::DocumentQueryScreen; -use crate::ui::dpns_contested_names_screen::{ +use crate::ui::dpns::dpns_contested_names_screen::{ DPNSContestedNamesScreen, DPNSSubscreen, IndividualVoteCastingStatus, }; use crate::ui::identities::identities_screen::IdentitiesScreen; use crate::ui::network_chooser_screen::NetworkChooserScreen; -use crate::ui::tool_screens::proof_log_screen::ProofLogScreen; -use crate::ui::tool_screens::transition_visualizer_screen::TransitionVisualizerScreen; -use crate::ui::wallet::wallets_screen::WalletsBalancesScreen; +use crate::ui::tools::proof_log_screen::ProofLogScreen; +use crate::ui::tools::transition_visualizer_screen::TransitionVisualizerScreen; +use crate::ui::wallets::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::dashcore::Network; diff --git a/src/backend_task/core/refresh_wallet_info.rs b/src/backend_task/core/refresh_wallet_info.rs index 349e585e..68ae2f9a 100644 --- a/src/backend_task/core/refresh_wallet_info.rs +++ b/src/backend_task/core/refresh_wallet_info.rs @@ -1,7 +1,7 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; -use crate::ui::wallet::wallets_screen::DerivationPathHelpers; +use crate::ui::wallets::wallets_screen::DerivationPathHelpers; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::dashcore::Address; use std::sync::{Arc, RwLock}; diff --git a/src/ui/components/dpns_subscreen_chooser_panel.rs b/src/ui/components/dpns_subscreen_chooser_panel.rs index 91f84879..088299aa 100644 --- a/src/ui/components/dpns_subscreen_chooser_panel.rs +++ b/src/ui/components/dpns_subscreen_chooser_panel.rs @@ -1,5 +1,5 @@ use crate::context::AppContext; -use crate::ui::dpns_contested_names_screen::DPNSSubscreen; +use crate::ui::dpns::dpns_contested_names_screen::DPNSSubscreen; use crate::ui::RootScreenType; use crate::{app::AppAction, ui}; use egui::{Color32, Context, Frame, Margin, RichText, SidePanel}; diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns/dpns_contested_names_screen.rs similarity index 99% rename from src/ui/dpns_contested_names_screen.rs rename to src/ui/dpns/dpns_contested_names_screen.rs index 85cc2d8d..93dddb2b 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns/dpns_contested_names_screen.rs @@ -1,6 +1,4 @@ -use super::components::dpns_subscreen_chooser_panel::add_dpns_subscreen_chooser_panel; use super::dpns_vote_scheduling_screen::ScheduleVoteScreen; -use super::{Screen, ScreenType}; use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::contested_names::ContestedResourceTask; use crate::backend_task::contested_names::ScheduledDPNSVote; @@ -9,10 +7,12 @@ use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::model::contested_name::{ContestState, ContestedName}; use crate::model::qualified_identity::{DPNSNameInfo, IdentityType, QualifiedIdentity}; +use crate::ui::components::dpns_subscreen_chooser_panel::add_dpns_subscreen_chooser_panel; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::identities::add_existing_identity_screen::AddExistingIdentityScreen; use crate::ui::{MessageType, RootScreenType, ScreenLike}; +use crate::ui::{Screen, ScreenType}; use chrono::{DateTime, LocalResult, TimeZone, Utc}; use chrono_humanize::HumanTime; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns/dpns_vote_scheduling_screen.rs similarity index 99% rename from src/ui/dpns_vote_scheduling_screen.rs rename to src/ui/dpns/dpns_vote_scheduling_screen.rs index df689742..4ebda601 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns/dpns_vote_scheduling_screen.rs @@ -14,8 +14,8 @@ use eframe::egui::Context; use eframe::egui::{self, Color32, RichText, Ui}; use std::sync::Arc; -use super::components::top_panel::add_top_panel; -use super::RootScreenType; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::RootScreenType; enum VoteOption { None, diff --git a/src/ui/dpns/mod.rs b/src/ui/dpns/mod.rs new file mode 100644 index 00000000..bc6530a1 --- /dev/null +++ b/src/ui/dpns/mod.rs @@ -0,0 +1,2 @@ +pub mod dpns_contested_names_screen; +pub mod dpns_vote_scheduling_screen; diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index 8ff9f50a..40c80b62 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -16,7 +16,7 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::identities::top_up_identity_screen::TopUpIdentityScreen; -use crate::ui::transfers::TransferScreen; +use crate::ui::identities::transfers::TransferScreen; use crate::ui::{RootScreenType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; diff --git a/src/ui/identities/mod.rs b/src/ui/identities/mod.rs index dabe88c8..22f22ef3 100644 --- a/src/ui/identities/mod.rs +++ b/src/ui/identities/mod.rs @@ -5,4 +5,5 @@ pub mod identities_screen; pub mod keys; pub mod register_dpns_name_screen; pub mod top_up_identity_screen; +pub mod transfers; pub mod withdraw_from_identity_screen; diff --git a/src/ui/transfers/mod.rs b/src/ui/identities/transfers/mod.rs similarity index 99% rename from src/ui/transfers/mod.rs rename to src/ui/identities/transfers/mod.rs index 91260ab3..28e19d9c 100644 --- a/src/ui/transfers/mod.rs +++ b/src/ui/identities/transfers/mod.rs @@ -19,8 +19,8 @@ use egui::{Color32, RichText}; use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; -use super::components::wallet_unlock::ScreenWithWalletUnlock; -use super::identities::register_dpns_name_screen::get_selected_wallet; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::identities::register_dpns_name_screen::get_selected_wallet; pub enum TransferCreditsStatus { NotStarted, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4b77e0aa..f91f0f5b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,23 +6,23 @@ use crate::model::qualified_identity::encrypted_key_storage::{ }; use crate::model::qualified_identity::QualifiedIdentity; use crate::ui::document_query_screen::DocumentQueryScreen; -use crate::ui::dpns_contested_names_screen::DPNSContestedNamesScreen; +use crate::ui::dpns::dpns_contested_names_screen::DPNSContestedNamesScreen; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::identities::keys::keys_screen::KeysScreen; use crate::ui::identities::top_up_identity_screen::TopUpIdentityScreen; +use crate::ui::identities::transfers::TransferScreen; use crate::ui::identities::withdraw_from_identity_screen::WithdrawalScreen; use crate::ui::network_chooser_screen::NetworkChooserScreen; -use crate::ui::tool_screens::proof_log_screen::ProofLogScreen; -use crate::ui::transfers::TransferScreen; -use crate::ui::wallet::import_wallet_screen::ImportWalletScreen; -use crate::ui::wallet::wallets_screen::WalletsBalancesScreen; +use crate::ui::tools::proof_log_screen::ProofLogScreen; +use crate::ui::wallets::import_wallet_screen::ImportWalletScreen; +use crate::ui::wallets::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use dpns_contested_names_screen::DPNSSubscreen; -use dpns_vote_scheduling_screen::ScheduleVoteScreen; +use dpns::dpns_contested_names_screen::DPNSSubscreen; +use dpns::dpns_vote_scheduling_screen::ScheduleVoteScreen; use egui::Context; use identities::add_existing_identity_screen::AddExistingIdentityScreen; use identities::add_new_identity_screen::AddNewIdentityScreen; @@ -31,18 +31,16 @@ use identities::register_dpns_name_screen::RegisterDpnsNameScreen; use std::fmt; use std::hash::Hash; use std::sync::Arc; -use tool_screens::transition_visualizer_screen::TransitionVisualizerScreen; -use wallet::add_new_wallet_screen::AddNewWalletScreen; +use tools::transition_visualizer_screen::TransitionVisualizerScreen; +use wallets::add_new_wallet_screen::AddNewWalletScreen; pub mod components; pub mod document_query_screen; -pub mod dpns_contested_names_screen; -pub mod dpns_vote_scheduling_screen; +pub mod dpns; pub(crate) mod identities; pub mod network_chooser_screen; -pub mod tool_screens; -pub mod transfers; -pub(crate) mod wallet; +pub mod tools; +pub(crate) mod wallets; pub mod withdrawal_statuses_screen; #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] diff --git a/src/ui/network_chooser_screen.rs b/src/ui/network_chooser_screen.rs index 93d72728..f8c41635 100644 --- a/src/ui/network_chooser_screen.rs +++ b/src/ui/network_chooser_screen.rs @@ -4,7 +4,7 @@ use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::wallet::add_new_wallet_screen::AddNewWalletScreen; +use crate::ui::wallets::add_new_wallet_screen::AddNewWalletScreen; use crate::ui::{RootScreenType, Screen, ScreenLike}; use dash_sdk::dpp::dashcore::Network; use dash_sdk::dpp::identity::TimestampMillis; diff --git a/src/ui/tool_screens/mod.rs b/src/ui/tools/mod.rs similarity index 100% rename from src/ui/tool_screens/mod.rs rename to src/ui/tools/mod.rs diff --git a/src/ui/tool_screens/proof_log_screen.rs b/src/ui/tools/proof_log_screen.rs similarity index 100% rename from src/ui/tool_screens/proof_log_screen.rs rename to src/ui/tools/proof_log_screen.rs diff --git a/src/ui/tool_screens/transition_visualizer_screen.rs b/src/ui/tools/transition_visualizer_screen.rs similarity index 100% rename from src/ui/tool_screens/transition_visualizer_screen.rs rename to src/ui/tools/transition_visualizer_screen.rs diff --git a/src/ui/wallet/add_new_wallet_screen.rs b/src/ui/wallets/add_new_wallet_screen.rs similarity index 99% rename from src/ui/wallet/add_new_wallet_screen.rs rename to src/ui/wallets/add_new_wallet_screen.rs index be502c46..bac18d0b 100644 --- a/src/ui/wallet/add_new_wallet_screen.rs +++ b/src/ui/wallets/add_new_wallet_screen.rs @@ -1,7 +1,7 @@ use crate::app::AppAction; use crate::context::AppContext; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::{wallet, ScreenLike}; +use crate::ui::{wallets, ScreenLike}; use eframe::egui::Context; use crate::model::wallet::encryption::{encrypt_message, DASH_SECRET_MESSAGE}; diff --git a/src/ui/wallet/import_wallet_screen.rs b/src/ui/wallets/import_wallet_screen.rs similarity index 99% rename from src/ui/wallet/import_wallet_screen.rs rename to src/ui/wallets/import_wallet_screen.rs index 7cd937a8..71ded1fb 100644 --- a/src/ui/wallet/import_wallet_screen.rs +++ b/src/ui/wallets/import_wallet_screen.rs @@ -6,7 +6,7 @@ use eframe::egui::Context; use crate::model::wallet::encryption::{encrypt_message, DASH_SECRET_MESSAGE}; use crate::model::wallet::{ClosedKeyItem, OpenWalletSeed, Wallet, WalletSeed}; -use crate::ui::wallet::add_new_wallet_screen::{ +use crate::ui::wallets::add_new_wallet_screen::{ DASH_BIP44_ACCOUNT_0_PATH_MAINNET, DASH_BIP44_ACCOUNT_0_PATH_TESTNET, }; use bip39::Mnemonic; diff --git a/src/ui/wallet/mod.rs b/src/ui/wallets/mod.rs similarity index 100% rename from src/ui/wallet/mod.rs rename to src/ui/wallets/mod.rs diff --git a/src/ui/wallet/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs similarity index 100% rename from src/ui/wallet/wallets_screen/mod.rs rename to src/ui/wallets/wallets_screen/mod.rs From e7bf32cf0f17695128ed3bd946a753f8aaf7c919 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Wed, 18 Dec 2024 22:00:10 +0700 Subject: [PATCH 08/14] feat: show success message in register name screen (#135) * feat: show success message in register name screen * add identity balance --- .../identities/register_dpns_name_screen.rs | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/ui/identities/register_dpns_name_screen.rs b/src/ui/identities/register_dpns_name_screen.rs index d1092b04..105004ef 100644 --- a/src/ui/identities/register_dpns_name_screen.rs +++ b/src/ui/identities/register_dpns_name_screen.rs @@ -16,11 +16,12 @@ use dash_sdk::dpp::identity::{Purpose, SecurityLevel, TimestampMillis}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::{Identifier, IdentityPublicKey}; use eframe::egui::Context; -use egui::{Color32, RichText}; +use egui::{Color32, RichText, Ui}; use std::sync::Arc; use std::sync::RwLock; use std::time::{SystemTime, UNIX_EPOCH}; +#[derive(PartialEq)] pub enum RegisterDpnsNameStatus { NotStarted, WaitingForResult(TimestampMillis), @@ -201,6 +202,32 @@ impl RegisterDpnsNameScreen { dpns_name_input, ))) } + + pub fn show_success(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + + // Center the content vertically and horizontally + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + ui.heading("Successfully registered dpns name."); + + ui.add_space(20.0); + + if ui.button("Back to DPNS screen").clicked() { + action = AppAction::PopScreenAndRefresh; + } + ui.add_space(5.0); + + if ui.button("Register another name").clicked() { + self.name_input = String::new(); + self.register_dpns_name_status = RegisterDpnsNameStatus::NotStarted; + } + }); + + action + } } impl ScreenLike for RegisterDpnsNameScreen { @@ -232,6 +259,11 @@ impl ScreenLike for RegisterDpnsNameScreen { ); egui::CentralPanel::default().show(ctx, |ui| { + if self.register_dpns_name_status == RegisterDpnsNameStatus::Complete { + action |= self.show_success(ui); + return; + } + ui.heading("Register DPNS Name"); ui.add_space(10.0); @@ -248,6 +280,10 @@ impl ScreenLike for RegisterDpnsNameScreen { ui.heading("1. Select Identity"); ui.add_space(5.0); self.render_identity_id_selection(ui); + ui.add_space(5.0); + if let Some(identity) = &self.selected_qualified_identity { + ui.label(format!("Identity balance: {:.6}", identity.0.identity.balance() as f64 * 1e-11)); + } ui.add_space(10.0); ui.separator(); @@ -352,9 +388,7 @@ impl ScreenLike for RegisterDpnsNameScreen { RegisterDpnsNameStatus::ErrorMessage(msg) => { ui.colored_label(egui::Color32::RED, format!("Error: {}", msg)); } - RegisterDpnsNameStatus::Complete => { - action = AppAction::PopScreenAndRefresh; - } + RegisterDpnsNameStatus::Complete => {} } ui.add_space(10.0); From 1129a58538663b1fd5d521e69c7da8916272d259 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 19 Dec 2024 13:41:51 +0700 Subject: [PATCH 09/14] chore: improve SDK reliability (#141) --- src/sdk_wrapper.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdk_wrapper.rs b/src/sdk_wrapper.rs index 21324784..cc586da8 100644 --- a/src/sdk_wrapper.rs +++ b/src/sdk_wrapper.rs @@ -16,8 +16,8 @@ pub fn initialize_sdk( let request_settings = RequestSettings { connect_timeout: Some(Duration::from_secs(10)), timeout: Some(Duration::from_secs(10)), - retries: None, - ban_failed_address: Some(false), + retries: Some(6), + ..Default::default() }; let sdk = SdkBuilder::new(address_list) From b6e82518dfa85aafcebf0ea40edacc1d4882103e Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:31:11 -0500 Subject: [PATCH 10/14] feat: contracts documents screen (#138) * feat: contracts documents screen * adding contracts roughly works * display both found and unfound contracts in success screen * progress on the plane without wifi * document queries working * almost done * impl indexes with more than one property * ok * ok * more features and working well * columns in the field selection popup * reorg buttons * feat: document query pagination (#142) * feat: document query pagination * update Cargo.toml with sdk fix * fix min width of pop up window * feat: pagination ui * fix unwrap --- Cargo.toml | 2 +- src/app.rs | 11 +- src/backend_task/contract.rs | 57 +- src/backend_task/document.rs | 53 +- src/backend_task/mod.rs | 17 +- src/context.rs | 20 +- src/database/contracts.rs | 14 + src/main.rs | 1 + src/ui/components/contract_chooser_panel.rs | 206 +++++- src/ui/components/left_panel.rs | 3 - .../add_contracts_screen.rs | 270 ++++++++ .../document_query_screen.rs | 627 ++++++++++++++++++ src/ui/contracts_documents/mod.rs | 2 + src/ui/document_query_screen.rs | 191 ------ src/ui/mod.rs | 20 +- src/utils/mod.rs | 1 + src/utils/parsers.rs | 29 + 17 files changed, 1276 insertions(+), 248 deletions(-) create mode 100644 src/ui/contracts_documents/add_contracts_screen.rs create mode 100644 src/ui/contracts_documents/document_query_screen.rs create mode 100644 src/ui/contracts_documents/mod.rs delete mode 100644 src/ui/document_query_screen.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/parsers.rs diff --git a/Cargo.toml b/Cargo.toml index 53032498..286258a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ strum = { version = "0.26.1", features = ["derive"] } bs58 = "0.5.0" base64 = "0.22.1" copypasta = "0.10.1" -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "a4f906acd127bc9856d72452eba002da16935209" } +dash-sdk = { git = "https://github.com/dashpay/platform", rev = "e2ed81f0a5af5ef74fc703097e2657349021094b" } thiserror = "1" serde = "1.0.197" serde_json = "1.0.120" diff --git a/src/app.rs b/src/app.rs index be15b30e..b2134aba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,7 @@ use crate::components::core_zmq_listener::{CoreZMQListener, ZMQMessage}; use crate::context::AppContext; use crate::database::Database; use crate::logging::initialize_logger; -use crate::ui::document_query_screen::DocumentQueryScreen; +use crate::ui::contracts_documents::document_query_screen::DocumentQueryScreen; use crate::ui::dpns::dpns_contested_names_screen::{ DPNSContestedNamesScreen, DPNSSubscreen, IndividualVoteCastingStatus, }; @@ -460,6 +460,15 @@ impl App for AppState { BackendTaskSuccessResult::ToppedUpIdentity(_) => { self.visible_screen_mut().display_task_result(message); } + BackendTaskSuccessResult::FetchedContract(_) => { + self.visible_screen_mut().display_task_result(message); + } + BackendTaskSuccessResult::FetchedContracts(_) => { + self.visible_screen_mut().display_task_result(message); + } + BackendTaskSuccessResult::PageDocuments(_, _) => { + self.visible_screen_mut().display_task_result(message); + } }, TaskResult::Error(message) => { self.visible_screen_mut() diff --git a/src/backend_task/contract.rs b/src/backend_task/contract.rs index a7aa3881..5be66fc5 100644 --- a/src/backend_task/contract.rs +++ b/src/backend_task/contract.rs @@ -1,25 +1,63 @@ use crate::context::AppContext; use dash_sdk::dpp::system_data_contracts::dpns_contract; -use dash_sdk::platform::{DataContract, Fetch, Identifier}; +use dash_sdk::platform::{DataContract, Fetch, FetchMany, Identifier}; use dash_sdk::Sdk; +use super::BackendTaskSuccessResult; + #[derive(Debug, Clone, PartialEq)] pub(crate) enum ContractTask { FetchDPNSContract, FetchContract(Identifier, Option), + FetchContracts(Vec), + RemoveContract(Identifier), } impl AppContext { - pub async fn run_contract_task(&self, task: ContractTask, sdk: &Sdk) -> Result<(), String> { + pub async fn run_contract_task( + &self, + task: ContractTask, + sdk: &Sdk, + ) -> Result { match task { ContractTask::FetchContract(identifier, name) => { match DataContract::fetch(sdk, identifier).await { Ok(Some(data_contract)) => self .db .insert_contract_if_not_exists(&data_contract, name.as_deref(), self) - .map_err(|e| e.to_string()), - Ok(None) => Ok(()), - Err(e) => Err(e.to_string()), + .map(|_| BackendTaskSuccessResult::FetchedContract(data_contract)) + .map_err(|e| { + format!( + "Error inserting contract into the database: {}", + e.to_string() + ) + }), + Ok(None) => Err("Contract not found".to_string()), + Err(e) => Err(format!("Error fetching contract: {}", e.to_string())), + } + } + ContractTask::FetchContracts(identifiers) => { + match DataContract::fetch_many(sdk, identifiers).await { + Ok(data_contracts) => { + let mut results = vec![]; + for data_contract in data_contracts { + if let Some(contract) = &data_contract.1 { + self.db + .insert_contract_if_not_exists(contract, None, self) + .map_err(|e| { + format!( + "Error inserting contract into the database: {}", + e.to_string() + ) + })?; + results.push(Some(contract.clone())); + } else { + results.push(None); + } + } + Ok(BackendTaskSuccessResult::FetchedContracts(results)) + } + Err(e) => Err(format!("Error fetching contracts: {}", e.to_string())), } } ContractTask::FetchDPNSContract => { @@ -29,11 +67,18 @@ impl AppContext { Ok(Some(data_contract)) => self .db .insert_contract_if_not_exists(&data_contract, Some("dpns"), self) + .map(|_| BackendTaskSuccessResult::FetchedContract(data_contract)) .map_err(|e| e.to_string()), Ok(None) => Err("No DPNS contract found".to_string()), - Err(e) => Err(e.to_string()), + Err(e) => Err(format!("Error fetching DPNS contract: {}", e.to_string())), } } + ContractTask::RemoveContract(identifier) => self + .remove_contract(&identifier) + .map(|_| { + BackendTaskSuccessResult::Message("Successfully removed contract".to_string()) + }) + .map_err(|e| format!("Error removing contract: {}", e.to_string())), } } } diff --git a/src/backend_task/document.rs b/src/backend_task/document.rs index e67ff2de..39742dc3 100644 --- a/src/backend_task/document.rs +++ b/src/backend_task/document.rs @@ -1,12 +1,14 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; -use dash_sdk::platform::{Document, DocumentQuery, FetchMany}; +use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; +use dash_sdk::platform::{Document, DocumentQuery, FetchMany, Identifier}; +use dash_sdk::query_types::IndexMap; use dash_sdk::Sdk; -pub type DocumentTypeName = String; #[derive(Debug, Clone, PartialEq)] pub(crate) enum DocumentTask { FetchDocuments(DocumentQuery), + FetchDocumentsPage(DocumentQuery), } impl AppContext { @@ -16,10 +18,49 @@ impl AppContext { sdk: &Sdk, ) -> Result { match task { - DocumentTask::FetchDocuments(drive_query) => Document::fetch_many(sdk, drive_query) - .await - .map(BackendTaskSuccessResult::Documents) - .map_err(|e| e.to_string()), + DocumentTask::FetchDocuments(document_query) => { + Document::fetch_many(sdk, document_query) + .await + .map(BackendTaskSuccessResult::Documents) + .map_err(|e| format!("Error fetching documents: {}", e.to_string())) + } + DocumentTask::FetchDocumentsPage(mut document_query) => { + // Set the limit for each page + document_query.limit = 100; + + // Initialize an empty IndexMap to accumulate documents for this page + let mut page_docs: IndexMap> = IndexMap::new(); + + // Fetch a single page + let docs_batch_result = Document::fetch_many(sdk, document_query.clone()) + .await + .map_err(|e| format!("Error fetching documents: {}", e))?; + + let batch_len = docs_batch_result.len(); + + // Insert the batch into the page map + for (id, doc_opt) in docs_batch_result { + page_docs.insert(id, doc_opt); + } + + // Determine if there's a next page + let has_next_page = batch_len == 100; + + // If there's a next page, set the 'start' parameter for the next cursor + let next_cursor = if has_next_page { + page_docs.keys().last().cloned().map(|last_doc_id| { + let id_bytes = last_doc_id.to_buffer(); + Start::StartAfter(id_bytes.to_vec()) + }) + } else { + None + }; + + Ok(BackendTaskSuccessResult::PageDocuments( + page_docs, + next_cursor, + )) + } } } } diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index 5183c62f..4c537135 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -8,15 +8,18 @@ use crate::backend_task::withdrawal_statuses::{WithdrawStatusPartialData, Withdr use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use contested_names::ScheduledDPNSVote; +use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::voting::votes::Vote; -use dash_sdk::query_types::Documents; +use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; +use dash_sdk::platform::{Document, Identifier}; +use dash_sdk::query_types::{Documents, IndexMap}; use std::sync::Arc; use tokio::sync::mpsc; pub mod contested_names; pub mod contract; pub mod core; -mod document; +pub mod document; pub mod identity; pub mod withdrawal_statuses; @@ -42,6 +45,9 @@ pub(crate) enum BackendTaskSuccessResult { SuccessfulVotes(Vec), CastScheduledVote(ScheduledDPNSVote), WithdrawalStatus(WithdrawStatusPartialData), + FetchedContract(DataContract), + FetchedContracts(Vec>), + PageDocuments(IndexMap>, Option), } impl BackendTaskSuccessResult {} @@ -65,10 +71,9 @@ impl AppContext { ) -> Result { let sdk = self.sdk.clone(); match task { - BackendTask::ContractTask(contract_task) => self - .run_contract_task(contract_task, &sdk) - .await - .map(|_| BackendTaskSuccessResult::None), + BackendTask::ContractTask(contract_task) => { + self.run_contract_task(contract_task, &sdk).await + } BackendTask::ContestedResourceTask(contested_resource_task) => { self.run_contested_resource_task(contested_resource_task, &sdk, sender) .await diff --git a/src/context.rs b/src/context.rs index 29733f7e..27b955c5 100644 --- a/src/context.rs +++ b/src/context.rs @@ -143,6 +143,7 @@ impl AppContext { .insert_local_qualified_identity(&identity.clone().into(), None, self) } + /// Inserts a local qualified identity into the database pub fn insert_local_qualified_identity( &self, qualified_identity: &QualifiedIdentity, @@ -155,6 +156,7 @@ impl AppContext { ) } + /// Updates a local qualified identity in the database pub fn update_local_qualified_identity( &self, qualified_identity: &QualifiedIdentity, @@ -163,6 +165,7 @@ impl AppContext { .update_local_qualified_identity(qualified_identity, self) } + /// Sets the alias for an identity pub fn set_alias(&self, identifier: &Identifier, new_alias: Option<&str>) -> Result<()> { self.db.set_alias(identifier, new_alias) } @@ -182,44 +185,54 @@ impl AppContext { ) } + /// Fetches all local qualified identities from the database pub fn load_local_qualified_identities(&self) -> Result> { let wallets = self.wallets.read().unwrap(); self.db.get_local_qualified_identities(self, &wallets) } + /// Fetches all voting identities from the database pub fn load_local_voting_identities(&self) -> Result> { self.db.get_local_voting_identities(self) } + /// Fetches all contested names from the database including past and active ones pub fn all_contested_names(&self) -> Result> { self.db.get_all_contested_names(self) } + /// Fetches all ongoing contested names from the database pub fn ongoing_contested_names(&self) -> Result> { self.db.get_ongoing_contested_names(self) } + /// Inserts scheduled votes into the database pub fn insert_scheduled_votes(&self, scheduled_votes: &Vec) -> Result<()> { self.db.insert_scheduled_votes(self, &scheduled_votes) } + /// Fetches all scheduled votes from the database pub fn get_scheduled_votes(&self) -> Result> { self.db.get_scheduled_votes(&self) } + /// Clears all scheduled votes from the database pub fn clear_all_scheduled_votes(&self) -> Result<()> { self.db.clear_all_scheduled_votes(self) } + /// Clears all executed scheduled votes from the database pub fn clear_executed_scheduled_votes(&self) -> Result<()> { self.db.clear_executed_scheduled_votes(self) } + /// Deletes a scheduled vote from the database pub fn delete_scheduled_vote(&self, identity_id: &[u8], contested_name: &String) -> Result<()> { self.db .delete_scheduled_vote(self, identity_id, &contested_name) } + /// Marks a scheduled vote as executed in the database pub fn mark_vote_executed(&self, identity_id: &[u8], contested_name: String) -> Result<()> { self.db .mark_vote_executed(self, identity_id, contested_name) @@ -270,7 +283,7 @@ impl AppContext { self.db.get_settings() } - /// Retrieves the DPNS contract along with other contracts from the database. + /// Retrieves all contracts from the database plus the DPNS contract from app context. pub fn get_contracts( &self, limit: Option, @@ -291,6 +304,11 @@ impl AppContext { Ok(contracts) } + // Remove contract from the database by ID + pub fn remove_contract(&self, contract_id: &Identifier) -> Result<()> { + self.db.remove_contract(contract_id.as_bytes(), &self) + } + pub(crate) fn received_transaction_finality( &self, tx: &Transaction, diff --git a/src/database/contracts.rs b/src/database/contracts.rs index 6f92374c..f2b979b3 100644 --- a/src/database/contracts.rs +++ b/src/database/contracts.rs @@ -184,4 +184,18 @@ impl Database { Ok(contracts) } + + pub fn remove_contract( + &self, + contract_id: &[u8], + app_context: &AppContext, + ) -> rusqlite::Result<()> { + let network = app_context.network_string(); + let conn = self.conn.lock().unwrap(); + conn.execute( + "DELETE FROM contract WHERE contract_id = ? AND network = ?", + rusqlite::params![contract_id, network], + )?; + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 0c939c79..ff595f53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod logging; mod model; mod sdk_wrapper; mod ui; +mod utils; include!(concat!(env!("OUT_DIR"), "/version.rs")); diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 10eb1896..056f4a6c 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -1,24 +1,37 @@ use crate::app::AppAction; +use crate::backend_task::contract::ContractTask; +use crate::backend_task::BackendTask; use crate::context::AppContext; -use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use crate::model::qualified_contract::QualifiedContract; +use crate::ui::contracts_documents::document_query_screen::DOCUMENT_PRIVATE_FIELDS; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dash_sdk::dpp::data_contract::document_type::Index; +use dash_sdk::dpp::data_contract::{ + accessors::v0::DataContractV0Getters, document_type::DocumentType, +}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; -use egui::{Context, Frame, Margin, SidePanel}; +use egui::{Color32, Context, Frame, Margin, RichText, SidePanel}; +use std::collections::HashMap; use std::sync::Arc; + pub fn add_contract_chooser_panel( ctx: &Context, current_search_term: &mut String, app_context: &Arc, + selected_data_contract: &mut QualifiedContract, + selected_document_type: &mut DocumentType, + selected_index: &mut Option, + document_query: &mut String, + pending_document_type: &mut DocumentType, + pending_fields_selection: &mut HashMap, ) -> AppAction { - let action = AppAction::None; + let mut action = AppAction::None; - // Fetch contracts from the app context let contracts = app_context.get_contracts(None, None).unwrap_or_else(|e| { eprintln!("Error fetching contracts: {}", e); vec![] }); - // Filter the contracts based on the search term let filtered_contracts: Vec<_> = contracts .iter() .filter(|contract| { @@ -40,42 +53,173 @@ pub fn add_contract_chooser_panel( .inner_margin(Margin::same(10.0)), ) .show(ctx, |ui| { - // Search bar at the top ui.horizontal(|ui| { - ui.label("Search:"); + ui.label("Filter contracts:"); ui.text_edit_singleline(current_search_term); }); - ui.separator(); // Separator below the search bar + ui.separator(); - // Display filtered contracts with nested document types and indexes ui.vertical(|ui| { for contract in filtered_contracts { - let name_or_id = contract - .alias - .clone() - .unwrap_or(contract.contract.id().to_string(Encoding::Base58)); - - // Expandable contract section - ui.collapsing(name_or_id, |ui| { - // Loop over the document types in the contract - for (doc_name, doc_type) in contract.contract.document_types() { - // Expandable section for each document type - ui.collapsing(doc_name, |ui| { - // Loop over the indexes in the document type - for index in doc_type.indexes().values() { - ui.label(format!("Index: {}", index.name)); - ui.indent("index_properties", |ui| { - for prop in &index.properties { - ui.label(format!("Property: {:?}", prop)); + ui.horizontal(|ui| { + let is_selected_contract = *selected_data_contract == *contract; + + let name_or_id = contract + .alias + .clone() + .unwrap_or(contract.contract.id().to_string(Encoding::Base58)); + + let contract_header_text = if is_selected_contract { + RichText::new(name_or_id).color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(name_or_id) + }; + + ui.collapsing(contract_header_text, |ui| { + for (doc_name, doc_type) in contract.contract.document_types() { + let is_selected_doc_type = *selected_document_type == *doc_type; + + let doc_type_header_text = if is_selected_doc_type { + RichText::new(doc_name.clone()) + .color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(doc_name.clone()) + }; + + let doc_resp = ui.collapsing(doc_type_header_text, |ui| { + // Display indexes as collapsible items + if doc_type.indexes().is_empty() { + ui.label("No indexes defined"); + } else { + for (index_name, index) in doc_type.indexes() { + let is_selected_index = + *selected_index == Some(index.clone()); + + let index_header_text = if is_selected_index { + RichText::new(format!("Index: {}", index_name)) + .color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(format!("Index: {}", index_name)) + }; + + let index_resp = + ui.collapsing(index_header_text, |ui| { + // Show index properties if expanded + for prop in &index.properties { + ui.label(format!("{:?}", prop)); + } + }); + + // Handle toggling of index + // If the index is selected (expanded), build a WHERE clause for all properties: + if index_resp.header_response.clicked() + && index_resp.body_response.is_some() + { + *selected_index = Some(index.clone()); + if let Ok(new_doc_type) = contract + .contract + .document_type_cloned_for_name(&doc_name) + { + *selected_document_type = new_doc_type; + *selected_data_contract = contract.clone(); + + // Build the WHERE clause using all property names + let conditions: Vec = index + .property_names() + .iter() + .map(|property_name| { + format!("`{}` = '___'", property_name) + }) + .collect(); + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!( + " WHERE {}", + conditions.join(" AND ") + ) + }; + + *document_query = format!( + "SELECT * FROM {}{}", + selected_document_type.name(), + where_clause + ); + } + } else if index_resp.header_response.clicked() + && index_resp.body_response.is_none() + { + // Index closed (collapsed) + *selected_index = None; + // Rebuild the query without index constraint + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); + } + } + } + }); + + // Check doc type toggling + if doc_resp.header_response.clicked() + && doc_resp.body_response.is_some() + { + if let Ok(new_doc_type) = + contract.contract.document_type_cloned_for_name(&doc_name) + { + *pending_document_type = new_doc_type.clone(); + *selected_document_type = new_doc_type.clone(); + *selected_data_contract = contract.clone(); + *selected_index = None; + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); + + // Now reinitialize the field selection + pending_fields_selection.clear(); + + // 1) Mark doc-type-defined fields as checked = true + for (field_name, _schema) in + new_doc_type.properties().iter() + { + pending_fields_selection + .insert(field_name.clone(), true); + } + for dash_field in DOCUMENT_PRIVATE_FIELDS { + pending_fields_selection + .insert(dash_field.to_string(), false); } - }); + } + } else if doc_resp.header_response.clicked() + && doc_resp.body_response.is_none() + { + // Doc type collapsed again: still have doc type & contract + // required, so do not clear them. Just clear index if any. + *selected_index = None; + *document_query = + format!("SELECT * FROM {}", selected_document_type.name()); } - }); - } - }); + } + }); - ui.add_space(5.0); // Spacing between contracts + // The Remove button + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + // Only show the remove button for non-DPNS contracts + if contract.alias != Some("dpns".to_string()) { + if ui.button("X").clicked() { + action |= AppAction::BackendTask(BackendTask::ContractTask( + ContractTask::RemoveContract( + contract.contract.id().clone(), + ), + )); + } + } + }); + }); } }); }); diff --git a/src/ui/components/left_panel.rs b/src/ui/components/left_panel.rs index 8887d845..1b10bdfe 100644 --- a/src/ui/components/left_panel.rs +++ b/src/ui/components/left_panel.rs @@ -82,9 +82,6 @@ pub fn add_left_panel( .show(ctx, |ui| { ui.vertical_centered(|ui| { for (label, screen_type, icon_path) in buttons.iter() { - if *screen_type == RootScreenType::RootScreenDocumentQuery { - continue; // Skip rendering the document button for now - } if *screen_type == RootScreenType::RootScreenWithdrawsStatus { continue; // Skip rendering the withdrawals button for now } diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs new file mode 100644 index 00000000..dbb3296f --- /dev/null +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -0,0 +1,270 @@ +use crate::app::AppAction; +use crate::backend_task::contract::ContractTask; +use crate::backend_task::BackendTask; +use crate::context::AppContext; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{BackendTaskSuccessResult, MessageType, ScreenLike}; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::identifier::Identifier; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::prelude::TimestampMillis; +use eframe::egui::{self, Color32, Context, RichText, Ui}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +const MAX_CONTRACTS: usize = 10; + +enum AddContractsStatus { + NotStarted, + WaitingForResult(TimestampMillis), + Complete(Vec), + ErrorMessage(String), +} + +pub struct AddContractsScreen { + pub app_context: Arc, + contract_ids_input: Vec, + add_contracts_status: AddContractsStatus, +} + +impl AddContractsScreen { + pub fn new(app_context: &Arc) -> Self { + Self { + app_context: app_context.clone(), + contract_ids_input: vec!["".to_string()], + add_contracts_status: AddContractsStatus::NotStarted, + } + } + + fn add_contract_field(&mut self) { + if self.contract_ids_input.len() < MAX_CONTRACTS { + self.contract_ids_input.push("".to_string()); + } + } + + fn parse_identifiers(&self) -> Result, String> { + let mut identifiers = Vec::new(); + for (i, input) in self.contract_ids_input.iter().enumerate() { + let trimmed = input.trim(); + if trimmed.is_empty() { + continue; // Empty fields are ignored + } + // Try hex first + let identifier = if let Ok(bytes) = hex::decode(trimmed) { + Identifier::from_bytes(&bytes) + .map_err(|e| format!("Invalid ID in field {}: {}", i + 1, e))? + } else { + // Try Base58 + Identifier::from_string(trimmed, Encoding::Base58) + .map_err(|e| format!("Invalid ID in field {}: {}", i + 1, e))? + }; + identifiers.push(identifier); + } + if identifiers.is_empty() { + return Err("No valid contract IDs entered.".to_string()); + } + Ok(identifiers) + } + + fn add_contracts_clicked(&mut self) -> AppAction { + match self.parse_identifiers() { + Ok(identifiers) => { + self.add_contracts_status = AddContractsStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + AppAction::BackendTask(BackendTask::ContractTask(ContractTask::FetchContracts( + identifiers, + ))) + } + Err(e) => { + self.add_contracts_status = AddContractsStatus::ErrorMessage(e); + AppAction::None + } + } + } + + fn show_input_fields(&mut self, ui: &mut Ui) { + ui.heading("Enter Contract Identifiers:"); + ui.add_space(5.0); + + for (i, contract_id) in self.contract_ids_input.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.label(format!("Contract {}:", i + 1)); + ui.text_edit_singleline(contract_id); + }); + ui.add_space(5.0); + } + + if self.contract_ids_input.len() < MAX_CONTRACTS { + if ui.button("Add Another Contract Field").clicked() { + self.add_contract_field(); + } + } + } + + fn show_success_screen(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + ui.heading("Successfully queried contracts"); + ui.add_space(10.0); + ui.label("Found and added the following contracts:"); + ui.add_space(10.0); + let mut not_found = vec![]; + + if let AddContractsStatus::Complete(options) = &self.add_contracts_status { + for id_string in self.contract_ids_input.clone() { + let trimmed_id_string = id_string.trim().to_string(); + if options.contains(&trimmed_id_string.to_string()) { + ui.colored_label(Color32::DARK_GREEN, trimmed_id_string); + } else { + not_found.push(trimmed_id_string); + } + } + } + + ui.add_space(20.0); + + if !not_found.is_empty() { + ui.label("The following contracts were not found:"); + ui.add_space(10.0); + for trimmed_id_string in not_found { + ui.colored_label(Color32::RED, trimmed_id_string); + } + } + + ui.add_space(20.0); + let button = + egui::Button::new(RichText::new("Back to Contracts").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + // Return to previous screen + action = AppAction::PopScreenAndRefresh; + } + }); + + action + } +} + +impl ScreenLike for AddContractsScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + // Not used + } + MessageType::Error => { + self.add_contracts_status = AddContractsStatus::ErrorMessage(message.to_string()); + } + MessageType::Info => { + // Not used + } + } + } + + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + match backend_task_success_result { + BackendTaskSuccessResult::FetchedContracts(maybe_found_contracts) => { + let maybe_contracts: Vec<_> = self + .contract_ids_input + .iter() + .filter(|input_id| { + maybe_found_contracts.iter().flatten().any(|contract| { + let trimmed = input_id.trim(); + contract.id().to_string(Encoding::Base58) == trimmed + || hex::encode(contract.id()) == trimmed + }) + }) + .cloned() + .collect(); + self.add_contracts_status = AddContractsStatus::Complete(maybe_contracts); + } + _ => { + // Nothing + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Add Contracts", AppAction::None), + ], + vec![], + ); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Add Contracts"); + ui.add_space(10.0); + + match &self.add_contracts_status { + AddContractsStatus::NotStarted | AddContractsStatus::ErrorMessage(_) => { + if let AddContractsStatus::ErrorMessage(msg) = &self.add_contracts_status { + ui.colored_label(Color32::RED, format!("Error: {}", msg)); + ui.add_space(10.0); + } + + // Show input fields + self.show_input_fields(ui); + + ui.add_space(10.0); + // Add Contracts Button + let button = + egui::Button::new(RichText::new("Add Contracts").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + action = self.add_contracts_clicked(); + } + } + AddContractsStatus::WaitingForResult(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed_seconds = now - start_time; + + let display_time = if elapsed_seconds < 60 { + format!( + "{} second{}", + elapsed_seconds, + if elapsed_seconds == 1 { "" } else { "s" } + ) + } else { + let minutes = elapsed_seconds / 60; + let seconds = elapsed_seconds % 60; + format!( + "{} minute{} and {} second{}", + minutes, + if minutes == 1 { "" } else { "s" }, + seconds, + if seconds == 1 { "" } else { "s" } + ) + }; + + ui.label(format!( + "Fetching contracts... Time taken so far: {}", + display_time + )); + } + AddContractsStatus::Complete(_) => { + action = self.show_success_screen(ui); + } + } + }); + + action + } +} diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs new file mode 100644 index 00000000..6bb3b8be --- /dev/null +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -0,0 +1,627 @@ +use crate::app::{AppAction, DesiredAppAction}; +use crate::backend_task::contract::ContractTask; +use crate::backend_task::document::DocumentTask::{self, FetchDocumentsPage}; // Updated import +use crate::backend_task::BackendTask; +use crate::context::AppContext; +use crate::model::qualified_contract::QualifiedContract; +use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{BackendTaskSuccessResult, MessageType, RootScreenType, ScreenLike, ScreenType}; +use crate::utils::parsers::{DocumentQueryTextInputParser, TextInputParser}; +use chrono::{DateTime, Utc}; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dash_sdk::dpp::data_contract::document_type::{DocumentType, Index}; +use dash_sdk::dpp::prelude::TimestampMillis; +use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; +use dash_sdk::platform::{Document, DocumentQuery, Identifier}; +use egui::{Context, Frame, Margin, ScrollArea, Ui}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// A list of Dash-specific fields that do not appear in the +/// normal document_type properties. +pub const DOCUMENT_PRIVATE_FIELDS: &[&str] = &[ + "$id", + "$ownerId", + "$version", + "$revision", + "$createdAt", + "$updatedAt", + "$transferredAt", + "$createdAtBlockHeight", + "$updatedAtBlockHeight", + "$transferredAtBlockHeight", + "$createdAtCoreBlockHeight", + "$updatedAtCoreBlockHeight", +]; + +pub struct DocumentQueryScreen { + pub app_context: Arc, + error_message: Option<(String, MessageType, DateTime)>, + contract_search_term: String, + document_search_term: String, + document_query: String, + document_display_mode: DocumentDisplayMode, + document_fields_selection: HashMap, + show_fields_dropdown: bool, + selected_data_contract: QualifiedContract, + selected_document_type: DocumentType, + selected_index: Option, + pub matching_documents: Vec, + document_query_status: DocumentQueryStatus, + confirm_remove_contract_popup: bool, + contract_to_remove: Option, + pending_document_type: DocumentType, + pending_fields_selection: HashMap, + // Pagination fields + current_page: usize, + pub next_cursors: Vec, + has_next_page: bool, + previous_cursors: Vec, +} + +#[derive(PartialEq, Eq, Clone)] +pub enum DocumentQueryStatus { + NotStarted, + WaitingForResult(TimestampMillis), + Complete, + ErrorMessage(String), +} + +#[derive(PartialEq, Eq, Clone)] +pub enum DocumentDisplayMode { + Json, + Yaml, +} + +impl DocumentQueryScreen { + pub fn new(app_context: &Arc) -> Self { + let dpns_contract = QualifiedContract { + contract: Arc::clone(&app_context.dpns_contract).as_ref().clone(), + alias: Some("dpns".to_string()), + }; + + let selected_document_type = dpns_contract + .contract + .document_type_cloned_for_name("domain") + .expect("Expected to find domain document type in DPNS contract"); + + let mut document_fields_selection = HashMap::new(); + for (field_name, _schema) in selected_document_type.properties().iter() { + document_fields_selection.insert(field_name.clone(), true); + } + for dash_field in DOCUMENT_PRIVATE_FIELDS { + document_fields_selection.insert((*dash_field).to_string(), false); + } + + let pending_document_type = selected_document_type.clone(); + let pending_fields_selection = document_fields_selection.clone(); + + Self { + app_context: app_context.clone(), + error_message: None, + contract_search_term: String::new(), + document_search_term: String::new(), + document_query: format!("SELECT * FROM {}", selected_document_type.name()), + document_display_mode: DocumentDisplayMode::Yaml, + document_fields_selection, + show_fields_dropdown: false, + selected_data_contract: dpns_contract, + selected_document_type, + selected_index: None, + matching_documents: vec![], + document_query_status: DocumentQueryStatus::NotStarted, + confirm_remove_contract_popup: false, + contract_to_remove: None, + pending_document_type, + pending_fields_selection, + // Initialize pagination fields + current_page: 1, + next_cursors: vec![], + has_next_page: false, + previous_cursors: Vec::new(), + } + } + + fn dismiss_error(&mut self) { + self.error_message = None; + } + + fn check_error_expiration(&mut self) { + if let Some((_, _, timestamp)) = &self.error_message { + let now = Utc::now(); + let elapsed = now.signed_duration_since(*timestamp); + + // Automatically dismiss the error message after 10 seconds + if elapsed.num_seconds() > 10 { + self.dismiss_error(); + } + } + } + + fn build_document_query_with_cursor(&self, cursor: &Start) -> DocumentQuery { + let mut query = DocumentQuery::new( + self.selected_data_contract.contract.clone(), + self.selected_document_type.name(), + ) + .expect("Expected to create a new DocumentQuery"); + if self.current_page == 1 { + query.start = None; + } else { + query.start = Some(cursor.clone()); + } + query + } + + fn get_previous_cursor(&mut self) -> Option { + self.previous_cursors.pop() + } + + fn get_next_cursor(&mut self) -> Option { + self.next_cursors.last().cloned() + } + + fn show_input_field(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + ui.horizontal(|ui| { + let button_width = 120.0; + let text_width = ui.available_width() - button_width; + + ui.add(egui::TextEdit::singleline(&mut self.document_query).desired_width(text_width)); + + let button = egui::Button::new( + egui::RichText::new("Fetch Documents").color(egui::Color32::WHITE), + ) + .fill(egui::Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + self.selected_document_type = self.pending_document_type.clone(); + self.document_fields_selection = self.pending_fields_selection.clone(); + + let parser = + DocumentQueryTextInputParser::new(self.selected_data_contract.contract.clone()); + match parser.parse_input(&self.document_query) { + Ok(parsed_query) => { + // Set the status to waiting and capture the current time + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.document_query_status = DocumentQueryStatus::WaitingForResult(now); + self.current_page = 1; // Reset to first page + self.next_cursors = vec![]; // Reset cursor + self.previous_cursors.clear(); // Clear previous cursors + action = AppAction::BackendTask(BackendTask::DocumentTask( + FetchDocumentsPage(parsed_query), + )); + } + Err(e) => { + self.document_query_status = DocumentQueryStatus::ErrorMessage(format!( + "Failed to parse query properly: {}", + e + )); + self.error_message = Some(( + format!("Failed to parse query properly: {}", e), + MessageType::Error, + Utc::now(), + )); + } + } + } + }); + + action + } + + fn show_output(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + ui.separator(); + ui.add_space(10.0); + + if !self.matching_documents.is_empty() { + ui.horizontal(|ui| { + ui.label("Filter documents:"); + ui.text_edit_singleline(&mut self.document_search_term); + + if ui.button("Select Properties").clicked() { + self.show_fields_dropdown = !self.show_fields_dropdown; + } + + // Display mode toggle + ui.label("Display as:"); + if ui + .selectable_label( + self.document_display_mode == DocumentDisplayMode::Yaml, + "YAML", + ) + .clicked() + { + self.document_display_mode = DocumentDisplayMode::Yaml; + } + if ui + .selectable_label( + self.document_display_mode == DocumentDisplayMode::Json, + "JSON", + ) + .clicked() + { + self.document_display_mode = DocumentDisplayMode::Json; + } + }); + + if self.show_fields_dropdown { + // 1) Partition fields into doc-type vs. dash + let dash_field_set: std::collections::HashSet<&str> = + DOCUMENT_PRIVATE_FIELDS.iter().cloned().collect(); + + let mut doc_type_fields = Vec::new(); + let mut dash_fields = Vec::new(); + + for (field_name, is_checked) in &mut self.document_fields_selection { + if dash_field_set.contains(field_name.as_str()) { + dash_fields.push((field_name, is_checked)); + } else { + doc_type_fields.push((field_name, is_checked)); + } + } + + egui::Window::new("Select Properties") + .collapsible(false) + .resizable(true) + .min_width(400.0) + .title_bar(false) + .show(ui.ctx(), |ui| { + ui.label("Check the properties to display:"); + ui.add_space(10.0); + + ui.columns(2, |columns| { + columns[0].heading("Document Properties"); + columns[0].add_space(5.0); + for (field_name, is_checked) in &mut doc_type_fields { + columns[0].checkbox(is_checked, field_name.clone()); + } + + columns[1].heading("Universal Properties"); + columns[1].add_space(5.0); + for (field_name, is_checked) in &mut dash_fields { + columns[1].checkbox(is_checked, field_name.clone()); + } + }); + + ui.separator(); + if ui.button("Close").clicked() { + self.show_fields_dropdown = false; + } + }); + } + } + + ui.add_space(5.0); + + let pagination_height = 30.0; + let max_scroll_height = ui.available_height() - pagination_height; + + ScrollArea::vertical() + .max_height(max_scroll_height) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + + match self.document_query_status { + DocumentQueryStatus::WaitingForResult(start_time) => { + let time_elapsed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + - start_time; + ui.horizontal(|ui| { + ui.label(format!( + "Fetching documents... Time taken so far: {} seconds", + time_elapsed + )); + ui.spinner(); + }); + } + DocumentQueryStatus::Complete => match self.document_display_mode { + DocumentDisplayMode::Json => { + self.show_filtered_docs(ui, DocumentDisplayMode::Json); + } + DocumentDisplayMode::Yaml => { + self.show_filtered_docs(ui, DocumentDisplayMode::Yaml); + } + }, + + DocumentQueryStatus::ErrorMessage(ref message) => { + self.error_message = + Some((message.to_string(), MessageType::Error, Utc::now())); + ui.colored_label(egui::Color32::DARK_RED, message); + } + _ => { + // Nothing + } + } + }); + + ui.add_space(10.0); + + if self.document_query_status == DocumentQueryStatus::Complete { + ui.horizontal(|ui| { + if self.current_page > 1 { + if ui.button("Previous Page").clicked() { + // Handle Previous Page + if let Some(prev_cursor) = self.get_previous_cursor() { + self.document_query_status = DocumentQueryStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + self.current_page -= 1; + self.next_cursors.pop(); + let parsed_query = self.build_document_query_with_cursor(&prev_cursor); + action = AppAction::BackendTask(BackendTask::DocumentTask( + DocumentTask::FetchDocumentsPage(parsed_query), + )); + } else { + self.document_query_status = DocumentQueryStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + self.current_page = 1; + let next_cursor = + self.get_next_cursor().unwrap_or(Start::StartAfter(vec![])); // Doesn't matter what the value is + let parsed_query = self.build_document_query_with_cursor(&next_cursor); + action = AppAction::BackendTask(BackendTask::DocumentTask( + DocumentTask::FetchDocumentsPage(parsed_query), + )); + } + } + } + + ui.label(format!("Page {}", self.current_page)); + + if self.has_next_page { + if ui.button("Next Page").clicked() { + // Handle Next Page + if let Some(next_cursor) = &self.get_next_cursor() { + self.document_query_status = DocumentQueryStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + if self.current_page > 1 { + self.previous_cursors.push( + self.next_cursors + .get(self.next_cursors.len() - 2) + .expect("Expected a previous cursor") + .clone(), + ); + } + self.current_page += 1; + let parsed_query = self.build_document_query_with_cursor(next_cursor); + action = AppAction::BackendTask(BackendTask::DocumentTask( + DocumentTask::FetchDocumentsPage(parsed_query), + )); + } + } + } + }); + } + + action + } + + fn show_filtered_docs(&mut self, ui: &mut egui::Ui, display_mode: DocumentDisplayMode) { + // 1) Convert each Document to a filtered string + let mut doc_strings = Vec::new(); + + for doc in &self.matching_documents { + if let Some(stringed) = doc_to_filtered_string( + doc, + &self.document_fields_selection, // or the user’s selected fields + display_mode.clone(), + ) { + // Optionally also filter by `document_search_term` here + if self.document_search_term.is_empty() + || stringed + .to_lowercase() + .contains(&self.document_search_term.to_lowercase()) + { + doc_strings.push(stringed); + } + } + } + + // 2) Concatenate them all with spacing + let mut combined_string = doc_strings.join("\n\n"); + + // 3) Display in multiline text + ui.add( + egui::TextEdit::multiline(&mut combined_string) + .desired_rows(10) + .desired_width(ui.available_width()) + .font(egui::TextStyle::Monospace), + ); + } + + fn show_remove_contract_popup(&mut self, ui: &mut egui::Ui) -> AppAction { + // If no contract is set, nothing to confirm + let contract_to_remove = match &self.contract_to_remove { + Some(contract) => contract.clone(), + None => { + self.confirm_remove_contract_popup = false; + return AppAction::None; + } + }; + + let mut app_action = AppAction::None; + let mut is_open = true; + + egui::Window::new("Confirm Remove Contract") + .collapsible(false) + .open(&mut is_open) + .show(ui.ctx(), |ui| { + ui.label(format!( + "Are you sure you want to remove contract \"{}\"?", + contract_to_remove + )); + + // Confirm button + if ui.button("Confirm").clicked() { + app_action = AppAction::BackendTask(BackendTask::ContractTask( + ContractTask::RemoveContract(contract_to_remove), + )); + self.confirm_remove_contract_popup = false; + self.contract_to_remove = None; + } + + // Cancel button + if ui.button("Cancel").clicked() { + self.confirm_remove_contract_popup = false; + self.contract_to_remove = None; + } + }); + + // If user closes the popup window (the [x] button), also reset state + if !is_open { + self.confirm_remove_contract_popup = false; + self.contract_to_remove = None; + } + app_action + } +} + +impl ScreenLike for DocumentQueryScreen { + fn refresh(&mut self) {} + + fn display_message(&mut self, message: &str, message_type: MessageType) { + // Only display the error message resulting from FetchDocuments backend task + if message.contains("Error fetching documents") { + self.document_query_status = DocumentQueryStatus::ErrorMessage(message.to_string()); + self.error_message = Some((message.to_string(), message_type, Utc::now())); + } + } + + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + match backend_task_success_result { + BackendTaskSuccessResult::Documents(documents) => { + self.matching_documents = documents + .iter() + .filter_map(|(_, doc)| doc.clone()) + .collect(); + self.document_query_status = DocumentQueryStatus::Complete; + } + BackendTaskSuccessResult::PageDocuments(page_docs, next_cursor) => { + self.matching_documents = page_docs + .iter() + .filter_map(|(_, doc)| doc.clone()) + .collect(); + self.has_next_page = next_cursor.is_some(); + if let Some(cursor) = next_cursor { + self.next_cursors.push(cursor.clone()); + } + self.document_query_status = DocumentQueryStatus::Complete; + } + _ => { + // Handle other variants + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + self.check_error_expiration(); + let add_contract_button = ( + "Add Contracts", + DesiredAppAction::AddScreenType(ScreenType::AddContracts), + ); + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![("Contracts", AppAction::None)], + vec![add_contract_button], + ); + + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenDocumentQuery, + ); + + action |= add_contract_chooser_panel( + ctx, + &mut self.contract_search_term, + &self.app_context, + &mut self.selected_data_contract, + &mut self.selected_document_type, + &mut self.selected_index, + &mut self.document_query, + &mut self.pending_document_type, + &mut self.pending_fields_selection, + ); + + if let AppAction::BackendTask(BackendTask::ContractTask(ContractTask::RemoveContract( + contract_id, + ))) = action + { + action = AppAction::None; + self.confirm_remove_contract_popup = true; + self.contract_to_remove = Some(contract_id); + } + + egui::CentralPanel::default() + .frame( + Frame::none() + .fill(ctx.style().visuals.panel_fill) + .inner_margin(Margin::same(10.0)), + ) + .show(ctx, |ui| { + action |= self.show_input_field(ui); + action |= self.show_output(ui); + + if self.confirm_remove_contract_popup { + action |= self.show_remove_contract_popup(ui); + } + }); + + action + } +} + +/// Convert a `Document` to a `serde_json::Value`, then filter out unselected fields, +/// then serialize the result to JSON/YAML. +fn doc_to_filtered_string( + doc: &Document, + selected_fields: &std::collections::HashMap, + display_mode: DocumentDisplayMode, +) -> Option { + // 1) Convert doc to a serde_json Value + let value = serde_json::to_value(doc).ok()?; + let obj = value.as_object()?; + + // 2) Build a new JSON object containing only the selected fields + let mut filtered_map = serde_json::Map::new(); + + for (field_name, &is_checked) in selected_fields { + if is_checked { + if let Some(field_value) = obj.get(field_name) { + filtered_map.insert(field_name.clone(), field_value.clone()); + } + } + } + + let filtered_value = serde_json::Value::Object(filtered_map); + + // 3) Convert filtered_value to the chosen format + let final_string = match display_mode { + DocumentDisplayMode::Json => serde_json::to_string_pretty(&filtered_value).ok()?, + DocumentDisplayMode::Yaml => serde_yaml::to_string(&filtered_value).ok()?, + }; + + Some(final_string) +} diff --git a/src/ui/contracts_documents/mod.rs b/src/ui/contracts_documents/mod.rs new file mode 100644 index 00000000..7a803090 --- /dev/null +++ b/src/ui/contracts_documents/mod.rs @@ -0,0 +1,2 @@ +pub mod add_contracts_screen; +pub mod document_query_screen; diff --git a/src/ui/document_query_screen.rs b/src/ui/document_query_screen.rs deleted file mode 100644 index 6354018e..00000000 --- a/src/ui/document_query_screen.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::app::AppAction; -use crate::backend_task::contested_names::ContestedResourceTask; -use crate::backend_task::BackendTask; -use crate::context::AppContext; -use crate::model::contested_name::ContestedName; -use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::top_panel::add_top_panel; -use crate::ui::{MessageType, RootScreenType, ScreenLike}; -use chrono::{DateTime, Utc}; -use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use egui::{Context, Ui}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SortColumn { - ContestedName, - LockedVotes, - AbstainVotes, - EndingTime, - LastUpdated, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SortOrder { - Ascending, - Descending, -} - -pub struct DocumentQueryScreen { - contested_names: Arc>>, - pub app_context: Arc, - error_message: Option<(String, MessageType, DateTime)>, - sort_column: SortColumn, - sort_order: SortOrder, - show_vote_popup: Option<(String, ContestedResourceTask)>, - contract_search_term: String, -} - -impl DocumentQueryScreen { - pub fn new(app_context: &Arc) -> Self { - let contested_names = Arc::new(Mutex::new( - app_context.all_contested_names().unwrap_or_default(), - )); - Self { - contested_names, - app_context: app_context.clone(), - error_message: None, - sort_column: SortColumn::ContestedName, - sort_order: SortOrder::Ascending, - show_vote_popup: None, - contract_search_term: String::new(), - } - } - - fn show_contested_name_details( - &mut self, - ui: &mut Ui, - contested_name: &ContestedName, - is_locked_votes_bold: bool, - max_contestant_votes: u32, - ) { - if let Some(contestants) = &contested_name.contestants { - for contestant in contestants { - let button_text = format!("{} - {} votes", contestant.name, contestant.votes); - - // Determine if this contestant's votes should be bold - let text = if contestant.votes == max_contestant_votes && !is_locked_votes_bold { - egui::RichText::new(button_text) - .strong() - .color(egui::Color32::from_rgb(0, 100, 0)) - } else { - egui::RichText::new(button_text) - }; - - if ui.button(text).clicked() { - self.show_vote_popup = Some(( - format!( - "Confirm Voting for Contestant {} for name \"{}\"", - contestant.id, contestant.name - ), - ContestedResourceTask::VoteOnDPNSName( - contested_name.normalized_contested_name.clone(), - ResourceVoteChoice::Abstain, - vec![], - ), - )); - } - } - } - } - - fn sort_contested_names(&self, contested_names: &mut Vec) { - contested_names.sort_by(|a, b| { - let order = match self.sort_column { - SortColumn::ContestedName => a - .normalized_contested_name - .cmp(&b.normalized_contested_name), - SortColumn::LockedVotes => a.locked_votes.cmp(&b.locked_votes), - SortColumn::AbstainVotes => a.abstain_votes.cmp(&b.abstain_votes), - SortColumn::EndingTime => a.end_time.cmp(&b.end_time), - SortColumn::LastUpdated => a.last_updated.cmp(&b.last_updated), - }; - - if self.sort_order == SortOrder::Descending { - order.reverse() - } else { - order - } - }); - } - - fn dismiss_error(&mut self) { - self.error_message = None; - } - - fn check_error_expiration(&mut self) { - if let Some((_, _, timestamp)) = &self.error_message { - let now = Utc::now(); - let elapsed = now.signed_duration_since(*timestamp); - - // Automatically dismiss the error message after 5 seconds - if elapsed.num_seconds() > 5 { - self.dismiss_error(); - } - } - } - - fn toggle_sort(&mut self, column: SortColumn) { - if self.sort_column == column { - self.sort_order = match self.sort_order { - SortOrder::Ascending => SortOrder::Descending, - SortOrder::Descending => SortOrder::Ascending, - }; - } else { - self.sort_column = column; - self.sort_order = SortOrder::Ascending; - } - } - - fn show_vote_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - if let Some((message, action)) = self.show_vote_popup.clone() { - ui.label(message); - - ui.horizontal(|ui| { - if ui.button("Vote Immediate").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask(action)); - self.show_vote_popup = None; - } else if ui.button("Vote Deferred").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask(action)); - self.show_vote_popup = None; - } else if ui.button("Cancel").clicked() { - self.show_vote_popup = None; - } - }); - } - app_action - } -} -impl ScreenLike for DocumentQueryScreen { - fn refresh(&mut self) { - let mut contested_names = self.contested_names.lock().unwrap(); - *contested_names = self.app_context.all_contested_names().unwrap_or_default(); - } - - fn display_message(&mut self, message: &str, message_type: MessageType) { - self.error_message = Some((message.to_string(), message_type, Utc::now())); - } - - fn ui(&mut self, ctx: &Context) -> AppAction { - self.check_error_expiration(); - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![("Document Queries", AppAction::None)], - vec![], - ); - - action |= add_left_panel( - ctx, - &self.app_context, - RootScreenType::RootScreenDocumentQuery, - ); - - action |= - add_contract_chooser_panel(ctx, &mut self.contract_search_term, &self.app_context); - - action - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f91f0f5b..59473532 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,7 +5,7 @@ use crate::model::qualified_identity::encrypted_key_storage::{ PrivateKeyData, WalletDerivationPath, }; use crate::model::qualified_identity::QualifiedIdentity; -use crate::ui::document_query_screen::DocumentQueryScreen; +use crate::ui::contracts_documents::document_query_screen::DocumentQueryScreen; use crate::ui::dpns::dpns_contested_names_screen::DPNSContestedNamesScreen; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; @@ -18,6 +18,7 @@ use crate::ui::tools::proof_log_screen::ProofLogScreen; use crate::ui::wallets::import_wallet_screen::ImportWalletScreen; use crate::ui::wallets::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; +use contracts_documents::add_contracts_screen::AddContractsScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; @@ -35,7 +36,7 @@ use tools::transition_visualizer_screen::TransitionVisualizerScreen; use wallets::add_new_wallet_screen::AddNewWalletScreen; pub mod components; -pub mod document_query_screen; +pub mod contracts_documents; pub mod dpns; pub(crate) mod identities; pub mod network_chooser_screen; @@ -145,6 +146,7 @@ pub enum ScreenType { TopUpIdentity(QualifiedIdentity), ScheduleVoteScreen(String, u64, Vec, ResourceVoteChoice), ScheduledVotes, + AddContracts, } impl ScreenType { @@ -229,6 +231,9 @@ impl ScreenType { ScreenType::ScheduledVotes => Screen::DPNSContestedNamesScreen( DPNSContestedNamesScreen::new(app_context, DPNSSubscreen::ScheduledVotes), ), + ScreenType::AddContracts => { + Screen::AddContractsScreen(AddContractsScreen::new(app_context)) + } } } } @@ -254,6 +259,7 @@ pub enum Screen { NetworkChooserScreen(NetworkChooserScreen), WalletsBalancesScreen(WalletsBalancesScreen), ScheduleVoteScreen(ScheduleVoteScreen), + AddContractsScreen(AddContractsScreen), } impl Screen { @@ -279,6 +285,7 @@ impl Screen { Screen::ImportWalletScreen(screen) => screen.app_context = app_context, Screen::ProofLogScreen(screen) => screen.app_context = app_context, Screen::ScheduleVoteScreen(screen) => screen.app_context = app_context, + Screen::AddContractsScreen(screen) => screen.app_context = app_context, } } } @@ -369,6 +376,7 @@ impl Screen { screen.identities.clone(), screen.vote_choice.clone(), ), + Screen::AddContractsScreen(_) => ScreenType::AddContracts, } } } @@ -396,6 +404,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.refresh(), Screen::ProofLogScreen(screen) => screen.refresh(), Screen::ScheduleVoteScreen(screen) => screen.refresh(), + Screen::AddContractsScreen(screen) => screen.refresh(), } } @@ -421,6 +430,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.refresh_on_arrival(), Screen::ProofLogScreen(screen) => screen.refresh_on_arrival(), Screen::ScheduleVoteScreen(screen) => screen.refresh_on_arrival(), + Screen::AddContractsScreen(screen) => screen.refresh_on_arrival(), } } @@ -446,6 +456,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.ui(ctx), Screen::ProofLogScreen(screen) => screen.ui(ctx), Screen::ScheduleVoteScreen(screen) => screen.ui(ctx), + Screen::AddContractsScreen(screen) => screen.ui(ctx), } } @@ -477,6 +488,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.display_message(message, message_type), Screen::ProofLogScreen(screen) => screen.display_message(message, message_type), Screen::ScheduleVoteScreen(screen) => screen.display_message(message, message_type), + Screen::AddContractsScreen(screen) => screen.display_message(message, message_type), } } @@ -542,6 +554,9 @@ impl ScreenLike for Screen { Screen::ScheduleVoteScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::AddContractsScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } } } @@ -567,6 +582,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.pop_on_success(), Screen::ProofLogScreen(screen) => screen.pop_on_success(), Screen::ScheduleVoteScreen(screen) => screen.pop_on_success(), + Screen::AddContractsScreen(screen) => screen.pop_on_success(), } } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 00000000..be756a00 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod parsers; diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs new file mode 100644 index 00000000..9adc8ac1 --- /dev/null +++ b/src/utils/parsers.rs @@ -0,0 +1,29 @@ +//! Parsers for text input. + +use dash_sdk::dpp::prelude::DataContract; +use dash_sdk::platform::{DocumentQuery, DriveDocumentQuery}; + +pub(crate) trait TextInputParser { + type Output; + fn parse_input(&self, input: &str) -> Result; +} + +pub(crate) struct DocumentQueryTextInputParser { + data_contract: DataContract, +} + +impl DocumentQueryTextInputParser { + pub(crate) fn new(data_contract: DataContract) -> Self { + DocumentQueryTextInputParser { data_contract } + } +} + +impl TextInputParser for DocumentQueryTextInputParser { + type Output = DocumentQuery; + + fn parse_input(&self, input: &str) -> Result { + DriveDocumentQuery::from_sql_expr(input, &self.data_contract, None) + .map(Into::into) + .map_err(|e| e.to_string()) + } +} From b40c5e2b9b2e945e1dde5fafd46eb45fd26c2d79 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:05:19 -0500 Subject: [PATCH 11/14] feat: broadcast decoded state transitions (#151) * feat: broadcast decoded state transitions * add file --- .../broadcast_state_transition.rs | 23 +++ src/backend_task/mod.rs | 7 + src/ui/tools/transition_visualizer_screen.rs | 172 +++++++++++++++--- 3 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 src/backend_task/broadcast_state_transition.rs diff --git a/src/backend_task/broadcast_state_transition.rs b/src/backend_task/broadcast_state_transition.rs new file mode 100644 index 00000000..4309c33d --- /dev/null +++ b/src/backend_task/broadcast_state_transition.rs @@ -0,0 +1,23 @@ +use dash_sdk::{ + dpp::state_transition::StateTransition, + platform::transition::broadcast::BroadcastStateTransition, Sdk, +}; + +use crate::context::AppContext; + +use super::BackendTaskSuccessResult; + +impl AppContext { + pub async fn broadcast_state_transition( + &self, + state_transition: StateTransition, + sdk: &Sdk, + ) -> Result { + match state_transition.broadcast_and_wait(sdk, None).await { + Ok(_) => Ok(BackendTaskSuccessResult::Message( + "State transition broadcasted successfully".to_string(), + )), + Err(e) => Err(format!("Error broadcasting state transition: {}", e)), + } + } +} diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index 4c537135..8a6a8fed 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -9,6 +9,7 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use contested_names::ScheduledDPNSVote; use dash_sdk::dpp::prelude::DataContract; +use dash_sdk::dpp::state_transition::StateTransition; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; use dash_sdk::platform::{Document, Identifier}; @@ -16,6 +17,7 @@ use dash_sdk::query_types::{Documents, IndexMap}; use std::sync::Arc; use tokio::sync::mpsc; +pub mod broadcast_state_transition; pub mod contested_names; pub mod contract; pub mod core; @@ -31,6 +33,7 @@ pub(crate) enum BackendTask { ContestedResourceTask(ContestedResourceTask), CoreTask(CoreTask), WithdrawalTask(WithdrawalsTask), + BroadcastStateTransition(StateTransition), } #[derive(Debug, Clone, PartialEq)] @@ -88,6 +91,10 @@ impl AppContext { BackendTask::WithdrawalTask(withdrawal_task) => { self.run_withdraws_task(withdrawal_task, &sdk).await } + BackendTask::BroadcastStateTransition(state_transition) => { + self.broadcast_state_transition(state_transition, &sdk) + .await + } } } } diff --git a/src/ui/tools/transition_visualizer_screen.rs b/src/ui/tools/transition_visualizer_screen.rs index d11a8506..ff72728e 100644 --- a/src/ui/tools/transition_visualizer_screen.rs +++ b/src/ui/tools/transition_visualizer_screen.rs @@ -1,20 +1,33 @@ use crate::app::AppAction; +use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tools_subscreen_chooser_panel::add_tools_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::{RootScreenType, ScreenLike}; +use crate::ui::{MessageType, RootScreenType, ScreenLike}; + use base64::{engine::general_purpose::STANDARD, Engine}; +use dash_sdk::dpp::prelude::TimestampMillis; use dash_sdk::dpp::serialization::PlatformDeserializable; use dash_sdk::dpp::state_transition::StateTransition; -use eframe::egui::{self, Context, ScrollArea, TextEdit, Ui}; +use eframe::egui::{self, Color32, Context, ScrollArea, TextEdit, Ui}; +use egui::RichText; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(PartialEq)] +pub enum TransitionBroadcastStatus { + NotStarted, + Submitting(TimestampMillis), + Error(String), + Complete, +} pub struct TransitionVisualizerScreen { pub app_context: Arc, input_data: String, parsed_json: Option, - error_message: Option, + broadcast_status: TransitionBroadcastStatus, } impl TransitionVisualizerScreen { @@ -23,14 +36,13 @@ impl TransitionVisualizerScreen { app_context: app_context.clone(), input_data: String::new(), parsed_json: None, - error_message: None, + broadcast_status: TransitionBroadcastStatus::NotStarted, } } fn parse_input(&mut self) { - // Clear previous messages + // Clear previous parse results, but let's not overwrite broadcast_status self.parsed_json = None; - self.error_message = None; // Try to decode the input as hex first let decoded_bytes = hex::decode(&self.input_data).or_else(|_| { @@ -48,23 +60,30 @@ impl TransitionVisualizerScreen { match serde_json::to_string_pretty(&state_transition) { Ok(json) => self.parsed_json = Some(json), Err(e) => { - self.error_message = - Some(format!("Failed to serialize to JSON: {}", e)) + self.broadcast_status = TransitionBroadcastStatus::Error(format!( + "Failed to serialize to JSON: {}", + e + )); } } } Err(e) => { - self.error_message = - Some(format!("Failed to parse state transition: {}", e)) + self.broadcast_status = TransitionBroadcastStatus::Error(format!( + "Failed to parse state transition: {}", + e + )); } } } - Err(e) => self.error_message = Some(e), + Err(e) => { + self.broadcast_status = TransitionBroadcastStatus::Error(e); + } } } fn show_input_field(&mut self, ui: &mut Ui) { ui.label("Enter hex or base64 encoded state transition:"); + ui.add_space(5.0); let response = ui.add( TextEdit::multiline(&mut self.input_data) .desired_rows(6) @@ -72,38 +91,145 @@ impl TransitionVisualizerScreen { .code_editor(), ); - // If the user changes the input, parse it again + ui.add_space(10.0); + if response.changed() { + // Re-parse self.parse_input(); } } - fn show_output(&self, ui: &mut Ui) { + fn show_output(&mut self, ui: &mut Ui) -> AppAction { + let mut app_action = AppAction::None; + ui.separator(); + ui.add_space(10.0); ui.label("Parsed State Transition:"); + // Show the JSON if we have it ScrollArea::vertical().show(ui, |ui| { - ui.set_width(ui.available_width()); // Make the scroll area take the entire width - if let Some(ref json) = self.parsed_json { + ui.add_space(5.0); ui.add( TextEdit::multiline(&mut json.clone()) .desired_rows(10) - .desired_width(ui.available_width()) // Make the output take the entire width - .font(egui::TextStyle::Monospace), // Use a monospace font for JSON + .desired_width(ui.available_width()) + .font(egui::TextStyle::Monospace), ); - } else if let Some(ref error) = self.error_message { - ui.colored_label(egui::Color32::RED, error); + + ui.add_space(10.0); + + // If we’re not done or not in the middle of broadcasting, we can show the button + if matches!( + self.broadcast_status, + TransitionBroadcastStatus::NotStarted | TransitionBroadcastStatus::Error(_) + ) { + if let TransitionBroadcastStatus::Submitting(_) = self.broadcast_status { + // Broadcast button + let mut new_style = (**ui.style()).clone(); + new_style.spacing.button_padding = egui::vec2(10.0, 5.0); + ui.set_style(new_style); + let button = egui::Button::new( + RichText::new("Broadcast Transition to Platform").color(Color32::WHITE), + ) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + // Mark as submitting + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.broadcast_status = TransitionBroadcastStatus::Submitting(now); + // Kick off the backend task + match StateTransition::deserialize_from_bytes( + &hex::decode(&self.input_data).unwrap(), + ) { + Ok(state_transition) => { + app_action = AppAction::BackendTask( + BackendTask::BroadcastStateTransition(state_transition), + ); + } + Err(e) => { + self.broadcast_status = TransitionBroadcastStatus::Error( + format!("Failed to parse state transition: {}", e), + ); + } + } + } + } + } } else { - ui.label("No valid state transition parsed yet."); + // If parsed_json is None + if matches!(self.broadcast_status, TransitionBroadcastStatus::NotStarted) { + ui.label("No valid state transition parsed yet."); + } } }); + + // Show status + ui.add_space(5.0); + match &self.broadcast_status { + TransitionBroadcastStatus::NotStarted => {} + TransitionBroadcastStatus::Submitting(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed_seconds = now - start_time; + + let display_time = if elapsed_seconds < 60 { + format!( + "{} second{}", + elapsed_seconds, + if elapsed_seconds == 1 { "" } else { "s" } + ) + } else { + let minutes = elapsed_seconds / 60; + let seconds = elapsed_seconds % 60; + format!( + "{} minute{} and {} second{}", + minutes, + if minutes == 1 { "" } else { "s" }, + seconds, + if seconds == 1 { "" } else { "s" } + ) + }; + + ui.label(format!( + "Broadcasting... Time taken so far: {}", + display_time + )); + } + TransitionBroadcastStatus::Error(msg) => { + ui.colored_label(Color32::DARK_RED, format!("Error: {}", msg)); + } + TransitionBroadcastStatus::Complete => { + ui.colored_label( + Color32::DARK_GREEN, + "Successfully broadcasted state transition.", + ); + } + } + + app_action } } impl ScreenLike for TransitionVisualizerScreen { - fn refresh(&mut self) { - // No-op for this screen + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + self.broadcast_status = TransitionBroadcastStatus::Complete; + } + MessageType::Error => { + self.broadcast_status = TransitionBroadcastStatus::Error(message.to_string()); + } + MessageType::Info => { + // Could do nothing or handle info + } + } } fn ui(&mut self, ctx: &Context) -> AppAction { @@ -124,7 +250,7 @@ impl ScreenLike for TransitionVisualizerScreen { egui::CentralPanel::default().show(ctx, |ui| { self.show_input_field(ui); - self.show_output(ui); + action |= self.show_output(ui); }); action From 3c1f48dba851e1c445cc7b8d61009e92542f403a Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:05:38 -0500 Subject: [PATCH 12/14] feat: bulk vote scheduling (#150) * feat: bulk vote scheduling * add file * good --- build.rs | 5 +- src/main.rs | 6 +- src/ui/dpns/dpns_bulk_vote_schedule_screen.rs | 306 ++++++++++++++++++ src/ui/dpns/dpns_contested_names_screen.rs | 212 ++++++++++-- src/ui/dpns/dpns_vote_scheduling_screen.rs | 2 +- src/ui/dpns/mod.rs | 1 + .../identities/add_new_identity_screen/mod.rs | 5 +- src/ui/mod.rs | 20 +- 8 files changed, 527 insertions(+), 30 deletions(-) create mode 100644 src/ui/dpns/dpns_bulk_vote_schedule_screen.rs diff --git a/build.rs b/build.rs index 223fc1c5..8f7cd2b6 100644 --- a/build.rs +++ b/build.rs @@ -12,5 +12,6 @@ fn main() { fs::write( dest_path, format!(r#"pub const VERSION: &str = "{}";"#, version), - ).expect("Failed to write version.rs"); -} \ No newline at end of file + ) + .expect("Failed to write version.rs"); +} diff --git a/src/main.rs b/src/main.rs index ff595f53..0dbdc580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use std::env; use crate::app_dir::{app_user_data_dir_path, create_app_user_data_directory_if_not_exists}; use crate::cpu_compatibility::check_cpu_compatibility; +use std::env; mod app; mod app_dir; @@ -22,8 +22,8 @@ include!(concat!(env!("OUT_DIR"), "/version.rs")); fn main() -> eframe::Result<()> { create_app_user_data_directory_if_not_exists() .expect("Failed to create app user_data directory"); - let app_data_dir = app_user_data_dir_path() - .expect("Failed to get app user_data directory path"); + let app_data_dir = + app_user_data_dir_path().expect("Failed to get app user_data directory path"); println!("running v{}", VERSION); check_cpu_compatibility(); // Initialize the Tokio runtime diff --git a/src/ui/dpns/dpns_bulk_vote_schedule_screen.rs b/src/ui/dpns/dpns_bulk_vote_schedule_screen.rs new file mode 100644 index 00000000..57fa115a --- /dev/null +++ b/src/ui/dpns/dpns_bulk_vote_schedule_screen.rs @@ -0,0 +1,306 @@ +use crate::app::AppAction; +use crate::backend_task::contested_names::ScheduledDPNSVote; +use crate::backend_task::{contested_names::ContestedResourceTask, BackendTask}; +use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::RootScreenType; +use crate::ui::{MessageType, ScreenLike}; +use chrono::offset::LocalResult; +use chrono::{Duration, TimeZone, Utc}; +use chrono_humanize::HumanTime; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use eframe::egui::Context; +use eframe::egui::{self, Color32, RichText, Ui}; +use std::sync::Arc; + +use super::dpns_contested_names_screen::SelectedVote; +use super::dpns_vote_scheduling_screen::VoteOption; + +pub struct BulkScheduleVoteScreen { + pub app_context: Arc, + /// All the name+choice pairs the user SHIFT-clicked + pub selected_votes: Vec, + + /// The user picks which identities to use, + /// plus how far in the future each identity’s vote is cast. + pub identities: Vec, + pub identity_options: Vec, + message: Option<(MessageType, String)>, +} + +impl BulkScheduleVoteScreen { + pub fn new(app_context: &Arc, selected_votes: Vec) -> Self { + // Query local voting identities from app_context + let identities = app_context + .db + .get_local_voting_identities(&app_context) + .unwrap_or_default(); + + // Initialize each identity’s “VoteOption” to “Scheduled(0,0,0)” + let identity_options = identities + .iter() + .map(|_| VoteOption::Scheduled { + days: 0, + hours: 0, + minutes: 0, + }) + .collect(); + + Self { + app_context: app_context.clone(), + selected_votes, + identities, + identity_options, + message: None, + } + } + + fn display_identity_options(&mut self, ui: &mut Ui) { + ui.heading("Select scheduling offsets (Days/Hours/Minutes) for each node:"); + ui.add_space(5.0); + + for (i, identity) in self.identities.iter().enumerate() { + ui.group(|ui| { + ui.horizontal(|ui| { + // Identity label + let identity_label = identity + .alias + .as_ref() + .map(|a| a.clone()) + .unwrap_or(identity.identity.id().to_string(Encoding::Base58)); + ui.label(format!("Identity: {}", identity_label)); + + // Dropdown for None or Scheduled + let current_option = &mut self.identity_options[i]; + egui::ComboBox::from_id_source(format!("combo_for_identity_{}", i)) + .width(100.0) + .selected_text(match current_option { + VoteOption::None => "None".to_string(), + VoteOption::Scheduled { .. } => "Scheduled".to_string(), + }) + .show_ui(ui, |ui| { + if ui + .selectable_label( + matches!(current_option, VoteOption::None), + "None", + ) + .clicked() + { + *current_option = VoteOption::None; + self.message = None; + } + if ui + .selectable_label( + matches!(current_option, VoteOption::Scheduled { .. }), + "Scheduled", + ) + .clicked() + { + // If we had a previous scheduled option, keep the old values: + let (days, hours, minutes) = match current_option { + VoteOption::Scheduled { + days, + hours, + minutes, + } => (*days, *hours, *minutes), + _ => (0, 0, 0), + }; + *current_option = VoteOption::Scheduled { + days, + hours, + minutes, + }; + self.message = None; + } + }); + + // If Scheduled is chosen, let the user pick how far in the future + if let VoteOption::Scheduled { + days, + hours, + minutes, + } = current_option + { + ui.label("Schedule Vote In:"); + ui.horizontal(|ui| { + ui.add(egui::DragValue::new(days).range(0..=14).prefix("Days: ")); + ui.add(egui::DragValue::new(hours).range(0..=23).prefix("Hours: ")); + ui.add(egui::DragValue::new(minutes).range(0..=59).prefix("Min: ")); + }); + } + }); + }); + + ui.add_space(10.0); + } + } + + fn schedule_votes(&mut self) -> AppAction { + // Build up a list of ScheduledDPNSVote for *all* selected votes + let mut all_scheduled = Vec::new(); + + for (identity, option) in self.identities.iter().zip(self.identity_options.iter()) { + if let VoteOption::Scheduled { + days, + hours, + minutes, + } = option + { + // Convert days/hours/minutes into a single timestamp + let now = Utc::now(); + let offset = Duration::days(*days as i64) + + Duration::hours(*hours as i64) + + Duration::minutes(*minutes as i64); + let scheduled_time = (now + offset).timestamp_millis() as u64; + + // For each selected vote in selected_votes + for sv in &self.selected_votes { + let scheduled_vote = ScheduledDPNSVote { + contested_name: sv.contested_name.clone(), + voter_id: identity.identity.id().clone(), + choice: sv.vote_choice.clone(), + unix_timestamp: scheduled_time, + executed_successfully: false, + }; + all_scheduled.push(scheduled_vote); + } + } + } + + if all_scheduled.is_empty() { + self.message = Some(( + MessageType::Error, + "No votes selected or scheduled.".to_string(), + )); + return AppAction::None; + } + + // Send them off to the backend + AppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::ScheduleDPNSVotes(all_scheduled), + )) + } + + fn show_success(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + ui.heading("Successfully scheduled votes."); + + ui.add_space(20.0); + + if ui.button("Go to Scheduled Votes Screen").clicked() { + action = AppAction::SetMainScreenThenPopScreen( + RootScreenType::RootScreenDPNSScheduledVotes, + ); + } + + ui.add_space(10.0); + + if ui.button("Go back to Active Contests").clicked() { + action = AppAction::PopScreenAndRefresh; + } + }); + + action + } +} + +impl ScreenLike for BulkScheduleVoteScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + self.message = Some((message_type, message.to_string())); + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("DPNS", AppAction::GoToMainScreen), + ("Bulk Schedule", AppAction::None), + ], + vec![], + ); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Bulk-Schedule Votes"); + ui.add_space(10.0); + + if self.selected_votes.is_empty() { + ui.colored_label(Color32::DARK_RED, "No votes selected. You can SHIFT-click on vote choices in the Active Contests table to select votes."); + return; + } + + ui.label("Please note that Dash Evo Tool must be running and connected to Platform in order for scheduled votes to execute at the specified time."); + ui.add_space(10.0); + + // Show a small table or list of the selected votes + ui.group(|ui| { + ui.heading("Selected votes:"); + ui.separator(); + for sv in &self.selected_votes { + // Convert the timestamp to a DateTime object using timestamp_millis_opt + let end_time = if let Some(end_time) = sv.end_time { + if let LocalResult::Single(datetime) = Utc.timestamp_millis_opt(end_time as i64) { + let iso_date = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); + let relative_time = HumanTime::from(datetime).to_string(); + let display_text = format!( + "{} ({})", + iso_date, relative_time + ); + display_text + } else { + "Invalid timestamp".to_string() + }} else { + "Error getting end time".to_string() + }; + + ui.label(format!( + "• Name: {} | Choice: {} | Contest End Time: {}", + sv.contested_name, sv.vote_choice, end_time + )); + } + }); + + ui.add_space(10.0); + self.display_identity_options(ui); + + let button = egui::Button::new(RichText::new("Schedule All Votes").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .rounding(3.0); + if ui.add(button).clicked() { + action = self.schedule_votes(); + } + + if let Some((msg_type, msg_text)) = &self.message { + ui.add_space(10.0); + match msg_type { + MessageType::Error => { + if msg_text.contains("No votes selected") { + ui.colored_label(Color32::RED, msg_text); + } else { + ui.colored_label(Color32::RED, msg_text); + } + } + MessageType::Success => { + if msg_text.contains("Votes scheduled") { + action = self.show_success(ui); + } else { + ui.colored_label(Color32::GREEN, msg_text); + } + } + MessageType::Info => { + ui.colored_label(Color32::DARK_BLUE, msg_text); + } + } + } + }); + + action + } +} diff --git a/src/ui/dpns/dpns_contested_names_screen.rs b/src/ui/dpns/dpns_contested_names_screen.rs index 93dddb2b..80dfdaef 100644 --- a/src/ui/dpns/dpns_contested_names_screen.rs +++ b/src/ui/dpns/dpns_contested_names_screen.rs @@ -57,6 +57,13 @@ pub enum IndividualVoteCastingStatus { Completed, } +#[derive(Clone, Debug, PartialEq)] +pub struct SelectedVote { + pub contested_name: String, + pub vote_choice: ResourceVoteChoice, + pub end_time: Option, +} + impl DPNSSubscreen { pub fn display_name(&self) -> &'static str { match self { @@ -75,6 +82,7 @@ pub struct DPNSContestedNamesScreen { contested_names: Arc>>, local_dpns_names: Arc>>, pub scheduled_votes: Arc>>, + pub selected_votes_for_scheduling: Vec, pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, sort_column: SortColumn, @@ -130,6 +138,7 @@ impl DPNSContestedNamesScreen { contested_names, local_dpns_names, scheduled_votes: scheduled_votes_with_status, + selected_votes_for_scheduling: Vec::new(), app_context: app_context.clone(), error_message: None, sort_column: SortColumn::ContestedName, @@ -169,18 +178,69 @@ impl DPNSContestedNamesScreen { egui::RichText::new(button_text) }; - if ui.button(text).clicked() { - self.show_vote_popup_info = Some(( - format!( - "Confirm Voting for Contestant {} for name \"{}\".", - contestant.id, contestant.name - ), - ContestedResourceTask::VoteOnDPNSName( - contested_name.normalized_contested_name.clone(), - ResourceVoteChoice::TowardsIdentity(contestant.id), - vec![], - ), - )); + // Check if this specific vote is already in selected_votes_for_scheduling + let is_selected = self.selected_votes_for_scheduling.iter().any(|sv| { + sv.contested_name == contested_name.normalized_contested_name + && sv.vote_choice == ResourceVoteChoice::TowardsIdentity(contestant.id) + }); + + // If is_selected, we change the button color + let button = if is_selected { + egui::Button::new(text).fill(Color32::from_rgb(0, 150, 255)) + } else { + egui::Button::new(text) + }; + + let response = ui.add(button); + if response.clicked() { + let shift_held = ui.input(|i| i.modifiers.shift_only()); + if shift_held { + // ADDED LOGIC + if let Some(pos) = + self.selected_votes_for_scheduling.iter().position(|sv| { + sv.contested_name == contested_name.normalized_contested_name + }) + { + // Already have a selection for this name + let existing_choice = + &self.selected_votes_for_scheduling[pos].vote_choice; + let new_choice = ResourceVoteChoice::TowardsIdentity(contestant.id); + + if *existing_choice == new_choice { + // SHIFT-clicked same => remove + self.selected_votes_for_scheduling.remove(pos); + } else { + // SHIFT-clicked different => remove old + add new + self.selected_votes_for_scheduling.remove(pos); + self.selected_votes_for_scheduling.push(SelectedVote { + contested_name: contested_name + .normalized_contested_name + .clone(), + vote_choice: new_choice, + end_time: contested_name.end_time, + }); + } + } else { + // No existing => add + self.selected_votes_for_scheduling.push(SelectedVote { + contested_name: contested_name.normalized_contested_name.clone(), + vote_choice: ResourceVoteChoice::TowardsIdentity(contestant.id), + end_time: contested_name.end_time, + }); + } + } else { + self.show_vote_popup_info = Some(( + format!( + "Confirm Voting for Contestant {} for name \"{}\".", + contestant.id, contestant.name + ), + ContestedResourceTask::VoteOnDPNSName( + contested_name.normalized_contested_name.clone(), + ResourceVoteChoice::TowardsIdentity(contestant.id), + vec![], + ), + )); + } } } } @@ -300,7 +360,7 @@ impl DPNSContestedNamesScreen { } } } else { - ui.label("Go to the Active Contests subscreen to schedule votes."); + ui.label("To schedule votes, go to the Active Contests subscreen, shift-click your choices, and then click the Schedule Votes button in the top-right."); } }); @@ -420,9 +480,63 @@ impl DPNSContestedNamesScreen { } else { egui::RichText::new("Fetching".to_string()) }; - // Vote button logic for locked votes - if ui.button(label_text).clicked() { - self.show_vote_popup_info = Some((format!("Confirm Voting to Lock the name \"{}\".", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Lock, vec![]))); + + // Check if this specific vote is already in selected_votes_for_scheduling + let is_selected = self.selected_votes_for_scheduling.iter().any(|sv| { + sv.contested_name == contested_name.normalized_contested_name + && sv.vote_choice == ResourceVoteChoice::Lock + }); + + // If is_selected, we change the button color + let button = if is_selected { + egui::Button::new(label_text).fill(Color32::from_rgb(0, 150, 255)) + } else { + egui::Button::new(label_text) + }; + + let response = ui.add(button); + if response.clicked() { + // Check if SHIFT is held + let shift_held = ui.input(|i| i.modifiers.shift_only()); + + if shift_held { + // ADDED LOGIC FOR "ONLY ONE CHOICE PER NAME" + // 1) Find if there's already a selection for this contested_name + if let Some(pos) = self.selected_votes_for_scheduling.iter().position(|sv| { + sv.contested_name == contested_name.normalized_contested_name + }) { + // There's already a selected choice for this contested name + if self.selected_votes_for_scheduling[pos].vote_choice == ResourceVoteChoice::Lock { + // SHIFT-clicked the same choice => remove it (toggle off) + self.selected_votes_for_scheduling.remove(pos); + } else { + // SHIFT-clicked a different choice => remove old, then add new + self.selected_votes_for_scheduling.remove(pos); + self.selected_votes_for_scheduling.push(SelectedVote { + contested_name: contested_name.normalized_contested_name.clone(), + vote_choice: ResourceVoteChoice::Lock, + end_time: contested_name.end_time, + }); + } + } else { + // No existing selection for this contested name => add it + self.selected_votes_for_scheduling.push(SelectedVote { + contested_name: contested_name.normalized_contested_name.clone(), + vote_choice: ResourceVoteChoice::Lock, + end_time: contested_name.end_time, + }); + } + } else { + // Normal click => existing immediate-vote popup + self.show_vote_popup_info = Some(( + format!("Confirm Voting to Lock the name \"{}\".", contested_name.normalized_contested_name.clone()), + ContestedResourceTask::VoteOnDPNSName( + contested_name.normalized_contested_name.clone(), + ResourceVoteChoice::Lock, + vec![], + ), + )); + } } }); row.col(|ui| { @@ -433,8 +547,52 @@ impl DPNSContestedNamesScreen { } else { "Fetching".to_string() }; - if ui.button(label_text).clicked() { - self.show_vote_popup_info = Some((format!("Confirm Voting to Abstain on distribution of \"{}\".", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Abstain, vec![]))); + + // Check if this specific vote is already in selected_votes_for_scheduling + let is_selected = self.selected_votes_for_scheduling.iter().any(|sv| { + sv.contested_name == contested_name.normalized_contested_name + && sv.vote_choice == ResourceVoteChoice::Abstain + }); + + // If is_selected, we change the button color + let button = if is_selected { + egui::Button::new(label_text).fill(Color32::from_rgb(0, 150, 255)) + } else { + egui::Button::new(label_text) + }; + + let response = ui.add(button); + if response.clicked() { + let shift_held = ui.input(|i| i.modifiers.shift_only()); + if shift_held { + // ADDED LOGIC FOR "ONLY ONE CHOICE PER NAME" + if let Some(pos) = self.selected_votes_for_scheduling.iter().position(|sv| { + sv.contested_name == contested_name.normalized_contested_name + }) { + if self.selected_votes_for_scheduling[pos].vote_choice == ResourceVoteChoice::Abstain + { + // Same => unselect + self.selected_votes_for_scheduling.remove(pos); + } else { + // Different => remove old, add new + self.selected_votes_for_scheduling.remove(pos); + self.selected_votes_for_scheduling.push(SelectedVote { + contested_name: contested_name.normalized_contested_name.clone(), + vote_choice: ResourceVoteChoice::Abstain, + end_time: contested_name.end_time, + }); + } + } else { + // No existing => add + self.selected_votes_for_scheduling.push(SelectedVote { + contested_name: contested_name.normalized_contested_name.clone(), + vote_choice: ResourceVoteChoice::Abstain, + end_time: contested_name.end_time, + }); + } + } else { + self.show_vote_popup_info = Some((format!("Confirm Voting to Abstain on distribution of \"{}\".", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Abstain, vec![]))); + } } }); row.col(|ui| { @@ -1193,6 +1351,7 @@ impl ScreenLike for DPNSContestedNamesScreen { self.check_error_expiration(); let has_identity_that_can_register = !self.user_identities.is_empty(); + let has_selected_votes = self.selected_votes_for_scheduling.len() > 0; // Determine the right-side buttons based on the current DPNSSubscreen let mut right_buttons = match self.dpns_subscreen { @@ -1208,7 +1367,19 @@ impl ScreenLike for DPNSContestedNamesScreen { ) }; - vec![refresh_button] + if has_selected_votes { + vec![ + refresh_button, + ( + "Schedule Votes", + DesiredAppAction::AddScreenType(ScreenType::BulkScheduleVoteScreen( + self.selected_votes_for_scheduling.clone(), + )), + ), + ] + } else { + vec![refresh_button] + } } DPNSSubscreen::Past => { @@ -1414,6 +1585,9 @@ impl ScreenLike for DPNSContestedNamesScreen { AppAction::SetMainScreen(_) => { self.refreshing = false; } + AppAction::AddScreen(Screen::BulkScheduleVoteScreen(_)) => { + self.selected_votes_for_scheduling.clear(); + } _ => {} } diff --git a/src/ui/dpns/dpns_vote_scheduling_screen.rs b/src/ui/dpns/dpns_vote_scheduling_screen.rs index 4ebda601..0a8e95ea 100644 --- a/src/ui/dpns/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns/dpns_vote_scheduling_screen.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use crate::ui::components::top_panel::add_top_panel; use crate::ui::RootScreenType; -enum VoteOption { +pub enum VoteOption { None, Scheduled { days: u32, hours: u32, minutes: u32 }, } diff --git a/src/ui/dpns/mod.rs b/src/ui/dpns/mod.rs index bc6530a1..7dcce2e9 100644 --- a/src/ui/dpns/mod.rs +++ b/src/ui/dpns/mod.rs @@ -1,2 +1,3 @@ +pub mod dpns_bulk_vote_schedule_screen; pub mod dpns_contested_names_screen; pub mod dpns_vote_scheduling_screen; diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index 5b42d981..a02ef30f 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -15,7 +15,6 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; use crate::ui::identities::funding_common::WalletFundedScreenStep; use crate::ui::{MessageType, ScreenLike}; -use arboard::Clipboard; use dash_sdk::dashcore_rpc::dashcore::transaction::special_transaction::TransactionPayload; use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dpp::balances::credits::Duffs; @@ -26,9 +25,7 @@ use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::platform::Identifier; use eframe::egui::Context; use egui::ahash::HashSet; -use egui::{Color32, ColorImage, ComboBox, ScrollArea, Ui}; -use image::Luma; -use qrcode::QrCode; +use egui::{ComboBox, ScrollArea, Ui}; use std::cmp::PartialEq; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 59473532..cae99116 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -22,7 +22,8 @@ use contracts_documents::add_contracts_screen::AddContractsScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use dpns::dpns_contested_names_screen::DPNSSubscreen; +use dpns::dpns_bulk_vote_schedule_screen::BulkScheduleVoteScreen; +use dpns::dpns_contested_names_screen::{DPNSSubscreen, SelectedVote}; use dpns::dpns_vote_scheduling_screen::ScheduleVoteScreen; use egui::Context; use identities::add_existing_identity_screen::AddExistingIdentityScreen; @@ -145,6 +146,7 @@ pub enum ScreenType { ProofLog, TopUpIdentity(QualifiedIdentity), ScheduleVoteScreen(String, u64, Vec, ResourceVoteChoice), + BulkScheduleVoteScreen(Vec), ScheduledVotes, AddContracts, } @@ -228,6 +230,9 @@ impl ScreenType { identities.clone(), vote_choice.clone(), )), + ScreenType::BulkScheduleVoteScreen(selected_votes) => Screen::BulkScheduleVoteScreen( + BulkScheduleVoteScreen::new(app_context, selected_votes.clone()), + ), ScreenType::ScheduledVotes => Screen::DPNSContestedNamesScreen( DPNSContestedNamesScreen::new(app_context, DPNSSubscreen::ScheduledVotes), ), @@ -259,6 +264,7 @@ pub enum Screen { NetworkChooserScreen(NetworkChooserScreen), WalletsBalancesScreen(WalletsBalancesScreen), ScheduleVoteScreen(ScheduleVoteScreen), + BulkScheduleVoteScreen(BulkScheduleVoteScreen), AddContractsScreen(AddContractsScreen), } @@ -285,6 +291,7 @@ impl Screen { Screen::ImportWalletScreen(screen) => screen.app_context = app_context, Screen::ProofLogScreen(screen) => screen.app_context = app_context, Screen::ScheduleVoteScreen(screen) => screen.app_context = app_context, + Screen::BulkScheduleVoteScreen(screen) => screen.app_context = app_context, Screen::AddContractsScreen(screen) => screen.app_context = app_context, } } @@ -376,6 +383,9 @@ impl Screen { screen.identities.clone(), screen.vote_choice.clone(), ), + Screen::BulkScheduleVoteScreen(screen) => { + ScreenType::BulkScheduleVoteScreen(screen.selected_votes.clone()) + } Screen::AddContractsScreen(_) => ScreenType::AddContracts, } } @@ -404,6 +414,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.refresh(), Screen::ProofLogScreen(screen) => screen.refresh(), Screen::ScheduleVoteScreen(screen) => screen.refresh(), + Screen::BulkScheduleVoteScreen(screen) => screen.refresh(), Screen::AddContractsScreen(screen) => screen.refresh(), } } @@ -430,6 +441,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.refresh_on_arrival(), Screen::ProofLogScreen(screen) => screen.refresh_on_arrival(), Screen::ScheduleVoteScreen(screen) => screen.refresh_on_arrival(), + Screen::BulkScheduleVoteScreen(screen) => screen.refresh_on_arrival(), Screen::AddContractsScreen(screen) => screen.refresh_on_arrival(), } } @@ -456,6 +468,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.ui(ctx), Screen::ProofLogScreen(screen) => screen.ui(ctx), Screen::ScheduleVoteScreen(screen) => screen.ui(ctx), + Screen::BulkScheduleVoteScreen(screen) => screen.ui(ctx), Screen::AddContractsScreen(screen) => screen.ui(ctx), } } @@ -488,6 +501,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.display_message(message, message_type), Screen::ProofLogScreen(screen) => screen.display_message(message, message_type), Screen::ScheduleVoteScreen(screen) => screen.display_message(message, message_type), + Screen::BulkScheduleVoteScreen(screen) => screen.display_message(message, message_type), Screen::AddContractsScreen(screen) => screen.display_message(message, message_type), } } @@ -554,6 +568,9 @@ impl ScreenLike for Screen { Screen::ScheduleVoteScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::BulkScheduleVoteScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } Screen::AddContractsScreen(screen) => { screen.display_task_result(backend_task_success_result) } @@ -582,6 +599,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.pop_on_success(), Screen::ProofLogScreen(screen) => screen.pop_on_success(), Screen::ScheduleVoteScreen(screen) => screen.pop_on_success(), + Screen::BulkScheduleVoteScreen(screen) => screen.pop_on_success(), Screen::AddContractsScreen(screen) => screen.pop_on_success(), } } From d2e21cf3a1671061f71d7b7cdd14bb90e2fbaad5 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:05:56 -0500 Subject: [PATCH 13/14] feat: update platform with sdk error deserialization (#143) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 286258a1..35898d48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ strum = { version = "0.26.1", features = ["derive"] } bs58 = "0.5.0" base64 = "0.22.1" copypasta = "0.10.1" -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "e2ed81f0a5af5ef74fc703097e2657349021094b" } +dash-sdk = { git = "https://github.com/dashpay/platform", rev = "8a000dfeb9c872c4bfedd8e8974ad1a9d2fdab78" } thiserror = "1" serde = "1.0.197" serde_json = "1.0.120" From c89ac35e1510aea5d9ba473d9406116a5869236c Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:14:47 +0700 Subject: [PATCH 14/14] fix: wallet unlock rendering (#145) * fix: wallet unlock rendering * combine the functions into one * function desc --- src/ui/identities/mod.rs | 116 ++++++++++++++++++ .../identities/register_dpns_name_screen.rs | 58 ++------- src/ui/identities/transfers/mod.rs | 24 ++-- .../withdraw_from_identity_screen.rs | 17 ++- 4 files changed, 154 insertions(+), 61 deletions(-) diff --git a/src/ui/identities/mod.rs b/src/ui/identities/mod.rs index 22f22ef3..d6d98e07 100644 --- a/src/ui/identities/mod.rs +++ b/src/ui/identities/mod.rs @@ -1,3 +1,23 @@ +use std::sync::{Arc, RwLock}; + +use dash_sdk::{ + dpp::{ + data_contract::accessors::v0::DataContractV0Getters, + identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0, + }, + platform::IdentityPublicKey, +}; + +use crate::{ + context::AppContext, + model::{ + qualified_identity::{ + encrypted_key_storage::PrivateKeyData, PrivateKeyTarget, QualifiedIdentity, + }, + wallet::Wallet, + }, +}; + pub mod add_existing_identity_screen; pub mod add_new_identity_screen; mod funding_common; @@ -7,3 +27,99 @@ pub mod register_dpns_name_screen; pub mod top_up_identity_screen; pub mod transfers; pub mod withdraw_from_identity_screen; + +/// Retrieves the appropriate wallet (if any) associated with the given identity. +/// +/// # Description +/// +/// This function tries to determine which wallet should be used, either via: +/// +/// - The DPNS-based approach (if [`AppContext`] is provided), which looks up +/// the `preorder` document type in the DPNS contract and retrieves the +/// document-signing key from the given [`QualifiedIdentity`]. +/// - The fallback approach (if `app_context` is `None`), which relies on a +/// directly provided key (`selected_key`). +/// +/// # Parameters +/// +/// - `qualified_identity`: A reference to the [`QualifiedIdentity`], which holds +/// the identity, keys, and associated wallets. +/// - `app_context`: Optional reference to the [`AppContext`] which contains the +/// DPNS contract. When present, DPNS logic is used to find the public key. +/// - `selected_key`: An optional reference to a chosen [`IdentityPublicKey`]. +/// When `app_context` is not provided, this is required to get the wallet. +/// - `error_message`: A mutable optional string where any error message will +/// be written if the function fails to retrieve a wallet. +/// +/// # Returns +/// +/// Returns `Some(Arc>)` if a matching wallet is found, or `None` +/// otherwise. If an error is encountered, an explanatory message is placed in +/// `error_message`. +/// +/// # Errors +/// +/// - If the DPNS document type can't be found or the identity is missing the +/// required DPNS signing key (when `app_context` is provided). +/// - If no `selected_key` is provided (when `app_context` is `None`). +/// - If the derived wallet derivation path is missing from the +/// [`QualifiedIdentity`]. +pub fn get_selected_wallet( + qualified_identity: &QualifiedIdentity, + app_context: Option<&AppContext>, // Used for DPNS-based logic (the first scenario). + selected_key: Option<&IdentityPublicKey>, // Used for direct-key logic (the fallback scenario). + error_message: &mut Option, +) -> Option>> { + // If `app_context` is provided, use the DPNS-based approach. + let public_key = if let Some(context) = app_context { + let dpns_contract = &context.dpns_contract; + + // Attempt to fetch the `preorder` document type from the DPNS contract. + let preorder_document_type = match dpns_contract.document_type_for_name("preorder") { + Ok(doc_type) => doc_type, + Err(e) => { + *error_message = Some(format!("DPNS preorder document type not found: {}", e)); + return None; + } + }; + + // Attempt to retrieve the public key from the identity. + match qualified_identity.document_signing_key(&preorder_document_type) { + Some(key) => key, + None => { + *error_message = Some( + "Identity doesn't have an authentication key for signing document transitions" + .to_string(), + ); + return None; + } + } + } else { + // Fallback: directly use the provided selected key. + match selected_key { + Some(key) => key, + None => { + *error_message = Some("No key provided when getting selected wallet".to_string()); + return None; + } + } + }; + + // Once we have the public key (either from DPNS or directly), look up + // the matching private key data in `qualified_identity`. + let key_lookup = (PrivateKeyTarget::PrivateKeyOnMainIdentity, public_key.id()); + if let Some((_, PrivateKeyData::AtWalletDerivationPath(wallet_derivation_path))) = + qualified_identity + .private_keys + .private_keys + .get(&key_lookup) + { + // If found, return the associated wallet (cloned to preserve Arc). + qualified_identity + .associated_wallets + .get(&wallet_derivation_path.wallet_seed_hash) + .cloned() + } else { + None + } +} diff --git a/src/ui/identities/register_dpns_name_screen.rs b/src/ui/identities/register_dpns_name_screen.rs index 105004ef..015c5d6f 100644 --- a/src/ui/identities/register_dpns_name_screen.rs +++ b/src/ui/identities/register_dpns_name_screen.rs @@ -2,8 +2,7 @@ use crate::app::AppAction; use crate::backend_task::identity::{IdentityTask, RegisterDpnsNameInput}; use crate::backend_task::BackendTask; use crate::context::AppContext; -use crate::model::qualified_identity::encrypted_key_storage::PrivateKeyData; -use crate::model::qualified_identity::{PrivateKeyTarget, QualifiedIdentity}; +use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -21,6 +20,8 @@ use std::sync::Arc; use std::sync::RwLock; use std::time::{SystemTime, UNIX_EPOCH}; +use super::get_selected_wallet; + #[derive(PartialEq)] pub enum RegisterDpnsNameStatus { NotStarted, @@ -78,7 +79,7 @@ impl RegisterDpnsNameScreen { let mut error_message: Option = None; let selected_wallet = if let Some(ref identity) = selected_qualified_identity { - get_selected_wallet(&identity.0, app_context, &mut error_message) + get_selected_wallet(&identity.0, Some(app_context), None, &mut error_message) } else { None }; @@ -108,8 +109,12 @@ impl RegisterDpnsNameScreen { // Set the selected_qualified_identity to the found identity self.selected_qualified_identity = Some(qi.clone()); // Update the selected wallet - self.selected_wallet = - get_selected_wallet(&qi.0, &self.app_context, &mut self.error_message); + self.selected_wallet = get_selected_wallet( + &qi.0, + Some(&self.app_context), + None, + &mut self.error_message, + ); } else { // If not found, you might want to handle this case // For now, we'll set selected_qualified_identity to None @@ -179,7 +184,8 @@ impl RegisterDpnsNameScreen { self.selected_qualified_identity = Some(qualified_identity.clone()); self.selected_wallet = get_selected_wallet( &qualified_identity.0, - &self.app_context, + Some(&self.app_context), + None, &mut self.error_message, ); } @@ -466,43 +472,3 @@ pub fn is_contested_name(name: &str) -> bool { } true } - -pub fn get_selected_wallet( - qualified_identity: &QualifiedIdentity, - app_context: &AppContext, - error_message: &mut Option, -) -> Option>> { - let dpns_contract = &app_context.dpns_contract; - - let preorder_document_type = match dpns_contract.document_type_for_name("preorder") { - Ok(doc_type) => doc_type, - Err(e) => { - *error_message = Some(format!("DPNS preorder document type not found: {}", e)); - return None; - } - }; - - let public_key = match qualified_identity.document_signing_key(&preorder_document_type) { - Some(key) => key, - None => { - *error_message = Some( - "Identity doesn't have an authentication key for signing document transitions" - .to_string(), - ); - return None; - } - }; - - let key = (PrivateKeyTarget::PrivateKeyOnMainIdentity, public_key.id()); - - if let Some((_, PrivateKeyData::AtWalletDerivationPath(wallet_derivation_path))) = - qualified_identity.private_keys.private_keys.get(&key) - { - qualified_identity - .associated_wallets - .get(&wallet_derivation_path.wallet_seed_hash) - .cloned() - } else { - None - } -} diff --git a/src/ui/identities/transfers/mod.rs b/src/ui/identities/transfers/mod.rs index 28e19d9c..a6a35898 100644 --- a/src/ui/identities/transfers/mod.rs +++ b/src/ui/identities/transfers/mod.rs @@ -20,7 +20,8 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::identities::register_dpns_name_screen::get_selected_wallet; + +use super::get_selected_wallet; pub enum TransferCreditsStatus { NotStarted, @@ -47,20 +48,19 @@ pub struct TransferScreen { impl TransferScreen { pub fn new(identity: QualifiedIdentity, app_context: &Arc) -> Self { let max_amount = identity.identity.balance(); - let selected_key = identity - .identity - .get_first_public_key_matching( - Purpose::TRANSFER, - SecurityLevel::full_range().into(), - KeyType::all_key_types().into(), - false, - ) - .cloned(); + let identity_clone = identity.identity.clone(); + let selected_key = identity_clone.get_first_public_key_matching( + Purpose::TRANSFER, + SecurityLevel::full_range().into(), + KeyType::all_key_types().into(), + false, + ); let mut error_message = None; - let selected_wallet = get_selected_wallet(&identity, app_context, &mut error_message); + let selected_wallet = + get_selected_wallet(&identity, None, selected_key, &mut error_message); Self { identity, - selected_key, + selected_key: selected_key.cloned(), receiver_identity_id: String::new(), amount: String::new(), transfer_credits_status: TransferCreditsStatus::NotStarted, diff --git a/src/ui/identities/withdraw_from_identity_screen.rs b/src/ui/identities/withdraw_from_identity_screen.rs index 22f5d716..47ff30e6 100644 --- a/src/ui/identities/withdraw_from_identity_screen.rs +++ b/src/ui/identities/withdraw_from_identity_screen.rs @@ -21,6 +21,7 @@ use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; +use super::get_selected_wallet; use super::keys::key_info_screen::KeyInfoScreen; pub enum WithdrawFromIdentityStatus { @@ -48,19 +49,29 @@ pub struct WithdrawalScreen { impl WithdrawalScreen { pub fn new(identity: QualifiedIdentity, app_context: &Arc) -> Self { let max_amount = identity.identity.balance(); + let identity_clone = identity.identity.clone(); + let selected_key = identity_clone.get_first_public_key_matching( + Purpose::TRANSFER, + SecurityLevel::full_range().into(), + KeyType::all_key_types().into(), + false, + ); + let mut error_message = None; + let selected_wallet = + get_selected_wallet(&identity, None, selected_key, &mut error_message); Self { identity, - selected_key: None, + selected_key: selected_key.cloned(), withdrawal_address: String::new(), withdrawal_amount: String::new(), max_amount, app_context: app_context.clone(), confirmation_popup: false, withdraw_from_identity_status: WithdrawFromIdentityStatus::NotStarted, - selected_wallet: None, + selected_wallet, wallet_password: String::new(), show_password: false, - error_message: None, + error_message, } }