diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index faa3c1e51..5ba48b074 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -5,7 +5,10 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::config::Config; use crate::context::AppContext; use crate::model::wallet::Wallet; -use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dashcore_rpc::dashcore::address::Payload; +use dash_sdk::dashcore_rpc::dashcore::hashes::{hash160, Hash}; +use dash_sdk::dashcore_rpc::dashcore::PubkeyHash; +use dash_sdk::dashcore_rpc::{dashcore, RpcApi}; use dash_sdk::dashcore_rpc::{Auth, Client}; use dash_sdk::dpp::dashcore::{Address, ChainLock, Network, OutPoint, Transaction, TxOut}; use std::sync::{Arc, RwLock}; @@ -16,6 +19,7 @@ pub(crate) enum CoreTask { GetBestChainLocks, RefreshWalletInfo(Arc>), StartDashQT(Network, Option, bool), + ProRegUpdateTx(String, Address, Address), } impl PartialEq for CoreTask { fn eq(&self, other: &Self) -> bool { @@ -24,6 +28,7 @@ impl PartialEq for CoreTask { (CoreTask::GetBestChainLocks, CoreTask::GetBestChainLocks) => true, (CoreTask::RefreshWalletInfo(_), CoreTask::RefreshWalletInfo(_)) => true, (CoreTask::StartDashQT(_, _, _), CoreTask::StartDashQT(_, _, _)) => true, + (CoreTask::ProRegUpdateTx(_, _, _), CoreTask::ProRegUpdateTx(_, _, _)) => true, _ => false, } } @@ -34,6 +39,7 @@ pub(crate) enum CoreItem { ReceivedAvailableUTXOTransaction(Transaction, Vec<(OutPoint, TxOut, Address)>), ChainLock(ChainLock, Network), ChainLocks(Option, Option), // Mainnet, Testnet + ProRegUpdateTx(String), } impl AppContext { @@ -135,6 +141,21 @@ impl AppContext { .start_dash_qt(network, custom_dash_qt, overwrite_dash_conf) .map_err(|e| e.to_string()) .map(|_| BackendTaskSuccessResult::None), + CoreTask::ProRegUpdateTx(pro_tx_hash, voting_address, payout_address) => self + .core_client + .get_protx_update_registrar( + pro_tx_hash.as_str(), + "", + payout_address, + voting_address, + None, + ) + .map(|pro_tx_hash| { + BackendTaskSuccessResult::CoreItem(CoreItem::ProRegUpdateTx( + pro_tx_hash.to_string(), + )) + }) + .map_err(|e| e.to_string()), } } } diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index a81c962be..3d61f41fa 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -16,6 +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::identities::update_identity_payout_address::UpdateIdentityPayoutScreen; use crate::ui::transfers::TransferScreen; use crate::ui::{RootScreenType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; @@ -428,49 +429,63 @@ impl IdentitiesScreen { )); }}); }); - row.col(|ui| { - Self::show_balance(ui, qualified_identity); - if ui.button("Withdraw").clicked() { - action = AppAction::AddScreen( - Screen::WithdrawalScreen(WithdrawalScreen::new( - qualified_identity.clone(), - &self.app_context, - )), - ); - } - if ui.button("Top up").clicked() { - action = AppAction::AddScreen( - Screen::TopUpIdentityScreen(TopUpIdentityScreen::new( - qualified_identity.clone(), - &self.app_context, - )), - ); - } - if ui.button("Transfer").clicked() { - action = AppAction::AddScreen(Screen::TransferScreen( - TransferScreen::new( - qualified_identity.clone(), - &self.app_context, - ), - )); - } - }); row.col(|ui| { ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 10.0; + Self::show_balance(ui, qualified_identity); ui.spacing_mut().item_spacing.x = 3.0; - - if ui.button("Refresh").clicked() { - action = - AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::RefreshIdentity( + if ui.button("Withdraw").clicked() { + action = AppAction::AddScreen( + Screen::WithdrawalScreen(WithdrawalScreen::new( + qualified_identity.clone(), + &self.app_context, + )), + ); + } + if ui.button("Top up").clicked() { + action = AppAction::AddScreen( + Screen::TopUpIdentityScreen(TopUpIdentityScreen::new( qualified_identity.clone(), + &self.app_context, + )), + ); + } + if ui.button("Transfer").clicked() { + action = AppAction::AddScreen(Screen::TransferScreen( + TransferScreen::new( + qualified_identity.clone(), + &self.app_context, ), )); - } - if ui.button("Remove").clicked() { - self.identity_to_remove = - Some(qualified_identity.clone()); - }}); + } + }); + }); + row.col(|ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 3.0; + if ui.button("Refresh").clicked() { + action = + AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::RefreshIdentity( + qualified_identity.clone(), + ), + )); + } + if ui.button("Remove").clicked() { + self.identity_to_remove = + Some(qualified_identity.clone()); + } + if qualified_identity.identity_type != IdentityType::User { + if ui.button("Update Payout Address").clicked() { + action = AppAction::AddScreen(Screen::UpdatePayoutAddressScreen( + UpdateIdentityPayoutScreen::new( + qualified_identity.clone(), + &self.app_context, + ), + )); + } + } + }); }); }); } diff --git a/src/ui/identities/mod.rs b/src/ui/identities/mod.rs index dabe88c8c..fb9b49413 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 update_identity_payout_address; pub mod withdraw_from_identity_screen; diff --git a/src/ui/identities/update_identity_payout_address.rs b/src/ui/identities/update_identity_payout_address.rs new file mode 100644 index 000000000..a409dcc48 --- /dev/null +++ b/src/ui/identities/update_identity_payout_address.rs @@ -0,0 +1,361 @@ +use crate::app::AppAction; +use crate::backend_task::core::CoreTask; +use crate::backend_task::BackendTask; +use crate::context::AppContext; +use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; +use crate::model::wallet::Wallet; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::{MessageType, ScreenLike}; +use dash_sdk::dashcore_rpc::dashcore::Address; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::TimestampMillis; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use eframe::egui::Context; +use egui::{ComboBox, Ui}; +use std::sync::atomic::Ordering; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub enum UpdateIdentityPayoutStatus { + NotStarted, + WaitingForResult(TimestampMillis), + ErrorMessage(String), + Complete, +} + +pub struct UpdateIdentityPayoutScreen { + pub app_context: Arc, + pub identity: QualifiedIdentity, + selected_wallet: Option>>, + selected_payout_address: Option
, + selected_funding_address: Option
, + update_payout_status: UpdateIdentityPayoutStatus, + wallet_password: String, + show_password: bool, + error_message: Option, +} + +impl UpdateIdentityPayoutScreen { + pub fn new(identity: QualifiedIdentity, app_context: &Arc) -> Self { + let selected_wallet = None; + Self { + app_context: app_context.clone(), + identity, + selected_wallet, + selected_payout_address: None, + selected_funding_address: None, + error_message: None, + update_payout_status: UpdateIdentityPayoutStatus::NotStarted, + wallet_password: String::new(), + show_password: false, + } + } + + fn render_wallet_selection(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + if self.app_context.has_wallet.load(Ordering::Relaxed) { + let wallets = &self.app_context.wallets.read().unwrap(); + let wallet_aliases: Vec = wallets + .values() + .map(|wallet| { + wallet + .read() + .unwrap() + .alias + .clone() + .unwrap_or_else(|| "Unnamed Wallet".to_string()) + }) + .collect(); + + let selected_wallet_alias = self + .selected_wallet + .as_ref() + .and_then(|wallet| wallet.read().ok()?.alias.clone()) + .unwrap_or_else(|| "Select".to_string()); + + // Display the ComboBox for wallet selection + ComboBox::from_label("") + .selected_text(selected_wallet_alias.clone()) + .show_ui(ui, |ui| { + for (idx, wallet) in wallets.values().enumerate() { + let wallet_alias = wallet_aliases[idx].clone(); + let is_selected = self + .selected_wallet + .as_ref() + .map_or(false, |selected| Arc::ptr_eq(selected, wallet)); + if ui + .selectable_label(is_selected, wallet_alias.clone()) + .clicked() + { + // Update the selected wallet + self.selected_wallet = Some(wallet.clone()); + } + } + }); + + ui.add_space(20.0); + } else { + ui.label("No wallets available."); + } + }); + } + + fn render_selected_wallet_payout_addresses(&mut self, ui: &mut Ui) { + if let Some(selected_wallet) = &self.selected_wallet { + // Acquire a read lock + let wallet = selected_wallet.read().unwrap(); + ui.add_space(20.0); + ui.heading("Select a Payout Address:"); + ui.add_space(5.0); + ui.push_id("payout_combo_id", |ui| { + ComboBox::from_label("") + .selected_text( + self.selected_payout_address + .as_ref() // Get a reference to the Option
+ .map(|address| address.to_string()) // Convert Address to String + .unwrap_or_else(|| "".to_string()), // Use default "" if None + ) + .show_ui(ui, |ui| { + for (address, _) in &wallet.known_addresses { + if ui + .selectable_value( + &mut self.selected_payout_address, + Some(address.clone()), + address.to_string(), + ) + .clicked() + {} + } + }); + }); + ui.add_space(20.0); + if let Some(selected_address) = &self.selected_payout_address { + if let Some(value) = wallet.address_balances.get(&selected_address) { + ui.label(format!("Selected Address has a balance of {} DASH", value)); + } else { + // TODO: Why sometimes balance is not found? + //ui.label("Balance NOT FOUND DASH".to_string()); + } + } + } + } + + fn render_selected_wallet_funding_addresses(&mut self, ui: &mut Ui) { + if let Some(selected_wallet) = &self.selected_wallet { + // Acquire a read lock + let wallet = selected_wallet.read().unwrap(); + ui.add_space(20.0); + ui.heading("Select a Funding Address:"); + ui.add_space(5.0); + ui.push_id("funding_combo_id", |ui| { + ComboBox::from_label("") + .selected_text( + self.selected_funding_address + .as_ref() // Get a reference to the Option
+ .map(|address| address.to_string()) // Convert Address to String + .unwrap_or_else(|| "".to_string()), // Use default "" if None + ) + .show_ui(ui, |ui| { + for (address, _) in &wallet.known_addresses { + if ui + .selectable_value( + &mut self.selected_funding_address, + Some(address.clone()), + address.to_string(), + ) + .clicked() + {} + } + }); + }); + ui.add_space(20.0); + if let Some(selected_address) = &self.selected_funding_address { + if let Some(value) = wallet.address_balances.get(&selected_address) { + ui.label(format!("Selected Address has a balance of {} DASH", value)); + } else { + // TODO: Why sometimes balance is not found? + //ui.label("Balance NOT FOUND DASH".to_string()); + } + } + } + } + + fn show_success(&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 updated payout address."); + + ui.add_space(20.0); + + if ui.button("Go back to Identities Screen").clicked() { + // Handle navigation back to the identities screen + action = AppAction::PopScreenAndRefresh; + } + }); + + action + } +} + +impl ScreenLike for UpdateIdentityPayoutScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + self.update_payout_status = UpdateIdentityPayoutStatus::Complete; + } + MessageType::Info => {} + MessageType::Error => { + self.update_payout_status = + UpdateIdentityPayoutStatus::ErrorMessage(message.to_string()); + } + } + } + + /// Renders the UI components for the withdrawal screen + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Identities", AppAction::GoToMainScreen), + ("Update Payout Address", AppAction::None), + ], + vec![], + ); + + egui::CentralPanel::default().show(ctx, |mut ui| { + if self.identity.identity_type == IdentityType::User { + ui.heading( + "Updating Payout Address for User identities is not allowed.".to_string(), + ); + return; + } + if !self.app_context.has_wallet.load(Ordering::Relaxed) { + ui.heading("Load a Wallet in order to continue.".to_string()); + return; + } + ui.heading("Update Payout Address".to_string()); + ui.add_space(20.0); + + ui.heading("Load Address from wallet".to_string()); + self.render_wallet_selection(&mut ui); + + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + + if needed_unlock && !just_unlocked { + return; + } + + if self.selected_wallet.is_some() { + self.render_selected_wallet_payout_addresses(&mut ui); + if self.selected_payout_address.is_some() { + self.render_selected_wallet_funding_addresses(&mut ui); + if self.selected_funding_address.is_some() { + ui.add_space(20.0); + ui.colored_label( + egui::Color32::ORANGE, + "The owner key of the Masternode/Evonode must be known to your wallet.", + ); + ui.add_space(20.0); + if ui.button("Update Payout Address").clicked() { + // 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.update_payout_status = + UpdateIdentityPayoutStatus::WaitingForResult(now); + action |= AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::ProRegUpdateTx( + self.identity.identity.id().to_string(Encoding::Hex), + self.selected_payout_address.clone().unwrap(), + self.selected_funding_address.clone().unwrap(), + ), + )); + } + + // Handle registration status messages + match &self.update_payout_status { + UpdateIdentityPayoutStatus::NotStarted => { + // Do nothing + } + UpdateIdentityPayoutStatus::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.add_space(20.0); + ui.label(format!("Waiting... Time taken so far: {}", display_time)); + } + UpdateIdentityPayoutStatus::ErrorMessage(msg) => { + ui.add_space(20.0); + ui.colored_label(egui::Color32::RED, format!("Error: {}", msg)); + } + UpdateIdentityPayoutStatus::Complete => { + action = self.show_success(ui); + } + } + } + } + } + }); + + action + } +} + +impl ScreenWithWalletUnlock for UpdateIdentityPayoutScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9a3513a62..57f7e50ff 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -11,6 +11,7 @@ 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::update_identity_payout_address::UpdateIdentityPayoutScreen; 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; @@ -18,6 +19,7 @@ use crate::ui::transfers::TransferScreen; use crate::ui::wallet::import_wallet_screen::ImportWalletScreen; use crate::ui::wallet::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; +use crate::ui::Screen::UpdatePayoutAddressScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dpns_contested_names_screen::DPNSSubscreen; @@ -123,6 +125,7 @@ pub enum ScreenType { AddNewWallet, AddExistingIdentity, TransitionVisualizer, + UpdatePayoutAddress(QualifiedIdentity), WithdrawalScreen(QualifiedIdentity), TransferScreen(QualifiedIdentity), AddKeyScreen(QualifiedIdentity), @@ -179,6 +182,9 @@ impl ScreenType { ScreenType::TransitionVisualizer => { Screen::TransitionVisualizerScreen(TransitionVisualizerScreen::new(app_context)) } + ScreenType::UpdatePayoutAddress(identity) => Screen::UpdatePayoutAddressScreen( + UpdateIdentityPayoutScreen::new(identity.clone(), app_context), + ), ScreenType::WithdrawalScreen(identity) => { Screen::WithdrawalScreen(WithdrawalScreen::new(identity.clone(), app_context)) } @@ -222,6 +228,7 @@ pub enum Screen { KeyInfoScreen(KeyInfoScreen), KeysScreen(KeysScreen), RegisterDpnsNameScreen(RegisterDpnsNameScreen), + UpdatePayoutAddressScreen(UpdateIdentityPayoutScreen), WithdrawalScreen(WithdrawalScreen), TopUpIdentityScreen(TopUpIdentityScreen), TransferScreen(TransferScreen), @@ -249,6 +256,7 @@ impl Screen { Screen::AddNewIdentityScreen(screen) => screen.app_context = app_context, Screen::RegisterDpnsNameScreen(screen) => screen.app_context = app_context, Screen::AddNewWalletScreen(screen) => screen.app_context = app_context, + Screen::UpdatePayoutAddressScreen(screen) => screen.app_context = app_context, Screen::TransferScreen(screen) => screen.app_context = app_context, Screen::TopUpIdentityScreen(screen) => screen.app_context = app_context, Screen::WalletsBalancesScreen(screen) => screen.app_context = app_context, @@ -333,6 +341,9 @@ impl Screen { Screen::TransferScreen(screen) => ScreenType::TransferScreen(screen.identity.clone()), Screen::WalletsBalancesScreen(_) => ScreenType::WalletsBalances, Screen::WithdrawsStatusScreen(_) => ScreenType::WithdrawsStatus, + Screen::UpdatePayoutAddressScreen(screen) => { + ScreenType::UpdatePayoutAddress(screen.identity.clone()) + } Screen::ImportWalletScreen(_) => ScreenType::ImportWallet, Screen::ProofLogScreen(_) => ScreenType::ProofLog, } @@ -361,6 +372,7 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.refresh(), Screen::WalletsBalancesScreen(screen) => screen.refresh(), Screen::ProofLogScreen(screen) => screen.refresh(), + Screen::UpdatePayoutAddressScreen(screen) => screen.refresh(), } } @@ -385,6 +397,7 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.refresh_on_arrival(), Screen::WalletsBalancesScreen(screen) => screen.refresh_on_arrival(), Screen::ProofLogScreen(screen) => screen.refresh_on_arrival(), + Screen::UpdatePayoutAddressScreen(screen) => screen.refresh_on_arrival(), } } @@ -409,6 +422,7 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.ui(ctx), Screen::WalletsBalancesScreen(screen) => screen.ui(ctx), Screen::ProofLogScreen(screen) => screen.ui(ctx), + Screen::UpdatePayoutAddressScreen(screen) => screen.ui(ctx), } } @@ -439,6 +453,9 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.display_message(message, message_type), Screen::WalletsBalancesScreen(screen) => screen.display_message(message, message_type), Screen::ProofLogScreen(screen) => screen.display_message(message, message_type), + Screen::UpdatePayoutAddressScreen(screen) => { + screen.display_message(message, message_type) + } } } @@ -501,6 +518,9 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::UpdatePayoutAddressScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } } } @@ -525,6 +545,7 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.pop_on_success(), Screen::WalletsBalancesScreen(screen) => screen.pop_on_success(), Screen::ProofLogScreen(screen) => screen.pop_on_success(), + Screen::UpdatePayoutAddressScreen(screen) => screen.pop_on_success(), } } }