From e090f898a6b8db024f11b2a2a7a851bd958bfd89 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Mon, 9 Dec 2024 22:08:05 +0700 Subject: [PATCH 01/28] feat: deferred voting --- src/backend_task/contested_names/mod.rs | 16 ++++ src/ui/dpns_contested_names_screen.rs | 98 +++++++++++++++++-------- src/ui/mod.rs | 34 ++++++++- 3 files changed, 117 insertions(+), 31 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index 05844899b..1c6274527 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -1,6 +1,7 @@ mod query_dpns_contested_resources; mod query_dpns_vote_contenders; mod query_ending_times; +mod schedule_dpns_vote; mod vote_on_dpns_name; use crate::app::TaskResult; @@ -16,6 +17,12 @@ use tokio::sync::mpsc; pub(crate) enum ContestedResourceTask { QueryDPNSContestedResources, QueryDPNSVoteContenders(String), + ScheduleDPNSVote( + String, + ResourceVoteChoice, + Vec, + Vec<(QualifiedIdentity, String)>, + ), VoteOnDPNSName(String, ResourceVoteChoice, Vec), } @@ -35,6 +42,15 @@ impl AppContext { .query_dpns_vote_contenders(name, sdk, sender) .await .map(|_| BackendTaskSuccessResult::None), + ContestedResourceTask::ScheduleDPNSVote( + name, + vote_choice, + voters, + voters_and_names, + ) => { + self.schedule_dpns_vote(name, *vote_choice, voters, sdk, sender) + .await + } ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) .await diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index fc5579486..476e4c689 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -1,4 +1,5 @@ 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; @@ -66,6 +67,7 @@ pub struct DPNSContestedNamesScreen { sort_column: SortColumn, sort_order: SortOrder, show_vote_popup_info: Option<(String, ContestedResourceTask)>, + pending_vote_action: Option, pub dpns_subscreen: DPNSSubscreen, refreshing: bool, } @@ -106,6 +108,7 @@ impl DPNSContestedNamesScreen { sort_column: SortColumn::ContestedName, sort_order: SortOrder::Ascending, show_vote_popup_info: None, + pending_vote_action: None, dpns_subscreen, refreshing: false, } @@ -700,6 +703,7 @@ impl DPNSContestedNamesScreen { } if ui.button("Cancel").clicked() { self.show_vote_popup_info = None; + self.pending_vote_action = None; } } else if let Some((message, action)) = self.show_vote_popup_info.clone() { ui.label(message); @@ -712,51 +716,85 @@ impl DPNSContestedNamesScreen { mut voters, ) = action { - // Iterate over the voting identities and create a button for each one - for identity in self.voting_identities.iter() { - if ui.button(identity.display_short_string()).clicked() { - // Add the selected identity to the `voters` field - voters.push(identity.clone()); + // If we haven't yet chosen any voters (pending_vote_action is None), we show the identities + if self.pending_vote_action.is_none() { + // Iterate over the voting identities and create a button for each one + for identity in self.voting_identities.iter() { + if ui.button(identity.display_short_string()).clicked() { + // Add the selected identity to the `voters` field + voters.push(identity.clone()); + + // Store the updated action, but don't finalize yet + let updated_action = ContestedResourceTask::VoteOnDPNSName( + contested_name.clone(), + vote_choice.clone(), + voters.clone(), + ); + self.pending_vote_action = Some(updated_action); + } + } - // Create a new `VoteOnDPNSName` task with updated voters + // Vote with all identities + if ui.button("All").clicked() { + voters.extend(self.voting_identities.iter().cloned()); let updated_action = ContestedResourceTask::VoteOnDPNSName( contested_name.clone(), vote_choice.clone(), - voters.clone(), // Updated voters - ); - - // Pass updated action to BackendTask - app_action = AppAction::BackendTask( - BackendTask::ContestedResourceTask(updated_action), + voters.clone(), ); + self.pending_vote_action = Some(updated_action); + } + } else { + // If we have a pending vote action, ask whether to vote now or schedule + ui.label("Would you like to vote now or schedule your votes?"); + if ui.button("Vote Now").clicked() { + // Finalize the vote now + app_action = + AppAction::BackendTask(BackendTask::ContestedResourceTask( + self.pending_vote_action.take().unwrap(), + )); self.show_vote_popup_info = None; } - } + if ui.button("Schedule").clicked() { + // Move to a scheduling screen instead + // Assume we have a ScheduleVoteScreen that takes the pending action data + let pending = self.pending_vote_action.take().unwrap(); + if let ContestedResourceTask::VoteOnDPNSName( + name_string, + vote_choice, + voters, + ) = pending + { + // Lock and get a reference to the contested names + let contested_names = self.contested_names.lock().unwrap(); - // Vote with all identities - if ui.button("All").clicked() { - for identity in self.voting_identities.iter() { - voters.push(identity.clone()); + // Find the contested name that matches the given name_string + let ending_time = contested_names + .iter() + .find(|cn| cn.normalized_contested_name == name_string) + .and_then(|cn| cn.end_time) + .unwrap_or_default(); + let contested_name = name_string.clone(); + let schedule_screen = ScheduleVoteScreen::new( + &self.app_context, + contested_name, + ending_time, + voters, + vote_choice, + ); + app_action = AppAction::AddScreen(Screen::ScheduleVoteScreen( + schedule_screen, + )); + } + self.show_vote_popup_info = None; } - - // Create a new `VoteOnDPNSName` task with all voters - let updated_action = ContestedResourceTask::VoteOnDPNSName( - contested_name.clone(), - vote_choice.clone(), - voters.clone(), // Updated voters - ); - - // Pass updated action to BackendTask - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask( - updated_action, - )); - self.show_vote_popup_info = None; } } // Add the "Cancel" button if ui.button("Cancel").clicked() { self.show_vote_popup_info = None; + self.pending_vote_action = None; } }); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9a3513a62..8dffeeb1a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -20,8 +20,10 @@ use crate::ui::wallet::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 egui::{Context, Widget}; +use dpns_vote_scheduling_screen::ScheduleVoteScreen; +use egui::Context; use identities::add_existing_identity_screen::AddExistingIdentityScreen; use identities::add_new_identity_screen::AddNewIdentityScreen; use identities::identities_screen::IdentitiesScreen; @@ -35,6 +37,7 @@ use wallet::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(crate) mod identities; pub mod network_chooser_screen; pub mod tool_screens; @@ -138,6 +141,7 @@ pub enum ScreenType { RegisterDpnsName, ProofLog, TopUpIdentity(QualifiedIdentity), + ScheduleVoteScreen(String, u64, Vec, ResourceVoteChoice), } impl ScreenType { @@ -207,6 +211,18 @@ impl ScreenType { Screen::ImportWalletScreen(ImportWalletScreen::new(app_context)) } ScreenType::ProofLog => Screen::ProofLogScreen(ProofLogScreen::new(app_context)), + ScreenType::ScheduleVoteScreen( + contested_name, + ending_time, + identities, + vote_choice, + ) => Screen::ScheduleVoteScreen(ScheduleVoteScreen::new( + app_context, + contested_name.clone(), + ending_time.clone(), + identities.clone(), + vote_choice.clone(), + )), } } } @@ -231,6 +247,7 @@ pub enum Screen { WithdrawsStatusScreen(WithdrawsStatusScreen), NetworkChooserScreen(NetworkChooserScreen), WalletsBalancesScreen(WalletsBalancesScreen), + ScheduleVoteScreen(ScheduleVoteScreen), } impl Screen { @@ -255,6 +272,7 @@ impl Screen { Screen::WithdrawsStatusScreen(screen) => screen.app_context = app_context, Screen::ImportWalletScreen(screen) => screen.app_context = app_context, Screen::ProofLogScreen(screen) => screen.app_context = app_context, + Screen::ScheduleVoteScreen(screen) => screen.app_context = app_context, } } } @@ -335,6 +353,12 @@ impl Screen { Screen::WithdrawsStatusScreen(_) => ScreenType::WithdrawsStatus, Screen::ImportWalletScreen(_) => ScreenType::ImportWallet, Screen::ProofLogScreen(_) => ScreenType::ProofLog, + Screen::ScheduleVoteScreen(screen) => ScreenType::ScheduleVoteScreen( + screen.contested_name.clone(), + screen.ending_time.clone(), + screen.identities.clone(), + screen.vote_choice.clone(), + ), } } } @@ -361,6 +385,7 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.refresh(), Screen::WalletsBalancesScreen(screen) => screen.refresh(), Screen::ProofLogScreen(screen) => screen.refresh(), + Screen::ScheduleVoteScreen(screen) => screen.refresh(), } } @@ -385,6 +410,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::ScheduleVoteScreen(screen) => screen.refresh_on_arrival(), } } @@ -409,6 +435,7 @@ impl ScreenLike for Screen { Screen::NetworkChooserScreen(screen) => screen.ui(ctx), Screen::WalletsBalancesScreen(screen) => screen.ui(ctx), Screen::ProofLogScreen(screen) => screen.ui(ctx), + Screen::ScheduleVoteScreen(screen) => screen.ui(ctx), } } @@ -439,6 +466,7 @@ 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::ScheduleVoteScreen(screen) => screen.display_message(message, message_type), } } @@ -501,6 +529,9 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::ScheduleVoteScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } } } @@ -525,6 +556,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::ScheduleVoteScreen(screen) => screen.pop_on_success(), } } } From 54e8bf457bfa2f31792dc07286040f605d44b94b Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 14:49:15 +0700 Subject: [PATCH 02/28] progress --- src/app.rs | 10 ++ src/backend_task/contested_names/mod.rs | 20 +-- src/context.rs | 7 + src/database/initialization.rs | 7 +- src/database/mod.rs | 1 + src/logging.rs | 2 +- .../dpns_subscreen_chooser_panel.rs | 7 + src/ui/dpns_contested_names_screen.rs | 140 +++++++++++++++++- src/ui/mod.rs | 12 ++ 9 files changed, 187 insertions(+), 19 deletions(-) diff --git a/src/app.rs b/src/app.rs index 30d1970dc..5536d4e35 100644 --- a/src/app.rs +++ b/src/app.rs @@ -145,6 +145,8 @@ impl AppState { DPNSContestedNamesScreen::new(&mainnet_app_context, DPNSSubscreen::Past); let mut dpns_my_usernames_screen = DPNSContestedNamesScreen::new(&mainnet_app_context, DPNSSubscreen::Owned); + let mut dpns_scheduled_votes_screen = + DPNSContestedNamesScreen::new(&mainnet_app_context, DPNSSubscreen::ScheduledVotes); let mut transition_visualizer_screen = TransitionVisualizerScreen::new(&mainnet_app_context); let mut proof_log_screen = ProofLogScreen::new(&mainnet_app_context); @@ -187,6 +189,10 @@ impl AppState { DPNSContestedNamesScreen::new(&testnet_app_context, DPNSSubscreen::Past); dpns_my_usernames_screen = DPNSContestedNamesScreen::new(&testnet_app_context, DPNSSubscreen::Owned); + dpns_scheduled_votes_screen = DPNSContestedNamesScreen::new( + &testnet_app_context, + DPNSSubscreen::ScheduledVotes, + ); transition_visualizer_screen = TransitionVisualizerScreen::new(testnet_app_context); document_query_screen = DocumentQueryScreen::new(testnet_app_context); wallets_balances_screen = WalletsBalancesScreen::new(testnet_app_context); @@ -244,6 +250,10 @@ impl AppState { RootScreenType::RootScreenDPNSOwnedNames, Screen::DPNSContestedNamesScreen(dpns_my_usernames_screen), ), + ( + RootScreenType::RootScreenDPNSScheduledVotes, + Screen::DPNSContestedNamesScreen(dpns_scheduled_votes_screen), + ), ( RootScreenType::RootScreenWalletsBalances, Screen::WalletsBalancesScreen(wallets_balances_screen), diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index 1c6274527..fc59727eb 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -1,7 +1,7 @@ mod query_dpns_contested_resources; mod query_dpns_vote_contenders; mod query_ending_times; -mod schedule_dpns_vote; +pub mod schedule_dpns_vote; mod vote_on_dpns_name; use crate::app::TaskResult; @@ -10,6 +10,7 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dash_sdk::Sdk; +use schedule_dpns_vote::ScheduledDPNSVote; use std::sync::Arc; use tokio::sync::mpsc; @@ -17,12 +18,7 @@ use tokio::sync::mpsc; pub(crate) enum ContestedResourceTask { QueryDPNSContestedResources, QueryDPNSVoteContenders(String), - ScheduleDPNSVote( - String, - ResourceVoteChoice, - Vec, - Vec<(QualifiedIdentity, String)>, - ), + ScheduleDPNSVote(Vec), VoteOnDPNSName(String, ResourceVoteChoice, Vec), } @@ -42,14 +38,8 @@ impl AppContext { .query_dpns_vote_contenders(name, sdk, sender) .await .map(|_| BackendTaskSuccessResult::None), - ContestedResourceTask::ScheduleDPNSVote( - name, - vote_choice, - voters, - voters_and_names, - ) => { - self.schedule_dpns_vote(name, *vote_choice, voters, sdk, sender) - .await + ContestedResourceTask::ScheduleDPNSVote(scheduled_votes) => { + self.schedule_dpns_vote(scheduled_votes).await } ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) diff --git a/src/context.rs b/src/context.rs index 54b15e7af..2b2a02be7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,3 +1,4 @@ +use crate::backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote; use crate::components::core_zmq_listener::ZMQConnectionEvent; use crate::config::{Config, NetworkConfig}; use crate::context_provider::Provider; @@ -255,6 +256,12 @@ impl AppContext { Ok(contracts) } + + /// Get scheduled votes + pub fn get_scheduled_votes(&self) -> Result> { + self.db.get_scheduled_votes(&self) + } + pub(crate) fn received_transaction_finality( &self, tx: &Transaction, diff --git a/src/database/initialization.rs b/src/database/initialization.rs index 5aec0d490..361cae293 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -4,7 +4,7 @@ use rusqlite::{params, Connection}; use std::fs; use std::path::Path; -pub const DEFAULT_DB_VERSION: u16 = 4; +pub const DEFAULT_DB_VERSION: u16 = 5; pub const DEFAULT_NETWORK: &str = "dash"; @@ -34,6 +34,9 @@ impl Database { fn apply_version_changes(&self, version: u16) -> rusqlite::Result<()> { match version { + 5 => { + self.initialize_scheduled_votes_table()?; + } 4 => { self.initialize_top_up_table()?; } @@ -347,8 +350,8 @@ impl Database { )?; self.initialize_proof_log_table()?; - self.initialize_top_up_table()?; + self.initialize_scheduled_votes_table()?; Ok(()) } diff --git a/src/database/mod.rs b/src/database/mod.rs index 942dfe471..4caef82bc 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -4,6 +4,7 @@ mod contracts; mod identities; mod initialization; mod proof_log; +mod scheduled_votes; mod settings; mod top_ups; mod utxo; diff --git a/src/logging.rs b/src/logging.rs index c6359f665..894ba3f2b 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -12,7 +12,7 @@ pub fn initialize_logger() { }; let filter = EnvFilter::try_new( - "error,dash_sdk=debug,tenderdash_abci=debug,drive=debug,drive_proof_verifier=debug,rs_dapi_client=debug", + "error,info,dash_sdk=debug,tenderdash_abci=debug,drive=debug,drive_proof_verifier=debug,rs_dapi_client=debug", ) .unwrap_or_else(|e| panic!("Failed to create EnvFilter: {:?}", e)); diff --git a/src/ui/components/dpns_subscreen_chooser_panel.rs b/src/ui/components/dpns_subscreen_chooser_panel.rs index c88a7771c..91f848792 100644 --- a/src/ui/components/dpns_subscreen_chooser_panel.rs +++ b/src/ui/components/dpns_subscreen_chooser_panel.rs @@ -11,6 +11,7 @@ pub fn add_dpns_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) DPNSSubscreen::Active, DPNSSubscreen::Past, DPNSSubscreen::Owned, + DPNSSubscreen::ScheduledVotes, ]; let active_screen = match app_context.get_settings() { @@ -18,6 +19,7 @@ pub fn add_dpns_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) ui::RootScreenType::RootScreenDPNSActiveContests => DPNSSubscreen::Active, ui::RootScreenType::RootScreenDPNSPastContests => DPNSSubscreen::Past, ui::RootScreenType::RootScreenDPNSOwnedNames => DPNSSubscreen::Owned, + ui::RootScreenType::RootScreenDPNSScheduledVotes => DPNSSubscreen::ScheduledVotes, _ => DPNSSubscreen::Active, }, _ => DPNSSubscreen::Active, // Fallback to Active screen if settings unavailable @@ -66,6 +68,11 @@ pub fn add_dpns_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) RootScreenType::RootScreenDPNSOwnedNames, ) } + DPNSSubscreen::ScheduledVotes => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenDPNSScheduledVotes, + ) + } } } diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 476e4c689..4b6e4a7c6 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -2,6 +2,7 @@ use super::components::dpns_subscreen_chooser_panel::add_dpns_subscreen_chooser_ use super::dpns_vote_scheduling_screen::ScheduleVoteScreen; use super::{Screen, ScreenType}; use crate::app::{AppAction, DesiredAppAction}; +use crate::backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote; use crate::backend_task::contested_names::ContestedResourceTask; use crate::backend_task::identity::IdentityTask; use crate::backend_task::BackendTask; @@ -44,6 +45,7 @@ pub enum DPNSSubscreen { Active, Past, Owned, + ScheduledVotes, } impl DPNSSubscreen { @@ -52,6 +54,7 @@ impl DPNSSubscreen { Self::Active => "Active contests", Self::Past => "Past contests", Self::Owned => "My usernames", + Self::ScheduledVotes => "Scheduled votes", } } } @@ -62,6 +65,7 @@ pub struct DPNSContestedNamesScreen { user_identities: Vec, contested_names: Arc>>, local_dpns_names: Arc>>, + scheduled_votes: Arc>>, pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, sort_column: SortColumn, @@ -84,12 +88,18 @@ impl DPNSContestedNamesScreen { Vec::new() }), DPNSSubscreen::Owned => Vec::new(), + DPNSSubscreen::ScheduledVotes => Vec::new(), })); let local_dpns_names = Arc::new(Mutex::new(match dpns_subscreen { DPNSSubscreen::Active => Vec::new(), DPNSSubscreen::Past => Vec::new(), DPNSSubscreen::Owned => app_context.local_dpns_names().unwrap_or_default(), + DPNSSubscreen::ScheduledVotes => Vec::new(), })); + let scheduled_votes = Arc::new(Mutex::new( + app_context.get_scheduled_votes().unwrap_or_default(), + )); + tracing::info!("Scheduled votes: {:?}", scheduled_votes); let voting_identities = app_context .db .get_local_voting_identities(&app_context) @@ -103,6 +113,7 @@ impl DPNSContestedNamesScreen { user_identities, contested_names, local_dpns_names, + scheduled_votes, app_context: app_context.clone(), error_message: None, sort_column: SortColumn::ContestedName, @@ -235,6 +246,14 @@ impl DPNSContestedNamesScreen { .color(egui::Color32::GRAY), ); } + DPNSSubscreen::ScheduledVotes => { + ui.label( + egui::RichText::new("No scheduled votes.") + .heading() + .strong() + .color(egui::Color32::GRAY), + ); + } } ui.add_space(10.0); ui.label("Please check back later or try refreshing the list."); @@ -255,6 +274,10 @@ impl DPNSContestedNamesScreen { IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, )); } + _ => { + // To Do: Some kind of refresh for scheduled votes maybe + app_action |= AppAction::None; + } } } } @@ -691,6 +714,95 @@ impl DPNSContestedNamesScreen { }); } + fn render_table_scheduled_votes(&mut self, ui: &mut Ui) { + let mut sorted_votes = { + let scheduled_votes_guard = self.scheduled_votes.lock().unwrap(); + let scheduled_votes = scheduled_votes_guard.clone(); + scheduled_votes + }; + + sorted_votes.sort_by(|a, b| match self.sort_column { + SortColumn::ContestedName => { + let order = a.contested_name.cmp(&b.contested_name); // Sort by DPNS Name + if self.sort_order == SortOrder::Descending { + order.reverse() + } else { + order + } + } + SortColumn::EndingTime => { + let order = a.time.cmp(&b.time); // Sort by Vote Time + if self.sort_order == SortOrder::Descending { + order.reverse() + } else { + order + } + } + _ => std::cmp::Ordering::Equal, + }); + + // Render table UI + egui::ScrollArea::vertical().show(ui, |ui| { + Frame::group(ui.style()) + .fill(ui.visuals().panel_fill) + .stroke(egui::Stroke::new( + 1.0, + ui.visuals().widgets.inactive.bg_stroke.color, + )) + .inner_margin(Margin::same(8.0)) + .show(ui, |ui| { + TableBuilder::new(ui) + .striped(true) + .resizable(true) + .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) + .column(Column::initial(200.0).resizable(true)) // DPNS Name + .column(Column::initial(200.0).resizable(true)) // Voter ID + .column(Column::initial(300.0).resizable(true)) // Choice + .column(Column::initial(300.0).resizable(true)) // Scheduled vote time + .header(30.0, |mut header| { + header.col(|ui| { + if ui.button("Name").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); + header.col(|ui| { + if ui.button("Voter").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); + header.col(|ui| { + if ui.button("Vote").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); + header.col(|ui| { + if ui.button("Scheduled Time").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); + }) + .body(|mut body| { + for vote in sorted_votes { + body.row(25.0, |mut row| { + row.col(|ui| { + ui.label(vote.contested_name); + }); + row.col(|ui| { + ui.label(vote.voter_id.to_string(Encoding::Base58)); + }); + row.col(|ui| { + ui.label(vote.choice.to_string()); + }); + row.col(|ui| { + ui.label(vote.time.to_string()); + }); + }); + } + }); + }); + }); + } + fn show_vote_popup(&mut self, ui: &mut Ui) -> AppAction { let mut app_action = AppAction::None; if self.voting_identities.is_empty() { @@ -709,7 +821,6 @@ impl DPNSContestedNamesScreen { ui.label(message); ui.horizontal(|ui| { - // Only modify `voters` if `action` is `VoteOnDPNSName` if let ContestedResourceTask::VoteOnDPNSName( contested_name, vote_choice, @@ -820,6 +931,9 @@ impl ScreenLike for DPNSContestedNamesScreen { DPNSSubscreen::Owned => { *dpns_names = self.app_context.local_dpns_names().unwrap_or_default(); } + DPNSSubscreen::ScheduledVotes => { + // To Do: Implement scheduled votes + } } } @@ -840,6 +954,7 @@ impl ScreenLike for DPNSContestedNamesScreen { let mut contested_names = self.contested_names.lock().unwrap(); let mut dpns_names = self.local_dpns_names.lock().unwrap(); + let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); match self.dpns_subscreen { DPNSSubscreen::Active => { *contested_names = self @@ -853,6 +968,9 @@ impl ScreenLike for DPNSContestedNamesScreen { DPNSSubscreen::Owned => { *dpns_names = self.app_context.local_dpns_names().unwrap_or_default(); } + DPNSSubscreen::ScheduledVotes => { + *scheduled_votes = self.app_context.get_scheduled_votes().unwrap_or_default(); + } } } @@ -882,6 +1000,7 @@ impl ScreenLike for DPNSContestedNamesScreen { IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, )), ), + DPNSSubscreen::ScheduledVotes => ("Refresh", DesiredAppAction::None), // To Do }; if self.refreshing { @@ -928,6 +1047,13 @@ impl ScreenLike for DPNSContestedNamesScreen { RootScreenType::RootScreenDPNSOwnedNames, ); } + DPNSSubscreen::ScheduledVotes => { + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenDPNSScheduledVotes, + ); + } } action |= add_dpns_subscreen_chooser_panel(ctx, self.app_context.as_ref()); @@ -978,6 +1104,11 @@ impl ScreenLike for DPNSContestedNamesScreen { let dpns_names = self.local_dpns_names.lock().unwrap(); !dpns_names.is_empty() }; + // Check if there are any scheduled votes to display + let has_scheduled_votes = { + let scheduled_votes = self.scheduled_votes.lock().unwrap(); + !scheduled_votes.is_empty() + }; // Render the proper table match self.dpns_subscreen { @@ -1002,6 +1133,13 @@ impl ScreenLike for DPNSContestedNamesScreen { action |= self.render_no_active_contests_or_owned_names(ui); } } + DPNSSubscreen::ScheduledVotes => { + if has_scheduled_votes { + self.render_table_scheduled_votes(ui); + } else { + action |= self.render_no_active_contests_or_owned_names(ui); + } + } } }); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8dffeeb1a..4b77e0aa1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -51,6 +51,7 @@ pub enum RootScreenType { RootScreenDPNSActiveContests, RootScreenDPNSPastContests, RootScreenDPNSOwnedNames, + RootScreenDPNSScheduledVotes, RootScreenDocumentQuery, RootScreenWalletsBalances, RootScreenToolsProofLogScreen, @@ -73,6 +74,7 @@ impl RootScreenType { RootScreenType::RootScreenNetworkChooser => 7, RootScreenType::RootScreenWithdrawsStatus => 8, RootScreenType::RootScreenToolsProofLogScreen => 9, + RootScreenType::RootScreenDPNSScheduledVotes => 10, } } @@ -89,6 +91,7 @@ impl RootScreenType { 7 => Some(RootScreenType::RootScreenNetworkChooser), 8 => Some(RootScreenType::RootScreenWithdrawsStatus), 9 => Some(RootScreenType::RootScreenToolsProofLogScreen), + 10 => Some(RootScreenType::RootScreenDPNSScheduledVotes), _ => None, } } @@ -109,6 +112,7 @@ impl From for ScreenType { RootScreenType::RootScreenNetworkChooser => ScreenType::NetworkChooser, RootScreenType::RootScreenWalletsBalances => ScreenType::WalletsBalances, RootScreenType::RootScreenToolsProofLogScreen => ScreenType::ProofLog, + RootScreenType::RootScreenDPNSScheduledVotes => ScreenType::ScheduledVotes, } } } @@ -142,6 +146,7 @@ pub enum ScreenType { ProofLog, TopUpIdentity(QualifiedIdentity), ScheduleVoteScreen(String, u64, Vec, ResourceVoteChoice), + ScheduledVotes, } impl ScreenType { @@ -223,6 +228,9 @@ impl ScreenType { identities.clone(), vote_choice.clone(), )), + ScreenType::ScheduledVotes => Screen::DPNSContestedNamesScreen( + DPNSContestedNamesScreen::new(app_context, DPNSSubscreen::ScheduledVotes), + ), } } } @@ -335,6 +343,10 @@ impl Screen { dpns_subscreen: DPNSSubscreen::Owned, .. }) => ScreenType::DPNSMyUsernames, + Screen::DPNSContestedNamesScreen(DPNSContestedNamesScreen { + dpns_subscreen: DPNSSubscreen::ScheduledVotes, + .. + }) => ScreenType::ScheduledVotes, Screen::TransitionVisualizerScreen(_) => ScreenType::TransitionVisualizer, Screen::WithdrawalScreen(screen) => { ScreenType::WithdrawalScreen(screen.identity.clone()) From 2333a21a735b86863b0b770ae96e6baa201f773d Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 14:49:35 +0700 Subject: [PATCH 03/28] progess --- .../contested_names/schedule_dpns_vote.rs | 35 +++ src/database/scheduled_votes.rs | 92 +++++++ src/ui/dpns_vote_scheduling_screen.rs | 238 ++++++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 src/backend_task/contested_names/schedule_dpns_vote.rs create mode 100644 src/database/scheduled_votes.rs create mode 100644 src/ui/dpns_vote_scheduling_screen.rs diff --git a/src/backend_task/contested_names/schedule_dpns_vote.rs b/src/backend_task/contested_names/schedule_dpns_vote.rs new file mode 100644 index 000000000..6bde86b4a --- /dev/null +++ b/src/backend_task/contested_names/schedule_dpns_vote.rs @@ -0,0 +1,35 @@ +use crate::backend_task::BackendTaskSuccessResult; +use crate::context::AppContext; +use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dash_sdk::platform::Identifier; +use std::sync::Arc; + +#[derive(Debug, Clone, PartialEq)] +pub struct ScheduledDPNSVote { + pub contested_name: String, + pub voter_id: Identifier, + pub choice: ResourceVoteChoice, + pub time: u64, +} + +impl AppContext { + /// Inserts votes into the local db to be cast later + pub(super) async fn schedule_dpns_vote( + self: &Arc, + scheduled_votes: &Vec, + ) -> Result { + for vote in scheduled_votes { + self.db + .insert_scheduled_vote( + vote.voter_id.as_slice(), + vote.contested_name.clone(), + vote.choice, + vote.time, + ) + .map_err(|e| format!("Failed to insert scheduled vote: {}", e))?; + } + Ok(BackendTaskSuccessResult::Message( + "Successfully scheduled votes".to_string(), + )) + } +} diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs new file mode 100644 index 000000000..86d5990f9 --- /dev/null +++ b/src/database/scheduled_votes.rs @@ -0,0 +1,92 @@ +use crate::{ + backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote, context::AppContext, + database::Database, +}; +use dash_sdk::{ + dpp::{ + platform_value::string_encoding::Encoding, + voting::vote_choices::resource_vote_choice::ResourceVoteChoice, + }, + platform::Identifier, +}; +use rusqlite::params; + +impl Database { + pub fn initialize_scheduled_votes_table(&self) -> rusqlite::Result<()> { + // Create the scheduled_votes table + self.execute( + "CREATE TABLE IF NOT EXISTS scheduled_votes ( + identity_id BLOB NOT NULL, + contested_name STRING NOT NULL, + vote_choice BLOB NOT NULL, + time INTEGER NOT NULL, + PRIMARY KEY (identity_id), + FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE + )", + [], + )?; + Ok(()) + } + + pub fn insert_scheduled_vote( + &self, + identity_id: &[u8], + contested_name: String, + vote_choice: ResourceVoteChoice, + time: u64, + ) -> rusqlite::Result<()> { + let vote_choice_string = vote_choice.to_string(); + self.execute( + "INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time) VALUES (?, ?, ?, ?)", + params![identity_id, contested_name, vote_choice_string, time], + )?; + Ok(()) + } + + pub fn get_scheduled_votes( + &self, + app_context: &AppContext, + ) -> rusqlite::Result> { + let network = app_context.network_string(); + + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare("SELECT * FROM scheduled_votes WHERE network = ?")?; + let votes_iter = stmt.query_map(params![network], |row| { + let voter_id_bytes: Vec = row.get(0)?; + let contested_name: String = row.get(1)?; + let vote_choice_string: String = row.get(2)?; + let time: u64 = row.get(3)?; + let vote_choice = match vote_choice_string.as_str() { + "Abstain" => ResourceVoteChoice::Abstain, + "Lock" => ResourceVoteChoice::Lock, + other => { + if let Some(inner) = other.strip_prefix("TowardsIdentity(") { + if let Some(inner) = inner.strip_suffix(')') { + let towards_id = inner.to_string(); + ResourceVoteChoice::TowardsIdentity( + Identifier::from_string(&towards_id, Encoding::Base58) + .expect("Expected valid identifier"), + ) + } else { + return Err(rusqlite::Error::InvalidQuery); + } + } else { + return Err(rusqlite::Error::InvalidQuery); + } + } + }; + let scheduled_vote = ScheduledDPNSVote { + voter_id: Identifier::from_bytes(&voter_id_bytes) + .expect("Expected valid identifier"), + contested_name, + choice: vote_choice, + time, + }; + + Ok(scheduled_vote) + })?; + + let scheduled_votes: rusqlite::Result> = votes_iter.collect(); + scheduled_votes + } +} diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs new file mode 100644 index 000000000..1cdbd367c --- /dev/null +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -0,0 +1,238 @@ +use crate::app::AppAction; +use crate::backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote; +use crate::backend_task::{contested_names::ContestedResourceTask, BackendTask}; +use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::ui::{MessageType, ScreenLike}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use eframe::egui::Context; +use eframe::egui::{self, Color32, RichText, Ui}; +use std::sync::Arc; + +/// The voting option a user can choose for each identity. +enum VoteOption { + None, + VoteNow, + Scheduled(String), +} + +pub struct ScheduleVoteScreen { + pub app_context: Arc, + pub contested_name: String, + pub ending_time: u64, + pub identities: Vec, + pub vote_choice: ResourceVoteChoice, + identity_options: Vec, + error_message: Option, +} + +impl ScheduleVoteScreen { + pub fn new( + app_context: &Arc, + contested_name: String, + ending_time: u64, + potential_voting_identities: Vec, + vote_choice: ResourceVoteChoice, + ) -> Self { + let identity_options = potential_voting_identities + .iter() + .map(|_| VoteOption::None) + .collect(); + Self { + app_context: app_context.clone(), + contested_name, + ending_time, + identities: potential_voting_identities, + vote_choice, + identity_options, + error_message: None, + } + } + + fn display_identity_options(&mut self, ui: &mut Ui) { + ui.heading("Schedule Votes for Identities"); + ui.add_space(10.0); + + ui.label(format!( + "Contest for name {} ends at {}", + self.contested_name, self.ending_time + )); + ui.add_space(10.0); + + // For each identity, show a row with their alias/ID and voting options + 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 or Radio buttons for None/VoteNow/Scheduled + // For simplicity, let's use a ComboBox: + let current_option = &mut self.identity_options[i]; + egui::ComboBox::from_label("") + .selected_text(match current_option { + VoteOption::None => "None".to_string(), + VoteOption::VoteNow => "Vote Now".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; + } + if ui + .selectable_label( + matches!(current_option, VoteOption::VoteNow), + "Vote Now", + ) + .clicked() + { + *current_option = VoteOption::VoteNow; + } + if ui + .selectable_label( + matches!(current_option, VoteOption::Scheduled(_)), + "Scheduled", + ) + .clicked() + { + // If we had a previous schedule time, keep it. Otherwise, empty string. + let old_time = match current_option { + VoteOption::Scheduled(s) => s.clone(), + _ => String::new(), + }; + *current_option = VoteOption::Scheduled(old_time); + } + }); + + // If Scheduled is chosen, display a text field for the schedule time + // To Do: This should be a date time selector rather than text input. + if let VoteOption::Scheduled(ref mut time_str) = current_option { + ui.label("Schedule Time (e.g. UNIX timestamp):"); + ui.text_edit_singleline(time_str); + } + }); + }); + + ui.add_space(10.0); + } + } + + fn cast_votes_button(&mut self) -> AppAction { + // Gather the voter identities and their chosen times + // For simplicity, let's assume the backend can handle a structure where: + // - If VoteNow, we submit immediately. + // - If Scheduled(time), we submit the schedule. + // - If None, we do not include that identity as a voter. + + // Filter only those with VoteNow or Scheduled + let mut voters = Vec::new(); + let mut scheduled_votes = Vec::new(); + + for (identity, option) in self.identities.iter().zip(self.identity_options.iter()) { + match option { + VoteOption::None => { + // Skip this identity + } + VoteOption::VoteNow => { + // Immediate vote + voters.push(identity.clone()); + } + VoteOption::Scheduled(time_str) => { + // Collect scheduled votes separately + // The backend task might need a structure that allows scheduling. + // If such a structure doesn’t exist yet, we might need to define one. + let scheduled_vote = ScheduledDPNSVote { + contested_name: self.contested_name.clone(), + voter_id: identity.identity.id().clone(), + choice: self.vote_choice, + time: time_str.parse().unwrap_or(0), + }; + scheduled_votes.push(scheduled_vote); + } + } + } + + // If no voters and no scheduled, return None to indicate nothing to do. + if voters.is_empty() && scheduled_votes.is_empty() { + self.error_message = Some("No votes selected.".to_string()); + return AppAction::None; + } + + let updated_action = ContestedResourceTask::ScheduleDPNSVote(scheduled_votes); + + AppAction::BackendTask(BackendTask::ContestedResourceTask(updated_action)) + } +} + +impl ScreenLike for ScheduleVoteScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + // Maybe nothing special here + } + MessageType::Info => { + // Informational messages + } + MessageType::Error => { + self.error_message = Some(message.to_string()); + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = AppAction::None; + + // A top panel or breadcrumb could be added similarly to the original code. + // For brevity, let's just add a simple label at the top. + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + ui.horizontal(|ui| { + if ui.button("Back").clicked() { + action = AppAction::PopScreen; + } + ui.label("Schedule Votes"); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Schedule Votes"); + ui.add_space(10.0); + + if let Some(err) = &self.error_message { + ui.colored_label(Color32::RED, format!("Error: {}", err)); + ui.add_space(10.0); + } + + // Display the identity options and scheduling fields + self.display_identity_options(ui); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Button to cast votes (now or scheduled) + let button = egui::Button::new(RichText::new("Cast Votes").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .rounding(3.0) + .min_size(egui::vec2(80.0, 30.0)); + + if ui.add(button).clicked() { + action = self.cast_votes_button(); + } + }); + + action + } +} From 7885c00a535498ed6aef1d6a92cc30d157132466 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 15:09:05 +0700 Subject: [PATCH 04/28] insert and get scheduled vote works --- .../contested_names/schedule_dpns_vote.rs | 1 + src/database/scheduled_votes.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/backend_task/contested_names/schedule_dpns_vote.rs b/src/backend_task/contested_names/schedule_dpns_vote.rs index 6bde86b4a..8abf3de72 100644 --- a/src/backend_task/contested_names/schedule_dpns_vote.rs +++ b/src/backend_task/contested_names/schedule_dpns_vote.rs @@ -25,6 +25,7 @@ impl AppContext { vote.contested_name.clone(), vote.choice, vote.time, + self, ) .map_err(|e| format!("Failed to insert scheduled vote: {}", e))?; } diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 86d5990f9..5a70b38ae 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -1,5 +1,5 @@ use crate::{ - backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote, context::AppContext, + app, backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote, context::AppContext, database::Database, }; use dash_sdk::{ @@ -17,10 +17,11 @@ impl Database { self.execute( "CREATE TABLE IF NOT EXISTS scheduled_votes ( identity_id BLOB NOT NULL, - contested_name STRING NOT NULL, - vote_choice BLOB NOT NULL, + contested_name TEXT NOT NULL, + vote_choice TEXT NOT NULL, time INTEGER NOT NULL, - PRIMARY KEY (identity_id), + network TEXT NOT NULL, + PRIMARY KEY (identity_id, contested_name), FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE )", [], @@ -34,11 +35,13 @@ impl Database { contested_name: String, vote_choice: ResourceVoteChoice, time: u64, + app_context: &AppContext, ) -> rusqlite::Result<()> { + let network = app_context.network_string(); let vote_choice_string = vote_choice.to_string(); self.execute( - "INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time) VALUES (?, ?, ?, ?)", - params![identity_id, contested_name, vote_choice_string, time], + "INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time, network) VALUES (?, ?, ?, ?, ?)", + params![identity_id, contested_name, vote_choice_string, time, network], )?; Ok(()) } From c906bcfefac88e3c4dae2b9194dcb44216046a05 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 19:07:32 +0700 Subject: [PATCH 05/28] it works --- src/app.rs | 68 ++++++++++++++++++- src/backend_task/contested_names/mod.rs | 17 +++++ .../contested_names/schedule_dpns_vote.rs | 4 +- src/backend_task/mod.rs | 2 + src/database/scheduled_votes.rs | 47 ++++++++++++- src/logging.rs | 2 +- src/ui/dpns_contested_names_screen.rs | 32 ++++++++- src/ui/dpns_vote_scheduling_screen.rs | 11 ++- 8 files changed, 168 insertions(+), 15 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5536d4e35..f356c06b0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,7 @@ use crate::app_dir::{ app_user_data_file_path, copy_env_file_if_not_exists, create_app_user_data_directory_if_not_exists, }; +use crate::backend_task::contested_names::ContestedResourceTask; use crate::backend_task::core::CoreItem; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::components::core_zmq_listener::{CoreZMQListener, ZMQMessage}; @@ -18,12 +19,13 @@ use crate::ui::wallet::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::dashcore::Network; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use derive_more::From; use eframe::{egui, App}; use std::collections::BTreeMap; use std::ops::BitOrAssign; use std::sync::{mpsc, Arc}; -use std::time::Instant; +use std::time::{Duration, Instant, SystemTime}; use std::vec; use tokio::sync::mpsc as tokiompsc; @@ -56,6 +58,7 @@ pub struct AppState { pub task_result_sender: tokiompsc::Sender, // Channel sender for sending task results pub task_result_receiver: tokiompsc::Receiver, // Channel receiver for receiving task results last_repaint: Instant, // Track the last time we requested a repaint + last_scheduled_vote_check: Instant, } #[derive(Debug, Clone, PartialEq)] @@ -291,6 +294,7 @@ impl AppState { task_result_sender, task_result_receiver, last_repaint, + last_scheduled_vote_check: Instant::now(), } } @@ -406,6 +410,7 @@ impl App for AppState { *status = event; } } + // Poll the receiver for any new task results while let Ok(task_result) = self.task_result_receiver.try_recv() { // Handle the result on the main thread @@ -427,6 +432,14 @@ impl App for AppState { BackendTaskSuccessResult::SuccessfulVotes(_) => { self.visible_screen_mut().refresh(); } + BackendTaskSuccessResult::CastScheduledVote(vote) => { + let _ = self.current_app_context().db.mark_vote_executed( + vote.voter_id.as_slice(), + vote.contested_name, + self.current_app_context(), + ); + self.visible_screen_mut().refresh(); + } BackendTaskSuccessResult::WithdrawalStatus(_) => { self.visible_screen_mut().display_task_result(message); } @@ -488,6 +501,59 @@ impl App for AppState { } } + // Check if a minute has passed + let now = Instant::now(); + if now.duration_since(self.last_scheduled_vote_check) > Duration::from_secs(60) { + self.last_scheduled_vote_check = now; + let app_context = self.current_app_context().clone(); + + // Query the database synchronously here + let db_votes = match app_context.db.get_scheduled_votes(&app_context) { + Ok(votes) => votes, + Err(e) => { + eprintln!("Error querying scheduled votes: {}", e); + return; + } + }; + + // Filter due votes + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let due_votes: Vec<_> = db_votes + .into_iter() + .filter(|v| v.unix_timestamp <= current_time) + .collect(); + + // For each due vote, construct a BackendTask and handle it + if !due_votes.is_empty() { + let local_identities = + match app_context.db.get_local_voting_identities(&app_context) { + Ok(identities) => identities, + Err(e) => { + eprintln!("Error querying local voting identities: {}", e); + return; + } + }; + + for vote in due_votes { + if let Some(voter) = local_identities + .iter() + .find(|i| i.identity.id() == vote.voter_id) + { + let task = BackendTask::ContestedResourceTask( + ContestedResourceTask::ExecuteScheduledVote(vote, voter.clone()), + ); + // Run the task directly: + self.handle_backend_task(task); + } else { + eprintln!("Voter not found for scheduled vote: {:?}", vote); + } + } + } + } + // Use a timer to repaint the UI every 0.05 seconds ctx.request_repaint_after(std::time::Duration::from_millis(50)); diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index fc59727eb..509212f29 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -19,6 +19,7 @@ pub(crate) enum ContestedResourceTask { QueryDPNSContestedResources, QueryDPNSVoteContenders(String), ScheduleDPNSVote(Vec), + ExecuteScheduledVote(ScheduledDPNSVote, QualifiedIdentity), VoteOnDPNSName(String, ResourceVoteChoice, Vec), } @@ -41,6 +42,22 @@ impl AppContext { ContestedResourceTask::ScheduleDPNSVote(scheduled_votes) => { self.schedule_dpns_vote(scheduled_votes).await } + ContestedResourceTask::ExecuteScheduledVote(scheduled_vote, voter) => self + .vote_on_dpns_name( + &scheduled_vote.contested_name, + scheduled_vote.choice, + &vec![voter.clone()], + sdk, + sender, + ) + .await + .map(|result| match result { + BackendTaskSuccessResult::SuccessfulVotes(_) => { + BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()) + } + _ => BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()), + }) + .map_err(|e| format!("Error casting scheduled vote: {}", e.to_string())), ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) .await diff --git a/src/backend_task/contested_names/schedule_dpns_vote.rs b/src/backend_task/contested_names/schedule_dpns_vote.rs index 8abf3de72..64cbde9f7 100644 --- a/src/backend_task/contested_names/schedule_dpns_vote.rs +++ b/src/backend_task/contested_names/schedule_dpns_vote.rs @@ -9,7 +9,7 @@ pub struct ScheduledDPNSVote { pub contested_name: String, pub voter_id: Identifier, pub choice: ResourceVoteChoice, - pub time: u64, + pub unix_timestamp: u64, } impl AppContext { @@ -24,7 +24,7 @@ impl AppContext { vote.voter_id.as_slice(), vote.contested_name.clone(), vote.choice, - vote.time, + vote.unix_timestamp, self, ) .map_err(|e| format!("Failed to insert scheduled vote: {}", e))?; diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index c9b9a2ae8..efa365975 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -7,6 +7,7 @@ use crate::backend_task::identity::IdentityTask; use crate::backend_task::withdrawal_statuses::{WithdrawStatusPartialData, WithdrawalsTask}; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; +use contested_names::schedule_dpns_vote::ScheduledDPNSVote; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::query_types::Documents; use std::sync::Arc; @@ -38,6 +39,7 @@ pub(crate) enum BackendTaskSuccessResult { RegisteredIdentity(QualifiedIdentity), ToppedUpIdentity(QualifiedIdentity), SuccessfulVotes(Vec), + CastScheduledVote(ScheduledDPNSVote), WithdrawalStatus(WithdrawStatusPartialData), } diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 5a70b38ae..3141a998b 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -20,6 +20,7 @@ impl Database { contested_name TEXT NOT NULL, vote_choice TEXT NOT NULL, time INTEGER NOT NULL, + executed INTEGER NOT NULL DEFAULT 0, network TEXT NOT NULL, PRIMARY KEY (identity_id, contested_name), FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE @@ -40,12 +41,26 @@ impl Database { let network = app_context.network_string(); let vote_choice_string = vote_choice.to_string(); self.execute( - "INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time, network) VALUES (?, ?, ?, ?, ?)", + "INSERT OR REPLACE INTO scheduled_votes (identity_id, contested_name, vote_choice, time, 0, network) VALUES (?, ?, ?, ?, ?)", params![identity_id, contested_name, vote_choice_string, time, network], )?; Ok(()) } + pub fn mark_vote_executed( + &self, + identity_id: &[u8], + contested_name: String, + app_context: &AppContext, + ) -> rusqlite::Result<()> { + let network = app_context.network_string(); + self.execute( + "UPDATE scheduled_votes SET executed = 1 WHERE identity_id = ? AND contested_name = ? AND network = ?", + params![identity_id, contested_name, network], + )?; + Ok(()) + } + pub fn get_scheduled_votes( &self, app_context: &AppContext, @@ -83,7 +98,7 @@ impl Database { .expect("Expected valid identifier"), contested_name, choice: vote_choice, - time, + unix_timestamp: time, }; Ok(scheduled_vote) @@ -92,4 +107,32 @@ impl Database { let scheduled_votes: rusqlite::Result> = votes_iter.collect(); scheduled_votes } + + /// Clear all past scheduled votes from the db + pub fn clear_all_past_scheduled_votes(&self, app_context: &AppContext) -> rusqlite::Result<()> { + let network = app_context.network_string(); + let conn = self.conn.lock().unwrap(); + + conn.execute( + "DELETE FROM scheduled_votes WHERE time < CAST(strftime('%s', 'now') AS INTEGER) * 1000 AND network = ?", + params![network], + )?; + + Ok(()) + } + + pub fn clear_executed_past_scheduled_votes( + &self, + app_context: &AppContext, + ) -> rusqlite::Result<()> { + let network = app_context.network_string(); + let conn = self.conn.lock().unwrap(); + + conn.execute( + "DELETE FROM scheduled_votes WHERE executed = 1 AND time < CAST(strftime('%s', 'now') AS INTEGER) * 1000 AND network = ?", + params![network], + )?; + + Ok(()) + } } diff --git a/src/logging.rs b/src/logging.rs index 894ba3f2b..c6359f665 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -12,7 +12,7 @@ pub fn initialize_logger() { }; let filter = EnvFilter::try_new( - "error,info,dash_sdk=debug,tenderdash_abci=debug,drive=debug,drive_proof_verifier=debug,rs_dapi_client=debug", + "error,dash_sdk=debug,tenderdash_abci=debug,drive=debug,drive_proof_verifier=debug,rs_dapi_client=debug", ) .unwrap_or_else(|e| panic!("Failed to create EnvFilter: {:?}", e)); diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 4b6e4a7c6..de2797581 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -96,10 +96,12 @@ impl DPNSContestedNamesScreen { DPNSSubscreen::Owned => app_context.local_dpns_names().unwrap_or_default(), DPNSSubscreen::ScheduledVotes => Vec::new(), })); + let _ = app_context + .db + .clear_executed_past_scheduled_votes(app_context); let scheduled_votes = Arc::new(Mutex::new( app_context.get_scheduled_votes().unwrap_or_default(), )); - tracing::info!("Scheduled votes: {:?}", scheduled_votes); let voting_identities = app_context .db .get_local_voting_identities(&app_context) @@ -731,7 +733,7 @@ impl DPNSContestedNamesScreen { } } SortColumn::EndingTime => { - let order = a.time.cmp(&b.time); // Sort by Vote Time + let order = a.unix_timestamp.cmp(&b.unix_timestamp); // Sort by Vote Time if self.sort_order == SortOrder::Descending { order.reverse() } else { @@ -794,7 +796,27 @@ impl DPNSContestedNamesScreen { ui.label(vote.choice.to_string()); }); row.col(|ui| { - ui.label(vote.time.to_string()); + // Assuming `scheduled_vote.unix_timestamp` is a u64 storing milliseconds since UNIX epoch: + if let LocalResult::Single(datetime) = + Utc.timestamp_millis_opt(vote.unix_timestamp as i64) + { + // Format the ISO date up to seconds + let iso_date = + datetime.format("%Y-%m-%d %H:%M:%S").to_string(); + + // Use chrono-humanize to get the relative time + let relative_time = + HumanTime::from(datetime).to_string(); + + // Combine both the ISO date and relative time + let display_text = + format!("{} ({})", iso_date, relative_time); + + ui.label(display_text); + } else { + // Handle case where the timestamp is invalid + ui.label("Invalid timestamp"); + } }); }); } @@ -954,6 +976,10 @@ impl ScreenLike for DPNSContestedNamesScreen { let mut contested_names = self.contested_names.lock().unwrap(); let mut dpns_names = self.local_dpns_names.lock().unwrap(); + let _ = self + .app_context + .db + .clear_executed_past_scheduled_votes(&self.app_context); let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); match self.dpns_subscreen { DPNSSubscreen::Active => { diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index 1cdbd367c..9671f0bd9 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -76,7 +76,7 @@ impl ScheduleVoteScreen { // Dropdown or Radio buttons for None/VoteNow/Scheduled // For simplicity, let's use a ComboBox: let current_option = &mut self.identity_options[i]; - egui::ComboBox::from_label("") + egui::ComboBox::from_label(identity_label) .selected_text(match current_option { VoteOption::None => "None".to_string(), VoteOption::VoteNow => "Vote Now".to_string(), @@ -120,7 +120,7 @@ impl ScheduleVoteScreen { // If Scheduled is chosen, display a text field for the schedule time // To Do: This should be a date time selector rather than text input. if let VoteOption::Scheduled(ref mut time_str) = current_option { - ui.label("Schedule Time (e.g. UNIX timestamp):"); + ui.label("Schedule Time (UNIX timestamp):"); ui.text_edit_singleline(time_str); } }); @@ -158,7 +158,7 @@ impl ScheduleVoteScreen { contested_name: self.contested_name.clone(), voter_id: identity.identity.id().clone(), choice: self.vote_choice, - time: time_str.parse().unwrap_or(0), + unix_timestamp: time_str.parse().unwrap_or(0), }; scheduled_votes.push(scheduled_vote); } @@ -181,7 +181,7 @@ impl ScreenLike for ScheduleVoteScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { match message_type { MessageType::Success => { - // Maybe nothing special here + self.error_message = Some(message.to_string()); } MessageType::Info => { // Informational messages @@ -225,8 +225,7 @@ impl ScreenLike for ScheduleVoteScreen { // Button to cast votes (now or scheduled) let button = egui::Button::new(RichText::new("Cast Votes").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) - .rounding(3.0) - .min_size(egui::vec2(80.0, 30.0)); + .rounding(3.0); if ui.add(button).clicked() { action = self.cast_votes_button(); From 7141627ac582a9718f34a8568f228d65577a81f3 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 19:24:18 +0700 Subject: [PATCH 06/28] progress --- src/backend_task/contested_names/mod.rs | 6 +++ src/database/scheduled_votes.rs | 13 ++++++ src/ui/dpns_contested_names_screen.rs | 7 ++- src/ui/dpns_vote_scheduling_screen.rs | 61 ++++++++++++------------- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index 509212f29..b8b6c142b 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -21,6 +21,7 @@ pub(crate) enum ContestedResourceTask { ScheduleDPNSVote(Vec), ExecuteScheduledVote(ScheduledDPNSVote, QualifiedIdentity), VoteOnDPNSName(String, ResourceVoteChoice, Vec), + ClearAllScheduledVotes, } impl AppContext { @@ -58,6 +59,11 @@ impl AppContext { _ => BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()), }) .map_err(|e| format!("Error casting scheduled vote: {}", e.to_string())), + ContestedResourceTask::ClearAllScheduledVotes => self + .db + .clear_all_scheduled_votes(self) + .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes + .map_err(|e| format!("Error clearing all scheduled votes: {}", e.to_string())), ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) .await diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 3141a998b..c815a2af8 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -108,6 +108,19 @@ impl Database { scheduled_votes } + /// Clear all scheduled votes from the db + pub fn clear_all_scheduled_votes(&self, app_context: &AppContext) -> rusqlite::Result<()> { + let network = app_context.network_string(); + let conn = self.conn.lock().unwrap(); + + conn.execute( + "DELETE FROM scheduled_votes WHERE network = ?", + params![network], + )?; + + Ok(()) + } + /// Clear all past scheduled votes from the db pub fn clear_all_past_scheduled_votes(&self, app_context: &AppContext) -> rusqlite::Result<()> { let network = app_context.network_string(); diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index de2797581..b9f36e79a 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -1026,7 +1026,12 @@ impl ScreenLike for DPNSContestedNamesScreen { IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, )), ), - DPNSSubscreen::ScheduledVotes => ("Refresh", DesiredAppAction::None), // To Do + DPNSSubscreen::ScheduledVotes => ( + "Clear All", + DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::ClearAllScheduledVotes, + )), + ), }; if self.refreshing { diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index 9671f0bd9..6f1224428 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -11,6 +11,8 @@ use eframe::egui::Context; use eframe::egui::{self, Color32, RichText, Ui}; use std::sync::Arc; +use super::components::top_panel::add_top_panel; + /// The voting option a user can choose for each identity. enum VoteOption { None, @@ -25,7 +27,7 @@ pub struct ScheduleVoteScreen { pub identities: Vec, pub vote_choice: ResourceVoteChoice, identity_options: Vec, - error_message: Option, + message: Option<(MessageType, String)>, } impl ScheduleVoteScreen { @@ -47,7 +49,7 @@ impl ScheduleVoteScreen { identities: potential_voting_identities, vote_choice, identity_options, - error_message: None, + message: None, } } @@ -167,7 +169,7 @@ impl ScheduleVoteScreen { // If no voters and no scheduled, return None to indicate nothing to do. if voters.is_empty() && scheduled_votes.is_empty() { - self.error_message = Some("No votes selected.".to_string()); + self.message = Some((MessageType::Error, "No votes selected.".to_string())); return AppAction::None; } @@ -179,42 +181,24 @@ impl ScheduleVoteScreen { impl ScreenLike for ScheduleVoteScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { - match message_type { - MessageType::Success => { - self.error_message = Some(message.to_string()); - } - MessageType::Info => { - // Informational messages - } - MessageType::Error => { - self.error_message = Some(message.to_string()); - } - } + self.message = Some((message_type, message.to_string())); } fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = AppAction::None; - - // A top panel or breadcrumb could be added similarly to the original code. - // For brevity, let's just add a simple label at the top. - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - ui.horizontal(|ui| { - if ui.button("Back").clicked() { - action = AppAction::PopScreen; - } - ui.label("Schedule Votes"); - }); - }); + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("DPNS", AppAction::GoToMainScreen), + ("Schedule Votes", AppAction::None), + ], + vec![], + ); egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Schedule Votes"); ui.add_space(10.0); - if let Some(err) = &self.error_message { - ui.colored_label(Color32::RED, format!("Error: {}", err)); - ui.add_space(10.0); - } - // Display the identity options and scheduling fields self.display_identity_options(ui); @@ -230,6 +214,21 @@ impl ScreenLike for ScheduleVoteScreen { if ui.add(button).clicked() { action = self.cast_votes_button(); } + + if let Some(message) = &self.message { + match message.0 { + MessageType::Error => { + ui.colored_label(Color32::DARK_RED, message.1.clone()); + } + MessageType::Success => { + ui.colored_label(Color32::DARK_GREEN, message.1.clone()); + } + MessageType::Info => { + ui.colored_label(Color32::DARK_BLUE, message.1.clone()); + } + } + ui.add_space(10.0); + } }); action From bb6a9e7898be49eaeadfd6b29f071f39e3a42ac7 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 21:25:01 +0700 Subject: [PATCH 07/28] cleaning it up --- src/backend_task/contested_names/mod.rs | 6 + .../contested_names/schedule_dpns_vote.rs | 1 + src/database/scheduled_votes.rs | 8 +- src/ui/components/top_panel.rs | 4 +- src/ui/dpns_contested_names_screen.rs | 233 +++++++++++++----- src/ui/dpns_vote_scheduling_screen.rs | 126 ++++++---- 6 files changed, 267 insertions(+), 111 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index b8b6c142b..7e53034fc 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -22,6 +22,7 @@ pub(crate) enum ContestedResourceTask { ExecuteScheduledVote(ScheduledDPNSVote, QualifiedIdentity), VoteOnDPNSName(String, ResourceVoteChoice, Vec), ClearAllScheduledVotes, + ClearExecutedScheduledVotes, } impl AppContext { @@ -64,6 +65,11 @@ impl AppContext { .clear_all_scheduled_votes(self) .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes .map_err(|e| format!("Error clearing all scheduled votes: {}", e.to_string())), + ContestedResourceTask::ClearExecutedScheduledVotes => self + .db + .clear_executed_past_scheduled_votes(self) + .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes + .map_err(|e| format!("Error clearing executed scheduled votes: {}", e.to_string())), ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) .await diff --git a/src/backend_task/contested_names/schedule_dpns_vote.rs b/src/backend_task/contested_names/schedule_dpns_vote.rs index 64cbde9f7..2b1a7afba 100644 --- a/src/backend_task/contested_names/schedule_dpns_vote.rs +++ b/src/backend_task/contested_names/schedule_dpns_vote.rs @@ -10,6 +10,7 @@ pub struct ScheduledDPNSVote { pub voter_id: Identifier, pub choice: ResourceVoteChoice, pub unix_timestamp: u64, + pub executed_successfully: bool, } impl AppContext { diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index c815a2af8..245923284 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -41,7 +41,7 @@ impl Database { let network = app_context.network_string(); let vote_choice_string = vote_choice.to_string(); self.execute( - "INSERT OR REPLACE INTO scheduled_votes (identity_id, contested_name, vote_choice, time, 0, network) VALUES (?, ?, ?, ?, ?)", + "INSERT OR REPLACE INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network) VALUES (?, ?, ?, ?, 0, ?)", params![identity_id, contested_name, vote_choice_string, time, network], )?; Ok(()) @@ -74,6 +74,11 @@ impl Database { let contested_name: String = row.get(1)?; let vote_choice_string: String = row.get(2)?; let time: u64 = row.get(3)?; + let executed_successfully: bool = match row.get(4)? { + 0 => false, + 1 => true, + _ => unreachable!(), + }; let vote_choice = match vote_choice_string.as_str() { "Abstain" => ResourceVoteChoice::Abstain, "Lock" => ResourceVoteChoice::Lock, @@ -99,6 +104,7 @@ impl Database { contested_name, choice: vote_choice, unix_timestamp: time, + executed_successfully, }; Ok(scheduled_vote) diff --git a/src/ui/components/top_panel.rs b/src/ui/components/top_panel.rs index a159e6cc7..e4c605d51 100644 --- a/src/ui/components/top_panel.rs +++ b/src/ui/components/top_panel.rs @@ -152,8 +152,10 @@ pub fn add_top_panel( // Right-aligned content with buttons ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add_space(5.0); + for (text, right_button_action) in right_buttons.into_iter().rev() { - ui.add_space(8.0); + ui.add_space(3.0); let font_id = egui::FontId::proportional(16.0); // Adjust font size as needed let color = Color32::WHITE; diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index b9f36e79a..c0b6a8d74 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -18,7 +18,7 @@ use chrono_humanize::HumanTime; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dash_sdk::platform::Identifier; -use egui::{Context, Frame, Margin, Ui}; +use egui::{Color32, Context, Frame, Margin, Ui}; use egui_extras::{Column, TableBuilder}; use itertools::Itertools; use std::sync::{Arc, Mutex}; @@ -96,9 +96,6 @@ impl DPNSContestedNamesScreen { DPNSSubscreen::Owned => app_context.local_dpns_names().unwrap_or_default(), DPNSSubscreen::ScheduledVotes => Vec::new(), })); - let _ = app_context - .db - .clear_executed_past_scheduled_votes(app_context); let scheduled_votes = Arc::new(Mutex::new( app_context.get_scheduled_votes().unwrap_or_default(), )); @@ -258,27 +255,29 @@ impl DPNSContestedNamesScreen { } } ui.add_space(10.0); - ui.label("Please check back later or try refreshing the list."); - ui.add_space(20.0); - if ui.button("Refresh").clicked() { - if self.refreshing { - app_action |= AppAction::None; - } else { - match self.dpns_subscreen { - DPNSSubscreen::Active | DPNSSubscreen::Past => { - app_action |= - AppAction::BackendTask(BackendTask::ContestedResourceTask( - ContestedResourceTask::QueryDPNSContestedResources, + if self.dpns_subscreen != DPNSSubscreen::ScheduledVotes { + ui.label("Please check back later or try refreshing the list."); + ui.add_space(20.0); + if ui.button("Refresh").clicked() { + if self.refreshing { + app_action |= AppAction::None; + } else { + match self.dpns_subscreen { + DPNSSubscreen::Active | DPNSSubscreen::Past => { + app_action |= + AppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::QueryDPNSContestedResources, + )); + } + DPNSSubscreen::Owned => { + app_action |= AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, )); - } - DPNSSubscreen::Owned => { - app_action |= AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, - )); - } - _ => { - // To Do: Some kind of refresh for scheduled votes maybe - app_action |= AppAction::None; + } + _ => { + // To Do: Some kind of refresh for scheduled votes maybe + app_action |= AppAction::None; + } } } } @@ -761,6 +760,7 @@ impl DPNSContestedNamesScreen { .column(Column::initial(200.0).resizable(true)) // Voter ID .column(Column::initial(300.0).resizable(true)) // Choice .column(Column::initial(300.0).resizable(true)) // Scheduled vote time + .column(Column::initial(200.0).resizable(true)) // Executed? .header(30.0, |mut header| { header.col(|ui| { if ui.button("Name").clicked() { @@ -782,18 +782,34 @@ impl DPNSContestedNamesScreen { self.toggle_sort(SortColumn::ContestedName); } }); + header.col(|ui| { + if ui.button("Executed").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); }) .body(|mut body| { for vote in sorted_votes { body.row(25.0, |mut row| { row.col(|ui| { - ui.label(vote.contested_name); + ui.add(egui::Label::new(vote.contested_name).truncate()); }); row.col(|ui| { - ui.label(vote.voter_id.to_string(Encoding::Base58)); + ui.add( + egui::Label::new( + vote.voter_id.to_string(Encoding::Hex), + ) + .truncate(), + ); }); row.col(|ui| { - ui.label(vote.choice.to_string()); + let display_text = match &vote.choice { + ResourceVoteChoice::TowardsIdentity(identifier) => { + identifier.to_string(Encoding::Base58) + } + other => other.to_string(), + }; + ui.add(egui::Label::new(display_text).truncate()); }); row.col(|ui| { // Assuming `scheduled_vote.unix_timestamp` is a u64 storing milliseconds since UNIX epoch: @@ -812,12 +828,20 @@ impl DPNSContestedNamesScreen { let display_text = format!("{} ({})", iso_date, relative_time); - ui.label(display_text); + ui.add(egui::Label::new(display_text).truncate()); } else { // Handle case where the timestamp is invalid ui.label("Invalid timestamp"); } }); + row.col(|ui| match vote.executed_successfully { + true => { + ui.colored_label(Color32::DARK_GREEN, "Yes"); + } + false => { + ui.label(""); + } + }); }); } }); @@ -976,10 +1000,6 @@ impl ScreenLike for DPNSContestedNamesScreen { let mut contested_names = self.contested_names.lock().unwrap(); let mut dpns_names = self.local_dpns_names.lock().unwrap(); - let _ = self - .app_context - .db - .clear_executed_past_scheduled_votes(&self.app_context); let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); match self.dpns_subscreen { DPNSSubscreen::Active => { @@ -1013,42 +1033,125 @@ impl ScreenLike for DPNSContestedNamesScreen { fn ui(&mut self, ctx: &Context) -> AppAction { self.check_error_expiration(); - let mut top_panel_refresh_button = match self.dpns_subscreen { - DPNSSubscreen::Active | DPNSSubscreen::Past => ( - "Refresh", - DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( - ContestedResourceTask::QueryDPNSContestedResources, - )), - ), - DPNSSubscreen::Owned => ( - "Refresh", - DesiredAppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, - )), - ), - DPNSSubscreen::ScheduledVotes => ( - "Clear All", - DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( - ContestedResourceTask::ClearAllScheduledVotes, - )), - ), - }; - if self.refreshing { - top_panel_refresh_button = ("Refreshing...", DesiredAppAction::None) - } let has_identity_that_can_register = !self.user_identities.is_empty(); - let right_buttons = if has_identity_that_can_register { - vec![ - ( - "Register Name", - DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), - ), - top_panel_refresh_button, - ] - } else { - vec![top_panel_refresh_button] + + // Determine the right-side buttons based on the current DPNSSubscreen + let right_buttons = match self.dpns_subscreen { + DPNSSubscreen::Active => { + // Active contests: show refresh or refreshing + let refresh_button = if self.refreshing { + ("Refreshing...", DesiredAppAction::None) + } else { + ( + "Refresh", + DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::QueryDPNSContestedResources, + )), + ) + }; + + let mut buttons = vec![refresh_button]; + if has_identity_that_can_register { + buttons.insert( + 0, + ( + "Register Name", + DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), + ), + ); + } + buttons + } + + DPNSSubscreen::Past => { + // Past contests: similar to Active + let refresh_button = if self.refreshing { + ("Refreshing...", DesiredAppAction::None) + } else { + ( + "Refresh", + DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::QueryDPNSContestedResources, + )), + ) + }; + + let mut buttons = vec![refresh_button]; + if has_identity_that_can_register { + buttons.insert( + 0, + ( + "Register Name", + DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), + ), + ); + } + buttons + } + + DPNSSubscreen::Owned => { + // Owned names: refresh or refreshing + let refresh_button = if self.refreshing { + ("Refreshing...", DesiredAppAction::None) + } else { + ( + "Refresh", + DesiredAppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames, + )), + ) + }; + + let mut buttons = vec![refresh_button]; + if has_identity_that_can_register { + buttons.insert( + 0, + ( + "Register Name", + DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), + ), + ); + } + buttons + } + + DPNSSubscreen::ScheduledVotes => { + // Scheduled votes: "Clear All" and "Clear Executed" instead of refresh + // If refreshing is happening, you might want to show "Refreshing..." (optional) + let mut buttons = vec![ + ( + "Clear All", + DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::ClearAllScheduledVotes, + )), + ), + ( + "Clear Executed", + DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::ClearExecutedScheduledVotes, + )), + ), + ]; + + if self.refreshing { + // Optionally replace the first button if you want to show a refreshing state + buttons[0] = ("Refreshing...", DesiredAppAction::None); + } + + if has_identity_that_can_register { + buttons.insert( + 0, + ( + "Register Name", + DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), + ), + ); + } + buttons + } }; + let mut action = add_top_panel( ctx, &self.app_context, diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index 6f1224428..e1813a12e 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -4,6 +4,9 @@ use crate::backend_task::{contested_names::ContestedResourceTask, BackendTask}; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; 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 dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; @@ -17,7 +20,7 @@ use super::components::top_panel::add_top_panel; enum VoteOption { None, VoteNow, - Scheduled(String), + Scheduled { days: u32, hours: u32, minutes: u32 }, } pub struct ScheduleVoteScreen { @@ -42,6 +45,8 @@ impl ScheduleVoteScreen { .iter() .map(|_| VoteOption::None) .collect(); + + // Default everything to 0 (i.e., "now") Self { app_context: app_context.clone(), contested_name, @@ -54,13 +59,28 @@ impl ScheduleVoteScreen { } fn display_identity_options(&mut self, ui: &mut Ui) { - ui.heading("Schedule Votes for Identities"); - ui.add_space(10.0); + // Convert the timestamp to a DateTime object using timestamp_millis_opt + if let LocalResult::Single(datetime) = Utc.timestamp_millis_opt(self.ending_time as i64) { + // Format the ISO date up to seconds + let iso_date = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); + + // Use chrono-humanize to get the relative time + let relative_time = HumanTime::from(datetime).to_string(); + + // Combine both the ISO date and relative time + let display_text = format!( + "Contest for name {} ends at {} ({})", + self.contested_name, iso_date, relative_time + ); - ui.label(format!( - "Contest for name {} ends at {}", - self.contested_name, self.ending_time - )); + ui.label(display_text); + } else { + // Handle case where the timestamp is invalid + ui.colored_label( + Color32::DARK_RED, + "Error getting contest ending time".to_string(), + ); + } ui.add_space(10.0); // For each identity, show a row with their alias/ID and voting options @@ -75,14 +95,13 @@ impl ScheduleVoteScreen { .unwrap_or(identity.identity.id().to_string(Encoding::Base58)); ui.label(format!("Identity: {}", identity_label)); - // Dropdown or Radio buttons for None/VoteNow/Scheduled - // For simplicity, let's use a ComboBox: + // Dropdown for None/VoteNow/Scheduled let current_option = &mut self.identity_options[i]; - egui::ComboBox::from_label(identity_label) + egui::ComboBox::from_id_salt(format!("combo_for_identity_{}", i)) .selected_text(match current_option { VoteOption::None => "None".to_string(), VoteOption::VoteNow => "Vote Now".to_string(), - VoteOption::Scheduled(_) => "Scheduled".to_string(), + VoteOption::Scheduled { .. } => "Scheduled".to_string(), }) .show_ui(ui, |ui| { if ui @@ -105,25 +124,41 @@ impl ScheduleVoteScreen { } if ui .selectable_label( - matches!(current_option, VoteOption::Scheduled(_)), + matches!(current_option, VoteOption::Scheduled { .. }), "Scheduled", ) .clicked() { - // If we had a previous schedule time, keep it. Otherwise, empty string. - let old_time = match current_option { - VoteOption::Scheduled(s) => s.clone(), - _ => String::new(), + // If we had a previous scheduled option, keep the old values. Otherwise, default to 0. + 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, }; - *current_option = VoteOption::Scheduled(old_time); } }); - // If Scheduled is chosen, display a text field for the schedule time - // To Do: This should be a date time selector rather than text input. - if let VoteOption::Scheduled(ref mut time_str) = current_option { - ui.label("Schedule Time (UNIX timestamp):"); - ui.text_edit_singleline(time_str); + // 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: ")); + }); } }); }); @@ -133,48 +168,53 @@ impl ScheduleVoteScreen { } fn cast_votes_button(&mut self) -> AppAction { - // Gather the voter identities and their chosen times - // For simplicity, let's assume the backend can handle a structure where: - // - If VoteNow, we submit immediately. - // - If Scheduled(time), we submit the schedule. - // - If None, we do not include that identity as a voter. - - // Filter only those with VoteNow or Scheduled let mut voters = Vec::new(); let mut scheduled_votes = Vec::new(); + // (Optional) Check if chosen_time is before ending_time, if ending_time is in the same units (ms). + // If ending_time is a UNIX ms timestamp, you can ensure: + // if chosen_time > ending_time { + // self.message = Some((MessageType::Error, "Scheduled time is after contest end time.".to_string())); + // return AppAction::None; + // } + for (identity, option) in self.identities.iter().zip(self.identity_options.iter()) { match option { - VoteOption::None => { - // Skip this identity - } + VoteOption::None => {} VoteOption::VoteNow => { - // Immediate vote voters.push(identity.clone()); } - VoteOption::Scheduled(time_str) => { - // Collect scheduled votes separately - // The backend task might need a structure that allows scheduling. - // If such a structure doesn’t exist yet, we might need to define one. + VoteOption::Scheduled { + days, + hours, + minutes, + } => { + let now = chrono::Utc::now(); + let offset = Duration::days((*days).into()) + + Duration::hours((*hours).into()) + + Duration::minutes((*minutes).into()); + + let scheduled_time = now + offset; + let chosen_time = scheduled_time.timestamp_millis() as u64; + let scheduled_vote = ScheduledDPNSVote { contested_name: self.contested_name.clone(), voter_id: identity.identity.id().clone(), choice: self.vote_choice, - unix_timestamp: time_str.parse().unwrap_or(0), + unix_timestamp: chosen_time, + executed_successfully: false, }; scheduled_votes.push(scheduled_vote); } } } - // If no voters and no scheduled, return None to indicate nothing to do. if voters.is_empty() && scheduled_votes.is_empty() { self.message = Some((MessageType::Error, "No votes selected.".to_string())); return AppAction::None; } let updated_action = ContestedResourceTask::ScheduleDPNSVote(scheduled_votes); - AppAction::BackendTask(BackendTask::ContestedResourceTask(updated_action)) } } @@ -199,14 +239,10 @@ impl ScreenLike for ScheduleVoteScreen { ui.heading("Schedule Votes"); ui.add_space(10.0); - // Display the identity options and scheduling fields self.display_identity_options(ui); - ui.add_space(10.0); - ui.separator(); ui.add_space(10.0); - // Button to cast votes (now or scheduled) let button = egui::Button::new(RichText::new("Cast Votes").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) .rounding(3.0); @@ -215,6 +251,8 @@ impl ScreenLike for ScheduleVoteScreen { action = self.cast_votes_button(); } + ui.add_space(10.0); + if let Some(message) = &self.message { match message.0 { MessageType::Error => { From 240cb9bf77c201146dfe7564b3c8761740d0a504 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 21:36:41 +0700 Subject: [PATCH 08/28] remove vote now option in vote scheduling screen --- src/ui/dpns_contested_names_screen.rs | 8 ++++---- src/ui/dpns_vote_scheduling_screen.rs | 17 +---------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index c0b6a8d74..dc865aca7 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -756,11 +756,11 @@ impl DPNSContestedNamesScreen { .striped(true) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::initial(200.0).resizable(true)) // DPNS Name + .column(Column::initial(100.0).resizable(true)) // DPNS Name .column(Column::initial(200.0).resizable(true)) // Voter ID - .column(Column::initial(300.0).resizable(true)) // Choice - .column(Column::initial(300.0).resizable(true)) // Scheduled vote time - .column(Column::initial(200.0).resizable(true)) // Executed? + .column(Column::initial(200.0).resizable(true)) // Choice + .column(Column::initial(200.0).resizable(true)) // Scheduled vote time + .column(Column::initial(100.0).resizable(true)) // Executed? .header(30.0, |mut header| { header.col(|ui| { if ui.button("Name").clicked() { diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index e1813a12e..20b648215 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -19,7 +19,6 @@ use super::components::top_panel::add_top_panel; /// The voting option a user can choose for each identity. enum VoteOption { None, - VoteNow, Scheduled { days: u32, hours: u32, minutes: u32 }, } @@ -100,7 +99,6 @@ impl ScheduleVoteScreen { egui::ComboBox::from_id_salt(format!("combo_for_identity_{}", i)) .selected_text(match current_option { VoteOption::None => "None".to_string(), - VoteOption::VoteNow => "Vote Now".to_string(), VoteOption::Scheduled { .. } => "Scheduled".to_string(), }) .show_ui(ui, |ui| { @@ -113,15 +111,6 @@ impl ScheduleVoteScreen { { *current_option = VoteOption::None; } - if ui - .selectable_label( - matches!(current_option, VoteOption::VoteNow), - "Vote Now", - ) - .clicked() - { - *current_option = VoteOption::VoteNow; - } if ui .selectable_label( matches!(current_option, VoteOption::Scheduled { .. }), @@ -168,7 +157,6 @@ impl ScheduleVoteScreen { } fn cast_votes_button(&mut self) -> AppAction { - let mut voters = Vec::new(); let mut scheduled_votes = Vec::new(); // (Optional) Check if chosen_time is before ending_time, if ending_time is in the same units (ms). @@ -181,9 +169,6 @@ impl ScheduleVoteScreen { for (identity, option) in self.identities.iter().zip(self.identity_options.iter()) { match option { VoteOption::None => {} - VoteOption::VoteNow => { - voters.push(identity.clone()); - } VoteOption::Scheduled { days, hours, @@ -209,7 +194,7 @@ impl ScheduleVoteScreen { } } - if voters.is_empty() && scheduled_votes.is_empty() { + if scheduled_votes.is_empty() { self.message = Some((MessageType::Error, "No votes selected.".to_string())); return AppAction::None; } From 3afe3561569de892b623bab59a2ca72a2a3f24f1 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Tue, 10 Dec 2024 22:28:12 +0700 Subject: [PATCH 09/28] more --- src/backend_task/contested_names/mod.rs | 14 +++-- src/database/scheduled_votes.rs | 30 ++++----- src/ui/dpns_contested_names_screen.rs | 81 ++++++++++++++++++++----- src/ui/dpns_vote_scheduling_screen.rs | 23 ++++--- 4 files changed, 106 insertions(+), 42 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index 7e53034fc..d6f7d437a 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -23,6 +23,7 @@ pub(crate) enum ContestedResourceTask { VoteOnDPNSName(String, ResourceVoteChoice, Vec), ClearAllScheduledVotes, ClearExecutedScheduledVotes, + DeleteScheduledVote(Vec, String), } impl AppContext { @@ -60,6 +61,10 @@ impl AppContext { _ => BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()), }) .map_err(|e| format!("Error casting scheduled vote: {}", e.to_string())), + ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { + self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) + .await + } ContestedResourceTask::ClearAllScheduledVotes => self .db .clear_all_scheduled_votes(self) @@ -70,10 +75,11 @@ impl AppContext { .clear_executed_past_scheduled_votes(self) .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes .map_err(|e| format!("Error clearing executed scheduled votes: {}", e.to_string())), - ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { - self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) - .await - } + ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => self + .db + .delete_scheduled_vote(voter_id, contested_name, self) + .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes + .map_err(|e| format!("Error clearing scheduled vote: {}", e.to_string())), } } } diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 245923284..041ba7bf7 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -47,6 +47,21 @@ impl Database { Ok(()) } + pub fn delete_scheduled_vote( + &self, + identity_id: &[u8], + contested_name: &str, + app_context: &AppContext, + ) -> rusqlite::Result<()> { + let network = app_context.network_string(); + let conn = self.conn.lock().unwrap(); + conn.execute( + "DELETE FROM scheduled_votes WHERE identity_id = ? AND contested_name = ? AND network = ?", + params![identity_id, contested_name, network], + )?; + Ok(()) + } + pub fn mark_vote_executed( &self, identity_id: &[u8], @@ -127,19 +142,6 @@ impl Database { Ok(()) } - /// Clear all past scheduled votes from the db - pub fn clear_all_past_scheduled_votes(&self, app_context: &AppContext) -> rusqlite::Result<()> { - let network = app_context.network_string(); - let conn = self.conn.lock().unwrap(); - - conn.execute( - "DELETE FROM scheduled_votes WHERE time < CAST(strftime('%s', 'now') AS INTEGER) * 1000 AND network = ?", - params![network], - )?; - - Ok(()) - } - pub fn clear_executed_past_scheduled_votes( &self, app_context: &AppContext, @@ -148,7 +150,7 @@ impl Database { let conn = self.conn.lock().unwrap(); conn.execute( - "DELETE FROM scheduled_votes WHERE executed = 1 AND time < CAST(strftime('%s', 'now') AS INTEGER) * 1000 AND network = ?", + "DELETE FROM scheduled_votes WHERE executed = 1 AND network = ?", params![network], )?; diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index dc865aca7..9441fefcb 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -15,6 +15,7 @@ use crate::ui::identities::add_existing_identity_screen::AddExistingIdentityScre use crate::ui::{MessageType, RootScreenType, ScreenLike}; use chrono::{DateTime, LocalResult, TimeZone, Utc}; use chrono_humanize::HumanTime; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dash_sdk::platform::Identifier; @@ -154,12 +155,13 @@ impl DPNSContestedNamesScreen { if ui.button(text).clicked() { self.show_vote_popup_info = Some(( format!( - "Confirm Voting for Contestant {} for name \"{}\".\n\nSelect the identity to vote with:", + "Confirm Voting for Contestant {} for name \"{}\".", contestant.id, contestant.name ), ContestedResourceTask::VoteOnDPNSName( contested_name.normalized_contested_name.clone(), - ResourceVoteChoice::TowardsIdentity(contestant.id),vec![] + ResourceVoteChoice::TowardsIdentity(contestant.id), + vec![], ), )); } @@ -402,7 +404,7 @@ impl DPNSContestedNamesScreen { }; // 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 \"{}\".\n\nSelect the identity to vote with:", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Lock, vec![]))); + 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| { @@ -414,7 +416,7 @@ impl DPNSContestedNamesScreen { "Fetching".to_string() }; if ui.button(label_text).clicked() { - self.show_vote_popup_info = Some((format!("Confirm Voting to Abstain on distribution of \"{}\".\n\nSelect the identity to vote with:", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Abstain, vec![]))); + 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| { @@ -715,7 +717,8 @@ impl DPNSContestedNamesScreen { }); } - fn render_table_scheduled_votes(&mut self, ui: &mut Ui) { + fn render_table_scheduled_votes(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; let mut sorted_votes = { let scheduled_votes_guard = self.scheduled_votes.lock().unwrap(); let scheduled_votes = scheduled_votes_guard.clone(); @@ -761,6 +764,7 @@ impl DPNSContestedNamesScreen { .column(Column::initial(200.0).resizable(true)) // Choice .column(Column::initial(200.0).resizable(true)) // Scheduled vote time .column(Column::initial(100.0).resizable(true)) // Executed? + .column(Column::initial(100.0).resizable(true)) // Actions .header(30.0, |mut header| { header.col(|ui| { if ui.button("Name").clicked() { @@ -787,12 +791,20 @@ impl DPNSContestedNamesScreen { self.toggle_sort(SortColumn::ContestedName); } }); + header.col(|ui| { + if ui.button("Actions").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); }) .body(|mut body| { for vote in sorted_votes { body.row(25.0, |mut row| { row.col(|ui| { - ui.add(egui::Label::new(vote.contested_name).truncate()); + ui.add( + egui::Label::new(vote.contested_name.clone()) + .truncate(), + ); }); row.col(|ui| { ui.add( @@ -812,25 +824,17 @@ impl DPNSContestedNamesScreen { ui.add(egui::Label::new(display_text).truncate()); }); row.col(|ui| { - // Assuming `scheduled_vote.unix_timestamp` is a u64 storing milliseconds since UNIX epoch: if let LocalResult::Single(datetime) = Utc.timestamp_millis_opt(vote.unix_timestamp as i64) { - // Format the ISO date up to seconds let iso_date = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); - - // Use chrono-humanize to get the relative time let relative_time = HumanTime::from(datetime).to_string(); - - // Combine both the ISO date and relative time let display_text = format!("{} ({})", iso_date, relative_time); - ui.add(egui::Label::new(display_text).truncate()); } else { - // Handle case where the timestamp is invalid ui.label("Invalid timestamp"); } }); @@ -842,11 +846,47 @@ impl DPNSContestedNamesScreen { ui.label(""); } }); + row.col(|ui| { + if ui.button("Remove").clicked() { + let identity_id_bytes = + vote.voter_id.as_bytes().to_vec(); + action = AppAction::BackendTask( + BackendTask::ContestedResourceTask( + ContestedResourceTask::DeleteScheduledVote( + identity_id_bytes, + vote.contested_name.clone(), + ), + ), + ); + } + if ui.button("Cast Now").clicked() { + let local_identities = + match self.app_context.db.get_local_voting_identities(&self.app_context) { + Ok(identities) => identities, + Err(e) => { + eprintln!("Error querying local voting identities: {}", e); + return; + } + }; + if let Some(voter) = local_identities + .iter() + .find(|i| i.identity.id() == vote.voter_id) + { + action = AppAction::BackendTask( + BackendTask::ContestedResourceTask( + ContestedResourceTask::ExecuteScheduledVote(vote, voter.clone()), + ), + ); + } + } + }); }); } }); }); }); + + action } fn show_vote_popup(&mut self, ui: &mut Ui) -> AppAction { @@ -866,6 +906,16 @@ impl DPNSContestedNamesScreen { } else if let Some((message, action)) = self.show_vote_popup_info.clone() { ui.label(message); + ui.add_space(10.0); + + if self.pending_vote_action.is_none() { + ui.label("Select the identity to vote with:"); + } else { + ui.label("Would you like to vote now or schedule your votes?"); + } + + ui.add_space(10.0); + ui.horizontal(|ui| { if let ContestedResourceTask::VoteOnDPNSName( contested_name, @@ -903,7 +953,6 @@ impl DPNSContestedNamesScreen { } } else { // If we have a pending vote action, ask whether to vote now or schedule - ui.label("Would you like to vote now or schedule your votes?"); if ui.button("Vote Now").clicked() { // Finalize the vote now app_action = @@ -1269,7 +1318,7 @@ impl ScreenLike for DPNSContestedNamesScreen { } DPNSSubscreen::ScheduledVotes => { if has_scheduled_votes { - self.render_table_scheduled_votes(ui); + action |= self.render_table_scheduled_votes(ui); } else { action |= self.render_no_active_contests_or_owned_names(ui); } diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index 20b648215..ea7aa5fa1 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -42,7 +42,11 @@ impl ScheduleVoteScreen { ) -> Self { let identity_options = potential_voting_identities .iter() - .map(|_| VoteOption::None) + .map(|_| VoteOption::Scheduled { + days: 0, + hours: 0, + minutes: 0, + }) .collect(); // Default everything to 0 (i.e., "now") @@ -110,6 +114,7 @@ impl ScheduleVoteScreen { .clicked() { *current_option = VoteOption::None; + self.message = None; } if ui .selectable_label( @@ -132,6 +137,7 @@ impl ScheduleVoteScreen { hours, minutes, }; + self.message = None; } }); @@ -159,13 +165,6 @@ impl ScheduleVoteScreen { fn cast_votes_button(&mut self) -> AppAction { let mut scheduled_votes = Vec::new(); - // (Optional) Check if chosen_time is before ending_time, if ending_time is in the same units (ms). - // If ending_time is a UNIX ms timestamp, you can ensure: - // if chosen_time > ending_time { - // self.message = Some((MessageType::Error, "Scheduled time is after contest end time.".to_string())); - // return AppAction::None; - // } - for (identity, option) in self.identities.iter().zip(self.identity_options.iter()) { match option { VoteOption::None => {} @@ -182,6 +181,14 @@ impl ScheduleVoteScreen { let scheduled_time = now + offset; let chosen_time = scheduled_time.timestamp_millis() as u64; + if chosen_time > self.ending_time { + self.message = Some(( + MessageType::Error, + "Scheduled time is after contest end time.".to_string(), + )); + return AppAction::None; + } + let scheduled_vote = ScheduledDPNSVote { contested_name: self.contested_name.clone(), voter_id: identity.identity.id().clone(), From 0177e927adaafae638860f58b2e4193b38813f72 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 11 Dec 2024 17:03:13 +0700 Subject: [PATCH 10/28] good --- src/app.rs | 15 +++++ src/ui/dpns_contested_names_screen.rs | 12 ++-- src/ui/dpns_vote_scheduling_screen.rs | 81 +++++++++++++++++---------- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/app.rs b/src/app.rs index f356c06b0..9c9acfae3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -64,6 +64,7 @@ pub struct AppState { #[derive(Debug, Clone, PartialEq)] pub enum DesiredAppAction { None, + Refresh, PopScreen, GoToMainScreen, SwitchNetwork(Network), @@ -75,6 +76,7 @@ impl DesiredAppAction { pub fn create_action(&self, app_context: &Arc) -> AppAction { match self { DesiredAppAction::None => AppAction::None, + DesiredAppAction::Refresh => AppAction::Refresh, DesiredAppAction::PopScreen => AppAction::PopScreen, DesiredAppAction::GoToMainScreen => AppAction::GoToMainScreen, DesiredAppAction::AddScreenType(screen_type) => { @@ -91,11 +93,13 @@ impl DesiredAppAction { #[derive(Debug, PartialEq)] pub enum AppAction { None, + Refresh, PopScreen, PopScreenAndRefresh, GoToMainScreen, SwitchNetwork(Network), SetMainScreen(RootScreenType), + SetMainScreenThenPop(RootScreenType), AddScreen(Screen), PopThenAddScreenToMainScreen(RootScreenType, Screen), BackendTask(BackendTask), @@ -562,6 +566,7 @@ impl App for AppState { match action { AppAction::AddScreen(screen) => self.screen_stack.push(screen), AppAction::None => {} + AppAction::Refresh => self.visible_screen_mut().refresh(), AppAction::PopScreen => { if !self.screen_stack.is_empty() { self.screen_stack.pop(); @@ -591,6 +596,16 @@ impl App for AppState { .update_settings(root_screen_type) .ok(); } + AppAction::SetMainScreenThenPop(root_screen_type) => { + self.selected_main_screen = root_screen_type; + self.active_root_screen_mut().refresh(); + self.current_app_context() + .update_settings(root_screen_type) + .ok(); + if !self.screen_stack.is_empty() { + self.screen_stack.pop(); + } + } AppAction::SwitchNetwork(network) => { self.change_network(network); self.current_app_context() diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index ef45ab676..d45e2f107 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -283,6 +283,8 @@ impl DPNSContestedNamesScreen { } } } + } else { + ui.label("Go to the Active Contests subscreen to schedule votes."); } }); @@ -1013,6 +1015,7 @@ impl ScreenLike for DPNSContestedNamesScreen { fn refresh(&mut self) { let mut contested_names = self.contested_names.lock().unwrap(); let mut dpns_names = self.local_dpns_names.lock().unwrap(); + let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); match self.dpns_subscreen { DPNSSubscreen::Active => { *contested_names = self @@ -1027,7 +1030,7 @@ impl ScreenLike for DPNSContestedNamesScreen { *dpns_names = self.app_context.local_dpns_names().unwrap_or_default(); } DPNSSubscreen::ScheduledVotes => { - // To Do: Implement scheduled votes + *scheduled_votes = self.app_context.get_scheduled_votes().unwrap_or_default(); } } } @@ -1167,8 +1170,8 @@ impl ScreenLike for DPNSContestedNamesScreen { DPNSSubscreen::ScheduledVotes => { // Scheduled votes: "Clear All" and "Clear Executed" instead of refresh - // If refreshing is happening, you might want to show "Refreshing..." (optional) let mut buttons = vec![ + ("Refresh", DesiredAppAction::Refresh), ( "Clear All", DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( @@ -1183,11 +1186,6 @@ impl ScreenLike for DPNSContestedNamesScreen { ), ]; - if self.refreshing { - // Optionally replace the first button if you want to show a refreshing state - buttons[0] = ("Refreshing...", DesiredAppAction::None); - } - if has_identity_that_can_register { buttons.insert( 0, diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index ea7aa5fa1..84c113be8 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -15,6 +15,7 @@ use eframe::egui::{self, Color32, RichText, Ui}; use std::sync::Arc; use super::components::top_panel::add_top_panel; +use super::{RootScreenType, ScreenType}; /// The voting option a user can choose for each identity. enum VoteOption { @@ -62,30 +63,6 @@ impl ScheduleVoteScreen { } fn display_identity_options(&mut self, ui: &mut Ui) { - // Convert the timestamp to a DateTime object using timestamp_millis_opt - if let LocalResult::Single(datetime) = Utc.timestamp_millis_opt(self.ending_time as i64) { - // Format the ISO date up to seconds - let iso_date = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); - - // Use chrono-humanize to get the relative time - let relative_time = HumanTime::from(datetime).to_string(); - - // Combine both the ISO date and relative time - let display_text = format!( - "Contest for name {} ends at {} ({})", - self.contested_name, iso_date, relative_time - ); - - ui.label(display_text); - } else { - // Handle case where the timestamp is invalid - ui.colored_label( - Color32::DARK_RED, - "Error getting contest ending time".to_string(), - ); - } - ui.add_space(10.0); - // For each identity, show a row with their alias/ID and voting options for (i, identity) in self.identities.iter().enumerate() { ui.group(|ui| { @@ -209,6 +186,35 @@ impl ScheduleVoteScreen { let updated_action = ContestedResourceTask::ScheduleDPNSVote(scheduled_votes); AppAction::BackendTask(BackendTask::ContestedResourceTask(updated_action)) } + + 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 scheduled votes."); + + ui.add_space(20.0); + + if ui.button("Go to Scheduled Votes Screen").clicked() { + // Handle navigation back to the identities screen + action = + AppAction::SetMainScreenThenPop(RootScreenType::RootScreenDPNSScheduledVotes); + } + + ui.add_space(10.0); + + if ui.button("Go back to Active Contests").clicked() { + // Handle navigation back to the identities screen + action = AppAction::PopScreenAndRefresh; + } + }); + + action + } } impl ScreenLike for ScheduleVoteScreen { @@ -231,18 +237,35 @@ impl ScreenLike for ScheduleVoteScreen { ui.heading("Schedule Votes"); ui.add_space(10.0); - self.display_identity_options(ui); + 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); + // Convert the timestamp to a DateTime object using timestamp_millis_opt + if let LocalResult::Single(datetime) = Utc.timestamp_millis_opt(self.ending_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!( + "Contest for name {} ends at {} ({})", + self.contested_name, iso_date, relative_time + ); + ui.label(display_text); + } else { + ui.colored_label( + Color32::DARK_RED, + "Error getting contest ending time".to_string(), + ); + } + ui.add_space(10.0); + + self.display_identity_options(ui); ui.add_space(10.0); - let button = egui::Button::new(RichText::new("Cast Votes").color(Color32::WHITE)) + let button = egui::Button::new(RichText::new("Schedule Votes").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) .rounding(3.0); - if ui.add(button).clicked() { action = self.cast_votes_button(); } - ui.add_space(10.0); if let Some(message) = &self.message { @@ -251,7 +274,7 @@ impl ScreenLike for ScheduleVoteScreen { ui.colored_label(Color32::DARK_RED, message.1.clone()); } MessageType::Success => { - ui.colored_label(Color32::DARK_GREEN, message.1.clone()); + action = self.show_success(ui); } MessageType::Info => { ui.colored_label(Color32::DARK_BLUE, message.1.clone()); From d4051f3bde6439e9ddd53bd4ac75fc645404ffff Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 11 Dec 2024 17:56:56 +0700 Subject: [PATCH 11/28] reviewing --- src/app.rs | 20 ++++---- src/backend_task/contested_names/mod.rs | 50 +++++++++---------- .../contested_names/schedule_dpns_vote.rs | 37 -------------- src/backend_task/mod.rs | 3 +- src/context.rs | 2 +- src/database/scheduled_votes.rs | 22 ++++---- src/ui/dpns_contested_names_screen.rs | 4 +- src/ui/dpns_vote_scheduling_screen.rs | 11 ++-- 8 files changed, 57 insertions(+), 92 deletions(-) delete mode 100644 src/backend_task/contested_names/schedule_dpns_vote.rs diff --git a/src/app.rs b/src/app.rs index 9c9acfae3..9312392a8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,7 +58,7 @@ pub struct AppState { pub task_result_sender: tokiompsc::Sender, // Channel sender for sending task results pub task_result_receiver: tokiompsc::Receiver, // Channel receiver for receiving task results last_repaint: Instant, // Track the last time we requested a repaint - last_scheduled_vote_check: Instant, + last_scheduled_vote_check: Instant, // Last time we checked if there are scheduled masternode votes to cast } #[derive(Debug, Clone, PartialEq)] @@ -99,7 +99,7 @@ pub enum AppAction { GoToMainScreen, SwitchNetwork(Network), SetMainScreen(RootScreenType), - SetMainScreenThenPop(RootScreenType), + SetMainScreenThenPopScreen(RootScreenType), AddScreen(Screen), PopThenAddScreenToMainScreen(RootScreenType, Screen), BackendTask(BackendTask), @@ -423,6 +423,9 @@ impl App for AppState { BackendTaskSuccessResult::None => { self.visible_screen_mut().pop_on_success(); } + BackendTaskSuccessResult::Refresh => { + self.visible_screen_mut().refresh(); + } BackendTaskSuccessResult::Message(message) => { self.visible_screen_mut() .display_message(&message, MessageType::Success); @@ -505,13 +508,13 @@ impl App for AppState { } } - // Check if a minute has passed + // Check if there are scheduled masternode votes to cast and if so, cast them let now = Instant::now(); if now.duration_since(self.last_scheduled_vote_check) > Duration::from_secs(60) { self.last_scheduled_vote_check = now; - let app_context = self.current_app_context().clone(); + let app_context = self.current_app_context(); - // Query the database synchronously here + // Query the database let db_votes = match app_context.db.get_scheduled_votes(&app_context) { Ok(votes) => votes, Err(e) => { @@ -547,9 +550,8 @@ impl App for AppState { .find(|i| i.identity.id() == vote.voter_id) { let task = BackendTask::ContestedResourceTask( - ContestedResourceTask::ExecuteScheduledVote(vote, voter.clone()), + ContestedResourceTask::CastScheduledVote(vote, voter.clone()), ); - // Run the task directly: self.handle_backend_task(task); } else { eprintln!("Voter not found for scheduled vote: {:?}", vote); @@ -596,9 +598,9 @@ impl App for AppState { .update_settings(root_screen_type) .ok(); } - AppAction::SetMainScreenThenPop(root_screen_type) => { + AppAction::SetMainScreenThenPopScreen(root_screen_type) => { self.selected_main_screen = root_screen_type; - self.active_root_screen_mut().refresh(); + self.active_root_screen_mut().refresh_on_arrival(); self.current_app_context() .update_settings(root_screen_type) .ok(); diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index d6f7d437a..e6621ed1e 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -1,7 +1,6 @@ mod query_dpns_contested_resources; mod query_dpns_vote_contenders; mod query_ending_times; -pub mod schedule_dpns_vote; mod vote_on_dpns_name; use crate::app::TaskResult; @@ -9,23 +8,31 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dash_sdk::platform::Identifier; use dash_sdk::Sdk; -use schedule_dpns_vote::ScheduledDPNSVote; use std::sync::Arc; use tokio::sync::mpsc; #[derive(Debug, Clone, PartialEq)] pub(crate) enum ContestedResourceTask { QueryDPNSContestedResources, - QueryDPNSVoteContenders(String), - ScheduleDPNSVote(Vec), - ExecuteScheduledVote(ScheduledDPNSVote, QualifiedIdentity), VoteOnDPNSName(String, ResourceVoteChoice, Vec), + ScheduleDPNSVotes(Vec), + CastScheduledVote(ScheduledDPNSVote, QualifiedIdentity), ClearAllScheduledVotes, ClearExecutedScheduledVotes, DeleteScheduledVote(Vec, String), } +#[derive(Debug, Clone, PartialEq)] +pub struct ScheduledDPNSVote { + pub contested_name: String, + pub voter_id: Identifier, + pub choice: ResourceVoteChoice, + pub unix_timestamp: u64, + pub executed_successfully: bool, +} + impl AppContext { pub async fn run_contested_resource_task( self: &Arc, @@ -38,14 +45,16 @@ impl AppContext { .query_dpns_contested_resources(sdk, sender) .await .map(|_| BackendTaskSuccessResult::None), - ContestedResourceTask::QueryDPNSVoteContenders(name) => self - .query_dpns_vote_contenders(name, sdk, sender) - .await - .map(|_| BackendTaskSuccessResult::None), - ContestedResourceTask::ScheduleDPNSVote(scheduled_votes) => { - self.schedule_dpns_vote(scheduled_votes).await + ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { + self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) + .await } - ContestedResourceTask::ExecuteScheduledVote(scheduled_vote, voter) => self + ContestedResourceTask::ScheduleDPNSVotes(scheduled_votes) => self + .db + .insert_scheduled_votes(self, scheduled_votes) + .map(|_| BackendTaskSuccessResult::Refresh) + .map_err(|e| format!("Error inserting scheduled votes: {}", e.to_string())), + ContestedResourceTask::CastScheduledVote(scheduled_vote, voter) => self .vote_on_dpns_name( &scheduled_vote.contested_name, scheduled_vote.choice, @@ -54,31 +63,22 @@ impl AppContext { sender, ) .await - .map(|result| match result { - BackendTaskSuccessResult::SuccessfulVotes(_) => { - BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()) - } - _ => BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone()), - }) + .map(|_| BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone())) .map_err(|e| format!("Error casting scheduled vote: {}", e.to_string())), - ContestedResourceTask::VoteOnDPNSName(name, vote_choice, voters) => { - self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) - .await - } ContestedResourceTask::ClearAllScheduledVotes => self .db .clear_all_scheduled_votes(self) - .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes + .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing all scheduled votes: {}", e.to_string())), ContestedResourceTask::ClearExecutedScheduledVotes => self .db .clear_executed_past_scheduled_votes(self) - .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes + .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing executed scheduled votes: {}", e.to_string())), ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => self .db .delete_scheduled_vote(voter_id, contested_name, self) - .map(|_| BackendTaskSuccessResult::SuccessfulVotes(vec![])) // this one refreshes + .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing scheduled vote: {}", e.to_string())), } } diff --git a/src/backend_task/contested_names/schedule_dpns_vote.rs b/src/backend_task/contested_names/schedule_dpns_vote.rs deleted file mode 100644 index 2b1a7afba..000000000 --- a/src/backend_task/contested_names/schedule_dpns_vote.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::backend_task::BackendTaskSuccessResult; -use crate::context::AppContext; -use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use dash_sdk::platform::Identifier; -use std::sync::Arc; - -#[derive(Debug, Clone, PartialEq)] -pub struct ScheduledDPNSVote { - pub contested_name: String, - pub voter_id: Identifier, - pub choice: ResourceVoteChoice, - pub unix_timestamp: u64, - pub executed_successfully: bool, -} - -impl AppContext { - /// Inserts votes into the local db to be cast later - pub(super) async fn schedule_dpns_vote( - self: &Arc, - scheduled_votes: &Vec, - ) -> Result { - for vote in scheduled_votes { - self.db - .insert_scheduled_vote( - vote.voter_id.as_slice(), - vote.contested_name.clone(), - vote.choice, - vote.unix_timestamp, - self, - ) - .map_err(|e| format!("Failed to insert scheduled vote: {}", e))?; - } - Ok(BackendTaskSuccessResult::Message( - "Successfully scheduled votes".to_string(), - )) - } -} diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index efa365975..5183c62f7 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -7,7 +7,7 @@ use crate::backend_task::identity::IdentityTask; use crate::backend_task::withdrawal_statuses::{WithdrawStatusPartialData, WithdrawalsTask}; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; -use contested_names::schedule_dpns_vote::ScheduledDPNSVote; +use contested_names::ScheduledDPNSVote; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::query_types::Documents; use std::sync::Arc; @@ -33,6 +33,7 @@ pub(crate) enum BackendTask { #[derive(Debug, Clone, PartialEq)] pub(crate) enum BackendTaskSuccessResult { None, + Refresh, Message(String), Documents(Documents), CoreItem(CoreItem), diff --git a/src/context.rs b/src/context.rs index 2b2a02be7..c0913a0f1 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,4 @@ -use crate::backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote; +use crate::backend_task::contested_names::ScheduledDPNSVote; use crate::components::core_zmq_listener::ZMQConnectionEvent; use crate::config::{Config, NetworkConfig}; use crate::context_provider::Provider; diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 041ba7bf7..891df647b 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -1,6 +1,5 @@ use crate::{ - app, backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote, context::AppContext, - database::Database, + backend_task::contested_names::ScheduledDPNSVote, context::AppContext, database::Database, }; use dash_sdk::{ dpp::{ @@ -30,20 +29,19 @@ impl Database { Ok(()) } - pub fn insert_scheduled_vote( + pub fn insert_scheduled_votes( &self, - identity_id: &[u8], - contested_name: String, - vote_choice: ResourceVoteChoice, - time: u64, app_context: &AppContext, + votes: &Vec, ) -> rusqlite::Result<()> { let network = app_context.network_string(); - let vote_choice_string = vote_choice.to_string(); - self.execute( - "INSERT OR REPLACE INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network) VALUES (?, ?, ?, ?, 0, ?)", - params![identity_id, contested_name, vote_choice_string, time, network], - )?; + for vote in votes { + let vote_choice = vote.choice.to_string(); + self.execute( + "INSERT OR REPLACE INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network) VALUES (?, ?, ?, ?, 0, ?)", + params![vote.voter_id.as_slice(), vote.contested_name, vote_choice, vote.unix_timestamp, network], + )?; + } Ok(()) } diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index d45e2f107..e4bec7c1c 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -2,8 +2,8 @@ use super::components::dpns_subscreen_chooser_panel::add_dpns_subscreen_chooser_ use super::dpns_vote_scheduling_screen::ScheduleVoteScreen; use super::{Screen, ScreenType}; use crate::app::{AppAction, DesiredAppAction}; -use crate::backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote; use crate::backend_task::contested_names::ContestedResourceTask; +use crate::backend_task::contested_names::ScheduledDPNSVote; use crate::backend_task::identity::IdentityTask; use crate::backend_task::BackendTask; use crate::context::AppContext; @@ -876,7 +876,7 @@ impl DPNSContestedNamesScreen { { action = AppAction::BackendTask( BackendTask::ContestedResourceTask( - ContestedResourceTask::ExecuteScheduledVote(vote, voter.clone()), + ContestedResourceTask::CastScheduledVote(vote, voter.clone()), ), ); } diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index 84c113be8..7689a75b0 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -1,5 +1,5 @@ use crate::app::AppAction; -use crate::backend_task::contested_names::schedule_dpns_vote::ScheduledDPNSVote; +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; @@ -15,7 +15,7 @@ use eframe::egui::{self, Color32, RichText, Ui}; use std::sync::Arc; use super::components::top_panel::add_top_panel; -use super::{RootScreenType, ScreenType}; +use super::RootScreenType; /// The voting option a user can choose for each identity. enum VoteOption { @@ -183,7 +183,7 @@ impl ScheduleVoteScreen { return AppAction::None; } - let updated_action = ContestedResourceTask::ScheduleDPNSVote(scheduled_votes); + let updated_action = ContestedResourceTask::ScheduleDPNSVotes(scheduled_votes); AppAction::BackendTask(BackendTask::ContestedResourceTask(updated_action)) } @@ -201,8 +201,9 @@ impl ScheduleVoteScreen { if ui.button("Go to Scheduled Votes Screen").clicked() { // Handle navigation back to the identities screen - action = - AppAction::SetMainScreenThenPop(RootScreenType::RootScreenDPNSScheduledVotes); + action = AppAction::SetMainScreenThenPopScreen( + RootScreenType::RootScreenDPNSScheduledVotes, + ); } ui.add_space(10.0); From 200d70695f8a864022bc90220dbc1d01092266be Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 11 Dec 2024 18:12:50 +0700 Subject: [PATCH 12/28] fix --- src/backend_task/contested_names/mod.rs | 6 +++--- src/ui/dpns_contested_names_screen.rs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index e6621ed1e..77d1900ee 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -21,7 +21,7 @@ pub(crate) enum ContestedResourceTask { CastScheduledVote(ScheduledDPNSVote, QualifiedIdentity), ClearAllScheduledVotes, ClearExecutedScheduledVotes, - DeleteScheduledVote(Vec, String), + DeleteScheduledVote(Identifier, String), } #[derive(Debug, Clone, PartialEq)] @@ -52,7 +52,7 @@ impl AppContext { ContestedResourceTask::ScheduleDPNSVotes(scheduled_votes) => self .db .insert_scheduled_votes(self, scheduled_votes) - .map(|_| BackendTaskSuccessResult::Refresh) + .map(|_| BackendTaskSuccessResult::Message("Votes scheduled".to_string())) .map_err(|e| format!("Error inserting scheduled votes: {}", e.to_string())), ContestedResourceTask::CastScheduledVote(scheduled_vote, voter) => self .vote_on_dpns_name( @@ -77,7 +77,7 @@ impl AppContext { .map_err(|e| format!("Error clearing executed scheduled votes: {}", e.to_string())), ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => self .db - .delete_scheduled_vote(voter_id, contested_name, self) + .delete_scheduled_vote(voter_id.as_slice(), contested_name, self) .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing scheduled vote: {}", e.to_string())), } diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index e4bec7c1c..92e2f5f7c 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -850,12 +850,10 @@ impl DPNSContestedNamesScreen { }); row.col(|ui| { if ui.button("Remove").clicked() { - let identity_id_bytes = - vote.voter_id.as_bytes().to_vec(); action = AppAction::BackendTask( BackendTask::ContestedResourceTask( ContestedResourceTask::DeleteScheduledVote( - identity_id_bytes, + vote.voter_id, vote.contested_name.clone(), ), ), From 6824e7fd6d0b23a23e87db1ddea4958444a21070 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 11 Dec 2024 18:28:56 +0700 Subject: [PATCH 13/28] fix --- src/backend_task/contested_names/mod.rs | 2 +- src/database/scheduled_votes.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index 77d1900ee..b7e421696 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -72,7 +72,7 @@ impl AppContext { .map_err(|e| format!("Error clearing all scheduled votes: {}", e.to_string())), ContestedResourceTask::ClearExecutedScheduledVotes => self .db - .clear_executed_past_scheduled_votes(self) + .clear_executed_scheduled_votes(self) .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing executed scheduled votes: {}", e.to_string())), ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => self diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 891df647b..1113abd2e 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -92,6 +92,7 @@ impl Database { 1 => true, _ => unreachable!(), }; + let vote_choice = match vote_choice_string.as_str() { "Abstain" => ResourceVoteChoice::Abstain, "Lock" => ResourceVoteChoice::Lock, @@ -111,6 +112,7 @@ impl Database { } } }; + let scheduled_vote = ScheduledDPNSVote { voter_id: Identifier::from_bytes(&voter_id_bytes) .expect("Expected valid identifier"), @@ -140,10 +142,7 @@ impl Database { Ok(()) } - pub fn clear_executed_past_scheduled_votes( - &self, - app_context: &AppContext, - ) -> rusqlite::Result<()> { + pub fn clear_executed_scheduled_votes(&self, app_context: &AppContext) -> rusqlite::Result<()> { let network = app_context.network_string(); let conn = self.conn.lock().unwrap(); From 377d7ecf710cbcb58e728c3b3001b0a83be76424 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 09:21:00 +0700 Subject: [PATCH 14/28] ok --- src/ui/dpns_contested_names_screen.rs | 70 +++++++-------------------- src/ui/dpns_vote_scheduling_screen.rs | 1 - 2 files changed, 18 insertions(+), 53 deletions(-) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 92e2f5f7c..56cd67650 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -277,8 +277,7 @@ impl DPNSContestedNamesScreen { )); } _ => { - // To Do: Some kind of refresh for scheduled votes maybe - app_action |= AppAction::None; + app_action |= AppAction::Refresh; } } } @@ -963,7 +962,6 @@ impl DPNSContestedNamesScreen { } if ui.button("Schedule").clicked() { // Move to a scheduling screen instead - // Assume we have a ScheduleVoteScreen that takes the pending action data let pending = self.pending_vote_action.take().unwrap(); if let ContestedResourceTask::VoteOnDPNSName( name_string, @@ -1087,9 +1085,8 @@ impl ScreenLike for DPNSContestedNamesScreen { let has_identity_that_can_register = !self.user_identities.is_empty(); // Determine the right-side buttons based on the current DPNSSubscreen - let right_buttons = match self.dpns_subscreen { + let mut right_buttons = match self.dpns_subscreen { DPNSSubscreen::Active => { - // Active contests: show refresh or refreshing let refresh_button = if self.refreshing { ("Refreshing...", DesiredAppAction::None) } else { @@ -1101,17 +1098,7 @@ impl ScreenLike for DPNSContestedNamesScreen { ) }; - let mut buttons = vec![refresh_button]; - if has_identity_that_can_register { - buttons.insert( - 0, - ( - "Register Name", - DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), - ), - ); - } - buttons + vec![refresh_button] } DPNSSubscreen::Past => { @@ -1127,17 +1114,7 @@ impl ScreenLike for DPNSContestedNamesScreen { ) }; - let mut buttons = vec![refresh_button]; - if has_identity_that_can_register { - buttons.insert( - 0, - ( - "Register Name", - DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), - ), - ); - } - buttons + vec![refresh_button] } DPNSSubscreen::Owned => { @@ -1153,22 +1130,12 @@ impl ScreenLike for DPNSContestedNamesScreen { ) }; - let mut buttons = vec![refresh_button]; - if has_identity_that_can_register { - buttons.insert( - 0, - ( - "Register Name", - DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), - ), - ); - } - buttons + vec![refresh_button] } DPNSSubscreen::ScheduledVotes => { - // Scheduled votes: "Clear All" and "Clear Executed" instead of refresh - let mut buttons = vec![ + // Scheduled votes: "Refresh", "Clear All", and "Clear Executed" + vec![ ("Refresh", DesiredAppAction::Refresh), ( "Clear All", @@ -1182,21 +1149,20 @@ impl ScreenLike for DPNSContestedNamesScreen { ContestedResourceTask::ClearExecutedScheduledVotes, )), ), - ]; - - if has_identity_that_can_register { - buttons.insert( - 0, - ( - "Register Name", - DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), - ), - ); - } - buttons + ] } }; + if has_identity_that_can_register { + right_buttons.insert( + 0, + ( + "Register Name", + DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), + ), + ); + } + let mut action = add_top_panel( ctx, &self.app_context, diff --git a/src/ui/dpns_vote_scheduling_screen.rs b/src/ui/dpns_vote_scheduling_screen.rs index 7689a75b0..bf3f80a0f 100644 --- a/src/ui/dpns_vote_scheduling_screen.rs +++ b/src/ui/dpns_vote_scheduling_screen.rs @@ -17,7 +17,6 @@ use std::sync::Arc; use super::components::top_panel::add_top_panel; use super::RootScreenType; -/// The voting option a user can choose for each identity. enum VoteOption { None, Scheduled { days: u32, hours: u32, minutes: u32 }, From 425cc063bdabb1d52df51528c5ba79309fe8fe18 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 13:52:26 +0700 Subject: [PATCH 15/28] ok --- src/app.rs | 10 +- .../contested_names/vote_on_dpns_name.rs | 16 +- src/context.rs | 8 + src/ui/dpns_contested_names_screen.rs | 197 ++++++++++++++---- src/ui/identities/identities_screen.rs | 2 +- 5 files changed, 185 insertions(+), 48 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9312392a8..be4671a3d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -445,6 +445,10 @@ impl App for AppState { vote.contested_name, self.current_app_context(), ); + self.visible_screen_mut().display_message( + "Successfully cast scheduled vote", + MessageType::Success, + ); self.visible_screen_mut().refresh(); } BackendTaskSuccessResult::WithdrawalStatus(_) => { @@ -530,7 +534,11 @@ impl App for AppState { .as_millis() as u64; let due_votes: Vec<_> = db_votes .into_iter() - .filter(|v| v.unix_timestamp <= current_time) + .filter(|v| { + v.unix_timestamp <= current_time + && !v.executed_successfully + && !(v.unix_timestamp + 120000 < current_time) + }) .collect(); // For each due vote, construct a BackendTask and handle it diff --git a/src/backend_task/contested_names/vote_on_dpns_name.rs b/src/backend_task/contested_names/vote_on_dpns_name.rs index 1685eccd9..82abdc7dd 100644 --- a/src/backend_task/contested_names/vote_on_dpns_name.rs +++ b/src/backend_task/contested_names/vote_on_dpns_name.rs @@ -23,8 +23,15 @@ impl AppContext { vote_choice: ResourceVoteChoice, voters: &Vec, sdk: &Sdk, - _sender: mpsc::Sender, + sender: mpsc::Sender, ) -> Result { + // Send a refresh task to the frontend + // In particular, use this to show the cast is in progress on Scheduled Votes Screen + sender + .send(TaskResult::Refresh) + .await + .map_err(|e| format!("Error voting: {}", e.to_string()))?; + // Fetch DPNS contract and document type information let data_contract = self.dpns_contract.as_ref(); let document_type = data_contract @@ -32,7 +39,7 @@ impl AppContext { .expect("expected document type"); let Some(contested_index) = document_type.find_contested_index() else { - return Err("No contested index on dpns domains".to_string()); + return Err("Error voting: No contested index on dpns domains".to_string()); }; // Hardcoded values for DPNS @@ -75,12 +82,13 @@ impl AppContext { vote_results.push(result); } else { return Err(format!( - "No associated voter identity for qualified identity: {:?}", + "Error voting: No associated voter identity for qualified identity: {:?}", qualified_identity.identity.id() )); } } + // To do: if the voter already voted previously, the previous vote count should be removed self.db .update_vote_count( name, @@ -88,7 +96,7 @@ impl AppContext { strength, vote_choice, ) - .map_err(|e| format!("error updating ending time: {}", e))?; + .map_err(|e| format!("Error voting: Error updating vote count: {}", e))?; Ok(BackendTaskSuccessResult::SuccessfulVotes(vote_results)) } diff --git a/src/context.rs b/src/context.rs index c0913a0f1..4d490191b 100644 --- a/src/context.rs +++ b/src/context.rs @@ -163,6 +163,10 @@ impl AppContext { .update_local_qualified_identity(qualified_identity, self) } + pub fn set_alias(&self, identifier: &Identifier, new_alias: Option<&str>) -> Result<()> { + self.db.set_alias(identifier, new_alias) + } + /// This is for before we know if Platform will accept the identity pub fn insert_local_qualified_identity_in_creation( &self, @@ -183,6 +187,10 @@ impl AppContext { self.db.get_local_qualified_identities(self, &wallets) } + pub fn load_local_voting_identities(&self) -> Result> { + self.db.get_local_voting_identities(self) + } + pub fn all_contested_names(&self) -> Result> { self.db.get_all_contested_names(self) } diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 56cd67650..dd8c45b6c 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -49,6 +49,14 @@ pub enum DPNSSubscreen { ScheduledVotes, } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum IndividualVoteCastingStatus { + NotStarted, + InProgress, + Failed, + Completed, +} + impl DPNSSubscreen { pub fn display_name(&self) -> &'static str { match self { @@ -66,13 +74,14 @@ pub struct DPNSContestedNamesScreen { user_identities: Vec, contested_names: Arc>>, local_dpns_names: Arc>>, - scheduled_votes: Arc>>, + scheduled_votes: Arc>>, pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, sort_column: SortColumn, sort_order: SortOrder, show_vote_popup_info: Option<(String, ContestedResourceTask)>, pending_vote_action: Option, + screen_casting_vote_in_progress: bool, pub dpns_subscreen: DPNSSubscreen, refreshing: bool, } @@ -97,8 +106,15 @@ impl DPNSContestedNamesScreen { DPNSSubscreen::Owned => app_context.local_dpns_names().unwrap_or_default(), DPNSSubscreen::ScheduledVotes => Vec::new(), })); - let scheduled_votes = Arc::new(Mutex::new( - app_context.get_scheduled_votes().unwrap_or_default(), + let scheduled_votes = app_context.get_scheduled_votes().unwrap_or_default(); + let scheduled_votes_with_status = Arc::new(Mutex::new( + scheduled_votes + .iter() + .map(|vote| match vote.executed_successfully { + true => (vote.clone(), IndividualVoteCastingStatus::Completed), + false => (vote.clone(), IndividualVoteCastingStatus::NotStarted), + }) + .collect::>(), )); let voting_identities = app_context .db @@ -113,13 +129,14 @@ impl DPNSContestedNamesScreen { user_identities, contested_names, local_dpns_names, - scheduled_votes, + scheduled_votes: scheduled_votes_with_status, app_context: app_context.clone(), error_message: None, sort_column: SortColumn::ContestedName, sort_order: SortOrder::Ascending, show_vote_popup_info: None, pending_vote_action: None, + screen_casting_vote_in_progress: false, dpns_subscreen, refreshing: false, } @@ -728,7 +745,7 @@ impl DPNSContestedNamesScreen { sorted_votes.sort_by(|a, b| match self.sort_column { SortColumn::ContestedName => { - let order = a.contested_name.cmp(&b.contested_name); // Sort by DPNS Name + let order = a.0.contested_name.cmp(&b.0.contested_name); // Sort by DPNS Name if self.sort_order == SortOrder::Descending { order.reverse() } else { @@ -736,7 +753,7 @@ impl DPNSContestedNamesScreen { } } SortColumn::EndingTime => { - let order = a.unix_timestamp.cmp(&b.unix_timestamp); // Sort by Vote Time + let order = a.0.unix_timestamp.cmp(&b.0.unix_timestamp); // Sort by Vote Time if self.sort_order == SortOrder::Descending { order.reverse() } else { @@ -768,7 +785,7 @@ impl DPNSContestedNamesScreen { .column(Column::initial(100.0).resizable(true)) // Actions .header(30.0, |mut header| { header.col(|ui| { - if ui.button("Name").clicked() { + if ui.button("Contested Name").clicked() { self.toggle_sort(SortColumn::ContestedName); } }); @@ -788,35 +805,33 @@ impl DPNSContestedNamesScreen { } }); header.col(|ui| { - if ui.button("Executed").clicked() { + if ui.button("Status").clicked() { self.toggle_sort(SortColumn::ContestedName); } }); header.col(|ui| { - if ui.button("Actions").clicked() { - self.toggle_sort(SortColumn::ContestedName); - } + ui.label("Actions"); }); }) .body(|mut body| { - for vote in sorted_votes { + for vote in sorted_votes.iter_mut() { body.row(25.0, |mut row| { row.col(|ui| { ui.add( - egui::Label::new(vote.contested_name.clone()) + egui::Label::new(vote.0.contested_name.clone()) .truncate(), ); }); row.col(|ui| { ui.add( egui::Label::new( - vote.voter_id.to_string(Encoding::Hex), + vote.0.voter_id.to_string(Encoding::Hex), ) .truncate(), ); }); row.col(|ui| { - let display_text = match &vote.choice { + let display_text = match &vote.0.choice { ResourceVoteChoice::TowardsIdentity(identifier) => { identifier.to_string(Encoding::Base58) } @@ -826,7 +841,7 @@ impl DPNSContestedNamesScreen { }); row.col(|ui| { if let LocalResult::Single(datetime) = - Utc.timestamp_millis_opt(vote.unix_timestamp as i64) + Utc.timestamp_millis_opt(vote.0.unix_timestamp as i64) { let iso_date = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); @@ -839,12 +854,18 @@ impl DPNSContestedNamesScreen { ui.label("Invalid timestamp"); } }); - row.col(|ui| match vote.executed_successfully { - true => { - ui.colored_label(Color32::DARK_GREEN, "Yes"); + row.col(|ui| match vote.1 { + IndividualVoteCastingStatus::NotStarted => { + ui.label("Pending"); } - false => { - ui.label(""); + IndividualVoteCastingStatus::InProgress => { + ui.label("Casting..."); + } + IndividualVoteCastingStatus::Failed => { + ui.colored_label(Color32::DARK_RED, "Failed"); + } + IndividualVoteCastingStatus::Completed => { + ui.colored_label(Color32::DARK_GREEN, "Complete"); } }); row.col(|ui| { @@ -852,30 +873,56 @@ impl DPNSContestedNamesScreen { action = AppAction::BackendTask( BackendTask::ContestedResourceTask( ContestedResourceTask::DeleteScheduledVote( - vote.voter_id, - vote.contested_name.clone(), + vote.0.voter_id.clone(), + vote.0.contested_name.clone(), ), ), ); } - if ui.button("Cast Now").clicked() { - let local_identities = - match self.app_context.db.get_local_voting_identities(&self.app_context) { - Ok(identities) => identities, - Err(e) => { - eprintln!("Error querying local voting identities: {}", e); - return; + + let cast_button = match vote.1 { + IndividualVoteCastingStatus::NotStarted => egui::Button::new("Cast Now"), + IndividualVoteCastingStatus::InProgress => egui::Button::new("Casting..."), + IndividualVoteCastingStatus::Failed => egui::Button::new("Cast Now"), + IndividualVoteCastingStatus::Completed => egui::Button::new("Completed"), + }; + + if !self.screen_casting_vote_in_progress && (vote.1 == IndividualVoteCastingStatus::NotStarted || vote.1 == IndividualVoteCastingStatus::Failed) { + if ui.add(cast_button).clicked() { + self.screen_casting_vote_in_progress = true; + + // Update the local vote + vote.1 = IndividualVoteCastingStatus::InProgress; + + // Now also update self.scheduled_votes + if let Ok(mut scheduled_guard) = self.scheduled_votes.lock() { + if let Some(sched_vote) = scheduled_guard.iter_mut().find(|(sv, _)| { + sv.voter_id == vote.0.voter_id && sv.contested_name == vote.0.contested_name + }) { + sched_vote.1 = IndividualVoteCastingStatus::InProgress; } - }; - if let Some(voter) = local_identities - .iter() - .find(|i| i.identity.id() == vote.voter_id) - { - action = AppAction::BackendTask( - BackendTask::ContestedResourceTask( - ContestedResourceTask::CastScheduledVote(vote, voter.clone()), - ), - ); + } + + // Trigger the CastScheduledVote task + let local_identities = + match self.app_context.load_local_voting_identities() { + Ok(identities) => identities, + Err(e) => { + eprintln!("Error querying local voting identities: {}", e); + return; + } + }; + + if let Some(voter) = local_identities + .iter() + .find(|i| i.identity.id() == vote.0.voter_id) + { + action = AppAction::BackendTask( + BackendTask::ContestedResourceTask( + ContestedResourceTask::CastScheduledVote(vote.0.clone(), voter.clone()), + ), + ); + } } } }); @@ -1026,7 +1073,32 @@ impl ScreenLike for DPNSContestedNamesScreen { *dpns_names = self.app_context.local_dpns_names().unwrap_or_default(); } DPNSSubscreen::ScheduledVotes => { - *scheduled_votes = self.app_context.get_scheduled_votes().unwrap_or_default(); + *scheduled_votes = { + let new_scheduled_votes = + self.app_context.get_scheduled_votes().unwrap_or_default(); + new_scheduled_votes + .iter() + .map(|new_vote| match new_vote.executed_successfully { + true => (new_vote.clone(), IndividualVoteCastingStatus::Completed), + false => scheduled_votes + .iter() + .find(|(old_vote, _)| { + old_vote.contested_name == new_vote.contested_name + && old_vote.voter_id == new_vote.voter_id + }) + .map(|(_, status)| { + if status == &IndividualVoteCastingStatus::InProgress { + (new_vote.clone(), IndividualVoteCastingStatus::InProgress) + } else { + (new_vote.clone(), IndividualVoteCastingStatus::NotStarted) + } + }) + .unwrap_or_else(|| { + (new_vote.clone(), IndividualVoteCastingStatus::NotStarted) + }), + }) + .collect::>() + } } } } @@ -1063,7 +1135,36 @@ impl ScreenLike for DPNSContestedNamesScreen { *dpns_names = self.app_context.local_dpns_names().unwrap_or_default(); } DPNSSubscreen::ScheduledVotes => { - *scheduled_votes = self.app_context.get_scheduled_votes().unwrap_or_default(); + *scheduled_votes = { + let new_scheduled_votes = + self.app_context.get_scheduled_votes().unwrap_or_default(); + new_scheduled_votes + .iter() + .map(|new_vote| match new_vote.executed_successfully { + true => (new_vote.clone(), IndividualVoteCastingStatus::Completed), + // If false, it could be failed, in progress, or not started + // Check screen state to see if vote is in progress + false => scheduled_votes + .iter() + .find(|(old_vote, _)| { + old_vote.contested_name == new_vote.contested_name + && old_vote.voter_id == new_vote.voter_id + }) + .map(|(_, status)| { + if status == &IndividualVoteCastingStatus::InProgress { + (new_vote.clone(), IndividualVoteCastingStatus::InProgress) + } else if status == &IndividualVoteCastingStatus::Failed { + (new_vote.clone(), IndividualVoteCastingStatus::Failed) + } else { + (new_vote.clone(), IndividualVoteCastingStatus::NotStarted) + } + }) + .unwrap_or_else(|| { + (new_vote.clone(), IndividualVoteCastingStatus::NotStarted) + }), + }) + .collect::>() + } } } } @@ -1076,6 +1177,18 @@ impl ScreenLike for DPNSContestedNamesScreen { { self.refreshing = false; } + if message.contains("Error casting scheduled vote") { + self.screen_casting_vote_in_progress = false; + let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); + for vote in scheduled_votes.iter_mut() { + if vote.1 == IndividualVoteCastingStatus::InProgress { + vote.1 = IndividualVoteCastingStatus::Failed; + } + } + } + if message.contains("Successfully cast scheduled vote") { + self.screen_casting_vote_in_progress = false; + } self.error_message = Some((message.to_string(), message_type, Utc::now())); } diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index b3e3e4883..8ff9f50aa 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -88,7 +88,7 @@ impl IdentitiesScreen { } else { identity_to_update.alias = Some(alias); } - match self.app_context.db.set_alias( + match self.app_context.set_alias( &identity_to_update.identity.id(), identity_to_update.alias.as_ref().map(|s| s.as_str()), ) { From 923f958a54e5ffdabc14e91ddff8a47c694b38be Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 14:27:51 +0700 Subject: [PATCH 16/28] ok --- src/ui/dpns_contested_names_screen.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index dd8c45b6c..b730625cb 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -790,14 +790,10 @@ impl DPNSContestedNamesScreen { } }); header.col(|ui| { - if ui.button("Voter").clicked() { - self.toggle_sort(SortColumn::ContestedName); - } + ui.label("Voter"); }); header.col(|ui| { - if ui.button("Vote").clicked() { - self.toggle_sort(SortColumn::ContestedName); - } + ui.label("Vote Choice"); }); header.col(|ui| { if ui.button("Scheduled Time").clicked() { @@ -805,9 +801,7 @@ impl DPNSContestedNamesScreen { } }); header.col(|ui| { - if ui.button("Status").clicked() { - self.toggle_sort(SortColumn::ContestedName); - } + ui.label("Status"); }); header.col(|ui| { ui.label("Actions"); From d0410010d912e3aa5a029b1c124565c3c40ba4ea Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 16:33:08 +0700 Subject: [PATCH 17/28] ok --- src/app.rs | 8 +++----- src/context.rs | 5 +++++ src/database/scheduled_votes.rs | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index be4671a3d..c18298fae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -440,11 +440,9 @@ impl App for AppState { self.visible_screen_mut().refresh(); } BackendTaskSuccessResult::CastScheduledVote(vote) => { - let _ = self.current_app_context().db.mark_vote_executed( - vote.voter_id.as_slice(), - vote.contested_name, - self.current_app_context(), - ); + let _ = self + .current_app_context() + .mark_vote_executed(vote.voter_id.as_slice(), vote.contested_name); self.visible_screen_mut().display_message( "Successfully cast scheduled vote", MessageType::Success, diff --git a/src/context.rs b/src/context.rs index 4d490191b..b00f1d701 100644 --- a/src/context.rs +++ b/src/context.rs @@ -199,6 +199,11 @@ impl AppContext { self.db.get_ongoing_contested_names(self) } + pub fn mark_vote_executed(&self, identity_id: &[u8], contested_name: String) -> Result<()> { + self.db + .mark_vote_executed(self, identity_id, contested_name) + } + /// Fetches the local identities from the database and then maps them to their DPNS names. pub fn local_dpns_names(&self) -> Result> { let wallets = self.wallets.read().unwrap(); diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 1113abd2e..980f4e246 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -62,9 +62,9 @@ impl Database { pub fn mark_vote_executed( &self, + app_context: &AppContext, identity_id: &[u8], contested_name: String, - app_context: &AppContext, ) -> rusqlite::Result<()> { let network = app_context.network_string(); self.execute( From ce2222cb099daee6d1dcfaea1d0cfe92d45e889e Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 16:40:39 +0700 Subject: [PATCH 18/28] ok --- src/ui/dpns_contested_names_screen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index b730625cb..ded529ff6 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -1050,6 +1050,7 @@ impl DPNSContestedNamesScreen { impl ScreenLike for DPNSContestedNamesScreen { fn refresh(&mut self) { + self.screen_casting_vote_in_progress = false; let mut contested_names = self.contested_names.lock().unwrap(); let mut dpns_names = self.local_dpns_names.lock().unwrap(); let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); From 00196e81dd44bb9a2a1f2bf2f9b57a0f11b9337d Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 16:47:44 +0700 Subject: [PATCH 19/28] ok --- src/ui/dpns_contested_names_screen.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index ded529ff6..465cd5be6 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -859,7 +859,7 @@ impl DPNSContestedNamesScreen { ui.colored_label(Color32::DARK_RED, "Failed"); } IndividualVoteCastingStatus::Completed => { - ui.colored_label(Color32::DARK_GREEN, "Complete"); + ui.colored_label(Color32::DARK_GREEN, "Casted"); } }); row.col(|ui| { @@ -1084,6 +1084,8 @@ impl ScreenLike for DPNSContestedNamesScreen { .map(|(_, status)| { if status == &IndividualVoteCastingStatus::InProgress { (new_vote.clone(), IndividualVoteCastingStatus::InProgress) + } else if status == &IndividualVoteCastingStatus::Failed { + (new_vote.clone(), IndividualVoteCastingStatus::Failed) } else { (new_vote.clone(), IndividualVoteCastingStatus::NotStarted) } From 6024caf50ddce76db73177e753205fa7db42011f Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 16:53:39 +0700 Subject: [PATCH 20/28] ok --- src/ui/dpns_contested_names_screen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 465cd5be6..5d2bfe86f 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -797,7 +797,7 @@ impl DPNSContestedNamesScreen { }); header.col(|ui| { if ui.button("Scheduled Time").clicked() { - self.toggle_sort(SortColumn::ContestedName); + self.toggle_sort(SortColumn::EndingTime); } }); header.col(|ui| { From fb85ea79f938e0dd94eb0ef91a9f5697e0e9687a Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 19:48:49 +0700 Subject: [PATCH 21/28] ok --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index c18298fae..26d003929 100644 --- a/src/app.rs +++ b/src/app.rs @@ -517,7 +517,7 @@ impl App for AppState { let app_context = self.current_app_context(); // Query the database - let db_votes = match app_context.db.get_scheduled_votes(&app_context) { + let db_votes = match app_context.get_scheduled_votes() { Ok(votes) => votes, Err(e) => { eprintln!("Error querying scheduled votes: {}", e); From 67c223ff1ac2e820741c6de0976dbb0730d34ba2 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 20:21:06 +0700 Subject: [PATCH 22/28] fix --- src/app.rs | 2 +- src/database/scheduled_votes.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 26d003929..2a26747b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -535,7 +535,7 @@ impl App for AppState { .filter(|v| { v.unix_timestamp <= current_time && !v.executed_successfully - && !(v.unix_timestamp + 120000 < current_time) + && !(v.unix_timestamp + 120000 < current_time) // Don't cast votes more than 2 minutes behind current time }) .collect(); diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index 980f4e246..e4a28e283 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -21,8 +21,7 @@ impl Database { time INTEGER NOT NULL, executed INTEGER NOT NULL DEFAULT 0, network TEXT NOT NULL, - PRIMARY KEY (identity_id, contested_name), - FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE + PRIMARY KEY (identity_id, contested_name) )", [], )?; From b359b95ca9342dfcd86f779aee06d177befec372 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 20:23:13 +0700 Subject: [PATCH 23/28] fix --- src/app.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2a26747b9..5f52ad65a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -541,14 +541,13 @@ impl App for AppState { // For each due vote, construct a BackendTask and handle it if !due_votes.is_empty() { - let local_identities = - match app_context.db.get_local_voting_identities(&app_context) { - Ok(identities) => identities, - Err(e) => { - eprintln!("Error querying local voting identities: {}", e); - return; - } - }; + let local_identities = match app_context.load_local_voting_identities() { + Ok(identities) => identities, + Err(e) => { + eprintln!("Error querying local voting identities: {}", e); + return; + } + }; for vote in due_votes { if let Some(voter) = local_identities From e3ddfd815551dad923f4973d0bd2db59bf8069b5 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 21:06:50 +0700 Subject: [PATCH 24/28] fix --- src/backend_task/contested_names/mod.rs | 12 ++++-------- src/context.rs | 17 +++++++++++++++++ src/database/scheduled_votes.rs | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/backend_task/contested_names/mod.rs b/src/backend_task/contested_names/mod.rs index b7e421696..e1164fdb3 100644 --- a/src/backend_task/contested_names/mod.rs +++ b/src/backend_task/contested_names/mod.rs @@ -50,8 +50,7 @@ impl AppContext { .await } ContestedResourceTask::ScheduleDPNSVotes(scheduled_votes) => self - .db - .insert_scheduled_votes(self, scheduled_votes) + .insert_scheduled_votes(scheduled_votes) .map(|_| BackendTaskSuccessResult::Message("Votes scheduled".to_string())) .map_err(|e| format!("Error inserting scheduled votes: {}", e.to_string())), ContestedResourceTask::CastScheduledVote(scheduled_vote, voter) => self @@ -66,18 +65,15 @@ impl AppContext { .map(|_| BackendTaskSuccessResult::CastScheduledVote(scheduled_vote.clone())) .map_err(|e| format!("Error casting scheduled vote: {}", e.to_string())), ContestedResourceTask::ClearAllScheduledVotes => self - .db - .clear_all_scheduled_votes(self) + .clear_all_scheduled_votes() .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing all scheduled votes: {}", e.to_string())), ContestedResourceTask::ClearExecutedScheduledVotes => self - .db - .clear_executed_scheduled_votes(self) + .clear_executed_scheduled_votes() .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing executed scheduled votes: {}", e.to_string())), ContestedResourceTask::DeleteScheduledVote(voter_id, contested_name) => self - .db - .delete_scheduled_vote(voter_id.as_slice(), contested_name, self) + .delete_scheduled_vote(voter_id.as_slice(), contested_name) .map(|_| BackendTaskSuccessResult::Refresh) .map_err(|e| format!("Error clearing scheduled vote: {}", e.to_string())), } diff --git a/src/context.rs b/src/context.rs index b00f1d701..1deaddd16 100644 --- a/src/context.rs +++ b/src/context.rs @@ -199,6 +199,23 @@ impl AppContext { self.db.get_ongoing_contested_names(self) } + pub fn insert_scheduled_votes(&self, scheduled_votes: &Vec) -> Result<()> { + self.db.insert_scheduled_votes(self, &scheduled_votes) + } + + pub fn clear_all_scheduled_votes(&self) -> Result<()> { + self.db.clear_all_scheduled_votes(self) + } + + pub fn clear_executed_scheduled_votes(&self) -> Result<()> { + self.db.clear_executed_scheduled_votes(self) + } + + pub fn delete_scheduled_vote(&self, identity_id: &[u8], contested_name: &String) -> Result<()> { + self.db + .delete_scheduled_vote(self, identity_id, &contested_name) + } + pub fn mark_vote_executed(&self, identity_id: &[u8], contested_name: String) -> Result<()> { self.db .mark_vote_executed(self, identity_id, contested_name) diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index e4a28e283..f12a55f6f 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -46,9 +46,9 @@ impl Database { pub fn delete_scheduled_vote( &self, + app_context: &AppContext, identity_id: &[u8], contested_name: &str, - app_context: &AppContext, ) -> rusqlite::Result<()> { let network = app_context.network_string(); let conn = self.conn.lock().unwrap(); From 99aba6a0542eadd36540f8cd959fe14d05ade901 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 21:11:50 +0700 Subject: [PATCH 25/28] fix --- src/context.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/context.rs b/src/context.rs index 1deaddd16..29733f7e6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -203,6 +203,10 @@ impl AppContext { self.db.insert_scheduled_votes(self, &scheduled_votes) } + pub fn get_scheduled_votes(&self) -> Result> { + self.db.get_scheduled_votes(&self) + } + pub fn clear_all_scheduled_votes(&self) -> Result<()> { self.db.clear_all_scheduled_votes(self) } @@ -287,11 +291,6 @@ impl AppContext { Ok(contracts) } - /// Get scheduled votes - pub fn get_scheduled_votes(&self) -> Result> { - self.db.get_scheduled_votes(&self) - } - pub(crate) fn received_transaction_finality( &self, tx: &Transaction, From abbd5d014357efa54d20ccd449f3da25815550ad Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 21:15:08 +0700 Subject: [PATCH 26/28] rename --- src/ui/dpns_contested_names_screen.rs | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 5d2bfe86f..067345579 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -80,8 +80,8 @@ pub struct DPNSContestedNamesScreen { sort_column: SortColumn, sort_order: SortOrder, show_vote_popup_info: Option<(String, ContestedResourceTask)>, - pending_vote_action: Option, - screen_casting_vote_in_progress: bool, + popup_pending_vote_action: Option, + vote_cast_in_progress: bool, pub dpns_subscreen: DPNSSubscreen, refreshing: bool, } @@ -135,8 +135,8 @@ impl DPNSContestedNamesScreen { sort_column: SortColumn::ContestedName, sort_order: SortOrder::Ascending, show_vote_popup_info: None, - pending_vote_action: None, - screen_casting_vote_in_progress: false, + popup_pending_vote_action: None, + vote_cast_in_progress: false, dpns_subscreen, refreshing: false, } @@ -881,9 +881,9 @@ impl DPNSContestedNamesScreen { IndividualVoteCastingStatus::Completed => egui::Button::new("Completed"), }; - if !self.screen_casting_vote_in_progress && (vote.1 == IndividualVoteCastingStatus::NotStarted || vote.1 == IndividualVoteCastingStatus::Failed) { + if !self.vote_cast_in_progress && (vote.1 == IndividualVoteCastingStatus::NotStarted || vote.1 == IndividualVoteCastingStatus::Failed) { if ui.add(cast_button).clicked() { - self.screen_casting_vote_in_progress = true; + self.vote_cast_in_progress = true; // Update the local vote vote.1 = IndividualVoteCastingStatus::InProgress; @@ -941,14 +941,14 @@ impl DPNSContestedNamesScreen { } if ui.button("Cancel").clicked() { self.show_vote_popup_info = None; - self.pending_vote_action = None; + self.popup_pending_vote_action = None; } } else if let Some((message, action)) = self.show_vote_popup_info.clone() { ui.label(message); ui.add_space(10.0); - if self.pending_vote_action.is_none() { + if self.popup_pending_vote_action.is_none() { ui.label("Select the identity to vote with:"); } else { ui.label("Would you like to vote now or schedule your votes?"); @@ -964,7 +964,7 @@ impl DPNSContestedNamesScreen { ) = action { // If we haven't yet chosen any voters (pending_vote_action is None), we show the identities - if self.pending_vote_action.is_none() { + if self.popup_pending_vote_action.is_none() { // Iterate over the voting identities and create a button for each one for identity in self.voting_identities.iter() { if ui.button(identity.display_short_string()).clicked() { @@ -977,7 +977,7 @@ impl DPNSContestedNamesScreen { vote_choice.clone(), voters.clone(), ); - self.pending_vote_action = Some(updated_action); + self.popup_pending_vote_action = Some(updated_action); } } @@ -989,7 +989,7 @@ impl DPNSContestedNamesScreen { vote_choice.clone(), voters.clone(), ); - self.pending_vote_action = Some(updated_action); + self.popup_pending_vote_action = Some(updated_action); } } else { // If we have a pending vote action, ask whether to vote now or schedule @@ -997,13 +997,13 @@ impl DPNSContestedNamesScreen { // Finalize the vote now app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask( - self.pending_vote_action.take().unwrap(), + self.popup_pending_vote_action.take().unwrap(), )); self.show_vote_popup_info = None; } if ui.button("Schedule").clicked() { // Move to a scheduling screen instead - let pending = self.pending_vote_action.take().unwrap(); + let pending = self.popup_pending_vote_action.take().unwrap(); if let ContestedResourceTask::VoteOnDPNSName( name_string, vote_choice, @@ -1039,7 +1039,7 @@ impl DPNSContestedNamesScreen { // Add the "Cancel" button if ui.button("Cancel").clicked() { self.show_vote_popup_info = None; - self.pending_vote_action = None; + self.popup_pending_vote_action = None; } }); } @@ -1050,7 +1050,7 @@ impl DPNSContestedNamesScreen { impl ScreenLike for DPNSContestedNamesScreen { fn refresh(&mut self) { - self.screen_casting_vote_in_progress = false; + self.vote_cast_in_progress = false; let mut contested_names = self.contested_names.lock().unwrap(); let mut dpns_names = self.local_dpns_names.lock().unwrap(); let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); @@ -1175,7 +1175,7 @@ impl ScreenLike for DPNSContestedNamesScreen { self.refreshing = false; } if message.contains("Error casting scheduled vote") { - self.screen_casting_vote_in_progress = false; + self.vote_cast_in_progress = false; let mut scheduled_votes = self.scheduled_votes.lock().unwrap(); for vote in scheduled_votes.iter_mut() { if vote.1 == IndividualVoteCastingStatus::InProgress { @@ -1184,7 +1184,7 @@ impl ScreenLike for DPNSContestedNamesScreen { } } if message.contains("Successfully cast scheduled vote") { - self.screen_casting_vote_in_progress = false; + self.vote_cast_in_progress = false; } self.error_message = Some((message.to_string(), message_type, Utc::now())); } From 80079aaf58279c102fe37c809f620ef486a34754 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 12 Dec 2024 21:48:05 +0700 Subject: [PATCH 27/28] screen refresh in listener --- src/app.rs | 18 +++++++++++++++++- src/ui/dpns_contested_names_screen.rs | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5f52ad65a..118519327 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,9 @@ 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::{DPNSContestedNamesScreen, DPNSSubscreen}; +use crate::ui::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; @@ -554,6 +556,20 @@ impl App for AppState { .iter() .find(|i| i.identity.id() == vote.voter_id) { + let dpns_screen = self + .main_screens + .get_mut(&RootScreenType::RootScreenDPNSScheduledVotes) + .unwrap(); + if let Screen::DPNSContestedNamesScreen(screen) = dpns_screen { + screen.vote_cast_in_progress = true; + screen + .scheduled_votes + .lock() + .unwrap() + .iter_mut() + .find(|(v, _)| v == &vote) + .map(|(_, s)| *s = IndividualVoteCastingStatus::InProgress); + } let task = BackendTask::ContestedResourceTask( ContestedResourceTask::CastScheduledVote(vote, voter.clone()), ); diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 067345579..85cc2d8d4 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -74,14 +74,14 @@ pub struct DPNSContestedNamesScreen { user_identities: Vec, contested_names: Arc>>, local_dpns_names: Arc>>, - scheduled_votes: Arc>>, + pub scheduled_votes: Arc>>, pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, sort_column: SortColumn, sort_order: SortOrder, show_vote_popup_info: Option<(String, ContestedResourceTask)>, popup_pending_vote_action: Option, - vote_cast_in_progress: bool, + pub vote_cast_in_progress: bool, pub dpns_subscreen: DPNSSubscreen, refreshing: bool, } From 83ac19cc26bdc9ddb7a2233bc3391e3f5cbfabfb Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 13 Dec 2024 16:55:43 +0700 Subject: [PATCH 28/28] coderabbit suggestions --- src/database/scheduled_votes.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/database/scheduled_votes.rs b/src/database/scheduled_votes.rs index f12a55f6f..0e2f46118 100644 --- a/src/database/scheduled_votes.rs +++ b/src/database/scheduled_votes.rs @@ -34,13 +34,16 @@ impl Database { votes: &Vec, ) -> rusqlite::Result<()> { let network = app_context.network_string(); + let mut conn = self.conn.lock().unwrap(); + let tx = conn.transaction()?; for vote in votes { let vote_choice = vote.choice.to_string(); - self.execute( + tx.execute( "INSERT OR REPLACE INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network) VALUES (?, ?, ?, ?, 0, ?)", params![vote.voter_id.as_slice(), vote.contested_name, vote_choice, vote.unix_timestamp, network], )?; } + tx.commit()?; Ok(()) } @@ -100,8 +103,15 @@ impl Database { if let Some(inner) = inner.strip_suffix(')') { let towards_id = inner.to_string(); ResourceVoteChoice::TowardsIdentity( - Identifier::from_string(&towards_id, Encoding::Base58) - .expect("Expected valid identifier"), + Identifier::from_string(&towards_id, Encoding::Base58).map_err( + |e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Blob, + Box::new(e), + ) + }, + )?, ) } else { return Err(rusqlite::Error::InvalidQuery); @@ -113,8 +123,13 @@ impl Database { }; let scheduled_vote = ScheduledDPNSVote { - voter_id: Identifier::from_bytes(&voter_id_bytes) - .expect("Expected valid identifier"), + voter_id: Identifier::from_bytes(&voter_id_bytes).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + 0, + rusqlite::types::Type::Blob, + Box::new(e), + ) + })?, contested_name, choice: vote_choice, unix_timestamp: time,