From 351a3c099448aa2851b1ddb9a15c24d75f6bb042 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 5 Nov 2024 16:36:46 +0100 Subject: [PATCH 1/7] much more work on registering an identity --- Cargo.toml | 1 - src/app.rs | 2 +- src/backend_task/core/mod.rs | 5 +- src/backend_task/identity/mod.rs | 3 +- .../identity/register_identity.rs | 51 +++ src/context.rs | 4 +- src/model/wallet/asset_lock_transaction.rs | 107 ++++- .../by_using_unused_asset_lock.rs | 115 +++++ .../by_using_unused_balance.rs | 70 +++ .../by_wallet_qr_code.rs | 189 ++++++++ .../mod.rs} | 433 ++++++++---------- src/ui/identities/identities_screen.rs | 32 +- src/ui/mod.rs | 186 +++++++- src/ui/wallet/import_wallet_screen.rs | 298 ++++++++++++ src/ui/wallet/mod.rs | 1 + 15 files changed, 1218 insertions(+), 279 deletions(-) create mode 100644 src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs create mode 100644 src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs create mode 100644 src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs rename src/ui/identities/{add_new_identity_screen.rs => add_new_identity_screen/mod.rs} (71%) create mode 100644 src/ui/wallet/import_wallet_screen.rs diff --git a/Cargo.toml b/Cargo.toml index 03f43f0a3..bd603c3e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ sha2 = "0.10.8" arboard = { version = "3.4.0", default-features = false, features = [ "windows-sys", ] } -ambassador = "0.4.1" directories = "5.0" rusqlite = { version = "0.32.1", features = ["functions"]} diff --git a/src/app.rs b/src/app.rs index 78f6ee698..7bdaa26fc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -57,7 +57,7 @@ pub struct AppState { last_repaint: Instant, // Track the last time we requested a repaint } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum DesiredAppAction { None, PopScreen, diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index 7cbd2c984..f291ea0f2 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -4,8 +4,7 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; -use dash_sdk::dpp::dashcore::{ChainLock, Network, OutPoint, Transaction}; -use dash_sdk::platform::proto::Proof; +use dash_sdk::dpp::dashcore::{Address, ChainLock, Network, OutPoint, Transaction}; use std::sync::{Arc, RwLock}; #[derive(Debug, Clone)] @@ -25,7 +24,7 @@ impl PartialEq for CoreTask { #[derive(Debug, Clone, PartialEq)] pub(crate) enum CoreItem { - ReceivedAvailableUTXOTransaction(Transaction, Vec), + ReceivedAvailableUTXOTransaction(Transaction, Vec<(OutPoint, Address)>), ChainLock(ChainLock, Network), } diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index 3a2c8c0b4..73c60aee0 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -16,7 +16,7 @@ use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey}; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::hashes::Hash; -use dash_sdk::dpp::dashcore::Transaction; +use dash_sdk::dpp::dashcore::{OutPoint, ScriptBuf, Transaction}; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; @@ -166,6 +166,7 @@ pub type IdentityIndex = u32; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IdentityRegistrationMethod { UseAssetLock(Address, AssetLockProof, Transaction), + FundWithUtxo(OutPoint, ScriptBuf, Address, IdentityIndex), FundWithWallet(Duffs, IdentityIndex), } diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index f47c37620..7207faff9 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -220,6 +220,57 @@ impl AppContext { tokio::time::sleep(Duration::from_millis(200)).await; } + (asset_lock_proof, asset_lock_proof_private_key, tx_id) + } + IdentityRegistrationMethod::FundWithUtxo( + utxo, + script_pubkey, + input_address, + identity_index, + ) => { + // Scope the write lock to avoid holding it across an await. + let (asset_lock_transaction, asset_lock_proof_private_key) = { + let mut wallet = wallet.write().unwrap(); + wallet.asset_lock_transaction_for_utxo( + sdk.network, + utxo, + script_pubkey, + input_address, + identity_index, + Some(self), + )? + }; + + let tx_id = asset_lock_transaction.txid(); + // todo: maybe one day we will want to use platform again, but for right now we use + // the local core as it is more stable + // let asset_lock_proof = self + // .broadcast_and_retrieve_asset_lock(&asset_lock_transaction, &change_address) + // .await + // .map_err(|e| e.to_string())?; + + { + let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); + proofs.insert(tx_id, None); + } + + self.core_client + .send_raw_transaction(&asset_lock_transaction) + .map_err(|e| e.to_string())?; + + let mut asset_lock_proof; + + loop { + { + let proofs = self.transactions_waiting_for_finality.lock().unwrap(); + if let Some(Some(proof)) = proofs.get(&tx_id) { + asset_lock_proof = proof.clone(); + break; + } + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + (asset_lock_proof, asset_lock_proof_private_key, tx_id) } }; diff --git a/src/context.rs b/src/context.rs index cde37055c..752a8341f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -214,7 +214,7 @@ impl AppContext { tx: &Transaction, islock: Option, chain_locked_height: Option, - ) -> rusqlite::Result> { + ) -> rusqlite::Result> { // Initialize a vector to collect wallet outpoints let mut wallet_outpoints = Vec::new(); @@ -254,7 +254,7 @@ impl AppContext { .insert(out_point.clone(), tx_out.clone()); // Insert the TxOut at the OutPoint // Collect the outpoint - wallet_outpoints.push(out_point.clone()); + wallet_outpoints.push((out_point.clone(), address.clone())); wallet .address_balances diff --git a/src/model/wallet/asset_lock_transaction.rs b/src/model/wallet/asset_lock_transaction.rs index f5cf1c1df..995a10681 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -6,8 +6,9 @@ use dash_sdk::dpp::dashcore::secp256k1::Message; use dash_sdk::dpp::dashcore::sighash::SighashCache; use dash_sdk::dpp::dashcore::transaction::special_transaction::asset_lock::AssetLockPayload; use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayload; -use dash_sdk::dpp::dashcore::{Address, Network, PrivateKey, ScriptBuf, Transaction, TxIn, TxOut}; - +use dash_sdk::dpp::dashcore::{ + Address, Network, OutPoint, PrivateKey, ScriptBuf, Transaction, TxIn, TxOut, +}; impl Wallet { pub fn asset_lock_transaction( &mut self, @@ -134,4 +135,106 @@ impl Wallet { Ok((tx, private_key, change_address)) } + + pub fn asset_lock_transaction_for_utxo( + &mut self, + network: Network, + utxo: OutPoint, + script_pubkey: ScriptBuf, + input_address: Address, + identity_index: u32, + register_addresses: Option<&AppContext>, + ) -> Result<(Transaction, PrivateKey), String> { + let secp = Secp256k1::new(); + let private_key = self.identity_registration_ecdsa_private_key( + network, + identity_index, + register_addresses, + )?; + let asset_lock_public_key = private_key.public_key(&secp); + + let one_time_key_hash = asset_lock_public_key.pubkey_hash(); + let fee = 3_000; + let output_amount = utxo.vout as u64 - fee; + + let payload_output = TxOut { + value: output_amount, + script_pubkey: ScriptBuf::new_p2pkh(&one_time_key_hash), + }; + let burn_output = TxOut { + value: output_amount, + script_pubkey: ScriptBuf::new_op_return(&[]), + }; + let payload = AssetLockPayload { + version: 1, + credit_outputs: vec![payload_output], + }; + + // we need to get all inputs from utxos to add them to the transaction + + let mut tx_in = TxIn::default(); + tx_in.previous_output = utxo.clone(); + + let sighash_u32 = 1u32; + + let mut tx: Transaction = Transaction { + version: 3, + lock_time: 0, + input: vec![tx_in], + output: vec![burn_output], + special_transaction_payload: Some(TransactionPayload::AssetLockPayloadType(payload)), + }; + + let cache = SighashCache::new(&tx); + + // Next, collect the sighashes for each input since that's what we need from the + // cache + let sighashes: Vec<_> = tx + .input + .iter() + .enumerate() + .map(|(i, input)| { + cache + .legacy_signature_hash(i, &script_pubkey, sighash_u32) + .expect("expected sighash") + }) + .collect(); + + // Now we can drop the cache to end the immutable borrow + drop(cache); + + tx.input + .iter_mut() + .zip(sighashes.into_iter()) + .try_for_each(|(input, sighash)| { + // You need to provide the actual script_pubkey of the UTXO being spent + let message = Message::from_digest(sighash.into()); + + let private_key = self + .private_key_for_address(&input_address, network)? + .ok_or("Expected address to be in wallet")?; + + // Sign the message with the private key + let sig = secp.sign_ecdsa(&message, &private_key.inner); + + // Serialize the DER-encoded signature and append the sighash type + let mut serialized_sig = sig.serialize_der().to_vec(); + + let mut sig_script = vec![serialized_sig.len() as u8 + 1]; + + sig_script.append(&mut serialized_sig); + + sig_script.push(1); + + let mut serialized_pub_key = private_key.public_key(&secp).serialize(); + + sig_script.push(serialized_pub_key.len() as u8); + sig_script.append(&mut serialized_pub_key); + // Create script_sig + input.script_sig = ScriptBuf::from_bytes(sig_script); + Ok::<(), String>(()) + })?; + + Ok((tx, private_key)) + } } diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs new file mode 100644 index 000000000..00270f45b --- /dev/null +++ b/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs @@ -0,0 +1,115 @@ +use crate::app::AppAction; +use crate::ui::identities::add_new_identity_screen::{ + AddNewIdentityScreen, AddNewIdentityWalletFundedScreenStep, FundingMethod, +}; +use egui::Ui; + +impl AddNewIdentityScreen { + fn render_choose_funding_asset_lock(&mut self, ui: &mut egui::Ui) { + // Ensure a wallet is selected + let Some(selected_wallet) = self.selected_wallet.clone() else { + ui.label("No wallet selected."); + return; + }; + + // Read the wallet to access unused asset locks + let wallet = selected_wallet.read().unwrap(); + + if wallet.unused_asset_locks.is_empty() { + ui.label("No unused asset locks available."); + return; + } + + ui.heading("Select an unused asset lock:"); + + // Track the index of the currently selected asset lock (if any) + let selected_index = self.funding_asset_lock.as_ref().and_then(|(_, proof, _)| { + wallet + .unused_asset_locks + .iter() + .position(|(_, _, _, _, p)| p.as_ref() == Some(proof)) + }); + + // Display the asset locks in a scrollable area + egui::ScrollArea::vertical().show(ui, |ui| { + for (index, (tx, address, amount, islock, proof)) in + wallet.unused_asset_locks.iter().enumerate() + { + ui.horizontal(|ui| { + let tx_id = tx.txid().to_string(); + let lock_amount = *amount as f64 * 1e-8; // Convert to DASH + let is_locked = if islock.is_some() { "Yes" } else { "No" }; + + // Display asset lock information with "Selected" if this one is selected + let selected_text = if Some(index) == selected_index { + " (Selected)" + } else { + "" + }; + + ui.label(format!( + "TxID: {}, Address: {}, Amount: {:.8} DASH, InstantLock: {}{}", + tx_id, address, lock_amount, is_locked, selected_text + )); + + // Button to select this asset lock + if ui.button("Select").clicked() { + // Update the selected asset lock + self.funding_asset_lock = Some(( + tx.clone(), + proof.clone().expect("Asset lock proof is required"), + address.clone(), + )); + + // Update the step to ready to create identity + let mut step = self.step.write().unwrap(); + *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; + } + }); + + ui.add_space(5.0); // Add space between each entry + } + }); + } + + pub fn render_ui_by_using_unused_asset_lock( + &mut self, + ui: &mut Ui, + mut step_number: u32, + ) -> AppAction { + let mut action = AppAction::None; + + // Extract the step from the RwLock to minimize borrow scope + let step = self.step.read().unwrap().clone(); + + ui.heading( + format!( + "{}. Choose the unused asset lock that you would like to use.", + step_number + ) + .as_str(), + ); + ui.add_space(10.0); + self.render_choose_funding_asset_lock(ui); + step_number += 1; + + if ui.button("Create Identity").clicked() { + action |= self.register_identity_clicked(FundingMethod::UseUnusedAssetLock); + } + + match step { + AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("Waiting for Platform acknowledgement"); + } + AddNewIdentityWalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + _ => {} + } + + if let Some(error_message) = self.error_message.as_ref() { + ui.heading(error_message); + } + action + } +} diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs new file mode 100644 index 000000000..993fee4c9 --- /dev/null +++ b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs @@ -0,0 +1,70 @@ +use crate::app::AppAction; +use crate::ui::identities::add_new_identity_screen::{ + AddNewIdentityScreen, AddNewIdentityWalletFundedScreenStep, FundingMethod, +}; +use egui::Ui; + +impl AddNewIdentityScreen { + fn show_wallet_balance(&self, ui: &mut egui::Ui) { + if let Some(selected_wallet) = &self.selected_wallet { + let wallet = selected_wallet.read().unwrap(); // Read lock on the wallet + + let total_balance: u64 = wallet.max_balance(); // Sum up all the balances + + let dash_balance = total_balance as f64 * 1e-8; // Convert to DASH units + + ui.horizontal(|ui| { + ui.label(format!("Wallet Balance: {:.8} DASH", dash_balance)); + }); + } else { + ui.label("No wallet selected"); + } + } + + pub fn render_ui_by_using_unused_balance( + &mut self, + ui: &mut Ui, + mut step_number: u32, + ) -> AppAction { + let mut action = AppAction::None; + + self.show_wallet_balance(ui); + + step_number += 1; + + ui.heading("2. How much of your wallet balance would you like to transfer?"); + step_number += 1; + + self.render_funding_amount_input(ui); + + // Extract the step from the RwLock to minimize borrow scope + let step = self.step.read().unwrap().clone(); + + let Ok(amount_dash) = self.funding_amount.parse::() else { + return action; + }; + + if ui.button("Create Identity").clicked() { + action = self.register_identity_clicked(FundingMethod::UseWalletBalance); + } + + match step { + AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("Waiting for Core Chain to produce proof of transfer of funds"); + } + AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("Waiting for Platform acknowledgement"); + } + AddNewIdentityWalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + _ => {} + } + + if let Some(error_message) = self.error_message.as_ref() { + ui.heading(error_message); + } + + action + } +} diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs new file mode 100644 index 000000000..042106260 --- /dev/null +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -0,0 +1,189 @@ +use crate::app::AppAction; +use crate::backend_task::identity::{ + IdentityRegistrationInfo, IdentityRegistrationMethod, IdentityTask, +}; +use crate::backend_task::BackendTask; +use crate::ui::identities::add_new_identity_screen::{ + copy_to_clipboard, generate_qr_code_image, AddNewIdentityScreen, + AddNewIdentityWalletFundedScreenStep, FundingMethod, +}; +use dash_sdk::dashcore_rpc::RpcApi; +use eframe::epaint::TextureHandle; +use egui::Ui; +use std::sync::Arc; + +impl AddNewIdentityScreen { + fn render_qr_code(&mut self, ui: &mut egui::Ui, amount: f64) -> Result<(), String> { + let (address, should_check_balance) = { + // Scope the write lock to ensure it's dropped before calling `start_balance_check`. + + if let Some(wallet_guard) = self.selected_wallet.as_ref() { + // Get the receive address + if self.funding_address.is_none() { + let mut wallet = wallet_guard.write().unwrap(); + let receive_address = wallet + .receive_address(self.app_context.network, Some(&self.app_context))?; + + if let Some(has_address) = self.core_has_funding_address { + if !has_address { + self.app_context + .core_client + .import_address( + &receive_address, + Some("Managed by Dash Evo Tool"), + Some(false), + ) + .map_err(|e| e.to_string())?; + } + self.funding_address = Some(receive_address); + } else { + let info = self + .app_context + .core_client + .get_address_info(&receive_address) + .map_err(|e| e.to_string())?; + + if !(info.is_watchonly || info.is_mine) { + self.app_context + .core_client + .import_address( + &receive_address, + Some("Managed by Dash Evo Tool"), + Some(false), + ) + .map_err(|e| e.to_string())?; + } + self.funding_address = Some(receive_address); + self.core_has_funding_address = Some(true); + } + + // Extract the address to return it outside this scope + (self.funding_address.as_ref().unwrap().clone(), true) + } else { + (self.funding_address.as_ref().unwrap().clone(), false) + } + } else { + return Err("No wallet selected".to_string()); + } + }; + + if should_check_balance { + // Now `address` is available, and all previous borrows are dropped. + self.start_balance_check(&address, ui.ctx()); + } + + let pay_uri = format!("{}?amount={:.4}", address.to_qr_uri(), amount); + + // Generate the QR code image + if let Ok(qr_image) = generate_qr_code_image(&pay_uri) { + let texture: TextureHandle = + ui.ctx() + .load_texture("qr_code", qr_image, egui::TextureOptions::LINEAR); + ui.image(&texture); + } else { + ui.label("Failed to generate QR code."); + } + + ui.add_space(10.0); + ui.label(&pay_uri); + + if ui.button("Copy").clicked() { + if let Err(e) = copy_to_clipboard(pay_uri.as_str()) { + self.copied_to_clipboard = Some(Some(e)); + } else { + self.copied_to_clipboard = Some(None); + } + } + + if let Some(error) = self.copied_to_clipboard.as_ref() { + if let Some(error) = error { + ui.label(format!("Failed to copy to clipboard: {}", error)); + } else { + ui.label("Address copied to clipboard."); + } + } + + Ok(()) + } + + pub fn render_ui_by_wallet_qr_code(&mut self, ui: &mut Ui, mut step_number: u32) -> AppAction { + let mut action = AppAction::None; + + let Some(selected_wallet) = &self.selected_wallet else { + return action; + }; + + // Extract the step from the RwLock to minimize borrow scope + let step = self.step.read().unwrap().clone(); + + let Ok(amount_dash) = self.funding_amount.parse::() else { + return action; + }; + + ui.add_space(10.0); + + ui.heading( + format!( + "{}. Select how much you would like to transfer?", + step_number + ) + .as_str(), + ); + step_number += 1; + + ui.add_space(8.0); + + self.render_funding_amount_input(ui); + + if let Err(e) = self.render_qr_code(ui, amount_dash) { + eprintln!("Error: {:?}", e); + } + + ui.add_space(20.0); + + match step { + AddNewIdentityWalletFundedScreenStep::ChooseFundingMethod => {} + AddNewIdentityWalletFundedScreenStep::WaitingOnFunds => { + ui.heading("Waiting for funds"); + } + AddNewIdentityWalletFundedScreenStep::FundsReceived => { + if let Some((utxo, script_buf, address)) = self.funding_utxo.clone() { + let identity_input = IdentityRegistrationInfo { + alias_input: self.alias_input.clone(), + keys: self.identity_keys.clone(), + wallet: Arc::clone(selected_wallet), // Clone the Arc reference + identity_registration_method: IdentityRegistrationMethod::FundWithUtxo( + utxo, + script_buf, + address, + self.identity_id_number, + ), + }; + + let mut step = self.step.write().unwrap(); + *step = AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock; + + // Create the backend task to register the identity + action |= AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::RegisterIdentity(identity_input), + )) + } + } + AddNewIdentityWalletFundedScreenStep::ReadyToCreate => {} + AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("Waiting for Core Chain to produce proof of transfer of funds"); + } + AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("Waiting for Platform acknowledgement"); + } + AddNewIdentityWalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + } + + if let Some(error_message) = self.error_message.as_ref() { + ui.heading(error_message); + } + action + } +} diff --git a/src/ui/identities/add_new_identity_screen.rs b/src/ui/identities/add_new_identity_screen/mod.rs similarity index 71% rename from src/ui/identities/add_new_identity_screen.rs rename to src/ui/identities/add_new_identity_screen/mod.rs index ba25d11f3..d9272cccf 100644 --- a/src/ui/identities/add_new_identity_screen.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -1,4 +1,9 @@ +mod by_using_unused_asset_lock; +mod by_using_unused_balance; +mod by_wallet_qr_code; + use crate::app::AppAction; +use crate::backend_task::core::CoreItem; use crate::backend_task::identity::{ IdentityKeys, IdentityRegistrationInfo, IdentityRegistrationMethod, IdentityTask, }; @@ -14,7 +19,7 @@ use arboard::Clipboard; use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::balances::credits::Duffs; -use dash_sdk::dpp::dashcore::{PrivateKey, Transaction}; +use dash_sdk::dpp::dashcore::{OutPoint, PrivateKey, ScriptBuf, Transaction, TxOut}; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; use dash_sdk::dpp::prelude::AssetLockProof; use eframe::egui::Context; @@ -23,6 +28,7 @@ use image::Luma; use qrcode::QrCode; use serde::Deserialize; use std::cmp::PartialEq; +use std::ptr::read; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -60,10 +66,12 @@ impl fmt::Display for FundingMethod { #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] pub enum AddNewIdentityWalletFundedScreenStep { ChooseFundingMethod, + WaitingOnFunds, FundsReceived, ReadyToCreate, WaitingForAssetLock, WaitingForPlatformAcceptance, + Success, } pub struct AddNewIdentityScreen { @@ -77,11 +85,14 @@ pub struct AddNewIdentityScreen { funding_method: Arc>, funding_amount: String, funding_amount_exact: Option, + funding_utxo: Option<(OutPoint, ScriptBuf, Address)>, alias_input: String, copied_to_clipboard: Option>, identity_keys: IdentityKeys, balance_check_handle: Option<(Arc, thread::JoinHandle<()>)>, error_message: Option, + show_pop_up_info: Option, + in_key_selection_advanced_mode: bool, pub app_context: Arc, } @@ -127,8 +138,9 @@ impl AddNewIdentityScreen { funding_address: None, funding_address_balance: Arc::new(RwLock::new(None)), funding_method: Arc::new(RwLock::new(FundingMethod::NoSelection)), - funding_amount: "0.2".to_string(), + funding_amount: "0.5".to_string(), funding_amount_exact: None, + funding_utxo: None, alias_input: String::new(), copied_to_clipboard: None, identity_keys: IdentityKeys { @@ -138,6 +150,8 @@ impl AddNewIdentityScreen { }, balance_check_handle: None, error_message: None, + show_pop_up_info: None, + in_key_selection_advanced_mode: false, app_context: app_context.clone(), } } @@ -223,99 +237,6 @@ impl AddNewIdentityScreen { } } - fn render_qr_code(&mut self, ui: &mut egui::Ui, amount: f64) -> Result<(), String> { - let (address, should_check_balance) = { - // Scope the write lock to ensure it's dropped before calling `start_balance_check`. - - if let Some(wallet_guard) = self.selected_wallet.as_ref() { - // Get the receive address - if self.funding_address.is_none() { - let mut wallet = wallet_guard.write().unwrap(); - let receive_address = wallet - .receive_address(self.app_context.network, Some(&self.app_context))?; - - if let Some(has_address) = self.core_has_funding_address { - if !has_address { - self.app_context - .core_client - .import_address( - &receive_address, - Some("Managed by Dash Evo Tool"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - self.funding_address = Some(receive_address); - } else { - let info = self - .app_context - .core_client - .get_address_info(&receive_address) - .map_err(|e| e.to_string())?; - - if !(info.is_watchonly || info.is_mine) { - self.app_context - .core_client - .import_address( - &receive_address, - Some("Managed by Dash Evo Tool"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - self.funding_address = Some(receive_address); - self.core_has_funding_address = Some(true); - } - - // Extract the address to return it outside this scope - (self.funding_address.as_ref().unwrap().clone(), true) - } else { - (self.funding_address.as_ref().unwrap().clone(), false) - } - } else { - return Err("No wallet selected".to_string()); - } - }; - - if should_check_balance { - // Now `address` is available, and all previous borrows are dropped. - self.start_balance_check(&address, ui.ctx()); - } - - let pay_uri = format!("{}?amount={:.4}", address.to_qr_uri(), amount); - - // Generate the QR code image - if let Ok(qr_image) = generate_qr_code_image(&pay_uri) { - let texture: TextureHandle = - ui.ctx() - .load_texture("qr_code", qr_image, egui::TextureOptions::LINEAR); - ui.image(&texture); - } else { - ui.label("Failed to generate QR code."); - } - - ui.add_space(10.0); - ui.label(pay_uri); - - if ui.button("Copy Address").clicked() { - if let Err(e) = copy_to_clipboard(&address.to_qr_uri()) { - self.copied_to_clipboard = Some(Some(e)); - } else { - self.copied_to_clipboard = Some(None); - } - } - - if let Some(error) = self.copied_to_clipboard.as_ref() { - if let Some(error) = error { - ui.label(format!("Failed to copy to clipboard: {}", error)); - } else { - ui.label("Address copied to clipboard."); - } - } - - Ok(()) - } - fn render_identity_index_input(&mut self, ui: &mut egui::Ui) { let mut index_changed = false; // Track if the index has changed @@ -345,21 +266,6 @@ impl AddNewIdentityScreen { } } - fn show_wallet_balance(&self, ui: &mut egui::Ui) { - if let Some(selected_wallet) = &self.selected_wallet { - let wallet = selected_wallet.read().unwrap(); // Read lock on the wallet - - let total_balance: u64 = wallet.max_balance(); // Sum up all the balances - - let dash_balance = total_balance as f64 * 1e-8; // Convert to DASH units - - ui.horizontal(|ui| { - ui.label(format!("Wallet Balance: {:.8} DASH", dash_balance)); - }); - } else { - ui.label("No wallet selected"); - } - } fn render_wallet_selection(&mut self, ui: &mut Ui) -> bool { if self.app_context.has_wallet.load(Ordering::Relaxed) { let wallets = &self.app_context.wallets.read().unwrap(); @@ -402,8 +308,42 @@ impl AddNewIdentityScreen { ui.add_space(10.0); true } else if let Some(wallet) = wallets.first() { - // Automatically select the only available wallet - self.selected_wallet = Some(wallet.clone()); + if self.selected_wallet.is_none() { + // Automatically select the only available wallet + self.selected_wallet = Some(wallet.clone()); + + let wallet = wallet.read().unwrap(); + + self.identity_keys.master_private_key = + Some(wallet.identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 0, + )); + // Update the additional keys input + self.identity_keys.keys_input = vec![ + ( + wallet.identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 1, + ), + KeyType::ECDSA_HASH160, + Purpose::AUTHENTICATION, + SecurityLevel::HIGH, + ), + ( + wallet.identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 2, + ), + KeyType::ECDSA_HASH160, + Purpose::TRANSFER, + SecurityLevel::CRITICAL, + ), + ]; + } false } else { false @@ -462,71 +402,51 @@ impl AddNewIdentityScreen { }); } - fn render_choose_funding_asset_lock(&mut self, ui: &mut egui::Ui) { - // Ensure a wallet is selected - let Some(selected_wallet) = self.selected_wallet.clone() else { - ui.label("No wallet selected."); - return; - }; - - // Read the wallet to access unused asset locks - let wallet = selected_wallet.read().unwrap(); - - if wallet.unused_asset_locks.is_empty() { - ui.label("No unused asset locks available."); - return; - } - - ui.heading("Select an unused asset lock:"); - - // Track the index of the currently selected asset lock (if any) - let selected_index = self.funding_asset_lock.as_ref().and_then(|(_, proof, _)| { - wallet - .unused_asset_locks - .iter() - .position(|(_, _, _, _, p)| p.as_ref() == Some(proof)) - }); - - // Display the asset locks in a scrollable area - egui::ScrollArea::vertical().show(ui, |ui| { - for (index, (tx, address, amount, islock, proof)) in - wallet.unused_asset_locks.iter().enumerate() - { - ui.horizontal(|ui| { - let tx_id = tx.txid().to_string(); - let lock_amount = *amount as f64 * 1e-8; // Convert to DASH - let is_locked = if islock.is_some() { "Yes" } else { "No" }; - - // Display asset lock information with "Selected" if this one is selected - let selected_text = if Some(index) == selected_index { - " (Selected)" - } else { - "" - }; - - ui.label(format!( - "TxID: {}, Address: {}, Amount: {:.8} DASH, InstantLock: {}{}", - tx_id, address, lock_amount, is_locked, selected_text - )); - - // Button to select this asset lock - if ui.button("Select").clicked() { - // Update the selected asset lock - self.funding_asset_lock = Some(( - tx.clone(), - proof.clone().expect("Asset lock proof is required"), - address.clone(), - )); + // Function to render the key selection mode (Default or Advanced) + fn render_key_selection(&mut self, ui: &mut egui::Ui) { + // Provide the selection toggle for Default or Advanced mode + ui.horizontal(|ui| { + ui.label("Key Selection Mode:"); - // Update the step to ready to create identity - let mut step = self.step.write().unwrap(); - *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; + ComboBox::from_id_salt("key_selection_mode") + .selected_text(if self.in_key_selection_advanced_mode { + "Advanced" + } else { + "Default" + }) + .show_ui(ui, |ui| { + if ui + .selectable_label( + !self.in_key_selection_advanced_mode, + "Default (Recommended)", + ) + .clicked() + { + self.in_key_selection_advanced_mode = false; + } + if ui + .selectable_label(self.in_key_selection_advanced_mode, "Advanced") + .clicked() + { + self.in_key_selection_advanced_mode = true; } }); + }); + + ui.add_space(10.0); - ui.add_space(5.0); // Add space between each entry + // Render additional key options only if "Advanced" mode is selected + if self.in_key_selection_advanced_mode { + // Render the master key input + if let Some(master_key) = self.identity_keys.master_private_key { + self.render_master_key(ui, master_key); } - }); + + // Render additional keys input (if any) and allow adding more keys + self.render_keys_input(ui); + } else { + ui.label("Default allows updating the identity, interacting with data contracts, transferring credits to other identities and to the core chain".to_string()); + } } fn render_keys_input(&mut self, ui: &mut egui::Ui) { @@ -540,7 +460,7 @@ impl AddNewIdentityScreen { ui.label(key.to_wif()); // Purpose selection - ComboBox::from_label("Purpose") + ComboBox::from_id_salt(format!("purpose_combo_{}", i)) .selected_text(format!("{:?}", purpose)) .show_ui(ui, |ui| { ui.selectable_value(purpose, Purpose::AUTHENTICATION, "AUTHENTICATION"); @@ -548,7 +468,7 @@ impl AddNewIdentityScreen { }); // Key Type selection with conditional filtering - ComboBox::from_label("Key Type") + ComboBox::from_id_salt(format!("key_type_combo_{}", i)) .selected_text(format!("{:?}", key_type)) .show_ui(ui, |ui| { ui.selectable_value(key_type, KeyType::ECDSA_HASH160, "ECDSA_HASH160"); @@ -562,7 +482,7 @@ impl AddNewIdentityScreen { }); // Security Level selection with conditional filtering - ComboBox::from_label("Security Level") + ComboBox::from_id_salt(format!("security_level_combo_{}", i)) .selected_text(format!("{:?}", security_level)) .show_ui(ui, |ui| { if *purpose == Purpose::TRANSFER { @@ -779,9 +699,23 @@ impl ScreenLike for AddNewIdentityScreen { fn display_message(&mut self, message: &str, _message_type: MessageType) { self.error_message = Some(message.to_string()); } - fn display_task_result(&mut self, _backend_task_success_result: BackendTaskSuccessResult) { + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { let mut step = self.step.write().unwrap(); - *step = AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance; + if *step == AddNewIdentityWalletFundedScreenStep::WaitingOnFunds { + if let Some(funding_address) = self.funding_address.as_ref() { + if let BackendTaskSuccessResult::CoreItem( + CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), + ) = backend_task_success_result + { + for (outpoint, address) in outpoints_with_addresses { + if funding_address == &address { + *step = + AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance; + } + } + } + } + } } fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = add_top_panel( @@ -797,7 +731,7 @@ impl ScreenLike for AddNewIdentityScreen { egui::CentralPanel::default().show(ctx, |ui| { ui.add_space(10.0); ui.heading("Follow these steps to create your identity!"); - ui.add_space(5.0); + ui.add_space(15.0); let mut step_number = 1; @@ -810,104 +744,103 @@ impl ScreenLike for AddNewIdentityScreen { return; }; - ui.heading( - format!("{}. Choose your funding method.", step_number).as_str() - ); - step_number += 1; - - ui.add_space(10.0); - self.render_funding_method(ui); - - // Extract the funding method from the RwLock to minimize borrow scope - let funding_method = self.funding_method.read().unwrap().clone(); - - if funding_method == FundingMethod::NoSelection { - return; - } - - ui.add_space(20.0); + // Display the heading with an info icon that shows a tooltip on hover + ui.horizontal(|ui| { + ui.heading(format!( + "{}. Choose an identity index. Leave this 0 if this is your first identity for this wallet.", + step_number + )); - if funding_method == FundingMethod::AddressWithQRCode { - ui.heading("2. Choose how much you would like to transfer to your new identity?"); - step_number += 1; + // Create a label with click sense and tooltip + let info_icon = egui::Label::new("ℹ").sense(egui::Sense::click()); + let response = ui.add(info_icon) + .on_hover_text("The identity index is an internal reference within the wallet. The wallet’s seed phrase can always be used to recover any identity, including this one, by using the same index."); - self.render_funding_amount_input(ui); - } + // Check if the label was clicked + if response.clicked() { + self.show_pop_up_info = Some("The identity index is an internal reference within the wallet. The wallet’s seed phrase can always be used to recover any identity, including this one, by using the same index.".to_string()); + } + }); - if funding_method == FundingMethod::UseWalletBalance { - self.show_wallet_balance(ui); + step_number += 1; - step_number += 1; + ui.add_space(8.0); - ui.heading("2. How much of your wallet balance would you like to transfer?"); - step_number += 1; + self.render_identity_index_input(ui); - self.render_funding_amount_input(ui); - } + ui.add_space(10.0); - // Extract the step from the RwLock to minimize borrow scope - let step = self.step.read().unwrap().clone(); + // Display the heading with an info icon that shows a tooltip on hover + ui.horizontal(|ui| { + ui.heading(format!( + "{}. Choose what keys you want to add to this new identity.", + step_number + )); - if funding_method != FundingMethod::UseUnusedAssetLock { - let Ok(amount_dash) = self.funding_amount.parse::() else { - return; - }; + // Create a label with click sense and tooltip + let info_icon = egui::Label::new("ℹ").sense(egui::Sense::click()); + let response = ui.add(info_icon) + .on_hover_text("Keys allow an identity to perform actions on the Blockchain. They are contained in your wallet and allow you to prove that the action you are making is really coming from yourself."); - if step == ChooseFundingMethod && funding_method == FundingMethod::AddressWithQRCode { - if let Err(e) = self.render_qr_code(ui, amount_dash) { - eprintln!("Error: {:?}", e); - } + // Check if the label was clicked + if response.clicked() { + self.show_pop_up_info = Some("Keys allow an identity to perform actions on the Blockchain. They are contained in your wallet and allow you to prove that the action you are making is really coming from yourself.".to_string()); } - } + }); - if step < FundsReceived && funding_method == FundingMethod::AddressWithQRCode { - ui.add_space(20.0); - ui.heading("...Waiting for funds to continue..."); - return; - } + step_number += 1; - if funding_method == FundingMethod::UseUnusedAssetLock { - ui.heading( - format!("{}. Choose the unused asset lock that you would like to use.", step_number).as_str() - ); - ui.add_space(10.0); - self.render_choose_funding_asset_lock(ui); - step_number += 1; - } + ui.add_space(8.0); + + self.render_key_selection(ui); ui.heading( - format!("{}. Choose an identity index. Leave this 0 if this is your first identity for this wallet.", step_number).as_str() + format!("{}. Choose your funding method.", step_number).as_str() ); - - self.render_identity_index_input(ui); + step_number += 1; ui.add_space(10.0); + self.render_funding_method(ui); - if let Some(key) = self.identity_keys.master_private_key { - self.render_master_key(ui, key); - } - - self.render_keys_input(ui); + // Extract the funding method from the RwLock to minimize borrow scope + let funding_method = self.funding_method.read().unwrap().clone(); - if step == ReadyToCreate || funding_method == FundingMethod::UseWalletBalance || funding_method == FundingMethod::UseUnusedAssetLock { - if ui.button("Create Identity").clicked() { - action = self.register_identity_clicked(funding_method); - } + if funding_method == FundingMethod::NoSelection { + return; } - if step == AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock { - ui.heading("Waiting for Asset Lock"); + match funding_method { + FundingMethod::NoSelection => return, + FundingMethod::UseUnusedAssetLock => { + action |= self.render_ui_by_using_unused_asset_lock(ui, step_number); + }, + FundingMethod::UseWalletBalance => { + action |= self.render_ui_by_using_unused_balance(ui, step_number); + }, + FundingMethod::AddressWithQRCode => { + action |= self.render_ui_by_wallet_qr_code(ui, step_number) + }, + FundingMethod::AttachedCoreWallet => return, } - if let Some(error_message) = self.error_message.as_ref() { - ui.heading(error_message); - } - if step == AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance { - ui.heading("Waiting for Platform Acknowledgement"); - } }); + // Show the popup window if `show_popup` is true + if let Some(show_pop_up_info_text) = self.show_pop_up_info.clone() { + egui::Window::new("Identity Index Information") + .collapsible(false) // Prevent collapsing + .resizable(false) // Prevent resizing + .show(ctx, |ui| { + ui.label(show_pop_up_info_text); + + // Add a close button to dismiss the popup + if ui.button("Close").clicked() { + self.show_pop_up_info = None + } + }); + } + action } } diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index 1c0e01ec9..8f78b4d86 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -535,27 +535,29 @@ impl ScreenLike for IdentitiesScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let right_buttons = { - let create_wallet_or_identity = if !self.app_context.has_wallet.load(Ordering::Relaxed) - { + let mut right_buttons = if !self.app_context.has_wallet.load(Ordering::Relaxed) { + [ + ( + "Import Wallet", + DesiredAppAction::AddScreenType(ScreenType::ImportWallet), + ), ( "Create Wallet", DesiredAppAction::AddScreenType(ScreenType::AddNewWallet), - ) - } else { - ( - "Create Identity", - DesiredAppAction::AddScreenType(ScreenType::AddNewIdentity), - ) - }; - vec![ - create_wallet_or_identity, - ( - "Load Identity", - DesiredAppAction::AddScreenType(ScreenType::AddExistingIdentity), ), ] + .to_vec() + } else { + [( + "Create Identity", + DesiredAppAction::AddScreenType(ScreenType::AddNewIdentity), + )] + .to_vec() }; + right_buttons.push(( + "Load Identity", + DesiredAppAction::AddScreenType(ScreenType::AddExistingIdentity), + )); let mut action = add_top_panel( ctx, &self.app_context, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f0a8a1a33..be4d5555f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,10 +10,10 @@ use crate::ui::keys_screen::KeysScreen; use crate::ui::network_chooser_screen::NetworkChooserScreen; use crate::ui::transfers::TransferScreen; use crate::ui::transition_visualizer_screen::TransitionVisualizerScreen; +use crate::ui::wallet::import_wallet_screen::ImportWalletScreen; use crate::ui::wallet::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawals::WithdrawalScreen; use crate::ui::withdraws_status_screen::WithdrawsStatusScreen; -use ambassador::{delegatable_trait, Delegate}; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dpns_contested_names_screen::DPNSSubscreen; @@ -114,6 +114,7 @@ pub enum ScreenType { DPNSMyUsernames, AddNewIdentity, WalletsBalances, + ImportWallet, AddNewWallet, AddExistingIdentity, TransitionVisualizer, @@ -188,17 +189,19 @@ impl ScreenType { ScreenType::WalletsBalances => { Screen::WalletsBalancesScreen(WalletsBalancesScreen::new(app_context)) } + ScreenType::ImportWallet => { + Screen::ImportWalletScreen(ImportWalletScreen::new(app_context)) + } } } } -#[derive(Delegate)] -#[delegate(ScreenLike)] pub enum Screen { IdentitiesScreen(IdentitiesScreen), DPNSContestedNamesScreen(DPNSContestedNamesScreen), DocumentQueryScreen(DocumentQueryScreen), AddNewWalletScreen(AddNewWalletScreen), + ImportWalletScreen(ImportWalletScreen), AddNewIdentityScreen(AddNewIdentityScreen), AddExistingIdentityScreen(AddExistingIdentityScreen), KeyInfoScreen(KeyInfoScreen), @@ -232,6 +235,7 @@ impl Screen { Screen::TransferScreen(screen) => screen.app_context = app_context, Screen::WalletsBalancesScreen(screen) => screen.app_context = app_context, Screen::WithdrawsStatusScreen(screen) => screen.app_context = app_context, + Screen::ImportWalletScreen(screen) => screen.app_context = app_context, } } } @@ -243,7 +247,6 @@ pub enum MessageType { Error, } -#[delegatable_trait] pub trait ScreenLike { fn refresh(&mut self) {} fn refresh_on_arrival(&mut self) { @@ -308,6 +311,181 @@ impl Screen { Screen::TransferScreen(screen) => ScreenType::TransferScreen(screen.identity.clone()), Screen::WalletsBalancesScreen(_) => ScreenType::WalletsBalances, Screen::WithdrawsStatusScreen(_) => ScreenType::WithdrawsStatus, + Screen::ImportWalletScreen(_) => ScreenType::ImportWallet, + } + } +} + +impl ScreenLike for Screen { + fn refresh(&mut self) { + match self { + Screen::IdentitiesScreen(screen) => screen.refresh(), + Screen::DPNSContestedNamesScreen(screen) => screen.refresh(), + Screen::DocumentQueryScreen(screen) => screen.refresh(), + Screen::AddNewWalletScreen(screen) => screen.refresh(), + Screen::ImportWalletScreen(screen) => screen.refresh(), + Screen::AddNewIdentityScreen(screen) => screen.refresh(), + Screen::AddExistingIdentityScreen(screen) => screen.refresh(), + Screen::KeyInfoScreen(screen) => screen.refresh(), + Screen::KeysScreen(screen) => screen.refresh(), + Screen::RegisterDpnsNameScreen(screen) => screen.refresh(), + Screen::WithdrawalScreen(screen) => screen.refresh(), + Screen::TransferScreen(screen) => screen.refresh(), + Screen::AddKeyScreen(screen) => screen.refresh(), + Screen::TransitionVisualizerScreen(screen) => screen.refresh(), + Screen::WithdrawsStatusScreen(screen) => screen.refresh(), + Screen::NetworkChooserScreen(screen) => screen.refresh(), + Screen::WalletsBalancesScreen(screen) => screen.refresh(), + } + } + + fn refresh_on_arrival(&mut self) { + match self { + Screen::IdentitiesScreen(screen) => screen.refresh_on_arrival(), + Screen::DPNSContestedNamesScreen(screen) => screen.refresh_on_arrival(), + Screen::DocumentQueryScreen(screen) => screen.refresh_on_arrival(), + Screen::AddNewWalletScreen(screen) => screen.refresh_on_arrival(), + Screen::ImportWalletScreen(screen) => screen.refresh_on_arrival(), + Screen::AddNewIdentityScreen(screen) => screen.refresh_on_arrival(), + Screen::AddExistingIdentityScreen(screen) => screen.refresh_on_arrival(), + Screen::KeyInfoScreen(screen) => screen.refresh_on_arrival(), + Screen::KeysScreen(screen) => screen.refresh_on_arrival(), + Screen::RegisterDpnsNameScreen(screen) => screen.refresh_on_arrival(), + Screen::WithdrawalScreen(screen) => screen.refresh_on_arrival(), + Screen::TransferScreen(screen) => screen.refresh_on_arrival(), + Screen::AddKeyScreen(screen) => screen.refresh_on_arrival(), + Screen::TransitionVisualizerScreen(screen) => screen.refresh_on_arrival(), + Screen::WithdrawsStatusScreen(screen) => screen.refresh_on_arrival(), + Screen::NetworkChooserScreen(screen) => screen.refresh_on_arrival(), + Screen::WalletsBalancesScreen(screen) => screen.refresh_on_arrival(), + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + match self { + Screen::IdentitiesScreen(screen) => screen.ui(ctx), + Screen::DPNSContestedNamesScreen(screen) => screen.ui(ctx), + Screen::DocumentQueryScreen(screen) => screen.ui(ctx), + Screen::AddNewWalletScreen(screen) => screen.ui(ctx), + Screen::ImportWalletScreen(screen) => screen.ui(ctx), + Screen::AddNewIdentityScreen(screen) => screen.ui(ctx), + Screen::AddExistingIdentityScreen(screen) => screen.ui(ctx), + Screen::KeyInfoScreen(screen) => screen.ui(ctx), + Screen::KeysScreen(screen) => screen.ui(ctx), + Screen::RegisterDpnsNameScreen(screen) => screen.ui(ctx), + Screen::WithdrawalScreen(screen) => screen.ui(ctx), + Screen::TransferScreen(screen) => screen.ui(ctx), + Screen::AddKeyScreen(screen) => screen.ui(ctx), + Screen::TransitionVisualizerScreen(screen) => screen.ui(ctx), + Screen::WithdrawsStatusScreen(screen) => screen.ui(ctx), + Screen::NetworkChooserScreen(screen) => screen.ui(ctx), + Screen::WalletsBalancesScreen(screen) => screen.ui(ctx), + } + } + + fn display_message(&mut self, message: &str, message_type: MessageType) { + match self { + Screen::IdentitiesScreen(screen) => screen.display_message(message, message_type), + Screen::DPNSContestedNamesScreen(screen) => { + screen.display_message(message, message_type) + } + Screen::DocumentQueryScreen(screen) => screen.display_message(message, message_type), + Screen::AddNewWalletScreen(screen) => screen.display_message(message, message_type), + Screen::ImportWalletScreen(screen) => screen.display_message(message, message_type), + Screen::AddNewIdentityScreen(screen) => screen.display_message(message, message_type), + Screen::AddExistingIdentityScreen(screen) => { + screen.display_message(message, message_type) + } + Screen::KeyInfoScreen(screen) => screen.display_message(message, message_type), + Screen::KeysScreen(screen) => screen.display_message(message, message_type), + Screen::RegisterDpnsNameScreen(screen) => screen.display_message(message, message_type), + Screen::WithdrawalScreen(screen) => screen.display_message(message, message_type), + Screen::TransferScreen(screen) => screen.display_message(message, message_type), + Screen::AddKeyScreen(screen) => screen.display_message(message, message_type), + Screen::TransitionVisualizerScreen(screen) => { + screen.display_message(message, message_type) + } + Screen::WithdrawsStatusScreen(screen) => screen.display_message(message, message_type), + Screen::NetworkChooserScreen(screen) => screen.display_message(message, message_type), + Screen::WalletsBalancesScreen(screen) => screen.display_message(message, message_type), + } + } + + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + match self { + Screen::IdentitiesScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::DPNSContestedNamesScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::DocumentQueryScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::AddNewWalletScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::ImportWalletScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::AddNewIdentityScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::AddExistingIdentityScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::KeyInfoScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::KeysScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::RegisterDpnsNameScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::WithdrawalScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::TransferScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::AddKeyScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::TransitionVisualizerScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::WithdrawsStatusScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::NetworkChooserScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } + Screen::WalletsBalancesScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } + } + } + + fn pop_on_success(&mut self) { + match self { + Screen::IdentitiesScreen(screen) => screen.pop_on_success(), + Screen::DPNSContestedNamesScreen(screen) => screen.pop_on_success(), + Screen::DocumentQueryScreen(screen) => screen.pop_on_success(), + Screen::AddNewWalletScreen(screen) => screen.pop_on_success(), + Screen::ImportWalletScreen(screen) => screen.pop_on_success(), + Screen::AddNewIdentityScreen(screen) => screen.pop_on_success(), + Screen::AddExistingIdentityScreen(screen) => screen.pop_on_success(), + Screen::KeyInfoScreen(screen) => screen.pop_on_success(), + Screen::KeysScreen(screen) => screen.pop_on_success(), + Screen::RegisterDpnsNameScreen(screen) => screen.pop_on_success(), + Screen::WithdrawalScreen(screen) => screen.pop_on_success(), + Screen::TransferScreen(screen) => screen.pop_on_success(), + Screen::AddKeyScreen(screen) => screen.pop_on_success(), + Screen::TransitionVisualizerScreen(screen) => screen.pop_on_success(), + Screen::WithdrawsStatusScreen(screen) => screen.pop_on_success(), + Screen::NetworkChooserScreen(screen) => screen.pop_on_success(), + Screen::WalletsBalancesScreen(screen) => screen.pop_on_success(), } } } diff --git a/src/ui/wallet/import_wallet_screen.rs b/src/ui/wallet/import_wallet_screen.rs new file mode 100644 index 000000000..25f38eacf --- /dev/null +++ b/src/ui/wallet/import_wallet_screen.rs @@ -0,0 +1,298 @@ +use crate::app::AppAction; +use crate::context::AppContext; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::ScreenLike; +use eframe::egui::Context; + +use crate::model::wallet::Wallet; +use crate::ui::components::entropy_grid::U256EntropyGrid; +use bip39::{Language, Mnemonic}; +use egui::{ + Color32, ComboBox, Direction, FontId, Frame, Grid, Layout, Margin, RichText, Stroke, TextStyle, + Ui, Vec2, +}; +use std::sync::atomic::Ordering; +use std::sync::{Arc, RwLock}; + +pub struct ImportWalletScreen { + seed_phrase: Option, + passphrase: String, + entropy_grid: U256EntropyGrid, + selected_language: Language, + alias_input: String, + wrote_it_down: bool, + pub app_context: Arc, +} + +impl ImportWalletScreen { + pub fn new(app_context: &Arc) -> Self { + Self { + seed_phrase: None, + passphrase: String::new(), + entropy_grid: U256EntropyGrid::new(), + selected_language: Language::English, + alias_input: String::new(), + wrote_it_down: false, + app_context: app_context.clone(), + } + } + + /// Generate a new seed phrase based on the selected language + fn generate_seed_phrase(&mut self) { + let mnemonic = Mnemonic::from_entropy_in( + self.selected_language, + &self.entropy_grid.random_number_with_user_input(), + ) + .expect("Failed to generate mnemonic"); + self.seed_phrase = Some(mnemonic); + } + + fn save_wallet(&mut self) -> AppAction { + if let Some(mnemonic) = &self.seed_phrase { + let seed = mnemonic.to_seed(self.passphrase.as_str()); + let wallet = Wallet { + seed, + address_balances: Default::default(), + known_addresses: Default::default(), + watched_addresses: Default::default(), + unused_asset_locks: Default::default(), + alias: None, + utxos: Default::default(), + is_main: true, + password_hint: None, + }; + + self.app_context + .db + .insert_wallet(&wallet, &self.app_context.network) + .ok(); + + // Acquire a write lock and add the new wallet + if let Ok(mut wallets) = self.app_context.wallets.write() { + wallets.push(Arc::new(RwLock::new(wallet))); + self.app_context.has_wallet.store(true, Ordering::Relaxed); + } else { + eprintln!("Failed to acquire write lock on wallets"); + } + + AppAction::GoToMainScreen // Navigate back to the main screen after saving + } else { + AppAction::None // No action if no seed phrase exists + } + } + + fn render_seed_phrase_input(&mut self, ui: &mut Ui) { + ui.add_space(15.0); // Add spacing from the top + ui.vertical(|ui| { + // Allocate a full-width container to center align the elements + let available_width = ui.available_width(); + + ui.allocate_ui_with_layout( + Vec2::new(available_width, 0.0), + egui::Layout::top_down(egui::Align::Min), + |ui| { + ui.horizontal(|ui| { + // Add spacing to align the combo box to the left of the center + let half_width = available_width / 2.0 - 400.0; // Adjust half-width with padding + ui.add_space(half_width); + + let style = ui.style_mut(); + + // Customize text size for the ComboBox + style.text_styles.insert( + TextStyle::Button, // Apply style to buttons (used in ComboBox entries) + FontId::proportional(24.0), // Set larger font size + ); + + ComboBox::from_label("") + .selected_text(format!("{:?}", self.selected_language)) + .width(200.0) + .height(40.0) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.selected_language, + Language::English, + "English", + ); + ui.selectable_value( + &mut self.selected_language, + Language::Spanish, + "Spanish", + ); + ui.selectable_value( + &mut self.selected_language, + Language::French, + "French", + ); + ui.selectable_value( + &mut self.selected_language, + Language::Italian, + "Italian", + ); + ui.selectable_value( + &mut self.selected_language, + Language::Portuguese, + "Portuguese", + ); + }); + + // Add a spacer between the combo box and the generate button + ui.add_space(20.0); // Adjust the space between elements + + let generate_button = + egui::Button::new(RichText::new("Generate").strong().size(24.0)) + .min_size(Vec2::new(150.0, 30.0)) + .rounding(5.0) + .stroke(Stroke::new(1.0, Color32::WHITE)); + + if ui.add(generate_button).clicked() { + self.generate_seed_phrase(); + } + }); + }, + ); + + ui.add_space(10.0); + + // Create a container with a fixed width (72% of the available width) + let frame_width = available_width * 0.72; + ui.allocate_ui_with_layout( + Vec2::new(frame_width, 300.0), // Set width and height of the container + egui::Layout::top_down(egui::Align::Min), + |ui| { + Frame::none() + .fill(Color32::WHITE) + .stroke(Stroke::new(1.0, Color32::BLACK)) + .rounding(5.0) + .inner_margin(Margin::same(10.0)) + .show(ui, |ui| { + let columns = 6; + let rows = 24 / columns; + + // Calculate the size of each grid cell + let column_width = frame_width / columns as f32; + let row_height = 300.0 / rows as f32; + + Grid::new("seed_phrase_grid") + .num_columns(columns) + .spacing((0.0, 0.0)) + .min_col_width(column_width) + .min_row_height(row_height) + .show(ui, |ui| { + if let Some(mnemonic) = &self.seed_phrase { + for (i, word) in mnemonic.words().enumerate() { + let word_text = RichText::new(word) + .size(row_height * 0.5) + .monospace(); + + ui.with_layout( + Layout::centered_and_justified( + Direction::LeftToRight, + ), + |ui| { + ui.label(word_text); + }, + ); + + if (i + 1) % columns == 0 { + ui.end_row(); + } + } + } else { + let word_text = + RichText::new("Seed Phrase").size(40.0).monospace(); + + ui.with_layout( + Layout::centered_and_justified(Direction::LeftToRight), + |ui| { + ui.label(word_text); + }, + ); + } + }); + }); + }, + ); + }); + } +} + +impl ScreenLike for ImportWalletScreen { + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Identities", AppAction::GoToMainScreen), + ("Create Wallet", AppAction::None), + ], + vec![], + ); + + egui::CentralPanel::default().show(ctx, |ui| { + // Add the scroll area to make the content scrollable + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) // Prevent shrinking when content is less than the available area + .show(ui, |ui| { + ui.add_space(10.0); + ui.heading("Follow these steps to create your wallet!"); + ui.add_space(5.0); + + self.entropy_grid.ui(ui); + + ui.add_space(5.0); + + ui.heading("2. Select your desired seed phrase language and press \"Generate\""); + self.render_seed_phrase_input(ui); + + ui.add_space(10.0); + + ui.heading( + "3. Write down the passphrase on a piece of paper and put it somewhere secure", + ); + + ui.add_space(10.0); + + // Add "I wrote it down" checkbox + ui.horizontal(|ui| { + ui.checkbox(&mut self.wrote_it_down, "I wrote it down"); + }); + + ui.add_space(20.0); + + ui.heading("4. Add an optional password that must be used to unlock the wallet"); + + ui.horizontal(|ui| { + ui.label("Optional Password:"); + ui.text_edit_singleline(&mut self.passphrase); + }); + + ui.add_space(20.0); + + ui.heading("5. Save the wallet"); + ui.add_space(5.0); + + // Centered "Save Wallet" button at the bottom + ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| { + let save_button = egui::Button::new( + RichText::new("Save Wallet").strong().size(30.0), + ) + .min_size(Vec2::new(300.0, 60.0)) + .rounding(10.0) + .stroke(Stroke::new(1.5, Color32::WHITE)) + .sense(if self.wrote_it_down { + egui::Sense::click() + } else { + egui::Sense::hover() + }); + + if ui.add(save_button).clicked() { + action = self.save_wallet(); // Trigger the save action + } + }); + }); + }); + + action + } +} diff --git a/src/ui/wallet/mod.rs b/src/ui/wallet/mod.rs index 4c0bcc1c3..8ced7a4dd 100644 --- a/src/ui/wallet/mod.rs +++ b/src/ui/wallet/mod.rs @@ -1,2 +1,3 @@ pub mod add_new_wallet_screen; +pub mod import_wallet_screen; pub mod wallets_screen; From 320f352727b8073ffb50d79a96f1924f7bc8583d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 5 Nov 2024 23:07:05 +0100 Subject: [PATCH 2/7] better identity registration --- src/backend_task/core/mod.rs | 6 ++- src/backend_task/identity/mod.rs | 4 +- .../identity/register_identity.rs | 10 ++-- src/context.rs | 6 +-- src/model/wallet/asset_lock_transaction.rs | 6 +-- src/model/wallet/mod.rs | 48 +++++++++++++++--- .../by_using_unused_balance.rs | 2 +- .../by_wallet_qr_code.rs | 50 +++++++++++-------- .../identities/add_new_identity_screen/mod.rs | 49 +++++++++++------- src/ui/wallet/wallets_screen.rs | 2 +- src/ui/withdraws_status_screen.rs | 8 ++- 11 files changed, 120 insertions(+), 71 deletions(-) diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index f291ea0f2..6b4377bcd 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -4,7 +4,9 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; -use dash_sdk::dpp::dashcore::{Address, ChainLock, Network, OutPoint, Transaction}; +use dash_sdk::dpp::dashcore::{ + Address, ChainLock, Network, OutPoint, ScriptBuf, Transaction, TxOut, +}; use std::sync::{Arc, RwLock}; #[derive(Debug, Clone)] @@ -24,7 +26,7 @@ impl PartialEq for CoreTask { #[derive(Debug, Clone, PartialEq)] pub(crate) enum CoreItem { - ReceivedAvailableUTXOTransaction(Transaction, Vec<(OutPoint, Address)>), + ReceivedAvailableUTXOTransaction(Transaction, Vec<(OutPoint, TxOut, Address)>), ChainLock(ChainLock, Network), } diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index 73c60aee0..9629db721 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -13,7 +13,7 @@ use crate::model::qualified_identity::{ }; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; -use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey}; +use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey, TxOut}; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{OutPoint, ScriptBuf, Transaction}; @@ -166,7 +166,7 @@ pub type IdentityIndex = u32; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IdentityRegistrationMethod { UseAssetLock(Address, AssetLockProof, Transaction), - FundWithUtxo(OutPoint, ScriptBuf, Address, IdentityIndex), + FundWithUtxo(OutPoint, TxOut, Address, IdentityIndex), FundWithWallet(Duffs, IdentityIndex), } diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index 7207faff9..df2a01ee0 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -3,7 +3,6 @@ use crate::backend_task::identity::{IdentityRegistrationInfo, IdentityRegistrati use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; -use dash_sdk::dapi_client::DapiRequestExecutor; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; use dash_sdk::dpp::dashcore::hashes::Hash; @@ -15,11 +14,8 @@ use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::dpp::state_transition::identity_create_transition::methods::IdentityCreateTransitionMethodsV0; use dash_sdk::dpp::state_transition::identity_create_transition::IdentityCreateTransition; use dash_sdk::dpp::version::PlatformVersion; -use dash_sdk::platform::proto::{get_epochs_info_request, GetEpochsInfoRequest}; use dash_sdk::platform::transition::put_identity::PutIdentity; -use dash_sdk::platform::types::evonode::EvoNode; -use dash_sdk::platform::{Fetch, FetchUnproved, Identity}; -use dash_sdk::query_types::EvoNodeStatus; +use dash_sdk::platform::{Fetch, Identity}; use std::time::Duration; use tokio::sync::mpsc; @@ -224,7 +220,7 @@ impl AppContext { } IdentityRegistrationMethod::FundWithUtxo( utxo, - script_pubkey, + tx_out, input_address, identity_index, ) => { @@ -234,7 +230,7 @@ impl AppContext { wallet.asset_lock_transaction_for_utxo( sdk.network, utxo, - script_pubkey, + tx_out, input_address, identity_index, Some(self), diff --git a/src/context.rs b/src/context.rs index 752a8341f..d87bb5b44 100644 --- a/src/context.rs +++ b/src/context.rs @@ -11,7 +11,7 @@ use dash_sdk::dashcore_rpc::dashcore::{InstantLock, Transaction}; use dash_sdk::dashcore_rpc::{Auth, Client}; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayload::AssetLockPayloadType; -use dash_sdk::dpp::dashcore::{Address, Network, OutPoint, Txid}; +use dash_sdk::dpp::dashcore::{Address, Network, OutPoint, ScriptBuf, TxOut, Txid}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; use dash_sdk::dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; @@ -214,7 +214,7 @@ impl AppContext { tx: &Transaction, islock: Option, chain_locked_height: Option, - ) -> rusqlite::Result> { + ) -> rusqlite::Result> { // Initialize a vector to collect wallet outpoints let mut wallet_outpoints = Vec::new(); @@ -254,7 +254,7 @@ impl AppContext { .insert(out_point.clone(), tx_out.clone()); // Insert the TxOut at the OutPoint // Collect the outpoint - wallet_outpoints.push((out_point.clone(), address.clone())); + wallet_outpoints.push((out_point.clone(), tx_out.clone(), address.clone())); wallet .address_balances diff --git a/src/model/wallet/asset_lock_transaction.rs b/src/model/wallet/asset_lock_transaction.rs index 995a10681..bee5d16d9 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -140,7 +140,7 @@ impl Wallet { &mut self, network: Network, utxo: OutPoint, - script_pubkey: ScriptBuf, + previous_tx_output: TxOut, input_address: Address, identity_index: u32, register_addresses: Option<&AppContext>, @@ -155,7 +155,7 @@ impl Wallet { let one_time_key_hash = asset_lock_public_key.pubkey_hash(); let fee = 3_000; - let output_amount = utxo.vout as u64 - fee; + let output_amount = previous_tx_output.value - fee; let payload_output = TxOut { value: output_amount, @@ -195,7 +195,7 @@ impl Wallet { .enumerate() .map(|(i, input)| { cache - .legacy_signature_hash(i, &script_pubkey, sighash_u32) + .legacy_signature_hash(i, &previous_tx_output.script_pubkey, sighash_u32) .expect("expected sighash") }) .collect(); diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 5f32fadb0..3ba35271d 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -143,6 +143,7 @@ impl Wallet { pub fn unused_bip_44_public_key( &mut self, network: Network, + skip_known_addresses_with_no_funds: bool, change: bool, register: Option<&AppContext>, ) -> Result<(PublicKey, DerivationPath), String> { @@ -151,7 +152,30 @@ impl Wallet { while found_unused_derivation_path.is_none() { let derivation_path = DerivationPath::bip_44_payment_path(network, 0, change, address_index); - if self.watched_addresses.get(&derivation_path).is_none() { + + if let Some(address_info) = self.watched_addresses.get(&derivation_path) { + // Address is known + let address = &address_info.address; + let balance = self.address_balances.get(address).cloned().unwrap_or(0); + + if balance > 0 { + // Address has funds, skip it + address_index += 1; + continue; + } + + // Address is known and has zero balance + if !skip_known_addresses_with_no_funds { + // We can use this address + found_unused_derivation_path = Some(derivation_path.clone()); + break; + } else { + // Skip known addresses with no funds + address_index += 1; + continue; + } + } else { + // Address is not known, proceed to register it let public_key = derivation_path .derive_pub_ecdsa_for_master_seed(&self.seed, network) .expect("derivation should not be able to fail") @@ -197,10 +221,8 @@ impl Wallet { self.known_addresses .insert(address, derivation_path.clone()); } - found_unused_derivation_path = Some(derivation_path); + found_unused_derivation_path = Some(derivation_path.clone()); break; - } else { - address_index += 1; } } @@ -303,10 +325,18 @@ impl Wallet { pub fn receive_address( &mut self, network: Network, + skip_known_addresses_with_no_funds: bool, register: Option<&AppContext>, ) -> Result { Ok(Address::p2pkh( - &self.unused_bip_44_public_key(network, false, register)?.0, + &self + .unused_bip_44_public_key( + network, + skip_known_addresses_with_no_funds, + false, + register, + )? + .0, network, )) } @@ -317,7 +347,7 @@ impl Wallet { register: Option<&AppContext>, ) -> Result<(Address, DerivationPath), String> { let (receive_public_key, derivation_path) = - self.unused_bip_44_public_key(network, false, register)?; + self.unused_bip_44_public_key(network, false, false, register)?; Ok(( Address::p2pkh(&receive_public_key, network), derivation_path, @@ -330,7 +360,9 @@ impl Wallet { register: Option<&AppContext>, ) -> Result { Ok(Address::p2pkh( - &self.unused_bip_44_public_key(network, true, register)?.0, + &self + .unused_bip_44_public_key(network, false, true, register)? + .0, network, )) } @@ -341,7 +373,7 @@ impl Wallet { register: Option<&AppContext>, ) -> Result<(Address, DerivationPath), String> { let (receive_public_key, derivation_path) = - self.unused_bip_44_public_key(network, true, register)?; + self.unused_bip_44_public_key(network, false, true, register)?; Ok(( Address::p2pkh(&receive_public_key, network), derivation_path, diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs index 993fee4c9..90a00d249 100644 --- a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs +++ b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs @@ -50,7 +50,7 @@ impl AddNewIdentityScreen { match step { AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("Waiting for Core Chain to produce proof of transfer of funds"); + ui.heading("Waiting for Core Chain to produce proof of transfer of funds."); } AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("Waiting for Platform acknowledgement"); diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs index 042106260..00def558d 100644 --- a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -21,8 +21,11 @@ impl AddNewIdentityScreen { // Get the receive address if self.funding_address.is_none() { let mut wallet = wallet_guard.write().unwrap(); - let receive_address = wallet - .receive_address(self.app_context.network, Some(&self.app_context))?; + let receive_address = wallet.receive_address( + self.app_context.network, + false, + Some(&self.app_context), + )?; if let Some(has_address) = self.core_has_funding_address { if !has_address { @@ -85,23 +88,27 @@ impl AddNewIdentityScreen { } ui.add_space(10.0); - ui.label(&pay_uri); - if ui.button("Copy").clicked() { - if let Err(e) = copy_to_clipboard(pay_uri.as_str()) { - self.copied_to_clipboard = Some(Some(e)); - } else { - self.copied_to_clipboard = Some(None); + ui.horizontal(|ui| { + ui.label(&pay_uri); + ui.add_space(8.0); + + if ui.button("Copy").clicked() { + if let Err(e) = copy_to_clipboard(pay_uri.as_str()) { + self.copied_to_clipboard = Some(Some(e)); + } else { + self.copied_to_clipboard = Some(None); + } } - } - if let Some(error) = self.copied_to_clipboard.as_ref() { - if let Some(error) = error { - ui.label(format!("Failed to copy to clipboard: {}", error)); - } else { - ui.label("Address copied to clipboard."); + if let Some(error) = self.copied_to_clipboard.as_ref() { + if let Some(error) = error { + ui.label(format!("Failed to copy to clipboard: {}", error)); + } else { + ui.label("Address copied to clipboard."); + } } - } + }); Ok(()) } @@ -109,10 +116,6 @@ impl AddNewIdentityScreen { pub fn render_ui_by_wallet_qr_code(&mut self, ui: &mut Ui, mut step_number: u32) -> AppAction { let mut action = AppAction::None; - let Some(selected_wallet) = &self.selected_wallet else { - return action; - }; - // Extract the step from the RwLock to minimize borrow scope let step = self.step.read().unwrap().clone(); @@ -147,14 +150,17 @@ impl AddNewIdentityScreen { ui.heading("Waiting for funds"); } AddNewIdentityWalletFundedScreenStep::FundsReceived => { - if let Some((utxo, script_buf, address)) = self.funding_utxo.clone() { + let Some(selected_wallet) = &self.selected_wallet else { + return action; + }; + if let Some((utxo, tx_out, address)) = self.funding_utxo.clone() { let identity_input = IdentityRegistrationInfo { alias_input: self.alias_input.clone(), keys: self.identity_keys.clone(), wallet: Arc::clone(selected_wallet), // Clone the Arc reference identity_registration_method: IdentityRegistrationMethod::FundWithUtxo( utxo, - script_buf, + tx_out, address, self.identity_id_number, ), @@ -171,7 +177,7 @@ impl AddNewIdentityScreen { } AddNewIdentityWalletFundedScreenStep::ReadyToCreate => {} AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("Waiting for Core Chain to produce proof of transfer of funds"); + ui.heading("Waiting for Core Chain to produce proof of transfer of funds."); } AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("Waiting for Platform acknowledgement"); diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index d9272cccf..fa61b6f66 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -11,9 +11,6 @@ use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; use crate::model::wallet::Wallet; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::identities::add_new_identity_screen::AddNewIdentityWalletFundedScreenStep::{ - ChooseFundingMethod, FundsReceived, ReadyToCreate, -}; use crate::ui::{MessageType, ScreenLike}; use arboard::Clipboard; use dash_sdk::dashcore_rpc::dashcore::Address; @@ -85,7 +82,7 @@ pub struct AddNewIdentityScreen { funding_method: Arc>, funding_amount: String, funding_amount_exact: Option, - funding_utxo: Option<(OutPoint, ScriptBuf, Address)>, + funding_utxo: Option<(OutPoint, TxOut, Address)>, alias_input: String, copied_to_clipboard: Option>, identity_keys: IdentityKeys, @@ -368,6 +365,7 @@ impl AddNewIdentityScreen { FundingMethod::NoSelection, "Please select funding method", ); + let wallet = selected_wallet.read().unwrap(); if wallet.has_unused_asset_lock() { if ui @@ -379,21 +377,36 @@ impl AddNewIdentityScreen { .changed() { self.update_identity_key(); + let mut step = self.step.write().unwrap(); // Write lock on step + *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; } } if wallet.has_balance() { - ui.selectable_value( + if ui + .selectable_value( + &mut *funding_method, + FundingMethod::UseWalletBalance, + "Use Wallet Balance", + ) + .changed() + { + let mut step = self.step.write().unwrap(); // Write lock on step + *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; + } + } + if ui + .selectable_value( &mut *funding_method, - FundingMethod::UseWalletBalance, - "Use Wallet Balance", - ); + FundingMethod::AddressWithQRCode, + "Address with QR Code", + ) + .changed() + { + let mut step = self.step.write().unwrap(); // Write lock on step + *step = AddNewIdentityWalletFundedScreenStep::WaitingOnFunds; } - ui.selectable_value( - &mut *funding_method, - FundingMethod::AddressWithQRCode, - "Address with QR Code", - ); + // Uncomment this if AttachedCoreWallet is available in the future // ui.selectable_value( // &mut *funding_method, // FundingMethod::AttachedCoreWallet, @@ -445,7 +458,7 @@ impl AddNewIdentityScreen { // Render additional keys input (if any) and allow adding more keys self.render_keys_input(ui); } else { - ui.label("Default allows updating the identity, interacting with data contracts, transferring credits to other identities and to the core chain".to_string()); + ui.label("Default allows updating the identity, interacting with data contracts, transferring credits to other identities and to the Core payment chain.".to_string()); } } @@ -707,10 +720,10 @@ impl ScreenLike for AddNewIdentityScreen { CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), ) = backend_task_success_result { - for (outpoint, address) in outpoints_with_addresses { + for (outpoint, tx_out, address) in outpoints_with_addresses { if funding_address == &address { - *step = - AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance; + *step = AddNewIdentityWalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some((outpoint, tx_out, address)) } } } @@ -794,6 +807,8 @@ impl ScreenLike for AddNewIdentityScreen { self.render_key_selection(ui); + ui.add_space(10.0); + ui.heading( format!("{}. Choose your funding method.", step_number).as_str() ); diff --git a/src/ui/wallet/wallets_screen.rs b/src/ui/wallet/wallets_screen.rs index 5dcec8131..187df863f 100644 --- a/src/ui/wallet/wallets_screen.rs +++ b/src/ui/wallet/wallets_screen.rs @@ -108,7 +108,7 @@ impl WalletsBalancesScreen { if let Some(wallet) = &self.selected_wallet { let result = { let mut wallet = wallet.write().unwrap(); - wallet.receive_address(self.app_context.network, Some(&self.app_context)) + wallet.receive_address(self.app_context.network, true, Some(&self.app_context)) }; // Now the immutable borrow of `wallet` is dropped, and we can use `self` mutably diff --git a/src/ui/withdraws_status_screen.rs b/src/ui/withdraws_status_screen.rs index 6003cc944..c34566dfe 100644 --- a/src/ui/withdraws_status_screen.rs +++ b/src/ui/withdraws_status_screen.rs @@ -7,10 +7,8 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use dash_sdk::dpp::dash_to_credits; use dash_sdk::dpp::data_contracts::withdrawals_contract::WithdrawalStatus; -use dash_sdk::dpp::document::DocumentV0Getters; use egui::{Color32, ComboBox, Context, Stroke, Ui, Vec2}; use egui_extras::{Column, TableBuilder}; -use itertools::Itertools; use std::sync::{Arc, RwLock}; pub struct WithdrawsStatusScreen { @@ -323,8 +321,8 @@ impl WithdrawsStatusScreen { } let total_pages = (data.withdrawals.len() + (self.pagination_items_per_page as usize) - 1) / (self.pagination_items_per_page as usize); - if (total_pages > 0) { - let mut current_page = self + if total_pages > 0 { + let current_page = self .pagination_current_page .min(total_pages.saturating_sub(1)); // Clamp to valid page range // Calculate the slice of data for the current page @@ -484,7 +482,7 @@ impl ScreenLike for WithdrawsStatusScreen { self.error_message = None; } - fn display_message(&mut self, message: &str, message_type: MessageType) { + fn display_message(&mut self, message: &str, _message_type: MessageType) { self.error_message = Some(message.to_string()); self.requested_data = false; } From a3478ac67e0fdb9c7d8bc84bad8d43e2e92fec9d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 6 Nov 2024 00:54:00 +0100 Subject: [PATCH 3/7] more work --- .../identity/add_key_to_identity.rs | 2 +- src/backend_task/identity/load_identity.rs | 2 +- src/backend_task/identity/mod.rs | 1 + src/backend_task/identity/refresh_identity.rs | 2 +- .../identity/register_identity.rs | 21 ++++++-- src/backend_task/identity/transfer.rs | 2 +- .../identity/withdraw_from_identity.rs | 2 +- src/context.rs | 20 ++++++-- src/database/identities.rs | 51 ++++++++++++++++--- src/database/initialization.rs | 6 ++- src/database/wallet.rs | 1 + src/model/wallet/mod.rs | 2 + .../by_wallet_qr_code.rs | 1 + .../identities/add_new_identity_screen/mod.rs | 12 ++++- src/ui/key_info_screen.rs | 2 +- src/ui/wallet/add_new_wallet_screen.rs | 1 + src/ui/wallet/import_wallet_screen.rs | 1 + 17 files changed, 104 insertions(+), 25 deletions(-) diff --git a/src/backend_task/identity/add_key_to_identity.rs b/src/backend_task/identity/add_key_to_identity.rs index fb3adec39..bcc1c5e7d 100644 --- a/src/backend_task/identity/add_key_to_identity.rs +++ b/src/backend_task/identity/add_key_to_identity.rs @@ -63,7 +63,7 @@ impl AppContext { } } - self.insert_local_qualified_identity(&qualified_identity) + self.insert_local_qualified_identity(&qualified_identity, None) .map_err(|e| format!("Database error: {}", e)) } } diff --git a/src/backend_task/identity/load_identity.rs b/src/backend_task/identity/load_identity.rs index 0b3abd863..4486257f0 100644 --- a/src/backend_task/identity/load_identity.rs +++ b/src/backend_task/identity/load_identity.rs @@ -201,7 +201,7 @@ impl AppContext { }; // Insert qualified identity into the database - self.insert_local_qualified_identity(&qualified_identity) + self.insert_local_qualified_identity(&qualified_identity, None) .map_err(|e| format!("Database error: {}", e))?; Ok(()) diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index 9629db721..553d02e02 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -175,6 +175,7 @@ pub struct IdentityRegistrationInfo { pub alias_input: String, pub keys: IdentityKeys, pub wallet: Arc>, + pub wallet_identity_index: u32, pub identity_registration_method: IdentityRegistrationMethod, } diff --git a/src/backend_task/identity/refresh_identity.rs b/src/backend_task/identity/refresh_identity.rs index 1d51fbb55..cc9d04c34 100644 --- a/src/backend_task/identity/refresh_identity.rs +++ b/src/backend_task/identity/refresh_identity.rs @@ -50,7 +50,7 @@ impl AppContext { qualified_identity_to_update.identity = refreshed_identity; // Insert the updated identity into local state - self.insert_local_qualified_identity(&qualified_identity_to_update) + self.insert_local_qualified_identity(&qualified_identity_to_update, None) .map_err(|e| e.to_string())?; // Send refresh message to refresh the Identities Screen diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index df2a01ee0..9e38a6c24 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -112,6 +112,7 @@ impl AppContext { alias_input, keys, wallet, + wallet_identity_index, identity_registration_method, } = input; @@ -121,6 +122,8 @@ impl AppContext { .await .map_err(|e| e.to_string())?; + let mut wallet_id; + let (asset_lock_proof, asset_lock_proof_private_key, tx_id) = match identity_registration_method { IdentityRegistrationMethod::UseAssetLock( @@ -130,6 +133,7 @@ impl AppContext { ) => { let tx_id = transaction.txid(); let wallet = wallet.read().unwrap(); + wallet_id = wallet.seed; let private_key = wallet .private_key_for_address(&address, self.network)? .ok_or("Asset Lock not valid for wallet")?; @@ -165,6 +169,7 @@ impl AppContext { // Scope the write lock to avoid holding it across an await. let (asset_lock_transaction, asset_lock_proof_private_key, change_address) = { let mut wallet = wallet.write().unwrap(); + wallet_id = wallet.seed; match wallet.asset_lock_transaction( sdk.network, amount, @@ -227,6 +232,7 @@ impl AppContext { // Scope the write lock to avoid holding it across an await. let (asset_lock_transaction, asset_lock_proof_private_key) = { let mut wallet = wallet.write().unwrap(); + wallet_id = wallet.seed; wallet.asset_lock_transaction_for_utxo( sdk.network, utxo, @@ -301,8 +307,12 @@ impl AppContext { qualified_identity.alias = Some(alias_input); } - self.insert_local_qualified_identity_in_creation(&qualified_identity) - .map_err(|e| e.to_string())?; + self.insert_local_qualified_identity_in_creation( + &qualified_identity, + wallet_id.as_slice(), + wallet_identity_index, + ) + .map_err(|e| e.to_string())?; self.db .set_asset_lock_identity_id_before_confirmation_by_network( tx_id.as_byte_array(), @@ -339,8 +349,11 @@ impl AppContext { qualified_identity.identity = updated_identity; - self.insert_local_qualified_identity(&qualified_identity) - .map_err(|e| e.to_string())?; + self.insert_local_qualified_identity( + &qualified_identity, + Some((wallet_id.as_slice(), wallet_identity_index)), + ) + .map_err(|e| e.to_string())?; self.db .set_asset_lock_identity_id(tx_id.as_byte_array(), Some(identity_id.as_slice())) .map_err(|e| e.to_string())?; diff --git a/src/backend_task/identity/transfer.rs b/src/backend_task/identity/transfer.rs index 60eca5aae..826cdeaff 100644 --- a/src/backend_task/identity/transfer.rs +++ b/src/backend_task/identity/transfer.rs @@ -28,7 +28,7 @@ impl AppContext { .await .map_err(|e| format!("Withdrawal error: {}", e))?; qualified_identity.identity.set_balance(remaining_balance); - self.insert_local_qualified_identity(&qualified_identity) + self.insert_local_qualified_identity(&qualified_identity, None) .map_err(|e| format!("Database error: {}", e)) } } diff --git a/src/backend_task/identity/withdraw_from_identity.rs b/src/backend_task/identity/withdraw_from_identity.rs index 4c9154b70..894f7647e 100644 --- a/src/backend_task/identity/withdraw_from_identity.rs +++ b/src/backend_task/identity/withdraw_from_identity.rs @@ -29,7 +29,7 @@ impl AppContext { .await .map_err(|e| format!("Withdrawal error: {}", e))?; qualified_identity.identity.set_balance(remaining_balance); - self.insert_local_qualified_identity(&qualified_identity) + self.insert_local_qualified_identity(&qualified_identity, None) .map_err(|e| format!("Database error: {}", e)) } } diff --git a/src/context.rs b/src/context.rs index d87bb5b44..29124eaec 100644 --- a/src/context.rs +++ b/src/context.rs @@ -123,24 +123,34 @@ impl AppContext { pub fn insert_local_identity(&self, identity: &Identity) -> Result<()> { self.db - .insert_local_qualified_identity(&identity.clone().into(), self) + .insert_local_qualified_identity(&identity.clone().into(), None, self) } pub fn insert_local_qualified_identity( &self, qualified_identity: &QualifiedIdentity, + wallet_and_identity_id_info: Option<(&[u8], u32)>, ) -> Result<()> { - self.db - .insert_local_qualified_identity(qualified_identity, self) + self.db.insert_local_qualified_identity( + qualified_identity, + wallet_and_identity_id_info, + self, + ) } /// This is for before we know if Platform will accept the identity pub fn insert_local_qualified_identity_in_creation( &self, qualified_identity: &QualifiedIdentity, + wallet_id: &[u8], + identity_index: u32, ) -> Result<()> { - self.db - .insert_local_qualified_identity_in_creation(qualified_identity, self) + self.db.insert_local_qualified_identity_in_creation( + qualified_identity, + wallet_id, + identity_index, + self, + ) } pub fn load_local_qualified_identities(&self) -> Result> { diff --git a/src/database/identities.rs b/src/database/identities.rs index 592192fb7..b54f3960e 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -1,6 +1,7 @@ use crate::context::AppContext; use crate::database::Database; use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::platform::Identifier; use rusqlite::params; @@ -25,6 +26,7 @@ impl Database { pub fn insert_local_qualified_identity( &self, qualified_identity: &QualifiedIdentity, + wallet_and_identity_id_info: Option<(&[u8], u32)>, app_context: &AppContext, ) -> rusqlite::Result<()> { let id = qualified_identity.identity.id().to_vec(); @@ -34,17 +36,40 @@ impl Database { let network = app_context.network_string(); - self.execute( - "INSERT OR REPLACE INTO identity (id, data, is_local, alias, identity_type, network) - VALUES (?, ?, 1, ?, ?, ?)", - params![id, data, alias, identity_type, network], - )?; + if let Some((wallet, wallet_index)) = wallet_and_identity_id_info { + // If wallet information is provided, insert with wallet and wallet_index + self.execute( + "INSERT OR REPLACE INTO identity + (id, data, is_local, alias, identity_type, network, wallet, wallet_index) + VALUES (?, ?, 1, ?, ?, ?, ?, ?)", + params![ + id, + data, + alias, + identity_type, + network, + wallet, + wallet_index + ], + )?; + } else { + // If wallet information is not provided, insert without wallet and wallet_index + self.execute( + "INSERT OR REPLACE INTO identity + (id, data, is_local, alias, identity_type, network) + VALUES (?, ?, 1, ?, ?, ?)", + params![id, data, alias, identity_type, network], + )?; + } + Ok(()) } pub fn insert_local_qualified_identity_in_creation( &self, qualified_identity: &QualifiedIdentity, + wallet_id: &[u8], + identity_index: u32, app_context: &AppContext, ) -> rusqlite::Result<()> { let id = qualified_identity.identity.id().to_vec(); @@ -55,10 +80,20 @@ impl Database { let network = app_context.network_string(); self.execute( - "INSERT OR REPLACE INTO identity (id, data, is_local, alias, identity_type, network, is_in_creation) - VALUES (?, ?, 1, ?, ?, ?, 1)", - params![id, data, alias, identity_type, network], + "INSERT OR REPLACE INTO identity + (id, data, is_local, alias, identity_type, network, is_in_creation, wallet, wallet_index) + VALUES (?, ?, 1, ?, ?, ?, 1, ?, ?)", + params![ + id, + data, + alias, + identity_type, + network, + wallet_id, + identity_index + ], )?; + Ok(()) } diff --git a/src/database/initialization.rs b/src/database/initialization.rs index 66eb9bc6e..7d48bb5b9 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -91,8 +91,12 @@ impl Database { is_local INTEGER NOT NULL, alias TEXT, info TEXT, + wallet BLOB, + wallet_index INTEGER, identity_type TEXT, - network TEXT NOT NULL + network TEXT NOT NULL, + CHECK ((wallet IS NOT NULL AND wallet_index IS NOT NULL) OR (wallet IS NULL AND wallet_index IS NULL)), + FOREIGN KEY (wallet) REFERENCES wallet(seed) ON DELETE CASCADE )", [], )?; diff --git a/src/database/wallet.rs b/src/database/wallet.rs index a810ed079..5d03a20fd 100644 --- a/src/database/wallet.rs +++ b/src/database/wallet.rs @@ -175,6 +175,7 @@ impl Database { watched_addresses: BTreeMap::new(), unused_asset_locks: vec![], alias, + identities: HashMap::new(), utxos: HashMap::new(), is_main, password_hint, diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 3ba35271d..58499a1cb 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -63,6 +63,7 @@ use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::prelude::AssetLockProof; +use dash_sdk::platform::Identity; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -106,6 +107,7 @@ pub struct Wallet { Option, )>, pub alias: Option, + pub identities: HashMap, pub utxos: HashMap>, pub is_main: bool, pub password_hint: Option, diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs index 00def558d..3ddca4ce8 100644 --- a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -158,6 +158,7 @@ impl AddNewIdentityScreen { alias_input: self.alias_input.clone(), keys: self.identity_keys.clone(), wallet: Arc::clone(selected_wallet), // Clone the Arc reference + wallet_identity_index: self.identity_id_number, identity_registration_method: IdentityRegistrationMethod::FundWithUtxo( utxo, tx_out, diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index fa61b6f66..4191a73be 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -296,7 +296,12 @@ impl AddNewIdentityScreen { .as_ref() .map_or(false, |selected| Arc::ptr_eq(selected, wallet)); - if ui.selectable_label(is_selected, wallet_alias).clicked() { + if ui.selectable_label(is_selected, wallet_alias).changed() { + { + let wallet = wallet.read().unwrap(); + self.identity_id_number = + wallet.identities.keys().copied().max().unwrap_or_default(); + } // Update the selected wallet self.selected_wallet = Some(wallet.clone()); } @@ -311,6 +316,9 @@ impl AddNewIdentityScreen { let wallet = wallet.read().unwrap(); + self.identity_id_number = + wallet.identities.keys().copied().max().unwrap_or_default(); + self.identity_keys.master_private_key = Some(wallet.identity_authentication_ecdsa_private_key( self.app_context.network, @@ -545,6 +553,7 @@ impl AddNewIdentityScreen { alias_input: self.alias_input.clone(), keys: self.identity_keys.clone(), wallet: Arc::clone(selected_wallet), // Clone the Arc reference + wallet_identity_index: self.identity_id_number, identity_registration_method: IdentityRegistrationMethod::UseAssetLock( address, funding_asset_lock, @@ -575,6 +584,7 @@ impl AddNewIdentityScreen { alias_input: self.alias_input.clone(), keys: self.identity_keys.clone(), wallet: Arc::clone(selected_wallet), // Clone the Arc reference + wallet_identity_index: self.identity_id_number, identity_registration_method: IdentityRegistrationMethod::FundWithWallet( amount, self.identity_id_number, diff --git a/src/ui/key_info_screen.rs b/src/ui/key_info_screen.rs index fba75fd13..48c7307e1 100644 --- a/src/ui/key_info_screen.rs +++ b/src/ui/key_info_screen.rs @@ -211,7 +211,7 @@ impl KeyInfoScreen { ); match self .app_context - .insert_local_qualified_identity(&self.identity) + .insert_local_qualified_identity(&self.identity, None) { Ok(_) => { self.error_message = None; diff --git a/src/ui/wallet/add_new_wallet_screen.rs b/src/ui/wallet/add_new_wallet_screen.rs index 1f605152c..214526317 100644 --- a/src/ui/wallet/add_new_wallet_screen.rs +++ b/src/ui/wallet/add_new_wallet_screen.rs @@ -57,6 +57,7 @@ impl AddNewWalletScreen { watched_addresses: Default::default(), unused_asset_locks: Default::default(), alias: None, + identities: Default::default(), utxos: Default::default(), is_main: true, password_hint: None, diff --git a/src/ui/wallet/import_wallet_screen.rs b/src/ui/wallet/import_wallet_screen.rs index 25f38eacf..c9967e7e7 100644 --- a/src/ui/wallet/import_wallet_screen.rs +++ b/src/ui/wallet/import_wallet_screen.rs @@ -57,6 +57,7 @@ impl ImportWalletScreen { watched_addresses: Default::default(), unused_asset_locks: Default::default(), alias: None, + identities: Default::default(), utxos: Default::default(), is_main: true, password_hint: None, From 708ceddd6628992c4dc03e46e3bd16d81739f8cd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 Nov 2024 07:42:26 +0100 Subject: [PATCH 4/7] wallet encryption --- Cargo.toml | 5 +- src/backend_task/core/mod.rs | 4 +- src/backend_task/identity/mod.rs | 3 +- .../identity/register_identity.rs | 6 +- src/context.rs | 12 +- src/database/asset_lock_transaction.rs | 20 +-- src/database/identities.rs | 1 - src/database/initialization.rs | 17 +- src/database/wallet.rs | 109 +++++++----- src/model/wallet/asset_lock_transaction.rs | 1 + src/model/wallet/encryption.rs | 155 ++++++++++++++++++ src/model/wallet/mod.rs | 143 ++++++++++++++-- src/ui/components/entropy_grid.rs | 2 +- .../by_wallet_qr_code.rs | 2 +- .../identities/add_new_identity_screen/mod.rs | 78 +++++---- src/ui/wallet/add_new_wallet_screen.rs | 144 ++++++++++++++-- src/ui/wallet/import_wallet_screen.rs | 39 +---- src/ui/wallet/wallets_screen.rs | 4 +- 18 files changed, 572 insertions(+), 173 deletions(-) create mode 100644 src/model/wallet/encryption.rs diff --git a/Cargo.toml b/Cargo.toml index bd603c3e7..2978cb12b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,4 +49,7 @@ bitflags = "2.6.0" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } rust-embed = "8.5.0" zmq = "0.10" -zeroize = "1.8.1" \ No newline at end of file +zeroize = "1.8.1" +zxcvbn = "3.1.0" +argon2 = "0.5" # For Argon2 key derivation +aes-gcm = "0.10"# For AES-256-GCM encryption \ No newline at end of file diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index 6b4377bcd..1deacef28 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -4,9 +4,7 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; -use dash_sdk::dpp::dashcore::{ - Address, ChainLock, Network, OutPoint, ScriptBuf, Transaction, TxOut, -}; +use dash_sdk::dpp::dashcore::{Address, ChainLock, Network, OutPoint, Transaction, TxOut}; use std::sync::{Arc, RwLock}; #[derive(Debug, Clone)] diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index 553d02e02..249ff91d5 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -16,7 +16,7 @@ use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey, TxOut}; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::hashes::Hash; -use dash_sdk::dpp::dashcore::{OutPoint, ScriptBuf, Transaction}; +use dash_sdk::dpp::dashcore::{OutPoint, Transaction}; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; @@ -29,7 +29,6 @@ use dash_sdk::Sdk; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::{Arc, RwLock}; use tokio::sync::mpsc; -use tracing_subscriber::util::SubscriberInitExt; #[derive(Debug, Clone, PartialEq)] pub struct IdentityInputToLoad { diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index 9e38a6c24..afd808fca 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -133,7 +133,7 @@ impl AppContext { ) => { let tx_id = transaction.txid(); let wallet = wallet.read().unwrap(); - wallet_id = wallet.seed; + wallet_id = wallet.seed_hash(); let private_key = wallet .private_key_for_address(&address, self.network)? .ok_or("Asset Lock not valid for wallet")?; @@ -169,7 +169,7 @@ impl AppContext { // Scope the write lock to avoid holding it across an await. let (asset_lock_transaction, asset_lock_proof_private_key, change_address) = { let mut wallet = wallet.write().unwrap(); - wallet_id = wallet.seed; + wallet_id = wallet.seed_hash(); match wallet.asset_lock_transaction( sdk.network, amount, @@ -232,7 +232,7 @@ impl AppContext { // Scope the write lock to avoid holding it across an await. let (asset_lock_transaction, asset_lock_proof_private_key) = { let mut wallet = wallet.write().unwrap(); - wallet_id = wallet.seed; + wallet_id = wallet.seed_hash(); wallet.asset_lock_transaction_for_utxo( sdk.network, utxo, diff --git a/src/context.rs b/src/context.rs index 29124eaec..827589281 100644 --- a/src/context.rs +++ b/src/context.rs @@ -11,7 +11,7 @@ use dash_sdk::dashcore_rpc::dashcore::{InstantLock, Transaction}; use dash_sdk::dashcore_rpc::{Auth, Client}; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayload::AssetLockPayloadType; -use dash_sdk::dpp::dashcore::{Address, Network, OutPoint, ScriptBuf, TxOut, Txid}; +use dash_sdk::dpp::dashcore::{Address, Network, OutPoint, TxOut, Txid}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; use dash_sdk::dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; @@ -253,7 +253,7 @@ impl AppContext { self.network, )?; self.db - .add_to_address_balance(&wallet.seed, &address, tx_out.value)?; + .add_to_address_balance(&wallet.seed_hash(), &address, tx_out.value)?; // Create the OutPoint and insert it into the wallet.utxos entry let out_point = OutPoint::new(tx.txid(), vout as u32); @@ -341,8 +341,12 @@ impl AppContext { .sum(); // Store the asset lock transaction in the database - self.db - .store_asset_lock_transaction(tx, amount, islock.as_ref(), &wallet.seed)?; + self.db.store_asset_lock_transaction( + tx, + amount, + islock.as_ref(), + &wallet.seed_hash(), + )?; let first = payload .credit_outputs diff --git a/src/database/asset_lock_transaction.rs b/src/database/asset_lock_transaction.rs index 20265136d..ac01b8d51 100644 --- a/src/database/asset_lock_transaction.rs +++ b/src/database/asset_lock_transaction.rs @@ -12,7 +12,7 @@ impl Database { tx: &Transaction, amount: u64, // Include amount as a parameter islock: Option<&InstantLock>, - wallet_seed: &[u8; 64], // Include wallet_seed as a parameter + wallet_seed_hash: &[u8; 32], // Include wallet_seed_hash as a parameter ) -> rusqlite::Result<()> { let tx_bytes = serialize(tx); let txid = tx.txid().to_string(); @@ -36,7 +36,7 @@ impl Database { conn.execute( sql, - params![&txid, &tx_bytes, amount, &islock_bytes, wallet_seed], + params![&txid, &tx_bytes, amount, &islock_bytes, wallet_seed_hash], )?; Ok(()) @@ -46,7 +46,7 @@ impl Database { pub fn get_asset_lock_transaction( &self, txid: &str, - ) -> rusqlite::Result, [u8; 64])>> { + ) -> rusqlite::Result, [u8; 32])>> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( @@ -69,11 +69,11 @@ impl Database { None }; - let wallet_seed_array: [u8; 64] = wallet_seed + let wallet_seed_hash: [u8; 32] = wallet_seed .try_into() .map_err(|_| rusqlite::Error::InvalidQuery)?; - Ok(Some((tx, amount, islock, wallet_seed_array))) + Ok(Some((tx, amount, islock, wallet_seed_hash))) } else { Ok(None) } @@ -150,7 +150,7 @@ impl Database { Option, Option, Option>, - [u8; 64], + [u8; 32], )>, > { let conn = self.conn.lock().unwrap(); @@ -179,7 +179,7 @@ impl Database { None }; - let wallet_seed_array: [u8; 64] = wallet_seed + let wallet_seed_array: [u8; 32] = wallet_seed .try_into() .map_err(|_| rusqlite::Error::InvalidQuery)?; @@ -200,7 +200,7 @@ impl Database { pub fn get_asset_lock_transactions_by_identity_id( &self, identity_id: &[u8], - ) -> rusqlite::Result, Option, [u8; 64])>> { + ) -> rusqlite::Result, Option, [u8; 32])>> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( @@ -226,11 +226,11 @@ impl Database { None }; - let wallet_seed_array: [u8; 64] = wallet_seed + let wallet_seed_hash: [u8; 32] = wallet_seed .try_into() .map_err(|_| rusqlite::Error::InvalidQuery)?; - results.push((tx, amount, islock, chain_locked_height, wallet_seed_array)); + results.push((tx, amount, islock, chain_locked_height, wallet_seed_hash)); } Ok(results) diff --git a/src/database/identities.rs b/src/database/identities.rs index b54f3960e..54cea75c5 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -1,7 +1,6 @@ use crate::context::AppContext; use crate::database::Database; use crate::model::qualified_identity::QualifiedIdentity; -use crate::model::wallet::Wallet; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::platform::Identifier; use rusqlite::params; diff --git a/src/database/initialization.rs b/src/database/initialization.rs index 7d48bb5b9..4226f96a0 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -16,9 +16,14 @@ impl Database { // Create the wallet table self.execute( "CREATE TABLE IF NOT EXISTS wallet ( - seed BLOB NOT NULL PRIMARY KEY, + seed_hash BLOB NOT NULL PRIMARY KEY, + encrypted_seed BLOB NOT NULL, + salt BLOB NOT NULL, + nonce BLOB NOT NULL, + master_ecdsa_epk BLOB NOT NULL, alias TEXT, is_main INTEGER, + uses_password INTEGER NOT NULL, password_hint TEXT, network TEXT NOT NULL )", @@ -28,14 +33,14 @@ impl Database { // Create wallet addresses self.execute( "CREATE TABLE IF NOT EXISTS wallet_addresses ( - seed BLOB NOT NULL, + seed_hash BLOB NOT NULL, address TEXT NOT NULL, derivation_path TEXT NOT NULL, balance INTEGER, path_reference INTEGER NOT NULL, path_type INTEGER NOT NULL, - PRIMARY KEY (seed, address), - FOREIGN KEY (seed) REFERENCES wallet(seed) ON DELETE CASCADE + PRIMARY KEY (seed_hash, address), + FOREIGN KEY (seed_hash) REFERENCES wallet(seed_hash) ON DELETE CASCADE )", [], )?; @@ -77,7 +82,7 @@ impl Database { wallet BLOB NOT NULL, FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE, FOREIGN KEY (identity_id_potentially_in_creation) REFERENCES identity(id), - FOREIGN KEY (wallet) REFERENCES wallet(seed) ON DELETE CASCADE + FOREIGN KEY (wallet) REFERENCES wallet(seed_hash) ON DELETE CASCADE )", [], )?; @@ -96,7 +101,7 @@ impl Database { identity_type TEXT, network TEXT NOT NULL, CHECK ((wallet IS NOT NULL AND wallet_index IS NOT NULL) OR (wallet IS NULL AND wallet_index IS NULL)), - FOREIGN KEY (wallet) REFERENCES wallet(seed) ON DELETE CASCADE + FOREIGN KEY (wallet) REFERENCES wallet(seed_hash) ON DELETE CASCADE )", [], )?; diff --git a/src/database/wallet.rs b/src/database/wallet.rs index 5d03a20fd..0a2e1531d 100644 --- a/src/database/wallet.rs +++ b/src/database/wallet.rs @@ -1,9 +1,11 @@ use crate::database::Database; -use crate::model::wallet::{AddressInfo, DerivationPathReference, DerivationPathType, Wallet}; +use crate::model::wallet::{ + AddressInfo, ClosedWalletSeed, DerivationPathReference, DerivationPathType, Wallet, WalletSeed, +}; use dash_sdk::dashcore_rpc::dashcore::transaction::special_transaction::TransactionPayload; use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dpp::balances::credits::Duffs; -use dash_sdk::dpp::dashcore::bip32::DerivationPath; +use dash_sdk::dpp::dashcore::bip32::{DerivationPath, ExtendedPubKey}; use dash_sdk::dpp::dashcore::consensus::deserialize; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{ @@ -18,16 +20,25 @@ use std::str::FromStr; impl Database { /// Insert a new wallet into the wallet table - pub fn insert_wallet(&self, wallet: &Wallet, network: &Network) -> rusqlite::Result<()> { + pub fn store_wallet(&self, wallet: &Wallet, network: &Network) -> rusqlite::Result<()> { let network_str = network.to_string(); + + // Serialize the extended public keys + let master_ecdsa_epk_bytes = wallet.master_ecdsa_extended_public_key.encode(); + self.execute( - "INSERT INTO wallet (seed, alias, is_main, password_hint, network) - VALUES (?, ?, ?, ?, ?)", + "INSERT INTO wallet (seed_hash, encrypted_seed, salt, nonce, master_ecdsa_epk, alias, is_main, uses_password, password_hint, network) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", params![ - wallet.seed, + wallet.seed_hash(), + wallet.encrypted_seed_slice(), + wallet.salt(), + wallet.nonce(), + master_ecdsa_epk_bytes, wallet.alias.clone(), wallet.is_main as i32, - wallet.password_hint.clone(), + wallet.uses_password, + wallet.password_hint().clone(), network_str ], )?; @@ -38,14 +49,14 @@ impl Database { /// If the alias is `None`, it sets the alias to NULL in the database. pub fn set_wallet_alias( &self, - seed: &[u8; 64], + seed_hash: &[u8; 32], new_alias: Option, ) -> rusqlite::Result<()> { let conn = self.conn.lock().unwrap(); conn.execute( "UPDATE wallet SET alias = ? WHERE seed = ?", - params![new_alias, seed], + params![new_alias, seed_hash], )?; Ok(()) @@ -54,13 +65,13 @@ impl Database { /// Update only the alias and is_main fields of a wallet pub fn update_wallet_alias_and_main( &self, - seed: &[u8; 64], + seed_hash: &[u8; 32], new_alias: Option, is_main: bool, ) -> rusqlite::Result<()> { self.execute( "UPDATE wallet SET alias = ?, is_main = ? WHERE seed = ?", - params![new_alias, is_main as i32, seed], + params![new_alias, is_main as i32, seed_hash], )?; Ok(()) } @@ -69,7 +80,7 @@ impl Database { /// If the address already exists, it does nothing. pub fn add_address( &self, - seed: &[u8; 64], + seed_hash: &[u8; 32], address: &Address, derivation_path: &DerivationPath, path_reference: DerivationPathReference, @@ -81,18 +92,19 @@ impl Database { // Step 1: Check if the address already exists for the given seed. let mut stmt = conn.prepare( "SELECT COUNT(1) FROM wallet_addresses - WHERE seed = ? AND address = ?", + WHERE seed_hash = ? AND address = ?", )?; - let count: u32 = stmt.query_row(params![seed, address.to_string()], |row| row.get(0))?; + let count: u32 = + stmt.query_row(params![seed_hash, address.to_string()], |row| row.get(0))?; // Step 2: If the address doesn't exist, insert it. if count == 0 { conn.execute( "INSERT INTO wallet_addresses - (seed, address, derivation_path, path_reference, path_type, balance) + (seed_hash, address, derivation_path, path_reference, path_type, balance) VALUES (?, ?, ?, ?, ?, ?)", params![ - seed, + seed_hash, address.to_string(), derivation_path.to_string(), path_reference as u32, @@ -107,15 +119,15 @@ impl Database { /// Update the balance of an existing address. pub fn update_address_balance( &self, - seed: &[u8; 64], + seed_hash: &[u8; 32], address: &Address, new_balance: u64, ) -> rusqlite::Result<()> { let rows_affected = self.execute( "UPDATE wallet_addresses SET balance = ? - WHERE seed = ? AND address = ?", - params![new_balance, seed, address.to_string()], + WHERE seed_hash = ? AND address = ?", + params![new_balance, seed_hash, address.to_string()], )?; if rows_affected == 0 { @@ -128,7 +140,7 @@ impl Database { /// Add a balance to an existing address. pub fn add_to_address_balance( &self, - seed: &[u8; 64], + seed_hash: &[u8; 32], address: &Address, additional_balance: u64, ) -> rusqlite::Result<()> { @@ -136,7 +148,7 @@ impl Database { "UPDATE wallet_addresses SET balance = balance + ? WHERE seed = ? AND address = ?", - params![additional_balance, seed, address.to_string()], + params![additional_balance, seed_hash, address.to_string()], )?; if rows_affected == 0 { @@ -152,24 +164,43 @@ impl Database { let conn = self.conn.lock().unwrap(); // Step 1: Retrieve all wallets for the given network. - let mut stmt = conn - .prepare("SELECT seed, alias, is_main, password_hint FROM wallet WHERE network = ?")?; + let mut stmt = conn.prepare( + "SELECT seed_hash, encrypted_seed, salt, nonce, master_ecdsa_epk, alias, is_main, uses_password, password_hint FROM wallet WHERE network = ?", + )?; - let mut wallets_map: BTreeMap<[u8; 64], Wallet> = BTreeMap::new(); + let mut wallets_map: BTreeMap<[u8; 32], Wallet> = BTreeMap::new(); let wallet_rows = stmt.query_map([network_str.clone()], |row| { - let seed: Vec = row.get(0)?; - let alias: Option = row.get(1)?; - let is_main: bool = row.get(2)?; - let password_hint: Option = row.get(3)?; - - let seed_array: [u8; 64] = seed.try_into().expect("Seed should be 64 bytes"); + let seed_hash: Vec = row.get(0)?; + let encrypted_seed: Vec = row.get(1)?; + let salt: Vec = row.get(2)?; + let nonce: Vec = row.get(3)?; + let master_ecdsa_epk_bytes: Vec = row.get(4)?; + let alias: Option = row.get(5)?; + let is_main: bool = row.get(6)?; + let uses_password: bool = row.get(7)?; + let password_hint: Option = row.get(7)?; + + // Reconstruct the extended public keys + let master_ecdsa_extended_public_key = ExtendedPubKey::decode(&master_ecdsa_epk_bytes) + .expect("Failed to decode ExtendedPubKey"); + + let seed_hash_array: [u8; 32] = + seed_hash.try_into().expect("Seed hash should be 32 bytes"); // Insert a new Wallet into the map wallets_map.insert( - seed_array, + seed_hash_array, Wallet { - seed: seed_array, + wallet_seed: WalletSeed::Closed(ClosedWalletSeed { + seed_hash: seed_hash_array, + encrypted_seed, + salt, + nonce, + password_hint, + }), + uses_password, + master_ecdsa_extended_public_key, address_balances: BTreeMap::new(), known_addresses: BTreeMap::new(), watched_addresses: BTreeMap::new(), @@ -178,7 +209,6 @@ impl Database { identities: HashMap::new(), utxos: HashMap::new(), is_main, - password_hint, }, ); Ok(()) @@ -191,18 +221,19 @@ impl Database { // Step 2: Retrieve all addresses, balances, and derivation paths associated with the wallets. let mut address_stmt = conn.prepare( - "SELECT seed, address, derivation_path, balance, path_reference, path_type FROM wallet_addresses", + "SELECT seed_hash, address, derivation_path, balance, path_reference, path_type FROM wallet_addresses", )?; let address_rows = address_stmt.query_map([], |row| { - let seed: Vec = row.get(0)?; + let seed_hash: Vec = row.get(0)?; let address: String = row.get(1)?; let derivation_path: String = row.get(2)?; let balance: Option = row.get(3)?; let path_reference: u32 = row.get(4)?; let path_type: u32 = row.get(5)?; - let seed_array: [u8; 64] = seed.try_into().expect("Seed should be 64 bytes"); + let seed_hash_array: [u8; 32] = + seed_hash.try_into().expect("Seed hash should be 32 bytes"); let address = Address::from_str(&address) .expect("Invalid address format") .assume_checked(); @@ -222,7 +253,7 @@ impl Database { let path_type = DerivationPathType::from_bits_truncate(path_type as u32); Ok(( - seed_array, + seed_hash_array, address, derivation_path, balance, @@ -312,7 +343,7 @@ impl Database { let islock_data: Option> = row.get(3)?; let chain_locked_height: Option = row.get(4)?; - let wallet_seed_array: [u8; 64] = + let wallet_seed_hash_array: [u8; 32] = wallet_seed.try_into().expect("Seed should be 64 bytes"); let tx: Transaction = deserialize(&tx_data).expect("Failed to deserialize transaction"); @@ -356,7 +387,7 @@ impl Database { (None, None) }; - Ok((wallet_seed_array, tx, address, amount, islock, proof)) + Ok((wallet_seed_hash_array, tx, address, amount, islock, proof)) })?; // Step 7: Add the asset lock transactions to the corresponding wallets. diff --git a/src/model/wallet/asset_lock_transaction.rs b/src/model/wallet/asset_lock_transaction.rs index bee5d16d9..ce600f4a4 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -9,6 +9,7 @@ use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayloa use dash_sdk::dpp::dashcore::{ Address, Network, OutPoint, PrivateKey, ScriptBuf, Transaction, TxIn, TxOut, }; + impl Wallet { pub fn asset_lock_transaction( &mut self, diff --git a/src/model/wallet/encryption.rs b/src/model/wallet/encryption.rs new file mode 100644 index 000000000..b36b5ddb8 --- /dev/null +++ b/src/model/wallet/encryption.rs @@ -0,0 +1,155 @@ +use aes_gcm::aead::Aead; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; +use argon2::{self, Argon2}; +use rand::rngs::OsRng; +use rand::RngCore; + +const SALT_SIZE: usize = 16; // 128-bit salt +const NONCE_SIZE: usize = 12; // 96-bit nonce for AES-GCM + +use crate::model::wallet::ClosedWalletSeed; +use sha2::{Digest, Sha256}; + +impl ClosedWalletSeed { + pub fn compute_seed_hash(seed: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(seed); + let result = hasher.finalize(); + let mut seed_hash = [0u8; 32]; + seed_hash.copy_from_slice(&result); + seed_hash + } + + /// Derive a key from the password and salt using Argon2. + fn derive_key(password: &str, salt: &[u8]) -> Result, String> { + let key_length = 32; // For AES-256, we use a 256-bit key (32 bytes) + + let mut key = vec![0u8; key_length]; + + // Using Argon2 with default parameters + let argon2 = Argon2::default(); + + // Deriving the key + argon2 + .hash_password_into(password.as_bytes(), salt, &mut key) + .map_err(|e| e.to_string())?; + + Ok(key) + } + + /// Encrypt the seed using AES-256-GCM. + pub(crate) fn encrypt_seed( + seed: &[u8], + password: &str, + ) -> Result<(Vec, Vec, Vec), String> { + // Generate a random salt + let mut salt = vec![0u8; SALT_SIZE]; + OsRng.fill_bytes(&mut salt); + + // Derive the key + let key = Self::derive_key(password, &salt)?; + + // Generate a random nonce + let mut nonce = vec![0u8; NONCE_SIZE]; + OsRng.fill_bytes(&mut nonce); + + // Create cipher instance + let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| e.to_string())?; + + // Encrypt the seed + let encrypted_seed = cipher + .encrypt(Nonce::from_slice(&nonce), seed) + .map_err(|e| e.to_string())?; + + Ok((encrypted_seed, salt, nonce)) + } + + /// Decrypt the seed using AES-256-GCM. + pub fn decrypt_seed(&self, password: &str) -> Result<[u8; 64], String> { + // Derive the key + let key = Self::derive_key(password, &self.salt)?; + + // Create cipher instance + let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| e.to_string())?; + + // Decrypt the seed + let seed = cipher + .decrypt( + Nonce::from_slice(&self.nonce), + self.encrypted_seed.as_slice(), + ) + .map_err(|e| e.to_string())?; + + let sized_seed = seed.try_into().map_err(|e: Vec| { + format!( + "invalid seed length, expected 64 bytes, got {} bytes", + e.len() + ) + })?; + + Ok(sized_seed) + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt_seed() { + let seed = [42u8; 64]; // A 64-byte seed filled with the value 42 + let password = "securepassword"; + + // Encrypt the seed using the encrypt_seed method + let (encrypted_seed, salt, nonce) = + ClosedWalletSeed::encrypt_seed(&seed, password).expect("Encryption failed"); + + // Compute the seed hash + let seed_hash = ClosedWalletSeed::compute_seed_hash(&seed); + + // Create a ClosedWalletSeed instance with the encrypted data + let closed_wallet_seed = ClosedWalletSeed { + seed_hash, + encrypted_seed, + salt, + nonce, + password_hint: None, // Set password hint if needed + }; + + // Decrypt the seed using the instance method + let decrypted_seed = closed_wallet_seed + .decrypt_seed(password) + .expect("Decryption failed"); + + // Verify that the decrypted seed matches the original seed + assert_eq!(seed, decrypted_seed); + } + + #[test] + fn test_incorrect_password() { + let seed = [42u8; 64]; // A 64-byte seed + let password = "securepassword"; + let wrong_password = "wrongpassword"; + + // Encrypt the seed using the encrypt_seed method + let (encrypted_seed, salt, nonce) = + ClosedWalletSeed::encrypt_seed(&seed, password).expect("Encryption failed"); + + // Compute the seed hash + let seed_hash = ClosedWalletSeed::compute_seed_hash(&seed); + + // Create a ClosedWalletSeed instance with the encrypted data + let closed_wallet_seed = ClosedWalletSeed { + seed_hash, + encrypted_seed, + salt, + nonce, + password_hint: None, + }; + + // Attempt to decrypt with the wrong password + let result = closed_wallet_seed.decrypt_seed(wrong_password); + + // Verify that decryption fails + assert!(result.is_err()); + } +} diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 58499a1cb..e6b18b177 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -1,7 +1,9 @@ mod asset_lock_transaction; +pub mod encryption; mod utxos; -use dash_sdk::dashcore_rpc::dashcore::bip32::KeyDerivationType; +use dash_sdk::dashcore_rpc::dashcore::bip32::{ExtendedPubKey, KeyDerivationType}; + use dash_sdk::dpp::dashcore::bip32::DerivationPath; use dash_sdk::dpp::dashcore::{ Address, InstantLock, Network, OutPoint, PrivateKey, PublicKey, Transaction, TxOut, @@ -64,6 +66,7 @@ use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::platform::Identity; +use zeroize::Zeroize; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -95,7 +98,9 @@ pub struct AddressInfo { #[derive(Debug, Clone, PartialEq)] pub struct Wallet { - pub(crate) seed: [u8; 64], + pub wallet_seed: WalletSeed, + pub uses_password: bool, + pub master_ecdsa_extended_public_key: ExtendedPubKey, pub address_balances: BTreeMap, pub known_addresses: BTreeMap, pub watched_addresses: BTreeMap, @@ -110,9 +115,67 @@ pub struct Wallet { pub identities: HashMap, pub utxos: HashMap>, pub is_main: bool, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum WalletSeed { + Open(OpenWalletSeed), + Closed(ClosedWalletSeed), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OpenWalletSeed { + pub seed: [u8; 64], + pub wallet_info: ClosedWalletSeed, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ClosedWalletSeed { + pub seed_hash: [u8; 32], // SHA-256 hash of the seed + pub encrypted_seed: Vec, + pub salt: Vec, + pub nonce: Vec, pub password_hint: Option, } +impl WalletSeed { + /// Opens the wallet by decrypting the seed using the provided password. + pub fn open(&mut self, password: &str) -> Result<(), String> { + match self { + WalletSeed::Open(_) => { + // Wallet is already open + Ok(()) + } + WalletSeed::Closed(closed_seed) => { + // Try to decrypt the seed + let seed = closed_seed.decrypt_seed(password)?; + let open_wallet_seed = OpenWalletSeed { + seed, + wallet_info: closed_seed.clone(), + }; + *self = WalletSeed::Open(open_wallet_seed); + Ok(()) + } + } + } + + /// Closes the wallet by securely erasing the seed and transitioning to Closed state. + pub fn close(&mut self) { + match self { + WalletSeed::Open(open_seed) => { + // Zeroize the seed + open_seed.seed.zeroize(); + // Transition back to ClosedWalletSeed + let closed_seed = open_seed.wallet_info.clone(); + *self = WalletSeed::Closed(closed_seed); + } + WalletSeed::Closed(_) => { + // Wallet is already closed + } + } + } +} + impl Wallet { pub fn has_balance(&self) -> bool { self.max_balance() > 0 @@ -126,6 +189,48 @@ impl Wallet { self.address_balances.values().sum::() } + fn seed_bytes(&self) -> Result<&[u8; 64], String> { + match &self.wallet_seed { + WalletSeed::Open(opened) => Ok(&opened.seed), + WalletSeed::Closed(_) => Err("Wallet is closed, please decrypt it first".to_string()), + } + } + + pub fn seed_hash(&self) -> [u8; 32] { + match &self.wallet_seed { + WalletSeed::Open(opened) => opened.wallet_info.seed_hash, + WalletSeed::Closed(closed) => closed.seed_hash, + } + } + + pub fn encrypted_seed_slice(&self) -> &[u8] { + match &self.wallet_seed { + WalletSeed::Open(opened) => opened.wallet_info.encrypted_seed.as_slice(), + WalletSeed::Closed(closed) => closed.encrypted_seed.as_slice(), + } + } + + pub fn salt(&self) -> &[u8] { + match &self.wallet_seed { + WalletSeed::Open(opened) => opened.wallet_info.salt.as_slice(), + WalletSeed::Closed(closed) => closed.salt.as_slice(), + } + } + + pub fn nonce(&self) -> &[u8] { + match &self.wallet_seed { + WalletSeed::Open(opened) => opened.wallet_info.nonce.as_slice(), + WalletSeed::Closed(closed) => closed.nonce.as_slice(), + } + } + + pub fn password_hint(&self) -> &Option { + match &self.wallet_seed { + WalletSeed::Open(opened) => &opened.wallet_info.password_hint, + WalletSeed::Closed(closed) => &closed.password_hint, + } + } + pub fn private_key_for_address( &self, address: &Address, @@ -135,11 +240,11 @@ impl Wallet { .get(address) .map(|derivation_path| { derivation_path - .derive_priv_ecdsa_for_master_seed(&self.seed, network) + .derive_priv_ecdsa_for_master_seed(self.seed_bytes()?, network) .map(|extended_private_key| extended_private_key.to_priv()) + .map_err(|e| e.to_string()) }) .transpose() - .map_err(|e| e.to_string()) } pub fn unused_bip_44_public_key( @@ -179,7 +284,7 @@ impl Wallet { } else { // Address is not known, proceed to register it let public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, network) + .derive_pub_ecdsa_for_master_seed(self.seed_bytes()?, network) .expect("derivation should not be able to fail") .to_pub(); if let Some(app_context) = register { @@ -202,7 +307,7 @@ impl Wallet { app_context .db .add_address( - &self.seed, + &self.seed_hash(), &address, &derivation_path, DerivationPathReference::BIP44, @@ -230,7 +335,7 @@ impl Wallet { let derivation_path = found_unused_derivation_path.unwrap(); let extended_public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, network) + .derive_pub_ecdsa_for_master_seed(self.seed_bytes()?, network) .expect("derivation should not be able to fail"); Ok((extended_public_key.to_pub(), derivation_path)) } @@ -247,8 +352,10 @@ impl Wallet { identity_index, key_index, ); - let extended_public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, network) + let secp = Secp256k1::new(); + let extended_public_key = self + .master_ecdsa_extended_public_key + .derive_pub(&secp, &derivation_path) .expect("derivation should not be able to fail"); extended_public_key.to_pub() } @@ -258,7 +365,7 @@ impl Wallet { network: Network, identity_index: u32, key_index: u32, - ) -> PrivateKey { + ) -> Result { let derivation_path = DerivationPath::identity_authentication_path( network, KeyDerivationType::ECDSA, @@ -266,9 +373,9 @@ impl Wallet { key_index, ); let extended_public_key = derivation_path - .derive_priv_ecdsa_for_master_seed(&self.seed, network) + .derive_priv_ecdsa_for_master_seed(self.seed_bytes()?, network) .expect("derivation should not be able to fail"); - extended_public_key.to_priv() + Ok(extended_public_key.to_priv()) } pub fn identity_registration_ecdsa_public_key( @@ -277,8 +384,10 @@ impl Wallet { index: u32, ) -> PublicKey { let derivation_path = DerivationPath::identity_registration_path(network, index); - let extended_public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, network) + let secp = Secp256k1::new(); + let extended_public_key = self + .master_ecdsa_extended_public_key + .derive_pub(&secp, &derivation_path) .expect("derivation should not be able to fail"); extended_public_key.to_pub() } @@ -291,7 +400,7 @@ impl Wallet { ) -> Result { let derivation_path = DerivationPath::identity_registration_path(network, index); let extended_private_key = derivation_path - .derive_priv_ecdsa_for_master_seed(&self.seed, network) + .derive_priv_ecdsa_for_master_seed(self.seed_bytes()?, network) .expect("derivation should not be able to fail"); let private_key = extended_private_key.to_priv(); @@ -301,7 +410,7 @@ impl Wallet { app_context .db .add_address( - &self.seed, + &self.seed_hash(), &address, &derivation_path, DerivationPathReference::BlockchainIdentityCreditRegistrationFunding, @@ -402,7 +511,7 @@ impl Wallet { // Update the database with the new balance. context .db - .update_address_balance(&self.seed, address, new_balance) + .update_address_balance(&self.seed_hash(), address, new_balance) .map_err(|e| e.to_string()) } } diff --git a/src/ui/components/entropy_grid.rs b/src/ui/components/entropy_grid.rs index 9ac4c224c..d75c2dd06 100644 --- a/src/ui/components/entropy_grid.rs +++ b/src/ui/components/entropy_grid.rs @@ -21,7 +21,7 @@ impl U256EntropyGrid { /// Render the UI and allow users to modify bits pub fn ui(&mut self, ui: &mut Ui) -> [u8; 32] { - ui.heading("1. Hover over this view to create extra randomness for the seed phrase"); + ui.heading("1. Hover over this view to create extra randomness for the seed phrase."); // Add padding around the grid ui.add_space(10.0); // Top padding diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs index 3ddca4ce8..764fe3f10 100644 --- a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -5,7 +5,7 @@ use crate::backend_task::identity::{ use crate::backend_task::BackendTask; use crate::ui::identities::add_new_identity_screen::{ copy_to_clipboard, generate_qr_code_image, AddNewIdentityScreen, - AddNewIdentityWalletFundedScreenStep, FundingMethod, + AddNewIdentityWalletFundedScreenStep, }; use dash_sdk::dashcore_rpc::RpcApi; use eframe::epaint::TextureHandle; diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index 4191a73be..1cfef7e49 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -319,30 +319,37 @@ impl AddNewIdentityScreen { self.identity_id_number = wallet.identities.keys().copied().max().unwrap_or_default(); - self.identity_keys.master_private_key = - Some(wallet.identity_authentication_ecdsa_private_key( - self.app_context.network, - 0, - 0, - )); + self.identity_keys.master_private_key = Some( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 0, + ) + .expect("expected to have decrypted wallet"), + ); // Update the additional keys input self.identity_keys.keys_input = vec![ ( - wallet.identity_authentication_ecdsa_private_key( - self.app_context.network, - 0, - 1, - ), + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 1, + ) + .expect("expected to have decrypted wallet"), KeyType::ECDSA_HASH160, Purpose::AUTHENTICATION, SecurityLevel::HIGH, ), ( - wallet.identity_authentication_ecdsa_private_key( - self.app_context.network, - 0, - 2, - ), + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 2, + ) + .expect("expected to have decrypted wallet"), KeyType::ECDSA_HASH160, Purpose::TRANSFER, SecurityLevel::CRITICAL, @@ -647,12 +654,15 @@ impl AddNewIdentityScreen { let identity_index = self.identity_id_number; // Update the master private key and keys input from the wallet - self.identity_keys.master_private_key = - Some(wallet.identity_authentication_ecdsa_private_key( - self.app_context.network, - identity_index, - 0, - )); + self.identity_keys.master_private_key = Some( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + identity_index, + 0, + ) + .expect("expected to have decrypted wallet"), + ); // Update the additional keys input self.identity_keys.keys_input = self @@ -662,11 +672,13 @@ impl AddNewIdentityScreen { .enumerate() .map(|(key_index, (_, key_type, purpose, security_level))| { ( - wallet.identity_authentication_ecdsa_private_key( - self.app_context.network, - identity_index, - key_index as u32 + 1, - ), + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + identity_index, + key_index as u32 + 1, + ) + .expect("expected to have decrypted wallet"), *key_type, *purpose, *security_level, @@ -683,11 +695,13 @@ impl AddNewIdentityScreen { // Add a new key with default parameters self.identity_keys.keys_input.push(( - wallet.identity_authentication_ecdsa_private_key( - self.app_context.network, - self.identity_id_number, - new_key_index, - ), + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + self.identity_id_number, + new_key_index, + ) + .expect("expected to have decrypted wallet"), KeyType::ECDSA_HASH160, // Default key type Purpose::AUTHENTICATION, // Default purpose SecurityLevel::HIGH, // Default security level diff --git a/src/ui/wallet/add_new_wallet_screen.rs b/src/ui/wallet/add_new_wallet_screen.rs index 214526317..7c55126c0 100644 --- a/src/ui/wallet/add_new_wallet_screen.rs +++ b/src/ui/wallet/add_new_wallet_screen.rs @@ -4,23 +4,28 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::ScreenLike; use eframe::egui::Context; -use crate::model::wallet::Wallet; +use crate::model::wallet::{ClosedWalletSeed, OpenWalletSeed, Wallet, WalletSeed}; use crate::ui::components::entropy_grid::U256EntropyGrid; use bip39::{Language, Mnemonic}; +use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; +use dash_sdk::dpp::dashcore::bip32::{ExtendedPrivKey, ExtendedPubKey}; use egui::{ Color32, ComboBox, Direction, FontId, Frame, Grid, Layout, Margin, RichText, Stroke, TextStyle, Ui, Vec2, }; use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; +use zxcvbn::zxcvbn; pub struct AddNewWalletScreen { seed_phrase: Option, - passphrase: String, + password: String, entropy_grid: U256EntropyGrid, selected_language: Language, alias_input: String, wrote_it_down: bool, + password_strength: f64, + estimated_time_to_crack: String, pub app_context: Arc, } @@ -28,11 +33,13 @@ impl AddNewWalletScreen { pub fn new(app_context: &Arc) -> Self { Self { seed_phrase: None, - passphrase: String::new(), + password: String::new(), entropy_grid: U256EntropyGrid::new(), selected_language: Language::English, alias_input: String::new(), wrote_it_down: false, + password_strength: 0.0, + estimated_time_to_crack: "".to_string(), app_context: app_context.clone(), } } @@ -49,23 +56,55 @@ impl AddNewWalletScreen { fn save_wallet(&mut self) -> AppAction { if let Some(mnemonic) = &self.seed_phrase { - let seed = mnemonic.to_seed(self.passphrase.as_str()); + let seed = mnemonic.to_seed(""); + + let (encrypted_seed, salt, nonce, uses_password) = if self.password.is_empty() { + (seed.to_vec(), vec![], vec![], false) + } else { + // Encrypt the seed to obtain encrypted_seed, salt, and nonce + let (encrypted_seed, salt, nonce) = + ClosedWalletSeed::encrypt_seed(&seed, self.password.as_str()) + .expect("Encryption failed"); + (encrypted_seed, salt, nonce, true) + }; + + // Generate master ECDSA extended private key + let master_ecdsa_extended_private_key = + ExtendedPrivKey::new_master(self.app_context.network, &seed) + .expect("Failed to create master ECDSA extended private key"); + let secp = Secp256k1::new(); + let master_ecdsa_extended_public_key = + ExtendedPubKey::from_priv(&secp, &master_ecdsa_extended_private_key); + + // Compute the seed hash + let seed_hash = ClosedWalletSeed::compute_seed_hash(&seed); + let wallet = Wallet { - seed, + wallet_seed: WalletSeed::Open(OpenWalletSeed { + seed, + wallet_info: ClosedWalletSeed { + seed_hash, + encrypted_seed, + salt, + nonce, + password_hint: None, // Set a password hint if needed + }, + }), + uses_password, + master_ecdsa_extended_public_key, address_balances: Default::default(), known_addresses: Default::default(), watched_addresses: Default::default(), unused_asset_locks: Default::default(), - alias: None, + alias: Some(self.alias_input.clone()), identities: Default::default(), utxos: Default::default(), is_main: true, - password_hint: None, }; self.app_context .db - .insert_wallet(&wallet, &self.app_context.network) + .store_wallet(&wallet, &self.app_context.network) .ok(); // Acquire a write lock and add the new wallet @@ -218,6 +257,31 @@ impl AddNewWalletScreen { } } +fn format_time(seconds: f64) -> String { + let minute = 60.0; + let hour = 60.0 * minute; + let day = 24.0 * hour; + let year = 365.25 * day; + let century = 100.0 * year; + let millennium = 10.0 * century; + + if seconds < minute { + format!("{:.2} seconds", seconds) + } else if seconds < hour { + format!("{:.2} minutes", seconds / minute) + } else if seconds < day { + format!("{:.2} hours", seconds / hour) + } else if seconds < year { + format!("{:.2} days", seconds / day) + } else if seconds < century { + format!("{:.2} years", seconds / year) + } else if seconds < millennium { + format!("{:.2} centuries", seconds / century) + } else { + format!("More than a millennium") + } +} + impl ScreenLike for AddNewWalletScreen { fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = add_top_panel( @@ -243,13 +307,13 @@ impl ScreenLike for AddNewWalletScreen { ui.add_space(5.0); - ui.heading("2. Select your desired seed phrase language and press \"Generate\""); + ui.heading("2. Select your desired seed phrase language and press \"Generate\"."); self.render_seed_phrase_input(ui); ui.add_space(10.0); ui.heading( - "3. Write down the passphrase on a piece of paper and put it somewhere secure", + "3. Write down the passphrase on a piece of paper and put it somewhere secure.", ); ui.add_space(10.0); @@ -261,16 +325,70 @@ impl ScreenLike for AddNewWalletScreen { ui.add_space(20.0); - ui.heading("4. Add an optional password that must be used to unlock the wallet"); + ui.heading("4. Add a password that must be used to unlock the wallet. (Optional but Recommended)"); + + ui.add_space(8.0); ui.horizontal(|ui| { ui.label("Optional Password:"); - ui.text_edit_singleline(&mut self.passphrase); + if ui.text_edit_singleline(&mut self.password).changed() { + if !self.password.is_empty() { + let estimate = zxcvbn(&self.password, &[]); + + // Convert Score to u8 + let score_u8 = u8::from(estimate.score()); + + // Use the score to determine password strength percentage + self.password_strength = score_u8 as f64 * 25.0; // Since score ranges from 0 to 4 + + // Get the estimated crack time in seconds + let estimated_seconds = estimate.crack_times().offline_slow_hashing_1e4_per_second(); + + // Format the estimated time to a human-readable string + self.estimated_time_to_crack = estimated_seconds.to_string(); + } else { + self.password_strength = 0.0; + self.estimated_time_to_crack = String::new(); + } + } }); + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.label("Password Strength:"); + + // Since score ranges from 0 to 4, adjust percentage accordingly + let strength_percentage = (self.password_strength / 100.0).min(1.0); + let color = match self.password_strength as i32 { + 0..=25 => Color32::RED, + 26..=50 => Color32::YELLOW, + 51..=75 => Color32::LIGHT_GREEN, + _ => Color32::GREEN, + }; + ui.add( + egui::ProgressBar::new(strength_percentage as f32) + .desired_width(200.0) + .show_percentage() + .text(match self.password_strength as i32 { + 0 => "None".to_string(), + 1..=25 => "Very Weak".to_string(), + 26..=50 => "Weak".to_string(), + 51..=75 => "Strong".to_string(), + _ => "Very Strong".to_string(), + }) + .fill(color), + ); + }); + + ui.add_space(10.0); + ui.label(format!( + "Estimated time to crack: {}", + self.estimated_time_to_crack + )); + ui.add_space(20.0); - ui.heading("5. Save the wallet"); + ui.heading("5. Save the wallet."); ui.add_space(5.0); // Centered "Save Wallet" button at the bottom diff --git a/src/ui/wallet/import_wallet_screen.rs b/src/ui/wallet/import_wallet_screen.rs index c9967e7e7..32fdd52fd 100644 --- a/src/ui/wallet/import_wallet_screen.rs +++ b/src/ui/wallet/import_wallet_screen.rs @@ -4,14 +4,12 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::ScreenLike; use eframe::egui::Context; -use crate::model::wallet::Wallet; use crate::ui::components::entropy_grid::U256EntropyGrid; use bip39::{Language, Mnemonic}; use egui::{ Color32, ComboBox, Direction, FontId, Frame, Grid, Layout, Margin, RichText, Stroke, TextStyle, Ui, Vec2, }; -use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; pub struct ImportWalletScreen { @@ -47,41 +45,6 @@ impl ImportWalletScreen { self.seed_phrase = Some(mnemonic); } - fn save_wallet(&mut self) -> AppAction { - if let Some(mnemonic) = &self.seed_phrase { - let seed = mnemonic.to_seed(self.passphrase.as_str()); - let wallet = Wallet { - seed, - address_balances: Default::default(), - known_addresses: Default::default(), - watched_addresses: Default::default(), - unused_asset_locks: Default::default(), - alias: None, - identities: Default::default(), - utxos: Default::default(), - is_main: true, - password_hint: None, - }; - - self.app_context - .db - .insert_wallet(&wallet, &self.app_context.network) - .ok(); - - // Acquire a write lock and add the new wallet - if let Ok(mut wallets) = self.app_context.wallets.write() { - wallets.push(Arc::new(RwLock::new(wallet))); - self.app_context.has_wallet.store(true, Ordering::Relaxed); - } else { - eprintln!("Failed to acquire write lock on wallets"); - } - - AppAction::GoToMainScreen // Navigate back to the main screen after saving - } else { - AppAction::None // No action if no seed phrase exists - } - } - fn render_seed_phrase_input(&mut self, ui: &mut Ui) { ui.add_space(15.0); // Add spacing from the top ui.vertical(|ui| { @@ -288,7 +251,7 @@ impl ScreenLike for ImportWalletScreen { }); if ui.add(save_button).clicked() { - action = self.save_wallet(); // Trigger the save action + // action = self.save_wallet(); // Trigger the save action } }); }); diff --git a/src/ui/wallet/wallets_screen.rs b/src/ui/wallet/wallets_screen.rs index 187df863f..f686a0582 100644 --- a/src/ui/wallet/wallets_screen.rs +++ b/src/ui/wallet/wallets_screen.rs @@ -216,10 +216,10 @@ impl WalletsBalancesScreen { wallet.alias = Some(alias.clone()); // Update the alias in the database - let seed = wallet.seed; + let seed_hash = wallet.seed_hash(); self.app_context .db - .set_wallet_alias(&seed, Some(alias.clone())) + .set_wallet_alias(&seed_hash, Some(alias.clone())) .ok(); } } From 37b418c158088f2493a6bfb458d5f16f77e66754 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 Nov 2024 09:16:46 +0100 Subject: [PATCH 5/7] more wallet encryption measures --- src/database/initialization.rs | 2 +- src/database/wallet.rs | 24 +++--- src/model/wallet/mod.rs | 53 +++++++----- src/ui/wallet/add_new_wallet_screen.rs | 108 ++++++++++++++++--------- 4 files changed, 121 insertions(+), 66 deletions(-) diff --git a/src/database/initialization.rs b/src/database/initialization.rs index 4226f96a0..7fb51b7ba 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -20,7 +20,7 @@ impl Database { encrypted_seed BLOB NOT NULL, salt BLOB NOT NULL, nonce BLOB NOT NULL, - master_ecdsa_epk BLOB NOT NULL, + master_ecdsa_bip44_account_0_epk BLOB NOT NULL, alias TEXT, is_main INTEGER, uses_password INTEGER NOT NULL, diff --git a/src/database/wallet.rs b/src/database/wallet.rs index 0a2e1531d..769b65569 100644 --- a/src/database/wallet.rs +++ b/src/database/wallet.rs @@ -24,17 +24,18 @@ impl Database { let network_str = network.to_string(); // Serialize the extended public keys - let master_ecdsa_epk_bytes = wallet.master_ecdsa_extended_public_key.encode(); + let master_ecdsa_bip44_account_0_epk_bytes = + wallet.master_bip44_ecdsa_extended_public_key.encode(); self.execute( - "INSERT INTO wallet (seed_hash, encrypted_seed, salt, nonce, master_ecdsa_epk, alias, is_main, uses_password, password_hint, network) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO wallet (seed_hash, encrypted_seed, salt, nonce, master_ecdsa_bip44_account_0_epk, alias, is_main, uses_password, password_hint, network) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", params![ wallet.seed_hash(), wallet.encrypted_seed_slice(), wallet.salt(), wallet.nonce(), - master_ecdsa_epk_bytes, + master_ecdsa_bip44_account_0_epk_bytes, wallet.alias.clone(), wallet.is_main as i32, wallet.uses_password, @@ -55,7 +56,7 @@ impl Database { let conn = self.conn.lock().unwrap(); conn.execute( - "UPDATE wallet SET alias = ? WHERE seed = ?", + "UPDATE wallet SET alias = ? WHERE seed_hash = ?", params![new_alias, seed_hash], )?; @@ -165,7 +166,7 @@ impl Database { // Step 1: Retrieve all wallets for the given network. let mut stmt = conn.prepare( - "SELECT seed_hash, encrypted_seed, salt, nonce, master_ecdsa_epk, alias, is_main, uses_password, password_hint FROM wallet WHERE network = ?", + "SELECT seed_hash, encrypted_seed, salt, nonce, master_ecdsa_bip44_account_0_epk, alias, is_main, uses_password, password_hint FROM wallet WHERE network = ?", )?; let mut wallets_map: BTreeMap<[u8; 32], Wallet> = BTreeMap::new(); @@ -175,15 +176,16 @@ impl Database { let encrypted_seed: Vec = row.get(1)?; let salt: Vec = row.get(2)?; let nonce: Vec = row.get(3)?; - let master_ecdsa_epk_bytes: Vec = row.get(4)?; + let master_ecdsa_bip44_account_0_epk_bytes: Vec = row.get(4)?; let alias: Option = row.get(5)?; let is_main: bool = row.get(6)?; let uses_password: bool = row.get(7)?; - let password_hint: Option = row.get(7)?; + let password_hint: Option = row.get(8)?; // Reconstruct the extended public keys - let master_ecdsa_extended_public_key = ExtendedPubKey::decode(&master_ecdsa_epk_bytes) - .expect("Failed to decode ExtendedPubKey"); + let master_ecdsa_extended_public_key = + ExtendedPubKey::decode(&master_ecdsa_bip44_account_0_epk_bytes) + .expect("Failed to decode ExtendedPubKey"); let seed_hash_array: [u8; 32] = seed_hash.try_into().expect("Seed hash should be 32 bytes"); @@ -200,7 +202,7 @@ impl Database { password_hint, }), uses_password, - master_ecdsa_extended_public_key, + master_bip44_ecdsa_extended_public_key: master_ecdsa_extended_public_key, address_balances: BTreeMap::new(), known_addresses: BTreeMap::new(), watched_addresses: BTreeMap::new(), diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index e6b18b177..5c247f5ee 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -2,7 +2,7 @@ mod asset_lock_transaction; pub mod encryption; mod utxos; -use dash_sdk::dashcore_rpc::dashcore::bip32::{ExtendedPubKey, KeyDerivationType}; +use dash_sdk::dashcore_rpc::dashcore::bip32::{ChildNumber, ExtendedPubKey, KeyDerivationType}; use dash_sdk::dpp::dashcore::bip32::DerivationPath; use dash_sdk::dpp::dashcore::{ @@ -100,7 +100,7 @@ pub struct AddressInfo { pub struct Wallet { pub wallet_seed: WalletSeed, pub uses_password: bool, - pub master_ecdsa_extended_public_key: ExtendedPubKey, + pub master_bip44_ecdsa_extended_public_key: ExtendedPubKey, pub address_balances: BTreeMap, pub known_addresses: BTreeMap, pub watched_addresses: BTreeMap, @@ -256,6 +256,18 @@ impl Wallet { ) -> Result<(PublicKey, DerivationPath), String> { let mut address_index = 0; let mut found_unused_derivation_path = None; + let mut known_public_key = None; + let derivation_path_extension = DerivationPath::from( + [ + ChildNumber::Normal { + index: change.into(), + }, + ChildNumber::Normal { + index: address_index, + }, + ] + .as_slice(), + ); while found_unused_derivation_path.is_none() { let derivation_path = DerivationPath::bip_44_payment_path(network, 0, change, address_index); @@ -275,6 +287,13 @@ impl Wallet { if !skip_known_addresses_with_no_funds { // We can use this address found_unused_derivation_path = Some(derivation_path.clone()); + let secp = Secp256k1::new(); + let public_key = self + .master_bip44_ecdsa_extended_public_key + .derive_pub(&secp, &derivation_path_extension) + .map_err(|e| e.to_string())? + .to_pub(); + known_public_key = Some(public_key.clone()); break; } else { // Skip known addresses with no funds @@ -282,11 +301,13 @@ impl Wallet { continue; } } else { - // Address is not known, proceed to register it - let public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(self.seed_bytes()?, network) - .expect("derivation should not be able to fail") + let secp = Secp256k1::new(); + let public_key = self + .master_bip44_ecdsa_extended_public_key + .derive_pub(&secp, &derivation_path_extension) + .map_err(|e| e.to_string())? .to_pub(); + known_public_key = Some(public_key.clone()); if let Some(app_context) = register { let address = Address::p2pkh(&public_key, network); app_context @@ -334,10 +355,8 @@ impl Wallet { } let derivation_path = found_unused_derivation_path.unwrap(); - let extended_public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(self.seed_bytes()?, network) - .expect("derivation should not be able to fail"); - Ok((extended_public_key.to_pub(), derivation_path)) + let known_public_key = known_public_key.unwrap(); + Ok((known_public_key, derivation_path)) } pub fn identity_authentication_ecdsa_public_key( @@ -345,19 +364,17 @@ impl Wallet { network: Network, identity_index: u32, key_index: u32, - ) -> PublicKey { + ) -> Result { let derivation_path = DerivationPath::identity_authentication_path( network, KeyDerivationType::ECDSA, identity_index, key_index, ); - let secp = Secp256k1::new(); - let extended_public_key = self - .master_ecdsa_extended_public_key - .derive_pub(&secp, &derivation_path) - .expect("derivation should not be able to fail"); - extended_public_key.to_pub() + let extended_public_key = derivation_path + .derive_pub_ecdsa_for_master_seed(self.seed_bytes()?, network) + .map_err(|e| e.to_string())?; + Ok(extended_public_key.to_pub()) } pub fn identity_authentication_ecdsa_private_key( @@ -386,7 +403,7 @@ impl Wallet { let derivation_path = DerivationPath::identity_registration_path(network, index); let secp = Secp256k1::new(); let extended_public_key = self - .master_ecdsa_extended_public_key + .master_bip44_ecdsa_extended_public_key .derive_pub(&secp, &derivation_path) .expect("derivation should not be able to fail"); extended_public_key.to_pub() diff --git a/src/ui/wallet/add_new_wallet_screen.rs b/src/ui/wallet/add_new_wallet_screen.rs index 7c55126c0..c1b8acea1 100644 --- a/src/ui/wallet/add_new_wallet_screen.rs +++ b/src/ui/wallet/add_new_wallet_screen.rs @@ -4,11 +4,16 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::ScreenLike; use eframe::egui::Context; -use crate::model::wallet::{ClosedWalletSeed, OpenWalletSeed, Wallet, WalletSeed}; +use crate::model::wallet::{ + ClosedWalletSeed, DerivationPathReference, DerivationPathType, OpenWalletSeed, Wallet, + WalletSeed, +}; use crate::ui::components::entropy_grid::U256EntropyGrid; use bip39::{Language, Mnemonic}; +use dash_sdk::dashcore_rpc::dashcore::bip32::{ChildNumber, DerivationPath}; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dpp::dashcore::bip32::{ExtendedPrivKey, ExtendedPubKey}; +use dash_sdk::dpp::dashcore::Network; use egui::{ Color32, ComboBox, Direction, FontId, Frame, Grid, Layout, Margin, RichText, Stroke, TextStyle, Ui, Vec2, @@ -17,6 +22,30 @@ use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; use zxcvbn::zxcvbn; +// Constants for feature purposes and sub-features +pub const BIP44_PURPOSE: u32 = 44; +pub const DASH_COIN_TYPE: u32 = 5; +pub const DASH_TESTNET_COIN_TYPE: u32 = 1; +pub const DASH_BIP44_ACCOUNT_0_PATH_MAINNET: [ChildNumber; 3] = [ + ChildNumber::Hardened { + index: BIP44_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_COIN_TYPE, + }, + ChildNumber::Hardened { index: 0 }, +]; + +pub const DASH_BIP44_ACCOUNT_0_PATH_TESTNET: [ChildNumber; 3] = [ + ChildNumber::Hardened { + index: BIP44_PURPOSE, + }, + ChildNumber::Hardened { + index: DASH_TESTNET_COIN_TYPE, + }, + ChildNumber::Hardened { index: 0 }, +]; + pub struct AddNewWalletScreen { seed_phrase: Option, password: String, @@ -26,6 +55,7 @@ pub struct AddNewWalletScreen { wrote_it_down: bool, password_strength: f64, estimated_time_to_crack: String, + error: Option, pub app_context: Arc, } @@ -40,6 +70,7 @@ impl AddNewWalletScreen { wrote_it_down: false, password_strength: 0.0, estimated_time_to_crack: "".to_string(), + error: None, app_context: app_context.clone(), } } @@ -54,7 +85,7 @@ impl AddNewWalletScreen { self.seed_phrase = Some(mnemonic); } - fn save_wallet(&mut self) -> AppAction { + fn save_wallet(&mut self) -> Result { if let Some(mnemonic) = &self.seed_phrase { let seed = mnemonic.to_seed(""); @@ -63,8 +94,7 @@ impl AddNewWalletScreen { } else { // Encrypt the seed to obtain encrypted_seed, salt, and nonce let (encrypted_seed, salt, nonce) = - ClosedWalletSeed::encrypt_seed(&seed, self.password.as_str()) - .expect("Encryption failed"); + ClosedWalletSeed::encrypt_seed(&seed, self.password.as_str())?; (encrypted_seed, salt, nonce, true) }; @@ -72,9 +102,17 @@ impl AddNewWalletScreen { let master_ecdsa_extended_private_key = ExtendedPrivKey::new_master(self.app_context.network, &seed) .expect("Failed to create master ECDSA extended private key"); + let bip44_root_derivation_path: DerivationPath = match self.app_context.network { + Network::Dash => DerivationPath::from(DASH_BIP44_ACCOUNT_0_PATH_MAINNET.as_slice()), + _ => DerivationPath::from(DASH_BIP44_ACCOUNT_0_PATH_TESTNET.as_slice()), + }; let secp = Secp256k1::new(); - let master_ecdsa_extended_public_key = - ExtendedPubKey::from_priv(&secp, &master_ecdsa_extended_private_key); + let master_bip44_ecdsa_extended_public_key = master_ecdsa_extended_private_key + .derive_priv(&secp, &bip44_root_derivation_path) + .map_err(|e| e.to_string())?; + + let master_bip44_ecdsa_extended_public_key = + ExtendedPubKey::from_priv(&secp, &master_bip44_ecdsa_extended_public_key); // Compute the seed hash let seed_hash = ClosedWalletSeed::compute_seed_hash(&seed); @@ -91,7 +129,7 @@ impl AddNewWalletScreen { }, }), uses_password, - master_ecdsa_extended_public_key, + master_bip44_ecdsa_extended_public_key, address_balances: Default::default(), known_addresses: Default::default(), watched_addresses: Default::default(), @@ -105,7 +143,7 @@ impl AddNewWalletScreen { self.app_context .db .store_wallet(&wallet, &self.app_context.network) - .ok(); + .map_err(|e| e.to_string())?; // Acquire a write lock and add the new wallet if let Ok(mut wallets) = self.app_context.wallets.write() { @@ -115,9 +153,9 @@ impl AddNewWalletScreen { eprintln!("Failed to acquire write lock on wallets"); } - AppAction::GoToMainScreen // Navigate back to the main screen after saving + Ok(AppAction::GoToMainScreen) // Navigate back to the main screen after saving } else { - AppAction::None // No action if no seed phrase exists + Ok(AppAction::None) // No action if no seed phrase exists } } @@ -257,31 +295,6 @@ impl AddNewWalletScreen { } } -fn format_time(seconds: f64) -> String { - let minute = 60.0; - let hour = 60.0 * minute; - let day = 24.0 * hour; - let year = 365.25 * day; - let century = 100.0 * year; - let millennium = 10.0 * century; - - if seconds < minute { - format!("{:.2} seconds", seconds) - } else if seconds < hour { - format!("{:.2} minutes", seconds / minute) - } else if seconds < day { - format!("{:.2} hours", seconds / hour) - } else if seconds < year { - format!("{:.2} days", seconds / day) - } else if seconds < century { - format!("{:.2} years", seconds / year) - } else if seconds < millennium { - format!("{:.2} centuries", seconds / century) - } else { - format!("More than a millennium") - } -} - impl ScreenLike for AddNewWalletScreen { fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = add_top_panel( @@ -406,12 +419,35 @@ impl ScreenLike for AddNewWalletScreen { }); if ui.add(save_button).clicked() { - action = self.save_wallet(); // Trigger the save action + match self.save_wallet() { + Ok(save_wallet_action) => { + action = save_wallet_action; + } + Err(e) => { + self.error = Some(e) + } + } } }); }); }); + // Display error popup if there's an error + if let Some(error_message) = self.error.as_ref() { + let error_message = error_message.clone(); + egui::Window::new("Error") + .resizable(false) + .collapsible(false) + .anchor(egui::Align2::CENTER_CENTER, Vec2::new(0.0, 0.0)) + .show(ctx, |ui| { + ui.label(error_message); + ui.add_space(10.0); + if ui.button("Close").clicked() { + self.error = None; // Clear the error to close the popup + } + }); + } + action } } From e2619d20180658f6e1828e163b1c89baa4b7a6ab Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 Nov 2024 10:32:34 +0100 Subject: [PATCH 6/7] more work --- src/database/initialization.rs | 2 + src/model/wallet/mod.rs | 26 +++ .../identities/add_new_identity_screen/mod.rs | 181 ++++++++++++++---- 3 files changed, 177 insertions(+), 32 deletions(-) diff --git a/src/database/initialization.rs b/src/database/initialization.rs index 7fb51b7ba..e69fbc984 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -1,5 +1,7 @@ use crate::database::Database; +pub const MIN_SUPPORTED_DB_VERSION: u16 = 0; + impl Database { pub fn initialize(&self) -> rusqlite::Result<()> { // Create the settings table diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 5c247f5ee..87004a619 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -159,6 +159,29 @@ impl WalletSeed { } } + /// Opens the wallet by decrypting the seed without using a password. + pub fn open_no_password(&mut self) -> Result<(), String> { + match self { + WalletSeed::Open(_) => { + // Wallet is already open + Ok(()) + } + WalletSeed::Closed(closed_seed) => { + let open_wallet_seed = + OpenWalletSeed { + seed: closed_seed.encrypted_seed.clone().try_into().map_err( + |e: Vec| { + format!("incorred seed size, expected 64 bytes, got {}", e.len()) + }, + )?, + wallet_info: closed_seed.clone(), + }; + *self = WalletSeed::Open(open_wallet_seed); + Ok(()) + } + } + } + /// Closes the wallet by securely erasing the seed and transitioning to Closed state. pub fn close(&mut self) { match self { @@ -177,6 +200,9 @@ impl WalletSeed { } impl Wallet { + pub fn is_open(&self) -> bool { + matches!(self.wallet_seed, WalletSeed::Open(_)) + } pub fn has_balance(&self) -> bool { self.max_balance() > 0 } diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index 1cfef7e49..c16d8ad53 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -9,7 +9,7 @@ use crate::backend_task::identity::{ }; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; -use crate::model::wallet::Wallet; +use crate::model::wallet::{Wallet, WalletSeed}; use crate::ui::components::top_panel::add_top_panel; use crate::ui::{MessageType, ScreenLike}; use arboard::Clipboard; @@ -30,6 +30,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{fmt, thread}; +use zeroize::Zeroize; #[derive(Debug, Clone, Deserialize)] struct KeyInfo { @@ -263,6 +264,54 @@ impl AddNewIdentityScreen { } } + fn render_wallet_unlock(&mut self, ui: &mut Ui) -> bool { + if let Some(wallet_guard) = self.selected_wallet.as_ref() { + let mut wallet = wallet_guard.write().unwrap(); + + // Only render the unlock prompt if the wallet requires a password and is locked + if wallet.uses_password && !wallet.is_open() { + ui.add_space(10.0); + ui.label("This wallet is locked. Please enter the password to unlock it:"); + + let mut password = String::new(); + let password_input = ui.add( + egui::TextEdit::singleline(&mut password) + .password(true) + .hint_text("Enter password"), + ); + + let unlocked = if password_input.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter)) + { + let unlocked = match wallet.wallet_seed.open(&password) { + Ok(_) => { + self.error_message = None; // Clear any previous error + true + } + Err(e) => { + self.error_message = Some(e); // Store the error message + false + } + }; + // Clear the password field after submission + password.zeroize(); + unlocked + } else { + false + }; + + // Display error message if the password was incorrect + if let Some(error_message) = &self.error_message { + ui.add_space(5.0); + ui.colored_label(Color32::RED, error_message); + } + + return unlocked; + } + } + false + } + fn render_wallet_selection(&mut self, ui: &mut Ui) -> bool { if self.app_context.has_wallet.load(Ordering::Relaxed) { let wallets = &self.app_context.wallets.read().unwrap(); @@ -316,45 +365,47 @@ impl AddNewIdentityScreen { let wallet = wallet.read().unwrap(); - self.identity_id_number = - wallet.identities.keys().copied().max().unwrap_or_default(); + if wallet.is_open() { + self.identity_id_number = + wallet.identities.keys().copied().max().unwrap_or_default(); - self.identity_keys.master_private_key = Some( - wallet - .identity_authentication_ecdsa_private_key( - self.app_context.network, - 0, - 0, - ) - .expect("expected to have decrypted wallet"), - ); - // Update the additional keys input - self.identity_keys.keys_input = vec![ - ( + self.identity_keys.master_private_key = Some( wallet .identity_authentication_ecdsa_private_key( self.app_context.network, 0, - 1, - ) - .expect("expected to have decrypted wallet"), - KeyType::ECDSA_HASH160, - Purpose::AUTHENTICATION, - SecurityLevel::HIGH, - ), - ( - wallet - .identity_authentication_ecdsa_private_key( - self.app_context.network, 0, - 2, ) .expect("expected to have decrypted wallet"), - KeyType::ECDSA_HASH160, - Purpose::TRANSFER, - SecurityLevel::CRITICAL, - ), - ]; + ); + // Update the additional keys input + self.identity_keys.keys_input = vec![ + ( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 1, + ) + .expect("expected to have decrypted wallet"), + KeyType::ECDSA_HASH160, + Purpose::AUTHENTICATION, + SecurityLevel::HIGH, + ), + ( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 2, + ) + .expect("expected to have decrypted wallet"), + KeyType::ECDSA_HASH160, + Purpose::TRANSFER, + SecurityLevel::CRITICAL, + ), + ]; + } } false } else { @@ -781,6 +832,72 @@ impl ScreenLike for AddNewIdentityScreen { return; }; + let should_ask_for_password = if let Some(wallet_guard) = self.selected_wallet.as_ref() { + let mut wallet = wallet_guard.write().unwrap(); + if !wallet.uses_password { + if let Err(e) = wallet.wallet_seed.open_no_password() { + self.error_message = Some(e); + } + false + } else if wallet.is_open() { + false + } else { + true + } + } else { + true + }; + + if should_ask_for_password { + let just_unlocked = self.render_wallet_unlock(ui); + if just_unlocked { + let wallet_guard = self.selected_wallet.as_ref().unwrap(); + let wallet = wallet_guard.read().unwrap(); + + self.identity_id_number = + wallet.identities.keys().copied().max().unwrap_or_default(); + + self.identity_keys.master_private_key = Some( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 0, + ) + .expect("expected to have decrypted wallet"), + ); + // Update the additional keys input + self.identity_keys.keys_input = vec![ + ( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 1, + ) + .expect("expected to have decrypted wallet"), + KeyType::ECDSA_HASH160, + Purpose::AUTHENTICATION, + SecurityLevel::HIGH, + ), + ( + wallet + .identity_authentication_ecdsa_private_key( + self.app_context.network, + 0, + 2, + ) + .expect("expected to have decrypted wallet"), + KeyType::ECDSA_HASH160, + Purpose::TRANSFER, + SecurityLevel::CRITICAL, + ), + ]; + } else { + return; + } + } + // Display the heading with an info icon that shows a tooltip on hover ui.horizontal(|ui| { ui.heading(format!( From 5811dc9a59676d3e5825972d5cf8cc103fc257b7 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 7 Nov 2024 10:49:23 +0100 Subject: [PATCH 7/7] more work --- src/context.rs | 8 ++++++++ src/database/identities.rs | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/context.rs b/src/context.rs index 827589281..2ff9d3461 100644 --- a/src/context.rs +++ b/src/context.rs @@ -138,6 +138,14 @@ impl AppContext { ) } + pub fn update_local_qualified_identity( + &self, + qualified_identity: &QualifiedIdentity, + ) -> Result<()> { + self.db + .update_local_qualified_identity(qualified_identity, self) + } + /// This is for before we know if Platform will accept the identity pub fn insert_local_qualified_identity_in_creation( &self, diff --git a/src/database/identities.rs b/src/database/identities.rs index 54cea75c5..05f0a546c 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -64,6 +64,31 @@ impl Database { Ok(()) } + pub fn update_local_qualified_identity( + &self, + qualified_identity: &QualifiedIdentity, + app_context: &AppContext, + ) -> rusqlite::Result<()> { + // Extract the fields from `qualified_identity` to use in the SQL update + let id = qualified_identity.identity.id().to_vec(); + let data = qualified_identity.to_bytes(); + let alias = qualified_identity.alias.clone(); + let identity_type = format!("{:?}", qualified_identity.identity_type); + + // Get the network string from the app context + let network = app_context.network_string(); + + // Execute the update statement + self.execute( + "UPDATE identity + SET data = ?, alias = ?, identity_type = ?, network = ?, is_local = 1 + WHERE id = ?", + params![data, alias, identity_type, network, id], + )?; + + Ok(()) + } + pub fn insert_local_qualified_identity_in_creation( &self, qualified_identity: &QualifiedIdentity,