diff --git a/Cargo.toml b/Cargo.toml index 8cecc62ee..4df37bf7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ strum = { version = "0.26.1", features = ["derive"] } bs58 = "0.5.0" base64 = "0.22.1" copypasta = "0.10.1" -dash-sdk = { git = "https://github.com/dashpay/platform", branch = "refactor/replaceBLSLibrary" } +dash-sdk = { git = "https://github.com/dashpay/platform", branch = "refactor/replaceBLSLibrary", features = ["tokio-sleep"] } thiserror = "1" serde = "1.0.197" serde_json = "1.0.120" diff --git a/src/app.rs b/src/app.rs index e21e8ff65..47408b2c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -256,7 +256,9 @@ impl App for AppState { // Handle the result on the main thread match task_result { TaskResult::Success(message) => match message { - BackendTaskSuccessResult::None => {} + BackendTaskSuccessResult::None => { + self.visible_screen_mut().pop_on_success(); + } BackendTaskSuccessResult::Message(message) => { self.visible_screen_mut() .display_message(&message, MessageType::Info); @@ -267,6 +269,9 @@ impl App for AppState { BackendTaskSuccessResult::CoreItem(_) => { self.visible_screen_mut().display_task_result(message); } + BackendTaskSuccessResult::SuccessfulVotes(_) => { + self.visible_screen_mut().refresh(); + } }, TaskResult::Error(message) => { self.visible_screen_mut() diff --git a/src/context.rs b/src/context.rs index 5966c9551..a74da44d4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -108,8 +108,12 @@ impl AppContext { self.db.get_local_qualified_identities(self) } - pub fn load_contested_names(&self) -> Result> { - self.db.get_contested_names(self) + pub fn all_contested_names(&self) -> Result> { + self.db.get_all_contested_names(self) + } + + pub fn ongoing_contested_names(&self) -> Result> { + self.db.get_ongoing_contested_names(self) } /// Updates the `start_root_screen` in the settings table diff --git a/src/database/contested_names.rs b/src/database/contested_names.rs index 7d8833868..9e52c9dc0 100644 --- a/src/database/contested_names.rs +++ b/src/database/contested_names.rs @@ -1,19 +1,28 @@ use crate::context::AppContext; use crate::database::Database; -use crate::model::contested_name::{Contestant, ContestedName}; +use crate::model::contested_name::{ContestState, Contestant, ContestedName}; use dash_sdk::dpp::dashcore::Network; use dash_sdk::dpp::data_contract::document_type::DocumentTypeRef; use dash_sdk::dpp::document::DocumentV0Getters; use dash_sdk::dpp::identifier::Identifier; use dash_sdk::dpp::identity::TimestampMillis; use dash_sdk::dpp::prelude::{BlockHeight, CoreBlockHeight}; +use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; +use dash_sdk::dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo; use dash_sdk::query_types::Contenders; use rusqlite::{params, params_from_iter, Result}; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::time::Duration; +use tracing::{error, info}; impl Database { - pub fn get_contested_names(&self, app_context: &AppContext) -> Result> { + pub fn get_all_contested_names(&self, app_context: &AppContext) -> Result> { let network = app_context.network_string(); + let contest_duration = if app_context.network == Network::Dash { + Duration::from_secs(60 * 60 * 24 * 14) + } else { + Duration::from_secs(60 * 90) + }; let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( "SELECT @@ -21,7 +30,8 @@ impl Database { cn.locked_votes, cn.abstain_votes, cn.awarded_to, - cn.ending_time, + cn.end_time, + cn.locked, cn.last_updated, c.identity_id, c.name, @@ -33,7 +43,7 @@ impl Database { i.info FROM contested_name cn LEFT JOIN contestant c - ON cn.normalized_contested_name = c.contest_id + ON cn.normalized_contested_name = c.normalized_contested_name AND cn.network = c.network LEFT JOIN identity i ON c.identity_id = i.id @@ -51,20 +61,174 @@ impl Database { let abstain_votes: Option = row.get(2)?; let awarded_to: Option> = row.get(3)?; let ending_time: Option = row.get(4)?; - let last_updated: Option = row.get(5)?; - let identity_id: Option> = row.get(6)?; - let contestant_name: Option = row.get(7)?; - let votes: Option = row.get(8)?; - let created_at: Option = row.get(9)?; - let created_at_block_height: Option = row.get(10)?; - let created_at_core_block_height: Option = row.get(11)?; - let document_id: Option> = row.get(12)?; - let identity_info: Option = row.get(13)?; + let locked: bool = row.get(5)?; + let last_updated: Option = row.get(6)?; + let identity_id: Option> = row.get(7)?; + let contestant_name: Option = row.get(8)?; + let votes: Option = row.get(9)?; + let created_at: Option = row.get(10)?; + let created_at_block_height: Option = row.get(11)?; + let created_at_core_block_height: Option = row.get(12)?; + let document_id: Option> = row.get(13)?; + let identity_info: Option = row.get(14)?; // Convert `awarded_to` to `Identifier` if it exists let awarded_to_id = awarded_to .map(|id| Identifier::from_bytes(&id).expect("Expected 32 bytes for awarded_to")); + let state = if locked { + ContestState::Locked + } else if let Some(awarded_to_id) = awarded_to_id { + ContestState::WonBy(awarded_to_id) + } else if let Some(created_at) = created_at { + let elapsed_time = Duration::from_millis( + (std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64) + .saturating_sub(created_at), + ); + + if elapsed_time <= contest_duration / 2 { + ContestState::Joinable + } else { + ContestState::Ongoing + } + } else { + ContestState::Unknown + }; + + // Create or get the contested name from the hashmap + let contested_name = contested_name_map + .entry(normalized_contested_name.clone()) + .or_insert(ContestedName { + normalized_contested_name: normalized_contested_name.clone(), + locked_votes, + abstain_votes, + awarded_to: awarded_to_id, + end_time: ending_time, + contestants: Some(Vec::new()), // Initialize as an empty vector + last_updated, + my_votes: BTreeMap::new(), // Assuming this is filled elsewhere + state, + }); + + // If there are contestant details in the row, add them + if let (Some(identity_id), Some(contestant_name), Some(votes), Some(document_id)) = + (identity_id, contestant_name, votes, document_id) + { + let contestant = Contestant { + id: Identifier::from_bytes(&identity_id) + .expect("Expected 32 bytes for identity_id"), + name: contestant_name, + info: identity_info.unwrap_or_default(), + votes, + created_at, + created_at_block_height, + created_at_core_block_height, + document_id: Identifier::from_bytes(&document_id) + .expect("Expected 32 bytes for document_id"), + }; + + // Add the contestant to the contestants list + if let Some(contestants) = &mut contested_name.contestants { + contestants.push(contestant); + } + } + + Ok(()) + })?; + + // Ensure all rows are processed without error + for row in rows { + row?; + } + + // Collect the values from the hashmap and return as a vector + Ok(contested_name_map.into_values().collect()) + } + + pub fn get_ongoing_contested_names( + &self, + app_context: &AppContext, + ) -> Result> { + let network = app_context.network_string(); + let contest_duration = if app_context.network == Network::Dash { + Duration::from_secs(60 * 60 * 24 * 14) + } else { + Duration::from_secs(60 * 90) + }; + let current_timestamp = std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64; + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT + cn.normalized_contested_name, + cn.locked_votes, + cn.abstain_votes, + cn.awarded_to, + cn.end_time, + cn.locked, + cn.last_updated, + c.identity_id, + c.name, + c.votes, + c.created_at, + c.created_at_block_height, + c.created_at_core_block_height, + c.document_id, + i.info + FROM contested_name cn + LEFT JOIN contestant c + ON cn.normalized_contested_name = c.normalized_contested_name + AND cn.network = c.network + LEFT JOIN identity i + ON c.identity_id = i.id + AND c.network = i.network + WHERE cn.network = ? + AND (cn.end_time IS NULL OR cn.end_time > ?)", + )?; + + // A hashmap to collect contested names, keyed by their normalized name + let mut contested_name_map: HashMap = HashMap::new(); + + // Iterate over the joined rows + let rows = stmt.query_map(params![network, current_timestamp], |row| { + let normalized_contested_name: String = row.get(0)?; + let locked_votes: Option = row.get(1)?; + let abstain_votes: Option = row.get(2)?; + let awarded_to: Option> = row.get(3)?; + let ending_time: Option = row.get(4)?; + let locked: bool = row.get(5)?; + let last_updated: Option = row.get(6)?; + let identity_id: Option> = row.get(7)?; + let contestant_name: Option = row.get(8)?; + let votes: Option = row.get(9)?; + let created_at: Option = row.get(10)?; + let created_at_block_height: Option = row.get(11)?; + let created_at_core_block_height: Option = row.get(12)?; + let document_id: Option> = row.get(13)?; + let identity_info: Option = row.get(14)?; + + // Convert `awarded_to` to `Identifier` if it exists + let awarded_to_id = awarded_to + .map(|id| Identifier::from_bytes(&id).expect("Expected 32 bytes for awarded_to")); + + let state = if locked { + ContestState::Locked + } else if let Some(awarded_to_id) = awarded_to_id { + ContestState::WonBy(awarded_to_id) + } else if let Some(created_at) = created_at { + let elapsed_time = Duration::from_millis( + (std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() as u64) + .saturating_sub(created_at), + ); + + if elapsed_time <= contest_duration / 2 { + ContestState::Joinable + } else { + ContestState::Ongoing + } + } else { + ContestState::Unknown + }; + // Create or get the contested name from the hashmap let contested_name = contested_name_map .entry(normalized_contested_name.clone()) @@ -73,10 +237,11 @@ impl Database { locked_votes, abstain_votes, awarded_to: awarded_to_id, - ending_time, + end_time: ending_time, contestants: Some(Vec::new()), // Initialize as an empty vector last_updated, my_votes: BTreeMap::new(), // Assuming this is filled elsewhere + state, }); // If there are contestant details in the row, add them @@ -148,19 +313,19 @@ impl Database { || awarded_to.as_ref().map(|id| { Identifier::from_bytes(id).expect("expected 32 bytes for awarded to") }) != contested_name.awarded_to - || ending_time != contested_name.ending_time; + || ending_time != contested_name.end_time; if should_update { // Update the entry if any field has changed self.execute( "UPDATE contested_name - SET locked_votes = ?, abstain_votes = ?, awarded_to = ?, ending_time = ? + SET locked_votes = ?, abstain_votes = ?, awarded_to = ?, end_time = ? WHERE normalized_contested_name = ? AND network = ?", params![ contested_name.locked_votes, contested_name.abstain_votes, contested_name.awarded_to.as_ref().map(|id| id.to_vec()), - contested_name.ending_time, + contested_name.end_time, contested_name.normalized_contested_name, network, ], @@ -170,14 +335,14 @@ impl Database { Err(rusqlite::Error::QueryReturnedNoRows) => { // If the contested name doesn't exist, insert it self.execute( - "INSERT INTO contested_name (normalized_contested_name, locked_votes, abstain_votes, awarded_to, ending_time, network) + "INSERT INTO contested_name (normalized_contested_name, locked_votes, abstain_votes, awarded_to, end_time, network) VALUES (?, ?, ?, ?, ?, ?)", params![ contested_name.normalized_contested_name, contested_name.locked_votes, contested_name.abstain_votes, contested_name.awarded_to.as_ref().map(|id| id.to_vec()), - contested_name.ending_time, + contested_name.end_time, network, ], )?; @@ -201,19 +366,57 @@ impl Database { pub fn insert_or_update_contenders( &self, - contest_id: &str, + normalized_contested_name: &str, contenders: &Contenders, dpns_domain_document_type: DocumentTypeRef, app_context: &AppContext, ) -> Result<()> { - if contenders.winner.is_some() { - return Ok(()); //todo - } let network = app_context.network_string(); + let last_updated = chrono::Utc::now().timestamp(); // Get the current timestamp + if let Some((winner, block_info)) = contenders.winner { + match winner { + ContestedDocumentVotePollWinnerInfo::NoWinner => {} + ContestedDocumentVotePollWinnerInfo::WonByIdentity(won_by) => { + let mut conn = self.conn.lock().unwrap(); + // Start a transaction + let tx = conn.transaction()?; + tx.execute( + "UPDATE contested_name + SET awarded_to = ?, last_updated = ?, end_time = ? + WHERE normalized_contested_name = ? AND network = ?", + params![ + won_by.to_vec(), + last_updated, + block_info.time_ms, + normalized_contested_name, + network, + ], + )?; + tx.commit()?; + } + ContestedDocumentVotePollWinnerInfo::Locked => { + let mut conn = self.conn.lock().unwrap(); + // Start a transaction + let tx = conn.transaction()?; + tx.execute( + "UPDATE contested_name + SET locked = 1, last_updated = ?, end_time = ? + WHERE normalized_contested_name = ? AND network = ?", + params![ + last_updated, + block_info.time_ms, + normalized_contested_name, + network, + ], + )?; + tx.commit()?; + } + } + return Ok(()); + } let mut conn = self.conn.lock().unwrap(); let locked_votes = contenders.lock_vote_tally.unwrap_or(0) as i64; let abstain_votes = contenders.abstain_vote_tally.unwrap_or(0) as i64; - let last_updated = chrono::Utc::now().timestamp(); // Get the current timestamp // Start a transaction let tx = conn.transaction()?; @@ -227,7 +430,7 @@ impl Database { locked_votes, abstain_votes, last_updated, - contest_id, + normalized_contested_name, network ], )?; @@ -259,11 +462,15 @@ impl Database { let mut stmt = tx.prepare( "SELECT votes FROM contestant - WHERE contest_id = ? AND identity_id = ? AND network = ?", + WHERE normalized_contested_name = ? AND identity_id = ? AND network = ?", )?; let result = stmt.query_row( - params![contest_id, identity_id_bytes.clone(), network], + params![ + normalized_contested_name, + identity_id_bytes.clone(), + network + ], |row| row.get::<_, u64>(0), ); @@ -274,10 +481,10 @@ impl Database { tx.execute( "UPDATE contestant SET votes = ? - WHERE contest_id = ? AND identity_id = ? AND network = ?", + WHERE normalized_contested_name = ? AND identity_id = ? AND network = ?", params![ contender.vote_tally().unwrap_or(0), - contest_id, + normalized_contested_name, identity_id_bytes, network, ], @@ -287,10 +494,10 @@ impl Database { Err(rusqlite::Error::QueryReturnedNoRows) => { // If the contestant doesn't exist, insert it tx.execute( - "INSERT INTO contestant (contest_id, identity_id, name, votes, created_at, created_at_block_height, created_at_core_block_height, document_id, network) + "INSERT INTO contestant (normalized_contested_name, identity_id, name, votes, created_at, created_at_block_height, created_at_core_block_height, document_id, network) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", params![ - contest_id, + normalized_contested_name, identity_id_bytes, name, contender.vote_tally().unwrap_or(0), @@ -307,7 +514,10 @@ impl Database { } // Commit the transaction - tx.commit()?; + if let Err(e) = tx.commit() { + error!("Transaction failed to commit: {:?}", e); + return Err(e); + } Ok(()) } @@ -392,8 +602,7 @@ impl Database { let mut new_names: Vec = Vec::new(); // Define the time limit (one hour ago in Unix timestamp format) - let one_hour_ago = chrono::Utc::now().timestamp() - 3600; - let two_weeks_ago = chrono::Utc::now().timestamp() - 1_209_600; + let half_a_minute_ago = chrono::Utc::now().timestamp() - 30; // Chunk the name_contests into smaller groups due to SQL parameter limits let chunk_size = 900; // Use a safe limit to stay below SQLite's limit @@ -404,7 +613,7 @@ impl Database { let query = format!( "SELECT normalized_contested_name, last_updated FROM contested_name - WHERE network = ? AND normalized_contested_name IN ({})", + WHERE network = ? AND normalized_contested_name IN ({}) and awarded_to IS NULL", placeholders ); @@ -428,12 +637,7 @@ impl Database { for row in rows { if let Ok((name, last_updated)) = row { existing_names.insert(name.clone()); - if last_updated.is_none() - || (app_context.network == Network::Testnet - && last_updated.unwrap() < one_hour_ago) - || (app_context.network == Network::Dash - && last_updated.unwrap() < two_weeks_ago) - { + if last_updated.is_none() || last_updated.unwrap() < half_a_minute_ago { names_to_be_updated.push((name, last_updated)); } } @@ -450,8 +654,8 @@ impl Database { // Insert new names into the database if !new_names.is_empty() { let mut insert_stmt = conn.prepare( - "INSERT INTO contested_name (normalized_contested_name, network, winner_type) - VALUES (?, ?, 0)", + "INSERT INTO contested_name (normalized_contested_name, network) + VALUES (?, ?)", )?; for name in &new_names { @@ -480,7 +684,7 @@ impl Database { let conn = self.conn.lock().unwrap(); // Prepare statement for selecting existing entries - let select_query = "SELECT ending_time + let select_query = "SELECT end_time FROM contested_name WHERE network = ? AND normalized_contested_name = ?"; @@ -488,7 +692,7 @@ impl Database { // Prepare statement for updating existing entries let update_query = "UPDATE contested_name - SET ending_time = ? + SET end_time = ? WHERE normalized_contested_name = ? AND network = ?"; let mut update_stmt = conn.prepare(update_query)?; @@ -513,4 +717,55 @@ impl Database { Ok(()) } + pub fn update_vote_count( + &self, + contested_name: &str, + network: &str, + vote_strength: u64, + vote_choice: ResourceVoteChoice, + ) -> Result<()> { + let mut conn = self.conn.lock().unwrap(); + let tx = conn.transaction()?; + + match vote_choice { + ResourceVoteChoice::TowardsIdentity(identity) => { + // Increment the contestant's vote count + tx.execute( + "UPDATE contestant + SET votes = votes + ? + WHERE normalized_contested_name = ? + AND identity_id = ? + AND network = ?", + params![vote_strength, contested_name, identity.to_vec(), network], + )?; + } + ResourceVoteChoice::Abstain => { + // Increment the abstain vote count in the contested_name table + tx.execute( + "UPDATE contested_name + SET abstain_votes = abstain_votes + ? + WHERE normalized_contested_name = ? AND network = ?", + params![vote_strength, contested_name, network], + )?; + } + ResourceVoteChoice::Lock => { + // Increment the locked vote count in the contested_name table + tx.execute( + "UPDATE contested_name + SET locked_votes = locked_votes + ? + WHERE normalized_contested_name = ? AND network = ?", + params![vote_strength, contested_name, network], + )?; + } + } + + // Commit the transaction + if let Err(e) = tx.commit() { + error!("Failed to commit transaction: {:?}", e); + return Err(e); + } + + info!("Vote tally updated successfully for '{}'", contested_name); + Ok(()) + } } diff --git a/src/database/identities.rs b/src/database/identities.rs index f7dde21a9..a7ff895f4 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -66,10 +66,52 @@ impl Database { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( - "SELECT id, data, alias, identity_type FROM identity WHERE is_local = 1 AND network = ? AND data IS NOT NULL", + "SELECT data FROM identity WHERE is_local = 1 AND network = ? AND data IS NOT NULL", )?; let identity_iter = stmt.query_map(params![network], |row| { - let data: Vec = row.get(1)?; + let data: Vec = row.get(0)?; + let identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + + Ok(identity) + })?; + + let identities: rusqlite::Result> = identity_iter.collect(); + identities + } + + pub fn get_local_voting_identities( + &self, + app_context: &AppContext, + ) -> rusqlite::Result> { + let network = app_context.network_string(); + + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT data FROM identity WHERE is_local = 1 AND network = ? AND identity_type != 'User' AND data IS NOT NULL", + )?; + let identity_iter = stmt.query_map(params![network], |row| { + let data: Vec = row.get(0)?; + let identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + + Ok(identity) + })?; + + let identities: rusqlite::Result> = identity_iter.collect(); + identities + } + + pub fn get_local_user_identities( + &self, + app_context: &AppContext, + ) -> rusqlite::Result> { + let network = app_context.network_string(); + + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT data FROM identity WHERE is_local = 1 AND network = ? AND identity_type = 'User' AND data IS NOT NULL", + )?; + let identity_iter = stmt.query_map(params![network], |row| { + let data: Vec = row.get(0)?; let identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); Ok(identity) diff --git a/src/database/initialization.rs b/src/database/initialization.rs index c47babbbf..25f0b5d02 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -38,15 +38,22 @@ impl Database { [], )?; + // Create the composite index for faster querying + self.execute( + "CREATE INDEX IF NOT EXISTS idx_identity_local_network_type + ON identity (is_local, network, identity_type)", + [], + )?; + // Create the contested names table self.execute( "CREATE TABLE IF NOT EXISTS contested_name ( - normalized_contested_name TEXT, + normalized_contested_name TEXT NOT NULL, locked_votes INTEGER, abstain_votes INTEGER, - winner_type INTEGER NOT NULL, awarded_to BLOB, - ending_time INTEGER, + end_time INTEGER, + locked INTEGER NOT NULL DEFAULT 0, last_updated INTEGER, network TEXT NOT NULL, PRIMARY KEY (normalized_contested_name, network) @@ -57,8 +64,8 @@ impl Database { // Create the contestants table self.execute( "CREATE TABLE IF NOT EXISTS contestant ( - contest_id TEXT, - identity_id BLOB, + normalized_contested_name TEXT NOT NULL, + identity_id BLOB NOT NULL, name TEXT, votes INTEGER, created_at INTEGER, @@ -66,9 +73,8 @@ impl Database { created_at_core_block_height INTEGER, document_id BLOB, network TEXT NOT NULL, - PRIMARY KEY (contest_id, identity_id, network), - FOREIGN KEY (contest_id) REFERENCES contested_names(contest_id), - FOREIGN KEY (identity_id) REFERENCES identities(id) + PRIMARY KEY (normalized_contested_name, identity_id, network), + FOREIGN KEY (normalized_contested_name, network) REFERENCES contested_name(normalized_contested_name, network) ON DELETE CASCADE )", [], )?; diff --git a/src/logging.rs b/src/logging.rs index 912b1d93e..0304db12e 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -10,7 +10,7 @@ pub fn initialize_logger() { }; let filter = EnvFilter::try_new( - "debug,dash_sdk=trace,tenderdash_abci=trace,drive=trace,drive_proof_verifier=trace,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/model/contested_name.rs b/src/model/contested_name.rs index 13e94d207..c3d89108a 100644 --- a/src/model/contested_name.rs +++ b/src/model/contested_name.rs @@ -5,6 +5,24 @@ use dash_sdk::dpp::prelude::{BlockHeight, CoreBlockHeight, Identifier}; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use std::collections::BTreeMap; +#[derive(Debug, Encode, Decode, Clone)] +pub enum ContestState { + Unknown, + Joinable, + Ongoing, + WonBy(Identifier), + Locked, +} + +impl ContestState { + pub fn state_is_votable(&self) -> bool { + match self { + ContestState::Joinable | ContestState::Ongoing => true, + _ => false, + } + } +} + #[derive(Debug, Encode, Decode, Clone)] pub struct ContestedName { pub normalized_contested_name: String, @@ -12,7 +30,8 @@ pub struct ContestedName { pub locked_votes: Option, pub abstain_votes: Option, pub awarded_to: Option, - pub ending_time: Option, + pub end_time: Option, + pub state: ContestState, pub last_updated: Option, pub my_votes: BTreeMap<(Identifier, EncryptedPrivateKeyTarget, KeyID), ResourceVoteChoice>, } diff --git a/src/model/qualified_identity.rs b/src/model/qualified_identity.rs index 1c4e5cec1..238fca6fe 100644 --- a/src/model/qualified_identity.rs +++ b/src/model/qualified_identity.rs @@ -13,6 +13,7 @@ use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicK use dash_sdk::dpp::identity::signer::Signer; use dash_sdk::dpp::identity::KeyType::{BIP13_SCRIPT_HASH, ECDSA_HASH160}; use dash_sdk::dpp::identity::{Identity, KeyID, KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::platform_value::BinaryData; use dash_sdk::dpp::state_transition::errors::InvalidIdentityPublicKeyTypeError; use dash_sdk::dpp::{bls_signatures, ed25519_dalek, ProtocolError}; @@ -27,6 +28,16 @@ pub enum IdentityType { Evonode, } +impl IdentityType { + pub fn vote_strength(&self) -> u64 { + match self { + IdentityType::User => 1, + IdentityType::Masternode => 1, + IdentityType::Evonode => 4, + } + } +} + impl Display for IdentityType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -141,6 +152,19 @@ impl QualifiedIdentity { .0 } + pub fn display_string(&self) -> String { + self.alias + .clone() + .unwrap_or(self.identity.id().to_string(Encoding::Base58)) + } + + pub fn display_short_string(&self) -> String { + self.alias.clone().unwrap_or_else(|| { + let id_str = self.identity.id().to_string(Encoding::Base58); + id_str.chars().take(5).collect() + }) + } + pub fn masternode_payout_address(&self, network: Network) -> Option
{ self.identity .get_first_public_key_matching( diff --git a/src/platform/contested_names/mod.rs b/src/platform/contested_names/mod.rs index fdc50f180..439ea59c2 100644 --- a/src/platform/contested_names/mod.rs +++ b/src/platform/contested_names/mod.rs @@ -5,6 +5,8 @@ mod vote_on_dpns_name; use crate::app::TaskResult; use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::platform::BackendTaskSuccessResult; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dash_sdk::Sdk; use std::sync::Arc; @@ -14,7 +16,7 @@ use tokio::sync::mpsc; pub(crate) enum ContestedResourceTask { QueryDPNSContestedResources, QueryDPNSVoteContenders(String), - VoteOnDPNSName(String, ResourceVoteChoice), + VoteOnDPNSName(String, ResourceVoteChoice, Vec), } impl AppContext { @@ -23,17 +25,19 @@ impl AppContext { task: ContestedResourceTask, sdk: &Sdk, sender: mpsc::Sender, - ) -> Result<(), String> { + ) -> Result { let sdk = sdk.clone(); match &task { - ContestedResourceTask::QueryDPNSContestedResources => { - self.query_dpns_contested_resources(sdk, sender).await - } - ContestedResourceTask::QueryDPNSVoteContenders(name) => { - self.query_dpns_vote_contenders(name, sdk, sender).await - } - ContestedResourceTask::VoteOnDPNSName(name, vote_choice) => { - self.vote_on_dpns_name(name, *vote_choice, sdk, sender) + ContestedResourceTask::QueryDPNSContestedResources => self + .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::VoteOnDPNSName(name, vote_choice, voters) => { + self.vote_on_dpns_name(name, *vote_choice, voters, sdk, sender) .await } // ContestedResourceTask::VoteOnContestedResource(vote_poll, vote_choice) => { // let mut vote = Vote::default(); diff --git a/src/platform/contested_names/vote_on_dpns_name.rs b/src/platform/contested_names/vote_on_dpns_name.rs index f1393ae7c..6049d5086 100644 --- a/src/platform/contested_names/vote_on_dpns_name.rs +++ b/src/platform/contested_names/vote_on_dpns_name.rs @@ -1,5 +1,7 @@ use crate::app::TaskResult; use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::platform::BackendTaskSuccessResult; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; @@ -19,20 +21,24 @@ impl AppContext { self: &Arc, name: &String, vote_choice: ResourceVoteChoice, + voters: &Vec, sdk: Sdk, - sender: mpsc::Sender, - ) -> Result<(), String> { - let qualified_identities = self.load_local_qualified_identities().unwrap_or_default(); - + _sender: mpsc::Sender, + ) -> Result { + // Fetch DPNS contract and document type information let data_contract = self.dpns_contract.as_ref(); let document_type = data_contract .document_type_for_name("domain") .expect("expected document type"); + let Some(contested_index) = document_type.find_contested_index() else { return Err("No contested index on dpns domains".to_string()); }; - let index_values = [Value::from("dash"), Value::Text(name.clone())]; // hardcoded for dpns + // Hardcoded values for DPNS + let index_values = [Value::from("dash"), Value::Text(name.clone())]; + + // Create the vote poll to use in the vote let vote_poll = ContestedDocumentResourceVotePoll { index_name: contested_index.name.clone(), index_values: index_values.to_vec(), @@ -40,24 +46,50 @@ impl AppContext { contract_id: data_contract.id(), }; - for qualified_identity in qualified_identities.iter().take(1) { + let mut vote_results = vec![]; + let mut strength = 0; + + // Iterate over the provided voters (QualifiedIdentity) + for qualified_identity in voters.iter() { if let Some((_, public_key)) = &qualified_identity.associated_voter_identity { + // Create the resource vote let resource_vote = ResourceVoteV0 { vote_poll: vote_poll.clone().into(), resource_vote_choice: vote_choice, }; let vote = Vote::ResourceVote(ResourceVote::V0(resource_vote)); - vote.put_to_platform_and_wait_for_response( - qualified_identity.identity.id(), - public_key, - &sdk, - qualified_identity, - None, - ) - .await - .map_err(|e| format!("Error voting: {}", e))?; + + // Submit the vote to the platform and await a response + let result = vote + .put_to_platform_and_wait_for_response( + qualified_identity.identity.id(), + public_key, + &sdk, + qualified_identity, + None, + ) + .await + .map_err(|e| format!("Error voting: {}", e))?; + + strength += qualified_identity.identity_type.vote_strength(); + vote_results.push(result); + } else { + return Err(format!( + "No associated voter identity for qualified identity: {:?}", + qualified_identity.identity.id() + )); } } - Ok(()) + + self.db + .update_vote_count( + name, + self.network.to_string().as_str(), + strength, + vote_choice, + ) + .map_err(|e| format!("error updating ending time: {}", e))?; + + Ok(BackendTaskSuccessResult::SuccessfulVotes(vote_results)) } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 8de527035..8ae370d57 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -5,6 +5,7 @@ use crate::platform::contract::ContractTask; use crate::platform::core::{CoreItem, CoreTask}; use crate::platform::document::DocumentTask; use crate::platform::identity::IdentityTask; +use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::query_types::Documents; use std::sync::Arc; use tokio::sync::mpsc; @@ -30,6 +31,7 @@ pub(crate) enum BackendTaskSuccessResult { Message(String), Documents(Documents), CoreItem(CoreItem), + SuccessfulVotes(Vec), } impl BackendTaskSuccessResult {} @@ -57,10 +59,10 @@ impl AppContext { .run_contract_task(contract_task, &sdk) .await .map(|_| BackendTaskSuccessResult::None), - BackendTask::ContestedResourceTask(contested_resource_task) => self - .run_contested_resource_task(contested_resource_task, &sdk, sender) - .await - .map(|_| BackendTaskSuccessResult::None), + BackendTask::ContestedResourceTask(contested_resource_task) => { + self.run_contested_resource_task(contested_resource_task, &sdk, sender) + .await + } BackendTask::IdentityTask(identity_task) => self .run_identity_task(identity_task, &sdk) .await diff --git a/src/ui/document_query_screen.rs b/src/ui/document_query_screen.rs index 93605978a..3f9226a73 100644 --- a/src/ui/document_query_screen.rs +++ b/src/ui/document_query_screen.rs @@ -43,7 +43,7 @@ pub struct DocumentQueryScreen { impl DocumentQueryScreen { pub fn new(app_context: &Arc) -> Self { let contested_names = Arc::new(Mutex::new( - app_context.load_contested_names().unwrap_or_default(), + app_context.all_contested_names().unwrap_or_default(), )); Self { contested_names, @@ -85,6 +85,7 @@ impl DocumentQueryScreen { ContestedResourceTask::VoteOnDPNSName( contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Abstain, + vec![], ), )); } @@ -100,7 +101,7 @@ impl DocumentQueryScreen { .cmp(&b.normalized_contested_name), SortColumn::LockedVotes => a.locked_votes.cmp(&b.locked_votes), SortColumn::AbstainVotes => a.abstain_votes.cmp(&b.abstain_votes), - SortColumn::EndingTime => a.ending_time.cmp(&b.ending_time), + SortColumn::EndingTime => a.end_time.cmp(&b.end_time), SortColumn::LastUpdated => a.last_updated.cmp(&b.last_updated), }; @@ -163,7 +164,7 @@ impl DocumentQueryScreen { impl ScreenLike for DocumentQueryScreen { fn refresh(&mut self) { let mut contested_names = self.contested_names.lock().unwrap(); - *contested_names = self.app_context.load_contested_names().unwrap_or_default(); + *contested_names = self.app_context.all_contested_names().unwrap_or_default(); } fn display_message(&mut self, message: &str, message_type: MessageType) { diff --git a/src/ui/dpns_contested_names_screen.rs b/src/ui/dpns_contested_names_screen.rs index 60d8121a2..701624ada 100644 --- a/src/ui/dpns_contested_names_screen.rs +++ b/src/ui/dpns_contested_names_screen.rs @@ -1,19 +1,24 @@ +use super::{Screen, ScreenType}; use crate::app::{AppAction, DesiredAppAction}; use crate::context::AppContext; use crate::model::contested_name::ContestedName; +use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; use crate::platform::contested_names::ContestedResourceTask; use crate::platform::BackendTask; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; +use crate::ui::identities::add_existing_identity_screen::AddExistingIdentityScreen; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use 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 egui::{Context, Frame, Margin, Ui}; use egui_extras::{Column, TableBuilder}; +use itertools::Itertools; use std::sync::{Arc, Mutex}; - -use super::ScreenType; +use tracing::error; #[derive(Clone, Copy, PartialEq, Eq)] enum SortColumn { @@ -31,26 +36,42 @@ enum SortOrder { } pub struct DPNSContestedNamesScreen { + // No need for Mutex as this can only refresh when entering screen + voting_identities: Arc>, + user_identities: Arc>, contested_names: Arc>>, pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, sort_column: SortColumn, sort_order: SortOrder, - show_vote_popup: Option<(String, ContestedResourceTask)>, + show_vote_popup_info: Option<(String, ContestedResourceTask)>, } impl DPNSContestedNamesScreen { pub fn new(app_context: &Arc) -> Self { let contested_names = Arc::new(Mutex::new( - app_context.load_contested_names().unwrap_or_default(), + app_context.ongoing_contested_names().unwrap_or_else(|e| { + error!("Failed to load contested names: {:?}", e); + Vec::new() // Use default value if loading fails + }), )); + let voting_identities = app_context + .db + .get_local_voting_identities(&app_context) + .unwrap_or_default(); + let user_identities = app_context + .db + .get_local_user_identities(&app_context) + .unwrap_or_default(); Self { + voting_identities: Arc::new(voting_identities), + user_identities: Arc::new(user_identities), contested_names, app_context: app_context.clone(), error_message: None, sort_column: SortColumn::ContestedName, sort_order: SortOrder::Ascending, - show_vote_popup: None, + show_vote_popup_info: None, } } @@ -75,14 +96,14 @@ impl DPNSContestedNamesScreen { }; if ui.button(text).clicked() { - self.show_vote_popup = Some(( + self.show_vote_popup_info = Some(( format!( - "Confirm Voting for Contestant {} for name \"{}\"", + "Confirm Voting for Contestant {} for name \"{}\".\n\nSelect the identity to vote with:", contestant.id, contestant.name ), ContestedResourceTask::VoteOnDPNSName( contested_name.normalized_contested_name.clone(), - ResourceVoteChoice::TowardsIdentity(contestant.id), + ResourceVoteChoice::TowardsIdentity(contestant.id),vec![] ), )); } @@ -98,7 +119,7 @@ impl DPNSContestedNamesScreen { .cmp(&b.normalized_contested_name), SortColumn::LockedVotes => a.locked_votes.cmp(&b.locked_votes), SortColumn::AbstainVotes => a.abstain_votes.cmp(&b.abstain_votes), - SortColumn::EndingTime => a.ending_time.cmp(&b.ending_time), + SortColumn::EndingTime => a.end_time.cmp(&b.end_time), SortColumn::LastUpdated => a.last_updated.cmp(&b.last_updated), }; @@ -138,30 +159,282 @@ impl DPNSContestedNamesScreen { } } + fn render_table(&mut self, ui: &mut Ui) { + // Clone the contested names vector to avoid holding the lock during UI rendering + let contested_names = { + let contested_names_guard = self.contested_names.lock().unwrap(); + let mut contested_names = contested_names_guard.clone(); + self.sort_contested_names(&mut contested_names); + contested_names + }; + + 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)) // Contested Name + .column(Column::initial(100.0).resizable(true)) // Locked Votes + .column(Column::initial(100.0).resizable(true)) // Abstain Votes + .column(Column::initial(200.0).resizable(true)) // Ending Time + .column(Column::initial(200.0).resizable(true)) // Last Updated + .column(Column::remainder()) // Contestants + .header(30.0, |mut header| { + header.col(|ui| { + if ui.button("Contested Name").clicked() { + self.toggle_sort(SortColumn::ContestedName); + } + }); + header.col(|ui| { + if ui.button("Locked Votes").clicked() { + self.toggle_sort(SortColumn::LockedVotes); + } + }); + header.col(|ui| { + if ui.button("Abstain Votes").clicked() { + self.toggle_sort(SortColumn::AbstainVotes); + } + }); + header.col(|ui| { + if ui.button("Ending Time").clicked() { + self.toggle_sort(SortColumn::EndingTime); + } + }); + header.col(|ui| { + if ui.button("Last Updated").clicked() { + self.toggle_sort(SortColumn::LastUpdated); + } + }); + header.col(|ui| { + ui.heading("Contestants"); + }); + }) + .body(|mut body| { + for contested_name in &contested_names { + body.row(25.0, |mut row| { + let locked_votes = contested_name.locked_votes.unwrap_or(0); + + // Find the highest contestant votes, if any + let max_contestant_votes = contested_name + .contestants + .as_ref() + .map(|contestants| { + contestants + .iter() + .map(|c| c.votes) + .max() + .unwrap_or(0) + }) + .unwrap_or(0); + + // Determine if locked votes have strict priority + let is_locked_votes_bold = + locked_votes > max_contestant_votes; + + row.col(|ui| { + ui.label(&contested_name.normalized_contested_name); + }); + row.col(|ui| { + let label_text = if let Some(locked_votes) = + contested_name.locked_votes + { + let label_text = format!("{}", locked_votes); + if is_locked_votes_bold { + egui::RichText::new(label_text).strong() + } else { + egui::RichText::new(label_text) + } + } else { + egui::RichText::new("Fetching".to_string()) + }; + // Vote button logic for locked votes + if ui.button(label_text).clicked() { + self.show_vote_popup_info = Some((format!("Confirm Voting to Lock the name \"{}\".\n\nSelect the identity to vote with:", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Lock, vec![]))); + } + }); + row.col(|ui| { + let label_text = if let Some(abstain_votes) = + contested_name.abstain_votes + { + format!("{}", abstain_votes) + } else { + "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![]))); + } + }); + row.col(|ui| { + if let Some(ending_time) = contested_name.end_time { + // Convert the timestamp to a DateTime object using timestamp_millis_opt + if let LocalResult::Single(datetime) = + Utc.timestamp_millis_opt(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!("{} ({})", iso_date, relative_time); + + ui.label(display_text); + } else { + // Handle case where the timestamp is invalid + ui.label("Invalid timestamp"); + } + } else { + ui.label("Fetching"); + } + }); + row.col(|ui| { + if let Some(last_updated) = contested_name.last_updated + { + // Convert the timestamp to a DateTime object using timestamp_millis_opt + if let LocalResult::Single(datetime) = + Utc.timestamp_opt(last_updated as i64, 0) + { + // Use chrono-humanize to get the relative time + let relative_time = + HumanTime::from(datetime).to_string(); + + ui.label(relative_time); + } else { + // Handle case where the timestamp is invalid + ui.label("Invalid timestamp"); + } + } else { + ui.label("Fetching"); + } + }); + row.col(|ui| { + self.show_contested_name_details( + ui, + contested_name, + is_locked_votes_bold, + max_contestant_votes, + ); + }); + }); + } + }); + }); + }); + } + fn show_vote_popup(&mut self, ui: &mut Ui) -> AppAction { let mut app_action = AppAction::None; - if let Some((message, action)) = self.show_vote_popup.clone() { + if self.voting_identities.is_empty() { + ui.label("Please load an Evonode or Masternode first before voting"); + if ui.button("I want to load one now").clicked() { + self.show_vote_popup_info = None; + let mut screen = AddExistingIdentityScreen::new(&self.app_context); + screen.identity_type = IdentityType::Evonode; + app_action = AppAction::AddScreen(Screen::AddExistingIdentityScreen(screen)); + } + if ui.button("Cancel").clicked() { + self.show_vote_popup_info = None; + } + } else if let Some((message, action)) = self.show_vote_popup_info.clone() { ui.label(message); ui.horizontal(|ui| { - if ui.button("Vote Immediate").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask(action)); - self.show_vote_popup = None; - } else if ui.button("Vote Deferred").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask(action)); - self.show_vote_popup = None; - } else if ui.button("Cancel").clicked() { - self.show_vote_popup = None; + // Only modify `voters` if `action` is `VoteOnDPNSName` + if let ContestedResourceTask::VoteOnDPNSName( + contested_name, + vote_choice, + 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()); + + // Create a new `VoteOnDPNSName` task with updated 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; + } + } + + // Vote with all identities + if ui.button("All").clicked() { + for identity in self.voting_identities.iter() { + voters.push(identity.clone()); + } + + // 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; } }); } + app_action } } + impl ScreenLike for DPNSContestedNamesScreen { fn refresh(&mut self) { let mut contested_names = self.contested_names.lock().unwrap(); - *contested_names = self.app_context.load_contested_names().unwrap_or_default(); + *contested_names = self + .app_context + .ongoing_contested_names() + .unwrap_or_default(); + } + + fn refresh_on_arrival(&mut self) { + self.voting_identities = self + .app_context + .db + .get_local_voting_identities(&self.app_context) + .unwrap_or_default() + .into(); + + self.user_identities = self + .app_context + .db + .get_local_user_identities(&self.app_context) + .unwrap_or_default() + .into(); } fn display_message(&mut self, message: &str, message_type: MessageType) { @@ -170,22 +443,29 @@ impl ScreenLike for DPNSContestedNamesScreen { fn ui(&mut self, ctx: &Context) -> AppAction { self.check_error_expiration(); - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![("Dash Evo Tool", AppAction::None)], + let has_identity_that_can_register = !self.user_identities.is_empty(); + let query = ( + "Refresh", + DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( + ContestedResourceTask::QueryDPNSContestedResources, + )), + ); + let right_buttons = if has_identity_that_can_register { vec![ ( "Register Name", DesiredAppAction::AddScreenType(ScreenType::RegisterDpnsName), ), - ( - "Refresh", - DesiredAppAction::BackendTask(BackendTask::ContestedResourceTask( - ContestedResourceTask::QueryDPNSContestedResources, - )), - ), - ], + query, + ] + } else { + vec![query] + }; + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![("Dash Evo Tool", AppAction::None)], + right_buttons, ); action |= add_left_panel( @@ -194,14 +474,6 @@ impl ScreenLike for DPNSContestedNamesScreen { RootScreenType::RootScreenDPNSContestedNames, ); - // Clone the contested names vector to avoid holding the lock during UI rendering - let contested_names = { - let contested_names_guard = self.contested_names.lock().unwrap(); - let mut contested_names = contested_names_guard.clone(); - self.sort_contested_names(&mut contested_names); - contested_names - }; - // Render the UI with the cloned contested_names vector egui::CentralPanel::default().show(ctx, |ui| { let error_message = self.error_message.clone(); @@ -231,7 +503,7 @@ impl ScreenLike for DPNSContestedNamesScreen { } // Show vote popup if active - if self.show_vote_popup.is_some() { + if self.show_vote_popup_info.is_some() { egui::Window::new("Vote Confirmation") .collapsible(false) .show(ui.ctx(), |ui| { @@ -239,171 +511,7 @@ impl ScreenLike for DPNSContestedNamesScreen { }); } - 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)) // Contested Name - .column(Column::initial(100.0).resizable(true)) // Locked Votes - .column(Column::initial(100.0).resizable(true)) // Abstain Votes - .column(Column::initial(200.0).resizable(true)) // Ending Time - .column(Column::initial(200.0).resizable(true)) // Last Updated - .column(Column::remainder()) // Contestants - .header(30.0, |mut header| { - header.col(|ui| { - if ui.button("Contested Name").clicked() { - self.toggle_sort(SortColumn::ContestedName); - } - }); - header.col(|ui| { - if ui.button("Locked Votes").clicked() { - self.toggle_sort(SortColumn::LockedVotes); - } - }); - header.col(|ui| { - if ui.button("Abstain Votes").clicked() { - self.toggle_sort(SortColumn::AbstainVotes); - } - }); - header.col(|ui| { - if ui.button("Ending Time").clicked() { - self.toggle_sort(SortColumn::EndingTime); - } - }); - header.col(|ui| { - if ui.button("Last Updated").clicked() { - self.toggle_sort(SortColumn::LastUpdated); - } - }); - header.col(|ui| { - ui.heading("Contestants"); - }); - }) - .body(|mut body| { - for contested_name in &contested_names { - body.row(25.0, |mut row| { - let locked_votes = contested_name.locked_votes.unwrap_or(0); - - // Find the highest contestant votes, if any - let max_contestant_votes = contested_name - .contestants - .as_ref() - .map(|contestants| { - contestants - .iter() - .map(|c| c.votes) - .max() - .unwrap_or(0) - }) - .unwrap_or(0); - - // Determine if locked votes have strict priority - let is_locked_votes_bold = - locked_votes > max_contestant_votes; - - row.col(|ui| { - ui.label(&contested_name.normalized_contested_name); - }); - row.col(|ui| { - let label_text = if let Some(locked_votes) = - contested_name.locked_votes - { - let label_text = format!("{}", locked_votes); - if is_locked_votes_bold { - egui::RichText::new(label_text).strong() - } else { - egui::RichText::new(label_text) - } - } else { - egui::RichText::new("Fetching".to_string()) - }; - // Vote button logic for locked votes - if ui.button(label_text).clicked() { - self.show_vote_popup = Some((format!("Confirm Voting to Lock the name \"{}\"", contested_name.normalized_contested_name.clone()), ContestedResourceTask::VoteOnDPNSName(contested_name.normalized_contested_name.clone(), ResourceVoteChoice::Lock))); - } - }); - row.col(|ui| { - let label_text = if let Some(abstain_votes) = - contested_name.abstain_votes - { - format!("{}", abstain_votes) - } else { - "Fetching".to_string() - }; - if ui.button(label_text).clicked() { - self.show_vote_popup = 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))); - } - }); - row.col(|ui| { - if let Some(ending_time) = contested_name.ending_time { - // Convert the timestamp to a DateTime object using timestamp_millis_opt - if let LocalResult::Single(datetime) = - Utc.timestamp_millis_opt(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!("{} ({})", iso_date, relative_time); - - ui.label(display_text); - } else { - // Handle case where the timestamp is invalid - ui.label("Invalid timestamp"); - } - } else { - ui.label("Fetching"); - } - }); - row.col(|ui| { - if let Some(last_updated) = contested_name.last_updated - { - // Convert the timestamp to a DateTime object using timestamp_millis_opt - if let LocalResult::Single(datetime) = - Utc.timestamp_opt(last_updated as i64, 0) - { - // Use chrono-humanize to get the relative time - let relative_time = - HumanTime::from(datetime).to_string(); - - ui.label(relative_time); - } else { - // Handle case where the timestamp is invalid - ui.label("Invalid timestamp"); - } - } else { - ui.label("Fetching"); - } - }); - row.col(|ui| { - self.show_contested_name_details( - ui, - contested_name, - is_locked_votes_bold, - max_contestant_votes, - ); - }); - }); - } - }); - }); - }); + self.render_table(ui); }); action diff --git a/src/ui/identities/add_existing_identity_screen.rs b/src/ui/identities/add_existing_identity_screen.rs index 7a861180f..e5f3234a5 100644 --- a/src/ui/identities/add_existing_identity_screen.rs +++ b/src/ui/identities/add_existing_identity_screen.rs @@ -80,7 +80,7 @@ pub enum AddIdentityStatus { pub struct AddExistingIdentityScreen { identity_id_input: String, - identity_type: IdentityType, + pub identity_type: IdentityType, alias_input: String, voting_private_key_input: String, owner_private_key_input: String, @@ -232,12 +232,12 @@ impl AddExistingIdentityScreen { } impl ScreenLike for AddExistingIdentityScreen { - fn display_message(&mut self, message: &str, message_type: MessageType) { - if message_type == MessageType::Info && message == "Success" { - self.add_identity_status = AddIdentityStatus::Complete; - } else { - self.add_identity_status = AddIdentityStatus::ErrorMessage(message.to_string()); - } + fn display_message(&mut self, message: &str, _message_type: MessageType) { + self.add_identity_status = AddIdentityStatus::ErrorMessage(message.to_string()); + } + + fn pop_on_success(&mut self) { + self.add_identity_status = AddIdentityStatus::Complete; } fn ui(&mut self, ctx: &Context) -> AppAction { diff --git a/src/ui/identities/register_dpns_name_screen.rs b/src/ui/identities/register_dpns_name_screen.rs index 04d598b67..2ceb82bd2 100644 --- a/src/ui/identities/register_dpns_name_screen.rs +++ b/src/ui/identities/register_dpns_name_screen.rs @@ -5,10 +5,16 @@ use crate::platform::identity::{IdentityTask, RegisterDpnsNameInput}; use crate::platform::BackendTask; use crate::ui::components::top_panel::add_top_panel; use crate::ui::{MessageType, ScreenLike}; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::identity::TimestampMillis; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::{Purpose, SecurityLevel, TimestampMillis}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::platform::IdentityPublicKey; use eframe::egui::Context; +use egui::ahash::HashMap; +use futures::StreamExt; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -20,8 +26,8 @@ pub enum RegisterDpnsNameStatus { } pub struct RegisterDpnsNameScreen { - qualified_identities: Vec, - selected_qualified_identity: Option, + qualified_identities: Vec<(QualifiedIdentity, Vec)>, + selected_qualified_identity: Option<(QualifiedIdentity, Vec)>, name_input: String, register_dpns_name_status: RegisterDpnsNameStatus, pub app_context: Arc, @@ -29,9 +35,36 @@ pub struct RegisterDpnsNameScreen { impl RegisterDpnsNameScreen { pub fn new(app_context: &Arc) -> Self { - let qualified_identities = app_context + let security_level_of_contract = app_context + .dpns_contract + .document_type_for_name("domain") + .unwrap() + .security_level_requirement(); + let security_level_requirements = SecurityLevel::CRITICAL..=security_level_of_contract; + + let qualified_identities: Vec<_> = app_context .load_local_qualified_identities() - .unwrap_or_default(); + .unwrap_or_default() + .into_iter() + .filter_map(|e| { + let keys = e + .identity + .public_keys() + .values() + .filter(|key| { + key.purpose() == Purpose::AUTHENTICATION + && security_level_requirements.contains(&key.security_level()) + && !key.is_disabled() + }) + .cloned() + .collect::>(); + if keys.is_empty() { + None + } else { + Some((e, keys)) + } + }) + .collect(); let selected_qualified_identity = qualified_identities.first().cloned(); Self { qualified_identities, @@ -52,9 +85,9 @@ impl RegisterDpnsNameScreen { self.selected_qualified_identity .as_ref() .map(|qi| { - qi.alias + qi.0.alias .as_ref() - .unwrap_or(&qi.identity.id().to_string(Encoding::Base58)) + .unwrap_or(&qi.0.identity.id().to_string(Encoding::Base58)) .clone() }) .unwrap_or_else(|| "Select an identity".to_string()), @@ -67,8 +100,12 @@ impl RegisterDpnsNameScreen { .selectable_value( &mut self.selected_qualified_identity, Some(qualified_identity.clone()), - qualified_identity.alias.as_ref().unwrap_or( - &qualified_identity.identity.id().to_string(Encoding::Base58), + qualified_identity.0.alias.as_ref().unwrap_or( + &qualified_identity + .0 + .identity + .id() + .to_string(Encoding::Base58), ), ) .clicked() @@ -85,7 +122,7 @@ impl RegisterDpnsNameScreen { return AppAction::None; }; let dpns_name_input = RegisterDpnsNameInput { - qualified_identity: qualified_identity.clone(), + qualified_identity: qualified_identity.0.clone(), name_input: self.name_input.trim().to_string(), }; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 516191528..1325f8e5b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -196,9 +196,14 @@ pub enum MessageType { #[enum_dispatch] pub trait ScreenLike { fn refresh(&mut self) {} + fn refresh_on_arrival(&mut self) {} fn ui(&mut self, ctx: &Context) -> AppAction; fn display_message(&mut self, _message: &str, _message_type: MessageType) {} - fn display_task_result(&mut self, _backend_task_success_result: BackendTaskSuccessResult) {} + fn display_task_result(&mut self, _backend_task_success_result: BackendTaskSuccessResult) { + self.display_message("Success", MessageType::Success) + } + + fn pop_on_success(&mut self) {} } // Implement Debug for Screen using the ScreenType