diff --git a/Cargo.toml b/Cargo.toml index 03f43f0a3..2978cb12b 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"]} @@ -50,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/app.rs b/src/app.rs index a3e953a13..e6d0ae8fb 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..1deacef28 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, TxOut}; 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, TxOut, Address)>), ChainLock(ChainLock, Network), } diff --git a/src/backend_task/identity/add_key_to_identity.rs b/src/backend_task/identity/add_key_to_identity.rs index ce8025e78..1041e4cb3 100644 --- a/src/backend_task/identity/add_key_to_identity.rs +++ b/src/backend_task/identity/add_key_to_identity.rs @@ -66,7 +66,7 @@ impl AppContext { } } - self.insert_local_qualified_identity(&qualified_identity) + self.update_local_qualified_identity(&qualified_identity) .map(|_| { BackendTaskSuccessResult::Message("Successfully added key to identity".to_string()) }) diff --git a/src/backend_task/identity/load_identity.rs b/src/backend_task/identity/load_identity.rs index d8c2b5def..314f152c3 100644 --- a/src/backend_task/identity/load_identity.rs +++ b/src/backend_task/identity/load_identity.rs @@ -203,7 +203,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(BackendTaskSuccessResult::Message( diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index a9bf7931b..6dfbafbd2 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -13,10 +13,10 @@ 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::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; use super::BackendTaskSuccessResult; @@ -168,6 +167,7 @@ pub type IdentityIndex = u32; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IdentityRegistrationMethod { UseAssetLock(Address, AssetLockProof, Transaction), + FundWithUtxo(OutPoint, TxOut, Address, IdentityIndex), FundWithWallet(Duffs, IdentityIndex), } @@ -176,6 +176,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 dba6e5ed8..faf281c31 100644 --- a/src/backend_task/identity/refresh_identity.rs +++ b/src/backend_task/identity/refresh_identity.rs @@ -52,7 +52,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 a62cc94fc..a1d5944e3 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -111,6 +111,7 @@ impl AppContext { alias_input, keys, wallet, + wallet_identity_index, identity_registration_method, } = input; @@ -120,6 +121,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( @@ -129,6 +132,7 @@ impl AppContext { ) => { let tx_id = transaction.txid(); let wallet = wallet.read().unwrap(); + wallet_id = wallet.seed_hash(); let private_key = wallet .private_key_for_address(&address, self.network)? .ok_or("Asset Lock not valid for wallet")?; @@ -164,6 +168,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_hash(); match wallet.asset_lock_transaction( sdk.network, amount, @@ -215,6 +220,58 @@ impl AppContext { tokio::time::sleep(Duration::from_millis(200)).await; } + (asset_lock_proof, asset_lock_proof_private_key, tx_id) + } + IdentityRegistrationMethod::FundWithUtxo( + utxo, + tx_out, + 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_id = wallet.seed_hash(); + wallet.asset_lock_transaction_for_utxo( + sdk.network, + utxo, + tx_out, + 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) } }; @@ -249,8 +306,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(), @@ -287,8 +348,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 1d63a42e0..945d42de6 100644 --- a/src/backend_task/identity/transfer.rs +++ b/src/backend_task/identity/transfer.rs @@ -30,10 +30,10 @@ 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.update_local_qualified_identity(&qualified_identity) .map(|_| { BackendTaskSuccessResult::Message("Successfully transferred credits".to_string()) }) - .map_err(|e| format!("Database error: {}", e)) + .map_err(|e| e.to_string()) } } diff --git a/src/backend_task/identity/withdraw_from_identity.rs b/src/backend_task/identity/withdraw_from_identity.rs index 1b299f3ee..cf10283f7 100644 --- a/src/backend_task/identity/withdraw_from_identity.rs +++ b/src/backend_task/identity/withdraw_from_identity.rs @@ -31,7 +31,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.update_local_qualified_identity(&qualified_identity) .map(|_| { BackendTaskSuccessResult::Message("Successfully withdrew from identity".to_string()) }) diff --git a/src/context.rs b/src/context.rs index cde37055c..2ff9d3461 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, 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; @@ -123,24 +123,42 @@ 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, + wallet_and_identity_id_info, + self, + ) + } + + pub fn update_local_qualified_identity( + &self, + qualified_identity: &QualifiedIdentity, ) -> Result<()> { self.db - .insert_local_qualified_identity(qualified_identity, self) + .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, 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> { @@ -214,7 +232,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(); @@ -243,7 +261,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); @@ -254,7 +272,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(), tx_out.clone(), address.clone())); wallet .address_balances @@ -331,8 +349,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 592192fb7..05f0a546c 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -25,6 +25,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 +35,65 @@ impl Database { let network = app_context.network_string(); + 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 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( - "INSERT OR REPLACE INTO identity (id, data, is_local, alias, identity_type, network) - VALUES (?, ?, 1, ?, ?, ?)", - params![id, data, alias, identity_type, network], + "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, + wallet_id: &[u8], + identity_index: u32, app_context: &AppContext, ) -> rusqlite::Result<()> { let id = qualified_identity.identity.id().to_vec(); @@ -55,10 +104,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..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 @@ -16,9 +18,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_bip44_account_0_epk BLOB NOT NULL, alias TEXT, is_main INTEGER, + uses_password INTEGER NOT NULL, password_hint TEXT, network TEXT NOT NULL )", @@ -28,14 +35,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 +84,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 )", [], )?; @@ -91,8 +98,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_hash) ON DELETE CASCADE )", [], )?; diff --git a/src/database/wallet.rs b/src/database/wallet.rs index a810ed079..769b65569 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,26 @@ 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_bip44_account_0_epk_bytes = + wallet.master_bip44_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_bip44_account_0_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_bip44_account_0_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 +50,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], + "UPDATE wallet SET alias = ? WHERE seed_hash = ?", + params![new_alias, seed_hash], )?; Ok(()) @@ -54,13 +66,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 +81,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 +93,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 +120,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 +141,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 +149,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,32 +165,52 @@ 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_bip44_account_0_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_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(8)?; + + // Reconstruct the extended public keys + 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"); // 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_bip44_ecdsa_extended_public_key: master_ecdsa_extended_public_key, address_balances: BTreeMap::new(), known_addresses: BTreeMap::new(), watched_addresses: BTreeMap::new(), unused_asset_locks: vec![], alias, + identities: HashMap::new(), utxos: HashMap::new(), is_main, - password_hint, }, ); Ok(()) @@ -190,18 +223,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(); @@ -221,7 +255,7 @@ impl Database { let path_type = DerivationPathType::from_bits_truncate(path_type as u32); Ok(( - seed_array, + seed_hash_array, address, derivation_path, balance, @@ -311,7 +345,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"); @@ -355,7 +389,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 f5cf1c1df..ce600f4a4 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -6,7 +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( @@ -134,4 +136,106 @@ impl Wallet { Ok((tx, private_key, change_address)) } + + pub fn asset_lock_transaction_for_utxo( + &mut self, + network: Network, + utxo: OutPoint, + previous_tx_output: TxOut, + 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 = previous_tx_output.value - 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, &previous_tx_output.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/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 5f32fadb0..87004a619 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::{ChildNumber, ExtendedPubKey, KeyDerivationType}; + use dash_sdk::dpp::dashcore::bip32::DerivationPath; use dash_sdk::dpp::dashcore::{ Address, InstantLock, Network, OutPoint, PrivateKey, PublicKey, Transaction, TxOut, @@ -63,6 +65,8 @@ 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; +use zeroize::Zeroize; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -94,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_bip44_ecdsa_extended_public_key: ExtendedPubKey, pub address_balances: BTreeMap, pub known_addresses: BTreeMap, pub watched_addresses: BTreeMap, @@ -106,12 +112,97 @@ pub struct Wallet { Option, )>, pub alias: Option, + 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(()) + } + } + } + + /// 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 { + 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 is_open(&self) -> bool { + matches!(self.wallet_seed, WalletSeed::Open(_)) + } pub fn has_balance(&self) -> bool { self.max_balance() > 0 } @@ -124,6 +215,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, @@ -133,29 +266,74 @@ 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( &mut self, network: Network, + skip_known_addresses_with_no_funds: bool, change: bool, register: Option<&AppContext>, ) -> 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); - if self.watched_addresses.get(&derivation_path).is_none() { - let public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, network) - .expect("derivation should not be able to fail") + + 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()); + 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 + address_index += 1; + continue; + } + } else { + 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 @@ -176,7 +354,7 @@ impl Wallet { app_context .db .add_address( - &self.seed, + &self.seed_hash(), &address, &derivation_path, DerivationPathReference::BIP44, @@ -197,18 +375,14 @@ 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; } } let derivation_path = found_unused_derivation_path.unwrap(); - let extended_public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, 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( @@ -216,7 +390,7 @@ impl Wallet { network: Network, identity_index: u32, key_index: u32, - ) -> PublicKey { + ) -> Result { let derivation_path = DerivationPath::identity_authentication_path( network, KeyDerivationType::ECDSA, @@ -224,9 +398,9 @@ impl Wallet { key_index, ); let extended_public_key = derivation_path - .derive_pub_ecdsa_for_master_seed(&self.seed, network) - .expect("derivation should not be able to fail"); - extended_public_key.to_pub() + .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( @@ -234,7 +408,7 @@ impl Wallet { network: Network, identity_index: u32, key_index: u32, - ) -> PrivateKey { + ) -> Result { let derivation_path = DerivationPath::identity_authentication_path( network, KeyDerivationType::ECDSA, @@ -242,9 +416,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( @@ -253,8 +427,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_bip44_ecdsa_extended_public_key + .derive_pub(&secp, &derivation_path) .expect("derivation should not be able to fail"); extended_public_key.to_pub() } @@ -267,7 +443,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(); @@ -277,7 +453,7 @@ impl Wallet { app_context .db .add_address( - &self.seed, + &self.seed_hash(), &address, &derivation_path, DerivationPathReference::BlockchainIdentityCreditRegistrationFunding, @@ -303,10 +479,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 +501,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 +514,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 +527,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, @@ -368,7 +554,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_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..90a00d249 --- /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..764fe3f10 --- /dev/null +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -0,0 +1,196 @@ +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, +}; +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, + false, + 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.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."); + } + } + }); + + Ok(()) + } + + pub fn render_ui_by_wallet_qr_code(&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(); + + 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 => { + 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 + wallet_identity_index: self.identity_id_number, + identity_registration_method: IdentityRegistrationMethod::FundWithUtxo( + utxo, + tx_out, + 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 61% rename from src/ui/identities/add_new_identity_screen.rs rename to src/ui/identities/add_new_identity_screen/mod.rs index ba25d11f3..c16d8ad53 100644 --- a/src/ui/identities/add_new_identity_screen.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -1,20 +1,22 @@ +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, }; 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::identities::add_new_identity_screen::AddNewIdentityWalletFundedScreenStep::{ - ChooseFundingMethod, FundsReceived, ReadyToCreate, -}; use crate::ui::{MessageType, ScreenLike}; 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,10 +25,12 @@ 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}; use std::{fmt, thread}; +use zeroize::Zeroize; #[derive(Debug, Clone, Deserialize)] struct KeyInfo { @@ -60,10 +64,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 +83,14 @@ pub struct AddNewIdentityScreen { funding_method: Arc>, funding_amount: String, funding_amount_exact: Option, + funding_utxo: Option<(OutPoint, TxOut, 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 +136,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 +148,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 +235,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 +264,54 @@ 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 + 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(); - let total_balance: u64 = wallet.max_balance(); // Sum up all the balances + // 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 dash_balance = total_balance as f64 * 1e-8; // Convert to DASH units + let mut password = String::new(); + let password_input = ui.add( + egui::TextEdit::singleline(&mut password) + .password(true) + .hint_text("Enter password"), + ); - ui.horizontal(|ui| { - ui.label(format!("Wallet Balance: {:.8} DASH", dash_balance)); - }); - } else { - ui.label("No wallet selected"); + 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(); @@ -393,7 +345,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()); } @@ -402,8 +359,54 @@ 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(); + + 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![ + ( + 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 { false @@ -428,6 +431,7 @@ impl AddNewIdentityScreen { FundingMethod::NoSelection, "Please select funding method", ); + let wallet = selected_wallet.read().unwrap(); if wallet.has_unused_asset_lock() { if ui @@ -439,21 +443,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, @@ -462,71 +481,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 { - "" - }; + // 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:"); - 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; + 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 payment chain.".to_string()); + } } fn render_keys_input(&mut self, ui: &mut egui::Ui) { @@ -540,7 +539,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 +547,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 +561,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 { @@ -612,6 +611,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, @@ -642,6 +642,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, @@ -704,12 +705,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 @@ -719,11 +723,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, @@ -740,11 +746,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 @@ -779,9 +787,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, tx_out, address) in outpoints_with_addresses { + if funding_address == &address { + *step = AddNewIdentityWalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some((outpoint, tx_out, address)) + } + } + } + } + } } fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = add_top_panel( @@ -797,7 +819,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 +832,171 @@ 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(); + 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 funding_method == FundingMethod::NoSelection { - return; + 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; + } } - 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.add_space(10.0); 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/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/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/add_new_wallet_screen.rs b/src/ui/wallet/add_new_wallet_screen.rs index 1f605152c..c1b8acea1 100644 --- a/src/ui/wallet/add_new_wallet_screen.rs +++ b/src/ui/wallet/add_new_wallet_screen.rs @@ -4,23 +4,58 @@ 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, 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, }; 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, - 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, + error: Option, pub app_context: Arc, } @@ -28,11 +63,14 @@ 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(), + error: None, app_context: app_context.clone(), } } @@ -47,25 +85,65 @@ 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(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())?; + (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 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_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); + 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_bip44_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) - .ok(); + .store_wallet(&wallet, &self.app_context.network) + .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() { @@ -75,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 } } @@ -242,13 +320,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); @@ -260,16 +338,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 @@ -287,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 } } diff --git a/src/ui/wallet/import_wallet_screen.rs b/src/ui/wallet/import_wallet_screen.rs new file mode 100644 index 000000000..32fdd52fd --- /dev/null +++ b/src/ui/wallet/import_wallet_screen.rs @@ -0,0 +1,262 @@ +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::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::{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 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; diff --git a/src/ui/wallet/wallets_screen.rs b/src/ui/wallet/wallets_screen.rs index 5dcec8131..f686a0582 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 @@ -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(); } } 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; }