diff --git a/src/ui/identities/register_dpns_name_screen.rs b/src/ui/identities/register_dpns_name_screen.rs index eb5a4cd54..ed32b5ba2 100644 --- a/src/ui/identities/register_dpns_name_screen.rs +++ b/src/ui/identities/register_dpns_name_screen.rs @@ -3,7 +3,9 @@ use crate::backend_task::identity::{IdentityTask, RegisterDpnsNameInput}; use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; use crate::ui::{MessageType, ScreenLike}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; @@ -13,7 +15,9 @@ use dash_sdk::dpp::identity::{Purpose, SecurityLevel, TimestampMillis}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::{Identifier, IdentityPublicKey}; use eframe::egui::Context; +use egui::{Color32, RichText}; use std::sync::Arc; +use std::sync::RwLock; use std::time::{SystemTime, UNIX_EPOCH}; pub enum RegisterDpnsNameStatus { @@ -30,6 +34,10 @@ pub struct RegisterDpnsNameScreen { name_input: String, register_dpns_name_status: RegisterDpnsNameStatus, pub app_context: Arc, + selected_wallet: Option>>, + wallet_password: String, + show_password: bool, + error_message: Option, } impl RegisterDpnsNameScreen { @@ -80,6 +88,11 @@ impl RegisterDpnsNameScreen { }) .collect(); let selected_qualified_identity = qualified_identities.first().cloned(); + let selected_wallet = if let Some(wallet) = app_context.wallets.read().unwrap().first() { + Some(wallet.clone()) + } else { + None + }; let show_identity_selector = qualified_identities.len() > 0; Self { show_identity_selector, @@ -88,49 +101,77 @@ impl RegisterDpnsNameScreen { name_input: String::new(), register_dpns_name_status: RegisterDpnsNameStatus::NotStarted, app_context: app_context.clone(), + selected_wallet, + wallet_password: String::new(), + show_password: false, + error_message: None, } } fn render_identity_id_selection(&mut self, ui: &mut egui::Ui) { - ui.horizontal(|ui| { - ui.label("Identity ID:"); - - // Create a ComboBox for selecting a Qualified Identity - egui::ComboBox::from_label("") - .selected_text( - self.selected_qualified_identity + if self.qualified_identities.len() == 1 { + // Only one identity, display it directly + let qualified_identity = &self.qualified_identities[0]; + ui.horizontal(|ui| { + ui.label("Identity ID:"); + ui.label( + qualified_identity + .0 + .alias .as_ref() - .map(|qi| { - qi.0.alias - .as_ref() - .unwrap_or(&qi.0.identity.id().to_string(Encoding::Base58)) - .clone() - }) - .unwrap_or_else(|| "Select an identity".to_string()), - ) - .show_ui(ui, |ui| { - // Loop through the qualified identities and display each as selectable - for qualified_identity in &self.qualified_identities { - // Display each QualifiedIdentity as a selectable item - if ui - .selectable_value( - &mut self.selected_qualified_identity, - Some(qualified_identity.clone()), - qualified_identity.0.alias.as_ref().unwrap_or( - &qualified_identity - .0 - .identity - .id() - .to_string(Encoding::Base58), - ), - ) - .clicked() - { - self.selected_qualified_identity = Some(qualified_identity.clone()); + .unwrap_or( + &qualified_identity + .0 + .identity + .id() + .to_string(Encoding::Base58), + ) + .clone(), + ); + }); + self.selected_qualified_identity = Some(qualified_identity.clone()); + } else { + // Multiple identities, display ComboBox + ui.horizontal(|ui| { + ui.label("Identity ID:"); + + // Create a ComboBox for selecting a Qualified Identity + egui::ComboBox::from_label("") + .selected_text( + self.selected_qualified_identity + .as_ref() + .map(|qi| { + qi.0.alias + .as_ref() + .unwrap_or(&qi.0.identity.id().to_string(Encoding::Base58)) + .clone() + }) + .unwrap_or_else(|| "Select an identity".to_string()), + ) + .show_ui(ui, |ui| { + // Loop through the qualified identities and display each as selectable + for qualified_identity in &self.qualified_identities { + // Display each QualifiedIdentity as a selectable item + if ui + .selectable_value( + &mut self.selected_qualified_identity, + Some(qualified_identity.clone()), + qualified_identity.0.alias.as_ref().unwrap_or( + &qualified_identity + .0 + .identity + .id() + .to_string(Encoding::Base58), + ), + ) + .clicked() + { + self.selected_qualified_identity = Some(qualified_identity.clone()); + } } - } - }); - }); + }); + }); + } } fn register_dpns_name_clicked(&mut self) -> AppAction { @@ -178,24 +219,77 @@ impl ScreenLike for RegisterDpnsNameScreen { egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Register DPNS Name"); + ui.add_space(10.0); + // Before proceeding, check if we need to render wallet unlock + let (needs_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + if needs_unlock { + if !just_unlocked { + return; + } + } + + // If no identities loaded, give message if self.qualified_identities.is_empty() { - ui.label("No qualified identities available to register a DPNS name."); + ui.colored_label( + egui::Color32::DARK_RED, + "No qualified identities available to register a DPNS name.", + ); return; } - if self.show_identity_selector { - // Select the identity to register the name for - self.render_identity_id_selection(ui); - } + // 1. Select the identity to register the name for + ui.heading("1. Select Identity"); + ui.add_space(5.0); + self.render_identity_id_selection(ui); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); - // Input for the name + // 2. Input for the name + ui.heading("2. Enter the Name to Register:"); + ui.add_space(5.0); ui.horizontal(|ui| { ui.label("Name (without \".dash\"):"); ui.text_edit_singleline(&mut self.name_input); }); + ui.add_space(10.0); + + // Display if the name is contested and the estimated cost + let name = self.name_input.trim(); + if !name.is_empty() && name.len() >= 3 { + if is_contested_name(&name.to_lowercase()) { + ui.colored_label( + egui::Color32::DARK_RED, + "This is a contested name.", + ); + ui.colored_label( + egui::Color32::DARK_RED, + "Cost ≈ 0.2 Dash", + ); + } else { + ui.colored_label( + egui::Color32::DARK_GREEN, + "This is not a contested name.", + ); + ui.colored_label( + egui::Color32::DARK_GREEN, + "Cost ≈ 0.0006 Dash", + ); + } + } - if ui.button("Register Name").clicked() { + ui.add_space(10.0); + + // Register button + let button = egui::Button::new(RichText::new("Register Name").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0) + .min_size(egui::vec2(80.0, 30.0)); + + if ui.add(button).clicked() { // Set the status to waiting and capture the current time let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -205,6 +299,7 @@ impl ScreenLike for RegisterDpnsNameScreen { action = self.register_dpns_name_clicked(); } + // Handle registration status messages match &self.register_dpns_name_status { RegisterDpnsNameStatus::NotStarted => { // Do nothing @@ -246,8 +341,79 @@ impl ScreenLike for RegisterDpnsNameScreen { action = AppAction::PopScreenAndRefresh; } } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // DPNS Name Constraints Explanation + ui.heading("DPNS Name Constraints:"); + ui.add_space(5.0); + ui.label(" • Minimum length: 3 characters"); + ui.label(" • Maximum length: 63 characters"); + ui.label(" • Allowed characters: letters (A-Z, case-insensitive), numbers (0-9), and hyphens (-)"); + ui.label(" • Cannot start or end with a hyphen (-)"); + ui.label(" • Names are case-sensitive"); + + ui.add_space(20.0); + + // Contested Names Explanation + ui.heading("Contested Names Info:"); + ui.add_space(5.0); + ui.label(" • To prevent name front-running, some names are contested and require a higher fee to register."); + ui.label(" • Masternodes vote whether or not to award contested names to contestants."); + ui.label(" • Contests last two weeks and new contenders can only join during the first week."); + ui.label(" • Contested names are those that are:"); + ui.label(" • Less than 20 characters long (i.e. “alice”, “quantumexplorer”)"); + ui.label(" • AND"); + ui.label(" • Contain no numbers or only contain the number(s) 0 and/or 1 (i.e. “bob”, “carol01”)"); }); action } } + +impl ScreenWithWalletUnlock for RegisterDpnsNameScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} + +pub fn is_contested_name(name: &str) -> bool { + let length = name.len(); + if length >= 20 { + return false; + } + for c in name.chars() { + if c.is_digit(10) { + if c != '0' && c != '1' { + return false; + } + } + } + true +}