From b80d80e19646be22a76e73980880a786488fc47c Mon Sep 17 00:00:00 2001 From: Odysseas Gabrielides Date: Wed, 27 Nov 2024 10:03:31 +0200 Subject: [PATCH 1/4] fix: relax avx512 incompatibility detection (#97) --- src/cpu_compatibility.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/cpu_compatibility.rs b/src/cpu_compatibility.rs index f9078d06..0048e1f4 100644 --- a/src/cpu_compatibility.rs +++ b/src/cpu_compatibility.rs @@ -34,18 +34,6 @@ pub fn check_cpu_compatibility() { .unwrap(); std::process::exit(1); } - let avx512_supported = cpuid - .get_extended_feature_info() - .map_or(false, |ext_info| ext_info.has_avx512f()); // AVX-512 Foundation - if !avx512_supported { - MessageDialog::new() - .set_type(native_dialog::MessageType::Error) - .set_title("Compatibility Error") - .set_text("Your CPU does not support AVX512 instructions. Please use a compatible CPU.") - .show_alert() - .unwrap(); - std::process::exit(1); - } } else { MessageDialog::new() .set_type(native_dialog::MessageType::Error) From 22b2c79a0f2648ce651ac234c7350f179da58af5 Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:03:37 +0700 Subject: [PATCH 2/4] feat: rename log file and update gitignore (#99) --- .gitignore | 15 +-------------- src/logging.rs | 3 +-- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index d2db760d..7e0d0719 100644 --- a/.gitignore +++ b/.gitignore @@ -22,17 +22,4 @@ Cargo.lock *.pdb .DS_Store .idea/ -**/.DS_Store - -# Explorer state -*_explorer.state -*_explorer.state.* -explorer.drive - -# Logs -explorer.log - -# Supporting files -supporting_files/strategy_exports/ -supporting_files/new_identity_private_keys.log -/identities.db +**/.DS_Store \ No newline at end of file diff --git a/src/logging.rs b/src/logging.rs index 37d6409f..c6359f66 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -5,8 +5,7 @@ use tracing_subscriber::EnvFilter; pub fn initialize_logger() { // Initialize log file, with improved error handling - let log_file_path = - app_user_data_file_path("explorer.log").expect("should create log file path"); + let log_file_path = app_user_data_file_path("det.log").expect("should create log file path"); let log_file = match std::fs::File::create(log_file_path) { Ok(file) => file, Err(e) => panic!("Failed to create log file: {:?}", e), From a70ba0242132e48f3f946168f798732404f01fce Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:04:30 +0700 Subject: [PATCH 3/4] feat: remove wallet private key option from env file (#100) --- .env.example | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 43284649..11b71879 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,6 @@ MAINNET_CORE_RPC_PORT=9998 MAINNET_CORE_RPC_USER=dashrpc MAINNET_CORE_RPC_PASSWORD=password MAINNET_INSIGHT_API_URL=https://insight.dash.org/insight-api -MAINNET_WALLET_PRIVATE_KEY= # Testnet configuration TESTNET_SHOW_IN_UI=true @@ -15,5 +14,4 @@ TESTNET_CORE_HOST=127.0.0.1 TESTNET_CORE_RPC_PORT=19998 TESTNET_CORE_RPC_USER=dashrpc TESTNET_CORE_RPC_PASSWORD=password -TESTNET_INSIGHT_API_URL=https://testnet-insight.dash.org/insight-api -TESTNET_WALLET_PRIVATE_KEY= \ No newline at end of file +TESTNET_INSIGHT_API_URL=https://testnet-insight.dash.org/insight-api \ No newline at end of file From f37eb9cf14ecf056d853d129e4fd02f2deb4e55f Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 27 Nov 2024 18:25:04 +0300 Subject: [PATCH 4/4] feat: top up identity (#101) --- Cargo.toml | 2 +- src/app.rs | 5 +- src/backend_task/core/start_dash_qt.rs | 2 - src/backend_task/identity/load_identity.rs | 7 +- .../identity/load_identity_from_wallet.rs | 4 +- src/backend_task/identity/mod.rs | 35 +- .../identity/register_identity.rs | 323 ++++++----- src/backend_task/identity/top_up_identity.rs | 328 ++++++++++++ src/backend_task/mod.rs | 4 +- src/database/identities.rs | 33 +- src/database/initialization.rs | 7 +- src/database/mod.rs | 1 + src/database/top_ups.rs | 44 ++ src/database/wallet.rs | 3 +- src/model/proof_log_item.rs | 1 - src/model/qualified_identity/mod.rs | 8 + src/model/wallet/asset_lock_transaction.rs | 107 +++- src/model/wallet/mod.rs | 26 + .../by_using_unused_asset_lock.rs | 8 +- .../by_using_unused_balance.rs | 8 +- .../by_wallet_qr_code.rs | 24 +- .../identities/add_new_identity_screen/mod.rs | 86 +-- src/ui/identities/funding_common.rs | 44 ++ src/ui/identities/identities_screen.rs | 30 +- src/ui/identities/mod.rs | 2 + .../by_using_unused_asset_lock.rs | 114 ++++ .../by_using_unused_balance.rs | 74 +++ .../by_wallet_qr_code.rs | 194 +++++++ .../identities/top_up_identity_screen/mod.rs | 503 ++++++++++++++++++ .../top_up_identity_screen/success_screen.rs | 27 + src/ui/mod.rs | 20 +- src/ui/tool_screens/proof_log_screen.rs | 2 +- .../transition_visualizer_screen.rs | 1 - 33 files changed, 1787 insertions(+), 290 deletions(-) create mode 100644 src/backend_task/identity/top_up_identity.rs create mode 100644 src/database/top_ups.rs create mode 100644 src/ui/identities/funding_common.rs create mode 100644 src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs create mode 100644 src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs create mode 100644 src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs create mode 100644 src/ui/identities/top_up_identity_screen/mod.rs create mode 100644 src/ui/identities/top_up_identity_screen/success_screen.rs diff --git a/Cargo.toml b/Cargo.toml index 7a517a8c..be738e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ strum = { version = "0.26.1", features = ["derive"] } bs58 = "0.5.0" base64 = "0.22.1" copypasta = "0.10.1" -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "c49af53698bad1070fcfc344ccaf6b3cc404e516" } +dash-sdk = { git = "https://github.com/dashpay/platform", rev = "f78f152403d789cdd091f3e88165671e975a6d63" } thiserror = "1" serde = "1.0.197" serde_json = "1.0.120" diff --git a/src/app.rs b/src/app.rs index d952724a..30d1970d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -423,6 +423,9 @@ impl App for AppState { BackendTaskSuccessResult::RegisteredIdentity(_) => { self.visible_screen_mut().display_task_result(message); } + BackendTaskSuccessResult::ToppedUpIdentity(_) => { + self.visible_screen_mut().display_task_result(message); + } }, TaskResult::Error(message) => { self.visible_screen_mut() @@ -457,7 +460,7 @@ impl App for AppState { let core_item = CoreItem::ReceivedAvailableUTXOTransaction(tx.clone(), utxos); self.visible_screen_mut() - .display_task_result(core_item.into()); + .display_task_result(BackendTaskSuccessResult::CoreItem(core_item)); } Err(e) => { eprintln!("Failed to store asset lock: {}", e); diff --git a/src/backend_task/core/start_dash_qt.rs b/src/backend_task/core/start_dash_qt.rs index 49d62626..2cf92c88 100644 --- a/src/backend_task/core/start_dash_qt.rs +++ b/src/backend_task/core/start_dash_qt.rs @@ -2,8 +2,6 @@ use crate::context::AppContext; use dash_sdk::dpp::dashcore::Network; use std::path::PathBuf; use std::process::{Command, Stdio}; -use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{env, io}; impl AppContext { diff --git a/src/backend_task/identity/load_identity.rs b/src/backend_task/identity/load_identity.rs index 3c68d9b2..60ab3cec 100644 --- a/src/backend_task/identity/load_identity.rs +++ b/src/backend_task/identity/load_identity.rs @@ -7,7 +7,6 @@ use crate::model::qualified_identity::PrivateKeyTarget::{ self, PrivateKeyOnMainIdentity, PrivateKeyOnVoterIdentity, }; use crate::model::qualified_identity::{DPNSNameInfo, IdentityType, QualifiedIdentity}; -use crate::model::wallet::WalletSeedHash; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dashcore_rpc::dashcore::PrivateKey; use dash_sdk::dpp::dashcore::hashes::Hash; @@ -15,14 +14,14 @@ use dash_sdk::dpp::document::DocumentV0Getters; use dash_sdk::dpp::identifier::MasternodeIdentifiers; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; -use dash_sdk::dpp::identity::{KeyType, SecurityLevel}; +use dash_sdk::dpp::identity::SecurityLevel; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::platform_value::Value; use dash_sdk::drive::query::{WhereClause, WhereOperator}; use dash_sdk::platform::{Document, DocumentQuery, Fetch, FetchMany, Identifier, Identity}; use dash_sdk::Sdk; use egui::ahash::HashMap; -use std::collections::{BTreeMap, HashSet}; +use std::collections::BTreeMap; impl AppContext { pub(super) async fn load_identity( @@ -295,6 +294,8 @@ impl AppContext { .iter() .map(|wallet| (wallet.read().unwrap().seed_hash(), wallet.clone())) .collect(), + wallet_index: None, //todo + top_ups: Default::default(), }; // Insert qualified identity into the database diff --git a/src/backend_task/identity/load_identity_from_wallet.rs b/src/backend_task/identity/load_identity_from_wallet.rs index 7160cdc5..97982e24 100644 --- a/src/backend_task/identity/load_identity_from_wallet.rs +++ b/src/backend_task/identity/load_identity_from_wallet.rs @@ -7,7 +7,7 @@ use crate::model::qualified_identity::qualified_identity_public_key::QualifiedId use crate::model::qualified_identity::{ DPNSNameInfo, IdentityType, PrivateKeyTarget, QualifiedIdentity, }; -use crate::model::wallet::{Wallet, WalletArcRef}; +use crate::model::wallet::WalletArcRef; use dash_sdk::dpp::dashcore::bip32::{DerivationPath, KeyDerivationType}; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::document::DocumentV0Getters; @@ -137,6 +137,8 @@ impl AppContext { wallet_arc_ref.wallet.read().unwrap().seed_hash(), wallet_arc_ref.wallet.clone(), )]), + wallet_index: Some(identity_index), + top_ups: Default::default(), }; // Insert qualified identity into the database diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index d41e846e..8b6f6c37 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -4,6 +4,7 @@ mod load_identity_from_wallet; mod refresh_identity; mod register_dpns_name; mod register_identity; +mod top_up_identity; mod transfer; mod withdraw_from_identity; @@ -19,7 +20,7 @@ use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey, TxOut}; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::hashes::Hash; -use dash_sdk::dpp::dashcore::{OutPoint, PublicKey, 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; @@ -191,30 +192,52 @@ impl IdentityKeys { } pub type IdentityIndex = u32; +pub type TopUpIndex = u32; #[derive(Debug, Clone, PartialEq, Eq)] -pub enum IdentityRegistrationMethod { +pub enum RegisterIdentityFundingMethod { UseAssetLock(Address, AssetLockProof, Transaction), FundWithUtxo(OutPoint, TxOut, Address, IdentityIndex), FundWithWallet(Duffs, IdentityIndex), } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TopUpIdentityFundingMethod { + UseAssetLock(Address, AssetLockProof, Transaction), + FundWithUtxo(OutPoint, TxOut, Address, IdentityIndex, TopUpIndex), + FundWithWallet(Duffs, IdentityIndex, TopUpIndex), +} + #[derive(Debug, Clone)] pub struct IdentityRegistrationInfo { pub alias_input: String, pub keys: IdentityKeys, pub wallet: Arc>, pub wallet_identity_index: u32, - pub identity_registration_method: IdentityRegistrationMethod, + pub identity_funding_method: RegisterIdentityFundingMethod, } impl PartialEq for IdentityRegistrationInfo { fn eq(&self, other: &Self) -> bool { self.alias_input == other.alias_input - && self.identity_registration_method == other.identity_registration_method + && self.identity_funding_method == other.identity_funding_method && self.keys == other.keys } } +#[derive(Debug, Clone)] +pub struct IdentityTopUpInfo { + pub qualified_identity: QualifiedIdentity, + pub wallet: Arc>, + pub identity_funding_method: TopUpIdentityFundingMethod, +} + +impl PartialEq for IdentityTopUpInfo { + fn eq(&self, other: &Self) -> bool { + self.qualified_identity == other.qualified_identity + && self.identity_funding_method == other.identity_funding_method + } +} + #[derive(Debug, Clone, PartialEq)] pub struct RegisterDpnsNameInput { pub qualified_identity: QualifiedIdentity, @@ -226,6 +249,7 @@ pub(crate) enum IdentityTask { LoadIdentity(IdentityInputToLoad), SearchIdentityFromWallet(WalletArcRef, IdentityIndex), RegisterIdentity(IdentityRegistrationInfo), + TopUpIdentity(IdentityTopUpInfo), AddKeyToIdentity(QualifiedIdentity, QualifiedIdentityPublicKey, [u8; 32]), WithdrawFromIdentity(QualifiedIdentity, Option
, Credits, Option), Transfer(QualifiedIdentity, Identifier, Credits, Option), @@ -440,6 +464,9 @@ impl AppContext { self.load_user_identity_from_wallet(sdk, wallet, identity_index) .await } + IdentityTask::TopUpIdentity(top_up_info) => { + self.top_up_identity(top_up_info, sender).await + } } } } diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index c460b833..c44cc1c6 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -1,5 +1,5 @@ use crate::app::TaskResult; -use crate::backend_task::identity::{IdentityRegistrationInfo, IdentityRegistrationMethod}; +use crate::backend_task::identity::{IdentityRegistrationInfo, RegisterIdentityFundingMethod}; use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; @@ -115,7 +115,7 @@ impl AppContext { keys, wallet, wallet_identity_index, - identity_registration_method, + identity_funding_method, } = input; let sdk = self.sdk.clone(); @@ -126,186 +126,181 @@ impl AppContext { let wallet_id; - let (asset_lock_proof, asset_lock_proof_private_key, tx_id) = - match identity_registration_method { - IdentityRegistrationMethod::UseAssetLock( - address, - asset_lock_proof, - transaction, - ) => { - let tx_id = transaction.txid(); - - // eprintln!("UseAssetLock: transaction id for {:#?} is {}", transaction, tx_id); - 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")?; - let asset_lock_proof = - if let AssetLockProof::Instant(instant_asset_lock_proof) = - asset_lock_proof.as_ref() - { - // we need to make sure the instant send asset lock is recent - let raw_transaction_info = self - .core_client - .get_raw_transaction_info(&tx_id, None) - .map_err(|e| e.to_string())?; - - if raw_transaction_info.chainlock - && raw_transaction_info.height.is_some() - && raw_transaction_info.confirmations.is_some() - && raw_transaction_info.confirmations.unwrap() > 8 - { - // we should use a chain lock instead - AssetLockProof::Chain(ChainAssetLockProof { - core_chain_locked_height: metadata.core_chain_locked_height, - out_point: OutPoint::new(tx_id, 0), - }) - } else { - AssetLockProof::Instant(instant_asset_lock_proof.clone()) - } - } else { - asset_lock_proof - }; - (asset_lock_proof, private_key, tx_id) - } - IdentityRegistrationMethod::FundWithWallet(amount, identity_index) => { - // Scope the write lock to avoid holding it across an await. - let (asset_lock_transaction, asset_lock_proof_private_key, _, used_utxos) = { - let mut wallet = wallet.write().unwrap(); - wallet_id = wallet.seed_hash(); - match wallet.asset_lock_transaction( - sdk.network, - amount, - true, - identity_index, - Some(self), - ) { - Ok(transaction) => transaction, - Err(_) => { - wallet - .reload_utxos(&self.core_client, self.network, Some(self)) - .map_err(|e| e.to_string())?; - wallet.asset_lock_transaction( - sdk.network, - amount, - true, - 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) + let (asset_lock_proof, asset_lock_proof_private_key, tx_id) = match identity_funding_method + { + RegisterIdentityFundingMethod::UseAssetLock(address, asset_lock_proof, transaction) => { + let tx_id = transaction.txid(); + + // eprintln!("UseAssetLock: transaction id for {:#?} is {}", transaction, tx_id); + 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")?; + let asset_lock_proof = if let AssetLockProof::Instant(instant_asset_lock_proof) = + asset_lock_proof.as_ref() + { + // we need to make sure the instant send asset lock is recent + let raw_transaction_info = self + .core_client + .get_raw_transaction_info(&tx_id, None) .map_err(|e| e.to_string())?; + if raw_transaction_info.chainlock + && raw_transaction_info.height.is_some() + && raw_transaction_info.confirmations.is_some() + && raw_transaction_info.confirmations.unwrap() > 8 { - let mut wallet = wallet.write().unwrap(); - wallet.utxos.retain(|_, utxo_map| { - utxo_map.retain(|outpoint, _| !used_utxos.contains_key(outpoint)); - !utxo_map.is_empty() // Keep addresses that still have UTXOs - }); - for utxo in used_utxos.keys() { - self.db - .drop_utxo(utxo, &self.network.to_string()) + // we should use a chain lock instead + AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: metadata.core_chain_locked_height, + out_point: OutPoint::new(tx_id, 0), + }) + } else { + AssetLockProof::Instant(instant_asset_lock_proof.clone()) + } + } else { + asset_lock_proof + }; + (asset_lock_proof, private_key, tx_id) + } + RegisterIdentityFundingMethod::FundWithWallet(amount, identity_index) => { + // Scope the write lock to avoid holding it across an await. + let (asset_lock_transaction, asset_lock_proof_private_key, _, used_utxos) = { + let mut wallet = wallet.write().unwrap(); + wallet_id = wallet.seed_hash(); + match wallet.registration_asset_lock_transaction( + sdk.network, + amount, + true, + identity_index, + Some(self), + ) { + Ok(transaction) => transaction, + Err(_) => { + wallet + .reload_utxos(&self.core_client, self.network, Some(self)) .map_err(|e| e.to_string())?; + wallet.registration_asset_lock_transaction( + sdk.network, + amount, + true, + 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); + } - let 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; + self.core_client + .send_raw_transaction(&asset_lock_transaction) + .map_err(|e| e.to_string())?; + + { + let mut wallet = wallet.write().unwrap(); + wallet.utxos.retain(|_, utxo_map| { + utxo_map.retain(|outpoint, _| !used_utxos.contains_key(outpoint)); + !utxo_map.is_empty() // Keep addresses that still have UTXOs + }); + for utxo in used_utxos.keys() { + self.db + .drop_utxo(utxo, &self.network.to_string()) + .map_err(|e| e.to_string())?; } - - (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.clone(), - input_address.clone(), - 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 asset_lock_proof; + + loop { { - let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); - proofs.insert(tx_id, None); + 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; + } - self.core_client - .send_raw_transaction(&asset_lock_transaction) - .map_err(|e| e.to_string())?; + (asset_lock_proof, asset_lock_proof_private_key, tx_id) + } + RegisterIdentityFundingMethod::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.registration_asset_lock_transaction_for_utxo( + sdk.network, + utxo, + tx_out.clone(), + input_address.clone(), + 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); + } - { - let mut wallet = wallet.write().unwrap(); - wallet.utxos.retain(|_, utxo_map| { - utxo_map.retain(|outpoint, _| outpoint != &utxo); - !utxo_map.is_empty() - }); - self.db - .drop_utxo(&utxo, &self.network.to_string()) - .map_err(|e| e.to_string())?; - } + self.core_client + .send_raw_transaction(&asset_lock_transaction) + .map_err(|e| e.to_string())?; + + { + let mut wallet = wallet.write().unwrap(); + wallet.utxos.retain(|_, utxo_map| { + utxo_map.retain(|outpoint, _| outpoint != &utxo); + !utxo_map.is_empty() + }); + self.db + .drop_utxo(&utxo, &self.network.to_string()) + .map_err(|e| e.to_string())?; + } - let asset_lock_proof; + let 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; - } + 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) + tokio::time::sleep(Duration::from_millis(200)).await; } - }; + + (asset_lock_proof, asset_lock_proof_private_key, tx_id) + } + }; let identity_id = asset_lock_proof .create_identifier() @@ -336,6 +331,8 @@ impl AppContext { wallet.read().unwrap().seed_hash(), wallet.clone(), )]), + wallet_index: Some(wallet_identity_index), + top_ups: Default::default(), }; if !alias_input.is_empty() { diff --git a/src/backend_task/identity/top_up_identity.rs b/src/backend_task/identity/top_up_identity.rs new file mode 100644 index 00000000..8b483b4f --- /dev/null +++ b/src/backend_task/identity/top_up_identity.rs @@ -0,0 +1,328 @@ +use crate::app::TaskResult; +use crate::backend_task::identity::{IdentityTopUpInfo, TopUpIdentityFundingMethod}; +use crate::backend_task::BackendTaskSuccessResult; +use crate::context::AppContext; +use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dpp::block::extended_epoch_info::ExtendedEpochInfo; +use dash_sdk::dpp::dashcore::hashes::Hash; +use dash_sdk::dpp::dashcore::OutPoint; +use dash_sdk::dpp::identity::accessors::{IdentityGettersV0, IdentitySettersV0}; +use dash_sdk::dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; +use dash_sdk::dpp::prelude::AssetLockProof; +use dash_sdk::dpp::state_transition::identity_topup_transition::methods::IdentityTopUpTransitionMethodsV0; +use dash_sdk::dpp::state_transition::identity_topup_transition::IdentityTopUpTransition; +use dash_sdk::dpp::version::PlatformVersion; +use dash_sdk::dpp::ProtocolError; +use dash_sdk::platform::transition::top_up_identity::TopUpIdentity; +use dash_sdk::platform::Fetch; +use dash_sdk::Error; +use std::time::Duration; +use tokio::sync::mpsc; + +impl AppContext { + pub(super) async fn top_up_identity( + &self, + input: IdentityTopUpInfo, + sender: mpsc::Sender, + ) -> Result { + let IdentityTopUpInfo { + mut qualified_identity, + wallet, + identity_funding_method, + } = input; + + let sdk = self.sdk.clone(); + + let (_, metadata) = ExtendedEpochInfo::fetch_with_metadata(&sdk, 0, None) + .await + .map_err(|e| e.to_string())?; + + let (asset_lock_proof, asset_lock_proof_private_key, tx_id, top_up_index) = + match identity_funding_method { + TopUpIdentityFundingMethod::UseAssetLock( + address, + asset_lock_proof, + transaction, + ) => { + let tx_id = transaction.txid(); + + // eprintln!("UseAssetLock: transaction id for {:#?} is {}", transaction, tx_id); + let wallet = wallet.read().unwrap(); + let private_key = wallet + .private_key_for_address(&address, self.network)? + .ok_or("Asset Lock not valid for wallet")?; + let asset_lock_proof = + if let AssetLockProof::Instant(instant_asset_lock_proof) = + asset_lock_proof.as_ref() + { + // we need to make sure the instant send asset lock is recent + let raw_transaction_info = self + .core_client + .get_raw_transaction_info(&tx_id, None) + .map_err(|e| e.to_string())?; + + if raw_transaction_info.chainlock + && raw_transaction_info.height.is_some() + && raw_transaction_info.confirmations.is_some() + && raw_transaction_info.confirmations.unwrap() > 8 + { + // we should use a chain lock instead + AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: metadata.core_chain_locked_height, + out_point: OutPoint::new(tx_id, 0), + }) + } else { + AssetLockProof::Instant(instant_asset_lock_proof.clone()) + } + } else { + asset_lock_proof + }; + (asset_lock_proof, private_key, tx_id, None) + } + TopUpIdentityFundingMethod::FundWithWallet( + amount, + identity_index, + top_up_index, + ) => { + // Scope the write lock to avoid holding it across an await. + let (asset_lock_transaction, asset_lock_proof_private_key, _, used_utxos) = { + let mut wallet = wallet.write().unwrap(); + match wallet.top_up_asset_lock_transaction( + sdk.network, + amount, + true, + identity_index, + top_up_index, + Some(self), + ) { + Ok(transaction) => transaction, + Err(_) => { + wallet + .reload_utxos(&self.core_client, self.network, Some(self)) + .map_err(|e| e.to_string())?; + wallet.top_up_asset_lock_transaction( + sdk.network, + amount, + true, + identity_index, + top_up_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 wallet = wallet.write().unwrap(); + wallet.utxos.retain(|_, utxo_map| { + utxo_map.retain(|outpoint, _| !used_utxos.contains_key(outpoint)); + !utxo_map.is_empty() // Keep addresses that still have UTXOs + }); + for utxo in used_utxos.keys() { + self.db + .drop_utxo(utxo, &self.network.to_string()) + .map_err(|e| e.to_string())?; + } + } + + let 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, + Some((amount, top_up_index)), + ) + } + TopUpIdentityFundingMethod::FundWithUtxo( + utxo, + tx_out, + input_address, + identity_index, + top_up_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.top_up_asset_lock_transaction_for_utxo( + sdk.network, + utxo, + tx_out.clone(), + input_address.clone(), + identity_index, + top_up_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 wallet = wallet.write().unwrap(); + wallet.utxos.retain(|_, utxo_map| { + utxo_map.retain(|outpoint, _| outpoint != &utxo); + !utxo_map.is_empty() + }); + self.db + .drop_utxo(&utxo, &self.network.to_string()) + .map_err(|e| e.to_string())?; + } + + let 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, + Some((tx_out.value, top_up_index)), + ) + } + }; + + self.db + .set_asset_lock_identity_id_before_confirmation_by_network( + tx_id.as_byte_array(), + qualified_identity.identity.id().as_bytes(), + ) + .map_err(|e| e.to_string())?; + + let updated_identity_balance = match qualified_identity + .identity + .top_up_identity( + &sdk, + asset_lock_proof.clone(), + &asset_lock_proof_private_key, + None, + ) + .await + { + Ok(updated_identity) => updated_identity, + Err(e) => { + if matches!(e, Error::Protocol(ProtocolError::UnknownVersionError(_))) { + qualified_identity + .identity + .top_up_identity( + &sdk, + asset_lock_proof.clone(), + &asset_lock_proof_private_key, + None, + ) + .await + .map_err(|e| { + let identity_create_transition = + IdentityTopUpTransition::try_from_identity( + &qualified_identity.identity, + asset_lock_proof, + asset_lock_proof_private_key.inner.as_ref(), + 0, + PlatformVersion::latest(), + None, + ) + .expect("expected to make transition"); + format!( + "error: {}, transaction is {:?}", + e.to_string(), + identity_create_transition + ) + })? + } else { + return Err(e.to_string()); + } + } + }; + + qualified_identity + .identity + .set_balance(updated_identity_balance); + + self.update_local_qualified_identity(&qualified_identity) + .map_err(|e| e.to_string())?; + + { + let mut wallet = wallet.write().unwrap(); + wallet + .unused_asset_locks + .retain(|(tx, _, _, _, _)| tx.txid() != tx_id); + } + + self.db + .set_asset_lock_identity_id( + tx_id.as_byte_array(), + qualified_identity.identity.id().as_bytes(), + ) + .map_err(|e| e.to_string())?; + + if let Some((amount, top_up_index)) = top_up_index { + self.db + .insert_top_up( + qualified_identity.identity.id().as_bytes(), + top_up_index, + amount, + ) + .map_err(|e| e.to_string())?; + } + + sender + .send(TaskResult::Success(BackendTaskSuccessResult::None)) + .await + .map_err(|e| e.to_string())?; + + Ok(BackendTaskSuccessResult::ToppedUpIdentity( + qualified_identity, + )) + } +} diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index e4e4fca2..c9b9a2ae 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -9,7 +9,6 @@ use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::query_types::Documents; -use derive_more::From; use std::sync::Arc; use tokio::sync::mpsc; @@ -30,13 +29,14 @@ pub(crate) enum BackendTask { WithdrawalTask(WithdrawalsTask), } -#[derive(Debug, Clone, PartialEq, From)] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum BackendTaskSuccessResult { None, Message(String), Documents(Documents), CoreItem(CoreItem), RegisteredIdentity(QualifiedIdentity), + ToppedUpIdentity(QualifiedIdentity), SuccessfulVotes(Vec), WithdrawalStatus(WithdrawStatusPartialData), } diff --git a/src/database/identities.rs b/src/database/identities.rs index 8c83ae79..01be1220 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -5,7 +5,8 @@ use crate::model::wallet::Wallet; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::platform::Identifier; use rusqlite::params; -use std::sync::{Arc, RwLock, RwLockReadGuard}; +use std::collections::BTreeMap; +use std::sync::{Arc, RwLock}; impl Database { /// Updates the alias of a specified identity. @@ -167,20 +168,48 @@ impl Database { let network = app_context.network_string(); let conn = self.conn.lock().unwrap(); + + // Prepare the main statement to select identities, including wallet_index let mut stmt = conn.prepare( - "SELECT data, alias FROM identity WHERE is_local = 1 AND network = ? AND data IS NOT NULL", + "SELECT data, alias, wallet_index FROM identity WHERE is_local = 1 AND network = ? AND data IS NOT NULL", )?; + + // Prepare the statement to select top-ups (will be used multiple times) + let mut top_up_stmt = + conn.prepare("SELECT top_up_index, amount FROM top_up WHERE identity_id = ?")?; + + // Iterate over each identity let identity_iter = stmt.query_map(params![network], |row| { let data: Vec = row.get(0)?; let alias: Option = row.get(1)?; + let wallet_index: Option = row.get(2)?; + let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); identity.alias = alias; + identity.wallet_index = wallet_index; + // Associate wallets identity.associated_wallets = wallets .iter() .map(|wallet| (wallet.read().unwrap().seed_hash(), wallet.clone())) .collect(); + // Retrieve the identity_id as bytes + let identity_id = identity.identity.id().to_buffer(); + + // Query the top_up table for this identity_id + let mut top_ups = BTreeMap::new(); + let mut rows = top_up_stmt.query(params![identity_id])?; + + while let Some(top_up_row) = rows.next()? { + let top_up_index: u32 = top_up_row.get(0)?; + let amount: u32 = top_up_row.get(1)?; + top_ups.insert(top_up_index, amount); + } + + // Assign the top_ups to the identity + identity.top_ups = top_ups; + Ok(identity) })?; diff --git a/src/database/initialization.rs b/src/database/initialization.rs index 2a828c88..5aec0d49 100644 --- a/src/database/initialization.rs +++ b/src/database/initialization.rs @@ -4,7 +4,7 @@ use rusqlite::{params, Connection}; use std::fs; use std::path::Path; -pub const DEFAULT_DB_VERSION: u16 = 3; +pub const DEFAULT_DB_VERSION: u16 = 4; pub const DEFAULT_NETWORK: &str = "dash"; @@ -34,6 +34,9 @@ impl Database { fn apply_version_changes(&self, version: u16) -> rusqlite::Result<()> { match version { + 4 => { + self.initialize_top_up_table()?; + } 3 => { self.add_custom_dash_qt_columns()?; } @@ -345,6 +348,8 @@ impl Database { self.initialize_proof_log_table()?; + self.initialize_top_up_table()?; + Ok(()) } diff --git a/src/database/mod.rs b/src/database/mod.rs index 83d06cf1..942dfe47 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -5,6 +5,7 @@ mod identities; mod initialization; mod proof_log; mod settings; +mod top_ups; mod utxo; mod wallet; diff --git a/src/database/top_ups.rs b/src/database/top_ups.rs new file mode 100644 index 00000000..d91fa11a --- /dev/null +++ b/src/database/top_ups.rs @@ -0,0 +1,44 @@ +use crate::database::Database; +use rusqlite::{params, OptionalExtension}; + +impl Database { + pub fn initialize_top_up_table(&self) -> rusqlite::Result<()> { + // Create the top_up table + self.execute( + "CREATE TABLE IF NOT EXISTS top_up ( + identity_id BLOB NOT NULL, + top_up_index INTEGER NOT NULL, + amount INTEGER NOT NULL, + PRIMARY KEY (identity_id, top_up_index), + FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE + )", + [], + )?; + Ok(()) + } + + pub fn get_next_top_up_index(&self, identity_id: &[u8]) -> rusqlite::Result { + let conn = self.conn.lock().unwrap(); + let max_index: Option = conn + .query_row( + "SELECT MAX(top_up_index) FROM top_up WHERE identity_id = ?", + params![identity_id], + |row| row.get(0), + ) + .optional()?; + Ok(max_index.unwrap_or(0) + 1) + } + + pub fn insert_top_up( + &self, + identity_id: &[u8], + top_up_index: u32, + amount: u64, + ) -> rusqlite::Result<()> { + self.execute( + "INSERT INTO top_up (identity_id, top_up_index, amount) VALUES (?, ?, ?)", + params![identity_id, top_up_index, amount], + )?; + Ok(()) + } +} diff --git a/src/database/wallet.rs b/src/database/wallet.rs index 1c971742..88316d58 100644 --- a/src/database/wallet.rs +++ b/src/database/wallet.rs @@ -438,7 +438,8 @@ impl Database { let (identity_data, wallet_seed_hash_array, wallet_index) = row?; if let Some(wallet) = wallets_map.get_mut(&wallet_seed_hash_array) { - let identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&identity_data); + let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&identity_data); + identity.wallet_index = Some(wallet_index); // Insert the identity into the wallet's identities HashMap with wallet_index as the key wallet.identities.insert(wallet_index, identity.identity); diff --git a/src/model/proof_log_item.rs b/src/model/proof_log_item.rs index 8a9cdc69..4517d410 100644 --- a/src/model/proof_log_item.rs +++ b/src/model/proof_log_item.rs @@ -34,7 +34,6 @@ pub enum RequestType { GetCurrentQuorumsInfo = 32, } -use dash_sdk::drive::query::PathQuery; use std::convert::TryFrom; impl From for u8 { diff --git a/src/model/qualified_identity/mod.rs b/src/model/qualified_identity/mod.rs index 79a2c7ac..99a8cd5b 100644 --- a/src/model/qualified_identity/mod.rs +++ b/src/model/qualified_identity/mod.rs @@ -96,6 +96,9 @@ pub struct QualifiedIdentity { pub private_keys: KeyStorage, pub dpns_names: Vec, pub associated_wallets: BTreeMap>>, + /// The index used to register the identity + pub wallet_index: Option, + pub top_ups: BTreeMap, } impl PartialEq for QualifiedIdentity { @@ -105,6 +108,7 @@ impl PartialEq for QualifiedIdentity { && self.associated_operator_identity == other.associated_operator_identity && self.associated_owner_key_id == other.associated_owner_key_id && self.identity_type == other.identity_type + && self.wallet_index == other.wallet_index && self.alias == other.alias && self.private_keys == other.private_keys && self.dpns_names == other.dpns_names @@ -146,6 +150,8 @@ impl Decode for QualifiedIdentity { private_keys: KeyStorage::decode(decoder)?, dpns_names: Vec::::decode(decoder)?, associated_wallets: BTreeMap::new(), // Initialize with an empty vector + wallet_index: None, + top_ups: Default::default(), }) } } @@ -353,6 +359,8 @@ impl From for QualifiedIdentity { private_keys: Default::default(), dpns_names: vec![], associated_wallets: BTreeMap::new(), + wallet_index: None, + top_ups: Default::default(), } } } diff --git a/src/model/wallet/asset_lock_transaction.rs b/src/model/wallet/asset_lock_transaction.rs index 769e8b86..2e8e0368 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -12,7 +12,7 @@ use dash_sdk::dpp::dashcore::{ use std::collections::BTreeMap; impl Wallet { - pub fn asset_lock_transaction( + pub fn registration_asset_lock_transaction( &mut self, network: Network, amount: u64, @@ -28,12 +28,69 @@ impl Wallet { ), String, > { - let secp = Secp256k1::new(); let private_key = self.identity_registration_ecdsa_private_key( network, identity_index, register_addresses, )?; + self.asset_lock_transaction_from_private_key( + network, + amount, + allow_take_fee_from_amount, + private_key, + register_addresses, + ) + } + + pub fn top_up_asset_lock_transaction( + &mut self, + network: Network, + amount: u64, + allow_take_fee_from_amount: bool, + identity_index: u32, + top_up_index: u32, + register_addresses: Option<&AppContext>, + ) -> Result< + ( + Transaction, + PrivateKey, + Option
, + BTreeMap, + ), + String, + > { + let private_key = self.identity_top_up_ecdsa_private_key( + network, + identity_index, + top_up_index, + register_addresses, + )?; + self.asset_lock_transaction_from_private_key( + network, + amount, + allow_take_fee_from_amount, + private_key, + register_addresses, + ) + } + + fn asset_lock_transaction_from_private_key( + &mut self, + network: Network, + amount: u64, + allow_take_fee_from_amount: bool, + private_key: PrivateKey, + register_addresses: Option<&AppContext>, + ) -> Result< + ( + Transaction, + PrivateKey, + Option
, + BTreeMap, + ), + String, + > { + let secp = Secp256k1::new(); let asset_lock_public_key = private_key.public_key(&secp); let one_time_key_hash = asset_lock_public_key.pubkey_hash(); @@ -168,7 +225,7 @@ impl Wallet { Ok((tx, private_key, change_address, utxos)) } - pub fn asset_lock_transaction_for_utxo( + pub fn registration_asset_lock_transaction_for_utxo( &mut self, network: Network, utxo: OutPoint, @@ -177,12 +234,54 @@ impl Wallet { 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, )?; + self.asset_lock_transaction_for_utxo_from_private_key( + network, + utxo, + previous_tx_output, + input_address, + private_key, + ) + } + + pub fn top_up_asset_lock_transaction_for_utxo( + &mut self, + network: Network, + utxo: OutPoint, + previous_tx_output: TxOut, + input_address: Address, + identity_index: u32, + top_up_index: u32, + register_addresses: Option<&AppContext>, + ) -> Result<(Transaction, PrivateKey), String> { + let private_key = self.identity_top_up_ecdsa_private_key( + network, + identity_index, + top_up_index, + register_addresses, + )?; + self.asset_lock_transaction_for_utxo_from_private_key( + network, + utxo, + previous_tx_output, + input_address, + private_key, + ) + } + + pub fn asset_lock_transaction_for_utxo_from_private_key( + &mut self, + network: Network, + utxo: OutPoint, + previous_tx_output: TxOut, + input_address: Address, + private_key: PrivateKey, + ) -> Result<(Transaction, PrivateKey), String> { + let secp = Secp256k1::new(); let asset_lock_public_key = private_key.public_key(&secp); let one_time_key_hash = asset_lock_public_key.pubkey_hash(); diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index d642115d..95adc989 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -614,6 +614,32 @@ impl Wallet { Ok(()) } + pub fn identity_top_up_ecdsa_private_key( + &mut self, + network: Network, + identity_index: u32, + top_up_index: u32, + register_addresses: Option<&AppContext>, + ) -> Result { + let derivation_path = + DerivationPath::identity_top_up_path(network, identity_index, top_up_index); + let extended_private_key = derivation_path + .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(); + + if let Some(app_context) = register_addresses { + self.register_address_from_private_key( + &private_key, + &derivation_path, + DerivationPathType::CREDIT_FUNDING, + DerivationPathReference::BlockchainIdentityCreditRegistrationFunding, + app_context, + )?; + } + Ok(private_key) + } + pub fn identity_registration_ecdsa_private_key( &mut self, network: Network, 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 index a5bca56d..98521cc2 100644 --- 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 @@ -1,6 +1,6 @@ use crate::app::AppAction; use crate::ui::identities::add_new_identity_screen::{ - AddNewIdentityScreen, AddNewIdentityWalletFundedScreenStep, FundingMethod, + AddNewIdentityScreen, FundingMethod, WalletFundedScreenStep, }; use egui::Ui; @@ -63,7 +63,7 @@ impl AddNewIdentityScreen { // Update the step to ready to create identity let mut step = self.step.write().unwrap(); - *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; + *step = WalletFundedScreenStep::ReadyToCreate; } }); @@ -98,10 +98,10 @@ impl AddNewIdentityScreen { } match step { - AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("Waiting for Platform acknowledgement"); } - AddNewIdentityWalletFundedScreenStep::Success => { + WalletFundedScreenStep::Success => { ui.heading("...Success..."); } _ => {} diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs index 3d67aea6..803acc18 100644 --- a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs +++ b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs @@ -1,6 +1,6 @@ use crate::app::AppAction; use crate::ui::identities::add_new_identity_screen::{ - AddNewIdentityScreen, AddNewIdentityWalletFundedScreenStep, FundingMethod, + AddNewIdentityScreen, FundingMethod, WalletFundedScreenStep, }; use egui::Ui; @@ -54,13 +54,13 @@ impl AddNewIdentityScreen { } match step { - AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { + WalletFundedScreenStep::WaitingForAssetLock => { ui.heading("Waiting for Core Chain to produce proof of transfer of funds."); } - AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("Waiting for Platform acknowledgement"); } - AddNewIdentityWalletFundedScreenStep::Success => { + WalletFundedScreenStep::Success => { ui.heading("...Success..."); } _ => {} diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs index e3f8433e..990d0268 100644 --- a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -1,12 +1,12 @@ use crate::app::AppAction; use crate::backend_task::identity::{ - IdentityRegistrationInfo, IdentityRegistrationMethod, IdentityTask, + IdentityRegistrationInfo, IdentityTask, RegisterIdentityFundingMethod, }; use crate::backend_task::BackendTask; use crate::ui::identities::add_new_identity_screen::{ - copy_to_clipboard, generate_qr_code_image, AddNewIdentityScreen, - AddNewIdentityWalletFundedScreenStep, + AddNewIdentityScreen, WalletFundedScreenStep, }; +use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; use dash_sdk::dashcore_rpc::RpcApi; use eframe::epaint::TextureHandle; use egui::Ui; @@ -144,11 +144,11 @@ impl AddNewIdentityScreen { ui.add_space(20.0); match step { - AddNewIdentityWalletFundedScreenStep::ChooseFundingMethod => {} - AddNewIdentityWalletFundedScreenStep::WaitingOnFunds => { + WalletFundedScreenStep::ChooseFundingMethod => {} + WalletFundedScreenStep::WaitingOnFunds => { ui.heading("=> Waiting for funds. <="); } - AddNewIdentityWalletFundedScreenStep::FundsReceived => { + WalletFundedScreenStep::FundsReceived => { let Some(selected_wallet) = &self.selected_wallet else { return action; }; @@ -158,7 +158,7 @@ impl AddNewIdentityScreen { 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( + identity_funding_method: RegisterIdentityFundingMethod::FundWithUtxo( utxo, tx_out, address, @@ -167,7 +167,7 @@ impl AddNewIdentityScreen { }; let mut step = self.step.write().unwrap(); - *step = AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock; + *step = WalletFundedScreenStep::WaitingForAssetLock; // Create the backend task to register the identity action |= AppAction::BackendTask(BackendTask::IdentityTask( @@ -175,14 +175,14 @@ impl AddNewIdentityScreen { )) } } - AddNewIdentityWalletFundedScreenStep::ReadyToCreate => {} - AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { + WalletFundedScreenStep::ReadyToCreate => {} + WalletFundedScreenStep::WaitingForAssetLock => { ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); } - AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("=> Waiting for Platform acknowledgement. <="); } - AddNewIdentityWalletFundedScreenStep::Success => { + WalletFundedScreenStep::Success => { ui.heading("...Success..."); } } diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index e99943dc..0a0d3d45 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -6,13 +6,14 @@ mod success_screen; use crate::app::AppAction; use crate::backend_task::core::CoreItem; use crate::backend_task::identity::{ - IdentityKeys, IdentityRegistrationInfo, IdentityRegistrationMethod, IdentityTask, + IdentityKeys, IdentityRegistrationInfo, IdentityTask, RegisterIdentityFundingMethod, }; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; use crate::model::wallet::Wallet; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::identities::funding_common::WalletFundedScreenStep; use crate::ui::{MessageType, ScreenLike}; use arboard::Clipboard; use dash_sdk::dashcore_rpc::dashcore::transaction::special_transaction::TransactionPayload; @@ -55,20 +56,9 @@ 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 { identity_id_number: u32, - step: Arc>, + step: Arc>, funding_asset_lock: Option<(Transaction, AssetLockProof, Address)>, selected_wallet: Option>>, core_has_funding_address: Option, @@ -91,35 +81,6 @@ pub struct AddNewIdentityScreen { successful_qualified_identity_id: Option, } -// Function to generate a QR code image from the address -fn generate_qr_code_image(pay_uri: &str) -> Result { - // Generate the QR code - let code = QrCode::new(pay_uri.as_bytes())?; - - // Render the QR code into an image buffer - let image = code.render::>().build(); - - // Convert the image buffer to ColorImage - let size = [image.width() as usize, image.height() as usize]; - let pixels = image.into_raw(); - let pixels: Vec = pixels - .into_iter() - .map(|p| { - let color = 255 - p; // Invert colors for better visibility - Color32::from_rgba_unmultiplied(color, color, color, 255) - }) - .collect(); - - Ok(ColorImage { size, pixels }) -} - -pub fn copy_to_clipboard(text: &str) -> Result<(), String> { - let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?; - clipboard - .set_text(text.to_string()) - .map_err(|e| e.to_string()) -} - impl AddNewIdentityScreen { pub fn new(app_context: &Arc) -> Self { let mut selected_wallet = None; @@ -189,9 +150,7 @@ impl AddNewIdentityScreen { Self { identity_id_number, - step: Arc::new(RwLock::new( - AddNewIdentityWalletFundedScreenStep::ChooseFundingMethod, - )), + step: Arc::new(RwLock::new(WalletFundedScreenStep::ChooseFundingMethod)), funding_asset_lock: None, selected_wallet, core_has_funding_address: None, @@ -514,7 +473,7 @@ impl AddNewIdentityScreen { { self.update_identity_key(); let mut step = self.step.write().unwrap(); // Write lock on step - *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; + *step = WalletFundedScreenStep::ReadyToCreate; } } if has_balance { @@ -532,7 +491,7 @@ impl AddNewIdentityScreen { self.funding_amount = format!("{:.4}", max_amount as f64 * 1e-8); } let mut step = self.step.write().unwrap(); // Write lock on step - *step = AddNewIdentityWalletFundedScreenStep::ReadyToCreate; + *step = WalletFundedScreenStep::ReadyToCreate; } } if ui @@ -544,7 +503,7 @@ impl AddNewIdentityScreen { .changed() { let mut step = self.step.write().unwrap(); // Write lock on step - *step = AddNewIdentityWalletFundedScreenStep::WaitingOnFunds; + *step = WalletFundedScreenStep::WaitingOnFunds; } // Uncomment this if AttachedCoreWallet is available in the future @@ -687,7 +646,7 @@ impl AddNewIdentityScreen { 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( + identity_funding_method: RegisterIdentityFundingMethod::UseAssetLock( address, funding_asset_lock, tx, @@ -695,7 +654,7 @@ impl AddNewIdentityScreen { }; let mut step = self.step.write().unwrap(); - *step = AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance; + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; AppAction::BackendTask(BackendTask::IdentityTask( IdentityTask::RegisterIdentity(identity_input), @@ -718,14 +677,14 @@ impl AddNewIdentityScreen { 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( + identity_funding_method: RegisterIdentityFundingMethod::FundWithWallet( amount, self.identity_id_number, ), }; let mut step = self.step.write().unwrap(); - *step = AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock; + *step = WalletFundedScreenStep::WaitingForAssetLock; // Create the backend task to register the identity AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::RegisterIdentity( @@ -902,8 +861,8 @@ impl ScreenLike for AddNewIdentityScreen { fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { let mut step = self.step.write().unwrap(); match *step { - AddNewIdentityWalletFundedScreenStep::ChooseFundingMethod => {} - AddNewIdentityWalletFundedScreenStep::WaitingOnFunds => { + WalletFundedScreenStep::ChooseFundingMethod => {} + WalletFundedScreenStep::WaitingOnFunds => { if let Some(funding_address) = self.funding_address.as_ref() { if let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), @@ -911,16 +870,16 @@ impl ScreenLike for AddNewIdentityScreen { { for (outpoint, tx_out, address) in outpoints_with_addresses { if funding_address == &address { - *step = AddNewIdentityWalletFundedScreenStep::FundsReceived; + *step = WalletFundedScreenStep::FundsReceived; self.funding_utxo = Some((outpoint, tx_out, address)) } } } } } - AddNewIdentityWalletFundedScreenStep::FundsReceived => {} - AddNewIdentityWalletFundedScreenStep::ReadyToCreate => {} - AddNewIdentityWalletFundedScreenStep::WaitingForAssetLock => { + WalletFundedScreenStep::FundsReceived => {} + WalletFundedScreenStep::ReadyToCreate => {} + WalletFundedScreenStep::WaitingForAssetLock => { if let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(tx, _), ) = backend_task_success_result @@ -947,21 +906,20 @@ impl ScreenLike for AddNewIdentityScreen { }) .is_some() { - *step = - AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance; + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; } } } } - AddNewIdentityWalletFundedScreenStep::WaitingForPlatformAcceptance => { + WalletFundedScreenStep::WaitingForPlatformAcceptance => { if let BackendTaskSuccessResult::RegisteredIdentity(qualified_identity) = backend_task_success_result { self.successful_qualified_identity_id = Some(qualified_identity.identity.id()); - *step = AddNewIdentityWalletFundedScreenStep::Success; + *step = WalletFundedScreenStep::Success; } } - AddNewIdentityWalletFundedScreenStep::Success => {} + WalletFundedScreenStep::Success => {} } } fn ui(&mut self, ctx: &Context) -> AppAction { @@ -978,7 +936,7 @@ impl ScreenLike for AddNewIdentityScreen { egui::CentralPanel::default().show(ctx, |ui| { ScrollArea::vertical().show(ui, |ui| { let step = {self.step.read().unwrap().clone()}; - if step == AddNewIdentityWalletFundedScreenStep::Success { + if step == WalletFundedScreenStep::Success { action |= self.show_success(ui); return; } diff --git a/src/ui/identities/funding_common.rs b/src/ui/identities/funding_common.rs new file mode 100644 index 00000000..6cce51ed --- /dev/null +++ b/src/ui/identities/funding_common.rs @@ -0,0 +1,44 @@ +use arboard::Clipboard; +use eframe::epaint::{Color32, ColorImage}; +use image::Luma; +use qrcode::QrCode; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +pub enum WalletFundedScreenStep { + ChooseFundingMethod, + WaitingOnFunds, + FundsReceived, + ReadyToCreate, + WaitingForAssetLock, + WaitingForPlatformAcceptance, + Success, +} + +// Function to generate a QR code image from the address +pub fn generate_qr_code_image(pay_uri: &str) -> Result { + // Generate the QR code + let code = QrCode::new(pay_uri.as_bytes())?; + + // Render the QR code into an image buffer + let image = code.render::>().build(); + + // Convert the image buffer to ColorImage + let size = [image.width() as usize, image.height() as usize]; + let pixels = image.into_raw(); + let pixels: Vec = pixels + .into_iter() + .map(|p| { + let color = 255 - p; // Invert colors for better visibility + Color32::from_rgba_unmultiplied(color, color, color, 255) + }) + .collect(); + + Ok(ColorImage { size, pixels }) +} + +pub fn copy_to_clipboard(text: &str) -> Result<(), String> { + let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?; + clipboard + .set_text(text.to_string()) + .map_err(|e| e.to_string()) +} diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index 587dfc05..784a54b1 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -1,3 +1,4 @@ +use super::withdraw_from_identity_screen::WithdrawalScreen; use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::identity::IdentityTask; use crate::backend_task::BackendTask; @@ -14,6 +15,7 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; +use crate::ui::identities::top_up_identity_screen::TopUpIdentityScreen; use crate::ui::transfers::TransferScreen; use crate::ui::{RootScreenType, Screen, ScreenLike, ScreenType}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; @@ -31,8 +33,6 @@ use std::collections::HashMap; use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; -use super::withdraw_from_identity_screen::WithdrawalScreen; - pub struct IdentitiesScreen { pub identities: Arc>>, pub app_context: Arc, @@ -297,11 +297,9 @@ impl IdentitiesScreen { .column(Column::initial(80.0).resizable(true)) // Name .column(Column::initial(330.0).resizable(true)) // Identity ID .column(Column::initial(60.0).resizable(true)) // In Wallet - .column(Column::initial(100.0).resizable(true)) // Balance .column(Column::initial(80.0).resizable(true)) // Type .column(Column::initial(80.0).resizable(true)) // Keys - .column(Column::initial(80.0).resizable(true)) // Withdraw - .column(Column::initial(80.0).resizable(true)) // Transfer + .column(Column::initial(140.0).resizable(true)) // Balance .column(Column::initial(80.0).resizable(true)) // Actions .header(30.0, |mut header| { header.col(|ui| { @@ -313,9 +311,6 @@ impl IdentitiesScreen { header.col(|ui| { ui.heading("In Wallet"); }); - header.col(|ui| { - ui.heading("Balance"); - }); header.col(|ui| { ui.heading("Type"); }); @@ -323,10 +318,7 @@ impl IdentitiesScreen { ui.heading("Keys"); }); header.col(|ui| { - ui.heading("Withdraw"); - }); - header.col(|ui| { - ui.heading("Transfer"); + ui.heading("Balance"); }); header.col(|ui| { ui.heading("Actions"); @@ -350,9 +342,6 @@ impl IdentitiesScreen { row.col(|ui| { self.show_in_wallet(ui, qualified_identity); }); - row.col(|ui| { - Self::show_balance(ui, qualified_identity); - }); row.col(|ui| { ui.label(format!("{}", qualified_identity.identity_type)); }); @@ -440,6 +429,7 @@ impl IdentitiesScreen { }}); }); row.col(|ui| { + Self::show_balance(ui, qualified_identity); if ui.button("Withdraw").clicked() { action = AppAction::AddScreen( Screen::WithdrawalScreen(WithdrawalScreen::new( @@ -448,8 +438,14 @@ impl IdentitiesScreen { )), ); } - }); - row.col(|ui| { + if ui.button("Top up").clicked() { + action = AppAction::AddScreen( + Screen::TopUpIdentityScreen(TopUpIdentityScreen::new( + qualified_identity.clone(), + &self.app_context, + )), + ); + } if ui.button("Transfer").clicked() { action = AppAction::AddScreen(Screen::TransferScreen( TransferScreen::new( diff --git a/src/ui/identities/mod.rs b/src/ui/identities/mod.rs index 4a35447c..dabe88c8 100644 --- a/src/ui/identities/mod.rs +++ b/src/ui/identities/mod.rs @@ -1,6 +1,8 @@ pub mod add_existing_identity_screen; pub mod add_new_identity_screen; +mod funding_common; pub mod identities_screen; pub mod keys; pub mod register_dpns_name_screen; +pub mod top_up_identity_screen; pub mod withdraw_from_identity_screen; diff --git a/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs new file mode 100644 index 00000000..edd41cfe --- /dev/null +++ b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs @@ -0,0 +1,114 @@ +use crate::app::AppAction; +use crate::ui::identities::add_new_identity_screen::FundingMethod; +use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; +use egui::Ui; + +impl TopUpIdentityScreen { + fn render_choose_funding_asset_lock(&mut self, ui: &mut egui::Ui) { + // Ensure a wallet is selected + let Some(selected_wallet) = self.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 = WalletFundedScreenStep::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, + 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); + + if ui.button("Create Identity").clicked() { + self.error_message = None; + action |= self.top_up_identity_clicked(FundingMethod::UseUnusedAssetLock); + } + + match step { + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("Waiting for Platform acknowledgement"); + } + WalletFundedScreenStep::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/top_up_identity_screen/by_using_unused_balance.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs new file mode 100644 index 00000000..0ad85ff4 --- /dev/null +++ b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs @@ -0,0 +1,74 @@ +use crate::app::AppAction; +use crate::ui::identities::add_new_identity_screen::FundingMethod; +use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; +use egui::Ui; + +impl TopUpIdentityScreen { + fn show_wallet_balance(&self, ui: &mut egui::Ui) { + if let Some(selected_wallet) = &self.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); + + ui.add_space(10.0); + + ui.heading(format!( + "{}. How much of your wallet balance would you like to transfer?", + step_number + )); + + step_number += 1; + + self.top_up_funding_amount_input(ui); + + // Extract the step from the RwLock to minimize borrow scope + let step = self.step.read().unwrap().clone(); + + let Ok(_) = self.funding_amount.parse::() else { + return action; + }; + + if ui.button("Top Up Identity").clicked() { + self.error_message = None; + action = self.top_up_identity_clicked(FundingMethod::UseWalletBalance); + } + + match step { + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("Waiting for Core Chain to produce proof of transfer of funds."); + } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("Waiting for Platform acknowledgement"); + } + WalletFundedScreenStep::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/top_up_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs new file mode 100644 index 00000000..dc8d8187 --- /dev/null +++ b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs @@ -0,0 +1,194 @@ +use crate::app::AppAction; +use crate::backend_task::identity::{IdentityTask, IdentityTopUpInfo, TopUpIdentityFundingMethod}; +use crate::backend_task::BackendTask; +use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; +use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; +use dash_sdk::dashcore_rpc::RpcApi; +use eframe::epaint::TextureHandle; +use egui::Ui; +use std::sync::Arc; + +impl TopUpIdentityScreen { + 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.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()); + } + }; + + 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, 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(), + ); + + ui.add_space(8.0); + + self.top_up_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 { + WalletFundedScreenStep::ChooseFundingMethod => {} + WalletFundedScreenStep::WaitingOnFunds => { + ui.heading("=> Waiting for funds. <="); + } + WalletFundedScreenStep::FundsReceived => { + let Some(selected_wallet) = &self.wallet else { + return action; + }; + if let Some((utxo, tx_out, address)) = self.funding_utxo.clone() { + let wallet_index = self.identity.wallet_index.unwrap_or(u32::MAX >> 1); + let top_up_index = self + .identity + .top_ups + .keys() + .max() + .cloned() + .map(|i| i + 1) + .unwrap_or_default(); + let identity_input = IdentityTopUpInfo { + qualified_identity: self.identity.clone(), + wallet: Arc::clone(selected_wallet), // Clone the Arc reference + identity_funding_method: TopUpIdentityFundingMethod::FundWithUtxo( + utxo, + tx_out, + address, + wallet_index, + top_up_index, + ), + }; + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForAssetLock; + + // Create the backend task to register the identity + action |= AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::TopUpIdentity(identity_input), + )) + } + } + WalletFundedScreenStep::ReadyToCreate => {} + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); + } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("=> Waiting for Platform acknowledgement. <="); + } + WalletFundedScreenStep::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/top_up_identity_screen/mod.rs b/src/ui/identities/top_up_identity_screen/mod.rs new file mode 100644 index 00000000..423225d2 --- /dev/null +++ b/src/ui/identities/top_up_identity_screen/mod.rs @@ -0,0 +1,503 @@ +mod by_using_unused_asset_lock; +mod by_using_unused_balance; +mod by_wallet_qr_code; +mod success_screen; + +use crate::app::AppAction; +use crate::backend_task::core::CoreItem; +use crate::backend_task::identity::{IdentityTask, IdentityTopUpInfo, TopUpIdentityFundingMethod}; +use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; +use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::identities::add_new_identity_screen::FundingMethod; +use crate::ui::identities::funding_common::WalletFundedScreenStep; +use crate::ui::{MessageType, ScreenLike}; +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::{OutPoint, Transaction, TxOut}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::prelude::AssetLockProof; +use eframe::egui::Context; +use eframe::epaint::ahash::HashSet; +use egui::{ComboBox, ScrollArea, Ui}; +use std::cmp::PartialEq; +use std::sync::atomic::Ordering; +use std::sync::{Arc, RwLock}; + +pub struct TopUpIdentityScreen { + pub identity: QualifiedIdentity, + step: Arc>, + funding_asset_lock: Option<(Transaction, AssetLockProof, Address)>, + wallet: Option>>, + core_has_funding_address: Option, + funding_address: Option
, + funding_address_balance: Arc>>, + funding_method: Arc>, + funding_amount: String, + funding_amount_exact: Option, + funding_utxo: Option<(OutPoint, TxOut, Address)>, + alias_input: String, + copied_to_clipboard: Option>, + error_message: Option, + show_password: bool, + wallet_password: String, + show_pop_up_info: Option, + in_key_selection_advanced_mode: bool, + pub app_context: Arc, +} + +impl TopUpIdentityScreen { + pub fn new(qualified_identity: QualifiedIdentity, app_context: &Arc) -> Self { + let selected_wallet = qualified_identity + .associated_wallets + .first_key_value() + .map(|(_, wallet)| wallet.clone()); + + Self { + identity: qualified_identity, + step: Arc::new(RwLock::new(WalletFundedScreenStep::ChooseFundingMethod)), + funding_asset_lock: None, + wallet: selected_wallet, + core_has_funding_address: None, + funding_address: None, + funding_address_balance: Arc::new(RwLock::new(None)), + funding_method: Arc::new(RwLock::new(FundingMethod::NoSelection)), + funding_amount: "0.5".to_string(), + funding_amount_exact: None, + funding_utxo: None, + alias_input: String::new(), + copied_to_clipboard: None, + error_message: None, + show_password: false, + wallet_password: "".to_string(), + show_pop_up_info: None, + in_key_selection_advanced_mode: false, + app_context: app_context.clone(), + } + } + + fn render_wallet_selection(&mut self, ui: &mut Ui) -> bool { + if self.app_context.has_wallet.load(Ordering::Relaxed) { + let wallets = &self.identity.associated_wallets; + if wallets.len() > 1 { + // Retrieve the alias of the currently selected wallet, if any + let selected_wallet_alias = self + .wallet + .as_ref() + .and_then(|wallet| wallet.read().ok()?.alias.clone()) + .unwrap_or_else(|| "Select".to_string()); + + ui.heading( + "1. Choose the wallet to use in which this identities keys will come from.", + ); + + ui.add_space(10.0); + + // Display the ComboBox for wallet selection + ComboBox::from_label("Select Wallet") + .selected_text(selected_wallet_alias) + .show_ui(ui, |ui| { + for wallet in wallets.values() { + let wallet_alias = wallet + .read() + .ok() + .and_then(|w| w.alias.clone()) + .unwrap_or_else(|| "Unnamed Wallet".to_string()); + + let is_selected = self + .wallet + .as_ref() + .map_or(false, |selected| Arc::ptr_eq(selected, wallet)); + + if ui.selectable_label(is_selected, wallet_alias).clicked() { + // Update the selected wallet + self.wallet = Some(wallet.clone()); + } + } + }); + ui.add_space(10.0); + true + } else if let Some(wallet) = wallets.values().next() { + if self.wallet.is_none() { + // Automatically select the only available wallet + self.wallet = Some(wallet.clone()); + } + false + } else { + false + } + } else { + false + } + } + + fn render_funding_method(&mut self, ui: &mut egui::Ui) { + let Some(selected_wallet) = self.wallet.clone() else { + return; + }; + let funding_method_arc = self.funding_method.clone(); + let mut funding_method = funding_method_arc.write().unwrap(); // Write lock on funding_method + + ComboBox::from_label("Funding Method") + .selected_text(format!("{}", *funding_method)) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut *funding_method, + FundingMethod::NoSelection, + "Please select funding method", + ); + + let (has_unused_asset_lock, has_balance) = { + let wallet = selected_wallet.read().unwrap(); + (wallet.has_unused_asset_lock(), wallet.has_balance()) + }; + + if has_unused_asset_lock { + if ui + .selectable_value( + &mut *funding_method, + FundingMethod::UseUnusedAssetLock, + "Use Unused Evo Funding Locks (recommended)", + ) + .changed() + { + let mut step = self.step.write().unwrap(); // Write lock on step + *step = WalletFundedScreenStep::ReadyToCreate; + } + } + if has_balance { + if ui + .selectable_value( + &mut *funding_method, + FundingMethod::UseWalletBalance, + "Use Wallet Balance", + ) + .changed() + { + if let Some(wallet) = &self.wallet { + let wallet = wallet.read().unwrap(); // Read lock on the wallet + let max_amount = wallet.max_balance(); + self.funding_amount = format!("{:.4}", max_amount as f64 * 1e-8); + } + let mut step = self.step.write().unwrap(); // Write lock on step + *step = WalletFundedScreenStep::ReadyToCreate; + } + } + if ui + .selectable_value( + &mut *funding_method, + FundingMethod::AddressWithQRCode, + "Address with QR Code", + ) + .changed() + { + let mut step = self.step.write().unwrap(); // Write lock on step + *step = WalletFundedScreenStep::WaitingOnFunds; + } + + // Uncomment this if AttachedCoreWallet is available in the future + // ui.selectable_value( + // &mut *funding_method, + // FundingMethod::AttachedCoreWallet, + // "Attached Core Wallet", + // ); + }); + } + fn top_up_identity_clicked(&mut self, funding_method: FundingMethod) -> AppAction { + let Some(selected_wallet) = &self.wallet else { + return AppAction::None; + }; + match funding_method { + FundingMethod::UseUnusedAssetLock => { + if let Some((tx, funding_asset_lock, address)) = self.funding_asset_lock.clone() { + let identity_input = IdentityTopUpInfo { + qualified_identity: self.identity.clone(), + wallet: Arc::clone(selected_wallet), // Clone the Arc reference + identity_funding_method: TopUpIdentityFundingMethod::UseAssetLock( + address, + funding_asset_lock, + tx, + ), + }; + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; + + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::TopUpIdentity( + identity_input, + ))) + } else { + AppAction::None + } + } + FundingMethod::UseWalletBalance => { + // Parse the funding amount or fall back to the default value + let amount = self.funding_amount_exact.unwrap_or_else(|| { + (self.funding_amount.parse::().unwrap_or_else(|_| 0.0) * 1e8) as u64 + }); + + if amount == 0 { + return AppAction::None; + } + let identity_input = IdentityTopUpInfo { + qualified_identity: self.identity.clone(), + wallet: Arc::clone(selected_wallet), // Clone the Arc reference + identity_funding_method: TopUpIdentityFundingMethod::FundWithWallet( + amount, + self.identity.wallet_index.unwrap_or(u32::MAX >> 1), + self.identity + .top_ups + .keys() + .max() + .cloned() + .map(|i| i + 1) + .unwrap_or_default(), + ), + }; + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForAssetLock; + + // Create the backend task to top_up the identity + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::TopUpIdentity( + identity_input, + ))) + } + _ => AppAction::None, + } + } + + fn top_up_funding_amount_input(&mut self, ui: &mut egui::Ui) { + let funding_method = self.funding_method.read().unwrap(); // Read lock on funding_method + + ui.horizontal(|ui| { + ui.label("Funding Amount (DASH):"); + + // Render the text input field for the funding amount + let amount_input = ui + .add( + egui::TextEdit::singleline(&mut self.funding_amount) + .hint_text("Enter amount (e.g., 0.1234)") + .desired_width(100.0), + ) + .lost_focus(); + + let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); + + if amount_input && enter_pressed { + // Optional: Validate the input when Enter is pressed + if self.funding_amount.parse::().is_err() { + ui.label("Invalid amount. Please enter a valid number."); + } + } + + // Check if the funding method is `UseWalletBalance` + if *funding_method == FundingMethod::UseWalletBalance { + // Safely access the selected wallet + if let Some(wallet) = &self.wallet { + let wallet = wallet.read().unwrap(); // Read lock on the wallet + if ui.button("Max").clicked() { + let max_amount = wallet.max_balance(); + self.funding_amount = format!("{:.4}", max_amount as f64 * 1e-8); + self.funding_amount_exact = Some(max_amount); + } + } + } + }); + } +} + +impl ScreenWithWalletUnlock for TopUpIdentityScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} + +impl ScreenLike for TopUpIdentityScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + if message_type == MessageType::Error { + self.error_message = Some(format!("Error topping up identity: {}", message)); + } else { + self.error_message = Some(message.to_string()); + } + } + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + let mut step = self.step.write().unwrap(); + match *step { + WalletFundedScreenStep::ChooseFundingMethod => {} + WalletFundedScreenStep::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 = WalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some((outpoint, tx_out, address)) + } + } + } + } + } + WalletFundedScreenStep::FundsReceived => {} + WalletFundedScreenStep::ReadyToCreate => {} + WalletFundedScreenStep::WaitingForAssetLock => { + if let BackendTaskSuccessResult::CoreItem( + CoreItem::ReceivedAvailableUTXOTransaction(tx, _), + ) = backend_task_success_result + { + if let Some(TransactionPayload::AssetLockPayloadType(asset_lock_payload)) = + tx.special_transaction_payload + { + if asset_lock_payload + .credit_outputs + .iter() + .find(|tx_out| { + let Ok(address) = Address::from_script( + &tx_out.script_pubkey, + self.app_context.network, + ) else { + return false; + }; + if let Some(wallet) = &self.wallet { + let wallet = wallet.read().unwrap(); + wallet.known_addresses.contains_key(&address) + } else { + false + } + }) + .is_some() + { + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; + } + } + } + } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + if let BackendTaskSuccessResult::ToppedUpIdentity(qualified_identity) = + backend_task_success_result + { + *step = WalletFundedScreenStep::Success; + } + } + WalletFundedScreenStep::Success => {} + } + } + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Identities", AppAction::GoToMainScreen), + ("Top Up Identity", AppAction::None), + ], + vec![], + ); + + egui::CentralPanel::default().show(ctx, |ui| { + ScrollArea::vertical().show(ui, |ui| { + let step = { self.step.read().unwrap().clone() }; + if step == WalletFundedScreenStep::Success { + action |= self.show_success(ui); + return; + } + ui.add_space(10.0); + ui.heading("Follow these steps to top up your identity!"); + ui.add_space(15.0); + + let mut step_number = 1; + + if self.render_wallet_selection(ui) { + // We had more than 1 wallet + step_number += 1; + } + + if self.wallet.is_none() { + return; + }; + + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + + if needed_unlock && !just_unlocked { + return; + } + + ui.add_space(10.0); + + ui.heading(format!("{}. Choose your funding method.", step_number).as_str()); + step_number += 1; + + ui.add_space(10.0); + self.render_funding_method(ui); + + // Extract the funding method from the RwLock to minimize borrow scope + let funding_method = self.funding_method.read().unwrap().clone(); + + if funding_method == FundingMethod::NoSelection { + return; + } + + 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, + } + }); + }); + + // 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/top_up_identity_screen/success_screen.rs b/src/ui/identities/top_up_identity_screen/success_screen.rs new file mode 100644 index 00000000..5a7bfe2e --- /dev/null +++ b/src/ui/identities/top_up_identity_screen/success_screen.rs @@ -0,0 +1,27 @@ +use crate::app::AppAction; +use crate::ui::identities::top_up_identity_screen::TopUpIdentityScreen; +use egui::Ui; + +impl TopUpIdentityScreen { + pub fn show_success(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + + // Center the content vertically and horizontally + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + ui.heading("Successfully topped up!"); + + ui.add_space(20.0); + + // Display the "Back to Identities" button + if ui.button("Back to Identities").clicked() { + // Handle navigation back to the identities screen + action = AppAction::PopScreenAndRefresh; + } + }); + + action + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 556879dd..9a3513a6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ use crate::ui::dpns_contested_names_screen::DPNSContestedNamesScreen; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; use crate::ui::identities::keys::keys_screen::KeysScreen; +use crate::ui::identities::top_up_identity_screen::TopUpIdentityScreen; use crate::ui::identities::withdraw_from_identity_screen::WithdrawalScreen; use crate::ui::network_chooser_screen::NetworkChooserScreen; use crate::ui::tool_screens::proof_log_screen::ProofLogScreen; @@ -20,7 +21,7 @@ use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dpns_contested_names_screen::DPNSSubscreen; -use egui::Context; +use egui::{Context, Widget}; use identities::add_existing_identity_screen::AddExistingIdentityScreen; use identities::add_new_identity_screen::AddNewIdentityScreen; use identities::identities_screen::IdentitiesScreen; @@ -136,6 +137,7 @@ pub enum ScreenType { NetworkChooser, RegisterDpnsName, ProofLog, + TopUpIdentity(QualifiedIdentity), } impl ScreenType { @@ -154,6 +156,9 @@ impl ScreenType { ScreenType::AddNewIdentity => { Screen::AddNewIdentityScreen(AddNewIdentityScreen::new(app_context)) } + ScreenType::TopUpIdentity(identity) => { + Screen::TopUpIdentityScreen(TopUpIdentityScreen::new(identity.clone(), app_context)) + } ScreenType::AddExistingIdentity => { Screen::AddExistingIdentityScreen(AddExistingIdentityScreen::new(app_context)) } @@ -218,6 +223,7 @@ pub enum Screen { KeysScreen(KeysScreen), RegisterDpnsNameScreen(RegisterDpnsNameScreen), WithdrawalScreen(WithdrawalScreen), + TopUpIdentityScreen(TopUpIdentityScreen), TransferScreen(TransferScreen), AddKeyScreen(AddKeyScreen), ProofLogScreen(ProofLogScreen), @@ -244,6 +250,7 @@ impl Screen { Screen::RegisterDpnsNameScreen(screen) => screen.app_context = app_context, Screen::AddNewWalletScreen(screen) => screen.app_context = app_context, Screen::TransferScreen(screen) => screen.app_context = app_context, + Screen::TopUpIdentityScreen(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, @@ -318,6 +325,9 @@ impl Screen { Screen::AddKeyScreen(screen) => ScreenType::AddKeyScreen(screen.identity.clone()), Screen::DocumentQueryScreen(_) => ScreenType::DocumentQueryScreen, Screen::AddNewIdentityScreen(_) => ScreenType::AddExistingIdentity, + Screen::TopUpIdentityScreen(screen) => { + ScreenType::TopUpIdentity(screen.identity.clone()) + } Screen::RegisterDpnsNameScreen(_) => ScreenType::RegisterDpnsName, Screen::AddNewWalletScreen(_) => ScreenType::AddNewWallet, Screen::TransferScreen(screen) => ScreenType::TransferScreen(screen.identity.clone()), @@ -338,6 +348,7 @@ impl ScreenLike for Screen { Screen::AddNewWalletScreen(screen) => screen.refresh(), Screen::ImportWalletScreen(screen) => screen.refresh(), Screen::AddNewIdentityScreen(screen) => screen.refresh(), + Screen::TopUpIdentityScreen(screen) => screen.refresh(), Screen::AddExistingIdentityScreen(screen) => screen.refresh(), Screen::KeyInfoScreen(screen) => screen.refresh(), Screen::KeysScreen(screen) => screen.refresh(), @@ -361,6 +372,7 @@ impl ScreenLike for Screen { Screen::AddNewWalletScreen(screen) => screen.refresh_on_arrival(), Screen::ImportWalletScreen(screen) => screen.refresh_on_arrival(), Screen::AddNewIdentityScreen(screen) => screen.refresh_on_arrival(), + Screen::TopUpIdentityScreen(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(), @@ -384,6 +396,7 @@ impl ScreenLike for Screen { Screen::AddNewWalletScreen(screen) => screen.ui(ctx), Screen::ImportWalletScreen(screen) => screen.ui(ctx), Screen::AddNewIdentityScreen(screen) => screen.ui(ctx), + Screen::TopUpIdentityScreen(screen) => screen.ui(ctx), Screen::AddExistingIdentityScreen(screen) => screen.ui(ctx), Screen::KeyInfoScreen(screen) => screen.ui(ctx), Screen::KeysScreen(screen) => screen.ui(ctx), @@ -409,6 +422,7 @@ impl ScreenLike for Screen { 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::TopUpIdentityScreen(screen) => screen.display_message(message, message_type), Screen::AddExistingIdentityScreen(screen) => { screen.display_message(message, message_type) } @@ -448,6 +462,9 @@ impl ScreenLike for Screen { Screen::AddNewIdentityScreen(screen) => { screen.display_task_result(backend_task_success_result.clone()) } + Screen::TopUpIdentityScreen(screen) => { + screen.display_task_result(backend_task_success_result.clone()) + } Screen::AddExistingIdentityScreen(screen) => { screen.display_task_result(backend_task_success_result.clone()) } @@ -495,6 +512,7 @@ impl ScreenLike for Screen { Screen::AddNewWalletScreen(screen) => screen.pop_on_success(), Screen::ImportWalletScreen(screen) => screen.pop_on_success(), Screen::AddNewIdentityScreen(screen) => screen.pop_on_success(), + Screen::TopUpIdentityScreen(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(), diff --git a/src/ui/tool_screens/proof_log_screen.rs b/src/ui/tool_screens/proof_log_screen.rs index 749acb5f..20447809 100644 --- a/src/ui/tool_screens/proof_log_screen.rs +++ b/src/ui/tool_screens/proof_log_screen.rs @@ -7,7 +7,7 @@ use crate::ui::components::top_panel::add_top_panel; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use dash_sdk::drive::grovedb::operations::proof::GroveDBProof; use dash_sdk::drive::query::PathQuery; -use eframe::egui::{self, Context, Grid, ScrollArea, TextEdit, Ui}; +use eframe::egui::{self, Context, Grid, ScrollArea, Ui}; use egui::text::LayoutJob; use egui::{Color32, FontId, Frame, Stroke, TextFormat, TextStyle, Vec2}; use regex::Regex; diff --git a/src/ui/tool_screens/transition_visualizer_screen.rs b/src/ui/tool_screens/transition_visualizer_screen.rs index ca852557..a0e07e6a 100644 --- a/src/ui/tool_screens/transition_visualizer_screen.rs +++ b/src/ui/tool_screens/transition_visualizer_screen.rs @@ -1,6 +1,5 @@ use crate::app::AppAction; use crate::context::AppContext; -use crate::ui::components::dpns_subscreen_chooser_panel::add_dpns_subscreen_chooser_panel; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::tools_subscreen_chooser_panel::add_tools_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel;