From c3d1e741238ade1217b06c7b4253b37b38491d96 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 28 Jul 2025 20:26:40 +0700 Subject: [PATCH 01/11] feat: implement asset lock creation and detail screens --- src/backend_task/core/create_asset_lock.rs | 65 +++ src/backend_task/core/mod.rs | 9 + src/ui/identities/funding_common.rs | 2 +- src/ui/identities/mod.rs | 2 +- src/ui/mod.rs | 56 ++- src/ui/tokens/tokens_screen/groups.rs | 2 +- src/ui/wallets/asset_lock_detail_screen.rs | 391 +++++++++++++++ src/ui/wallets/create_asset_lock_screen.rs | 530 +++++++++++++++++++++ src/ui/wallets/mod.rs | 2 + src/ui/wallets/wallets_screen/mod.rs | 27 +- 10 files changed, 1080 insertions(+), 6 deletions(-) create mode 100644 src/backend_task/core/create_asset_lock.rs create mode 100644 src/ui/wallets/asset_lock_detail_screen.rs create mode 100644 src/ui/wallets/create_asset_lock_screen.rs diff --git a/src/backend_task/core/create_asset_lock.rs b/src/backend_task/core/create_asset_lock.rs new file mode 100644 index 000000000..cfa5851de --- /dev/null +++ b/src/backend_task/core/create_asset_lock.rs @@ -0,0 +1,65 @@ +use crate::backend_task::BackendTaskSuccessResult; +use crate::context::AppContext; +use crate::model::wallet::Wallet; +use dash_sdk::dashcore_rpc::RpcApi; +use std::sync::{Arc, RwLock}; + +impl AppContext { + pub fn create_asset_lock( + &self, + wallet: Arc>, + amount: u64, // Amount in credits + ) -> Result { + // Create the asset lock transaction + let (asset_lock_transaction, _private_key, _change_address, used_utxos) = { + let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; + + // Use identity index 0 for now (this could be made configurable) + let identity_index = 0u32; + + wallet_guard.registration_asset_lock_transaction( + self.network, + amount, + true, // allow_take_fee_from_amount + identity_index, + Some(self), + )? + }; + + let tx_id = asset_lock_transaction.txid(); + + // Insert the transaction into waiting for finality + { + let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); + proofs.insert(tx_id, None); + } + + // Broadcast the transaction + self.core_client + .read() + .expect("Core client lock was poisoned") + .send_raw_transaction(&asset_lock_transaction) + .map_err(|e| format!("Failed to broadcast asset lock transaction: {}", e))?; + + // Update wallet UTXOs + { + let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; + wallet_guard.utxos.retain(|_, utxo_map| { + utxo_map.retain(|outpoint, _| !used_utxos.contains_key(outpoint)); + !utxo_map.is_empty() // Keep addresses that still have UTXOs + }); + + // Drop used UTXOs from database + for utxo in used_utxos.keys() { + self.db + .drop_utxo(utxo, &self.network.to_string()) + .map_err(|e| e.to_string())?; + } + } + + Ok(BackendTaskSuccessResult::Message(format!( + "Asset lock transaction broadcast successfully. TX ID: {}", + tx_id + ))) + } +} diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index 72f168837..cb53f0f81 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -1,3 +1,4 @@ +mod create_asset_lock; mod refresh_wallet_info; mod start_dash_qt; @@ -19,6 +20,7 @@ pub enum CoreTask { GetBestChainLocks, RefreshWalletInfo(Arc>), StartDashQT(Network, PathBuf, bool), + CreateAssetLock(Arc>, u64), // wallet, amount in credits } impl PartialEq for CoreTask { fn eq(&self, other: &Self) -> bool { @@ -34,6 +36,10 @@ impl PartialEq for CoreTask { CoreTask::StartDashQT(_, _, _), CoreTask::StartDashQT(_, _, _) ) + | ( + CoreTask::CreateAssetLock(_, _), + CoreTask::CreateAssetLock(_, _) + ) ) } } @@ -113,6 +119,9 @@ impl AppContext { .start_dash_qt(network, custom_dash_qt, overwrite_dash_conf) .map_err(|e| e.to_string()) .map(|_| BackendTaskSuccessResult::None), + CoreTask::CreateAssetLock(wallet, amount) => self + .create_asset_lock(wallet, amount) + .map_err(|e| format!("Error creating asset lock: {}", e)), } } diff --git a/src/ui/identities/funding_common.rs b/src/ui/identities/funding_common.rs index 4dd6802d9..0ae7c7302 100644 --- a/src/ui/identities/funding_common.rs +++ b/src/ui/identities/funding_common.rs @@ -4,7 +4,7 @@ use egui::Vec2; use image::Luma; use qrcode::QrCode; -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] pub enum WalletFundedScreenStep { ChooseFundingMethod, WaitingOnFunds, diff --git a/src/ui/identities/mod.rs b/src/ui/identities/mod.rs index 4640b75cd..eeec7b7b3 100644 --- a/src/ui/identities/mod.rs +++ b/src/ui/identities/mod.rs @@ -20,7 +20,7 @@ use crate::{ pub mod add_existing_identity_screen; pub mod add_new_identity_screen; -mod funding_common; +pub mod funding_common; pub mod identities_screen; pub mod keys; pub mod register_dpns_name_screen; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b7fb41094..85b13acf4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,6 +5,7 @@ use crate::model::qualified_identity::QualifiedIdentity; use crate::model::qualified_identity::encrypted_key_storage::{ PrivateKeyData, WalletDerivationPath, }; +use crate::model::wallet::Wallet; use crate::ui::contracts_documents::contracts_documents_screen::DocumentQueryScreen; use crate::ui::contracts_documents::document_action_screen::{ DocumentActionScreen, DocumentActionType, @@ -26,6 +27,8 @@ use crate::ui::tools::document_visualizer_screen::DocumentVisualizerScreen; use crate::ui::tools::platform_info_screen::PlatformInfoScreen; use crate::ui::tools::proof_log_screen::ProofLogScreen; use crate::ui::tools::proof_visualizer_screen::ProofVisualizerScreen; +use crate::ui::wallets::asset_lock_detail_screen::AssetLockDetailScreen; +use crate::ui::wallets::create_asset_lock_screen::CreateAssetLockScreen; use crate::ui::wallets::import_wallet_screen::ImportWalletScreen; use crate::ui::wallets::wallets_screen::WalletsBalancesScreen; use contracts_documents::add_contracts_screen::AddContractsScreen; @@ -43,6 +46,7 @@ use identities::register_dpns_name_screen::RegisterDpnsNameScreen; use std::fmt; use std::hash::Hash; use std::sync::Arc; +use std::sync::RwLock; use tokens::burn_tokens_screen::BurnTokensScreen; use tokens::claim_tokens_screen::ClaimTokensScreen; use tokens::destroy_frozen_funds_screen::DestroyFrozenFundsScreen; @@ -172,7 +176,7 @@ impl From for ScreenType { } } -#[derive(Debug, PartialEq, Clone, Default)] +#[derive(Debug, Clone, Default)] pub enum ScreenType { #[default] Identities, @@ -233,6 +237,10 @@ pub enum ScreenType { UpdateTokenConfigScreen(IdentityTokenInfo), PurchaseTokenScreen(IdentityTokenInfo), SetTokenPriceScreen(IdentityTokenInfo), + + // Wallet screens + AssetLockDetail([u8; 32], usize), + CreateAssetLock(Arc>), } impl ScreenType { @@ -413,6 +421,12 @@ impl ScreenType { ScreenType::SetTokenPriceScreen(identity_token_info) => Screen::SetTokenPriceScreen( SetTokenPriceScreen::new(identity_token_info.clone(), app_context), ), + ScreenType::AssetLockDetail(wallet_seed_hash, index) => Screen::AssetLockDetailScreen( + AssetLockDetailScreen::new(*wallet_seed_hash, *index, app_context), + ), + ScreenType::CreateAssetLock(wallet) => Screen::CreateAssetLockScreen( + CreateAssetLockScreen::new(wallet.clone(), app_context), + ), } } } @@ -463,6 +477,8 @@ pub enum Screen { AddTokenById(AddTokenByIdScreen), PurchaseTokenScreen(PurchaseTokenScreen), SetTokenPriceScreen(SetTokenPriceScreen), + AssetLockDetailScreen(AssetLockDetailScreen), + CreateAssetLockScreen(CreateAssetLockScreen), } impl Screen { @@ -512,6 +528,8 @@ impl Screen { Screen::AddTokenById(screen) => screen.app_context = app_context, Screen::PurchaseTokenScreen(screen) => screen.app_context = app_context, Screen::SetTokenPriceScreen(screen) => screen.app_context = app_context, + Screen::AssetLockDetailScreen(screen) => screen.app_context = app_context, + Screen::CreateAssetLockScreen(screen) => screen.app_context = app_context, } } } @@ -537,6 +555,20 @@ pub trait ScreenLike { fn pop_on_success(&mut self) {} } +// Manual PartialEq implementation for ScreenType +impl PartialEq for ScreenType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ScreenType::CreateAssetLock(a), ScreenType::CreateAssetLock(b)) => Arc::ptr_eq(a, b), + (ScreenType::AssetLockDetail(a1, a2), ScreenType::AssetLockDetail(b1, b2)) => { + a1 == b1 && a2 == b2 + } + // For all other variants, use discriminant comparison + _ => std::mem::discriminant(self) == std::mem::discriminant(other), + } + } +} + // Implement Debug for Screen using the ScreenType impl fmt::Debug for Screen { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -668,6 +700,12 @@ impl Screen { Screen::SetTokenPriceScreen(screen) => { ScreenType::SetTokenPriceScreen(screen.identity_token_info.clone()) } + Screen::AssetLockDetailScreen(screen) => { + ScreenType::AssetLockDetail(screen.wallet_seed_hash, screen.asset_lock_index) + } + Screen::CreateAssetLockScreen(screen) => { + ScreenType::CreateAssetLock(screen.wallet.clone()) + } Screen::TokensScreen(_) => { // Default fallback for any unmatched TokensScreen variants ScreenType::TokenBalances @@ -723,6 +761,8 @@ impl ScreenLike for Screen { Screen::AddTokenById(screen) => screen.refresh(), Screen::PurchaseTokenScreen(screen) => screen.refresh(), Screen::SetTokenPriceScreen(screen) => screen.refresh(), + Screen::AssetLockDetailScreen(screen) => screen.refresh(), + Screen::CreateAssetLockScreen(screen) => screen.refresh(), } } @@ -772,6 +812,8 @@ impl ScreenLike for Screen { Screen::AddTokenById(screen) => screen.refresh_on_arrival(), Screen::PurchaseTokenScreen(screen) => screen.refresh_on_arrival(), Screen::SetTokenPriceScreen(screen) => screen.refresh_on_arrival(), + Screen::AssetLockDetailScreen(screen) => screen.refresh_on_arrival(), + Screen::CreateAssetLockScreen(screen) => screen.refresh_on_arrival(), } } @@ -821,6 +863,8 @@ impl ScreenLike for Screen { Screen::AddTokenById(screen) => screen.ui(ctx), Screen::PurchaseTokenScreen(screen) => screen.ui(ctx), Screen::SetTokenPriceScreen(screen) => screen.ui(ctx), + Screen::AssetLockDetailScreen(screen) => screen.ui(ctx), + Screen::CreateAssetLockScreen(screen) => screen.ui(ctx), } } @@ -886,6 +930,8 @@ impl ScreenLike for Screen { Screen::AddTokenById(screen) => screen.display_message(message, message_type), Screen::PurchaseTokenScreen(screen) => screen.display_message(message, message_type), Screen::SetTokenPriceScreen(screen) => screen.display_message(message, message_type), + Screen::AssetLockDetailScreen(screen) => screen.display_message(message, message_type), + Screen::CreateAssetLockScreen(screen) => screen.display_message(message, message_type), } } @@ -1009,6 +1055,12 @@ impl ScreenLike for Screen { Screen::SetTokenPriceScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::AssetLockDetailScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } + Screen::CreateAssetLockScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } } } @@ -1058,6 +1110,8 @@ impl ScreenLike for Screen { Screen::AddTokenById(screen) => screen.pop_on_success(), Screen::PurchaseTokenScreen(screen) => screen.pop_on_success(), Screen::SetTokenPriceScreen(screen) => screen.pop_on_success(), + Screen::AssetLockDetailScreen(screen) => screen.pop_on_success(), + Screen::CreateAssetLockScreen(screen) => screen.pop_on_success(), } } } diff --git a/src/ui/tokens/tokens_screen/groups.rs b/src/ui/tokens/tokens_screen/groups.rs index 191572984..6414cd930 100644 --- a/src/ui/tokens/tokens_screen/groups.rs +++ b/src/ui/tokens/tokens_screen/groups.rs @@ -164,7 +164,7 @@ impl TokensScreen { .members .iter() .enumerate() - .filter_map(|(i, m)| if i != j && !m.identity_str.is_empty() { + .filter_map(|(i, m)| if i != j && !m.identity_str.is_empty() { let identifier = Identifier::from_string(&m.identity_str, Encoding::Base58).ok()?; Some(identifier) } else { diff --git a/src/ui/wallets/asset_lock_detail_screen.rs b/src/ui/wallets/asset_lock_detail_screen.rs new file mode 100644 index 000000000..fa1dd417b --- /dev/null +++ b/src/ui/wallets/asset_lock_detail_screen.rs @@ -0,0 +1,391 @@ +use crate::app::AppAction; +use crate::context::AppContext; +use crate::model::wallet::Wallet; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::theme::DashColors; +use crate::ui::{MessageType, RootScreenType, ScreenLike}; +use chrono::{DateTime, Utc}; +use dash_sdk::dashcore_rpc::dashcore::{Address, InstantLock, Transaction}; +use dash_sdk::dpp::fee::Credits; +use dash_sdk::dpp::prelude::AssetLockProof; +use eframe::egui::{self, Context, Ui}; +use egui::{Color32, Frame, Margin, RichText}; +use std::sync::{Arc, RwLock}; + +pub struct AssetLockDetailScreen { + pub wallet_seed_hash: [u8; 32], + pub asset_lock_index: usize, + pub app_context: Arc, + message: Option<(String, MessageType, DateTime)>, + wallet: Option>>, + wallet_password: String, + show_password: bool, + error_message: Option, +} + +impl AssetLockDetailScreen { + pub fn new( + wallet_seed_hash: [u8; 32], + asset_lock_index: usize, + app_context: &Arc, + ) -> Self { + // Find the wallet by seed hash + let wallet = app_context + .wallets + .read() + .unwrap() + .values() + .find(|w| w.read().unwrap().seed_hash() == wallet_seed_hash) + .cloned(); + + Self { + wallet_seed_hash, + asset_lock_index, + app_context: app_context.clone(), + message: None, + wallet, + wallet_password: String::new(), + show_password: false, + error_message: None, + } + } + + fn get_asset_lock_data( + &self, + ) -> Option<( + Transaction, + Address, + Credits, + Option, + Option, + )> { + self.wallet.as_ref().and_then(|wallet| { + let wallet = wallet.read().unwrap(); + wallet + .unused_asset_locks + .get(self.asset_lock_index) + .cloned() + }) + } + + fn render_asset_lock_info(&mut self, ui: &mut Ui) { + let dark_mode = ui.ctx().style().visuals.dark_mode; + + if let Some((tx, address, amount, _islock, proof)) = self.get_asset_lock_data() { + Frame::new() + .fill(DashColors::surface(dark_mode)) + .corner_radius(5.0) + .inner_margin(Margin::same(15)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .show(ui, |ui| { + ui.heading(RichText::new("Asset Lock Details").color(DashColors::text_primary(dark_mode))); + ui.add_space(10.0); + + // Transaction Information + ui.label(RichText::new("Transaction Information").strong().color(DashColors::text_primary(dark_mode))); + ui.separator(); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Transaction ID:"); + ui.label(RichText::new(tx.txid().to_string()).font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Address:"); + ui.label(RichText::new(address.to_string()).font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Amount:"); + let dash_amount = amount.to_string().parse::().unwrap_or(0) as f64 * 1e-8; + ui.label(RichText::new(format!("{:.8} DASH ({} duffs)", dash_amount, amount)) + .strong() + .color(DashColors::text_primary(dark_mode))); + }); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Asset Lock Proof Type:"); + let (proof_type, color) = match &proof { + Some(AssetLockProof::Instant(_)) => ("Instant Send Locked", DashColors::success_color(dark_mode)), + Some(AssetLockProof::Chain(_)) => ("Chain Locked", DashColors::success_color(dark_mode)), + None => ("Waiting for Lock", DashColors::warning_color(dark_mode)), + }; + ui.label(RichText::new(proof_type).color(color)); + }); + ui.add_space(5.0); + + // Asset Lock Proof Details + if let Some(proof) = &proof { + ui.add_space(15.0); + ui.label(RichText::new("Asset Lock Proof Details").strong().color(DashColors::text_primary(dark_mode))); + ui.separator(); + ui.add_space(5.0); + + // Show specific proof details based on type + match proof { + AssetLockProof::Instant(instant_proof) => { + ui.horizontal(|ui| { + ui.label("Type:"); + ui.label(RichText::new("Instant Send").font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + + // The instant lock is in the instant_proof + ui.horizontal(|ui| { + ui.label("InstantLock TxID:"); + ui.label(RichText::new(instant_proof.instant_lock.txid.to_string()).font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Output Index:"); + ui.label(RichText::new(instant_proof.output_index.to_string()).font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + } + AssetLockProof::Chain(chain_proof) => { + ui.horizontal(|ui| { + ui.label("Type:"); + ui.label(RichText::new("Chain Lock").font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Core Chain Locked Height:"); + ui.label(RichText::new(chain_proof.core_chain_locked_height.to_string()).font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("OutPoint:"); + ui.label(RichText::new(format!("{}:{}", chain_proof.out_point.txid, chain_proof.out_point.vout)).font(egui::FontId::monospace(12.0))); + }); + ui.add_space(5.0); + } + } + + // Asset Lock Proof Hex + ui.add_space(10.0); + + // Serialize the proof to get hex + let proof_hex = match serde_json::to_vec(proof) { + Ok(bytes) => hex::encode(bytes), + Err(e) => format!("Error serializing proof: {}", e), + }; + + ui.horizontal(|ui| { + ui.label("Asset Lock Proof (hex):"); + if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + ui.ctx().copy_text(proof_hex.clone()); + self.display_message("Asset lock proof copied to clipboard", MessageType::Success); + } + }); + ui.add_space(5.0); + + // Display hex in a scrollable area with monospace font + egui::ScrollArea::horizontal() + .id_salt("proof_hex") + .show(ui, |ui| { + ui.label(RichText::new(&proof_hex).font(egui::FontId::monospace(10.0)).color(DashColors::text_secondary(dark_mode))); + }); + + ui.add_space(10.0); + ui.collapsing("View Raw Proof Details", |ui| { + ui.label(RichText::new(format!("{:#?}", proof)).font(egui::FontId::monospace(10.0))); + }); + } + + // Private Key Section (requires wallet unlock) + ui.add_space(20.0); + ui.label(RichText::new("Private Key Information").strong().color(DashColors::text_primary(dark_mode))); + ui.separator(); + ui.add_space(5.0); + + let (needs_unlock, unlocked) = self.render_wallet_unlock_if_needed(ui); + + if !needs_unlock || unlocked { + if let Some(wallet_arc) = self.wallet.clone() { + let wallet = wallet_arc.read().unwrap(); + + // Find the private key for this address + if let Some(derivation_path) = wallet.known_addresses.get(&address).cloned() { + drop(wallet); // Release the read lock before getting write lock + let wallet = wallet_arc.write().unwrap(); + match wallet.private_key_at_derivation_path(&derivation_path) { + Ok(private_key) => { + let wif = private_key.to_wif(); + drop(wallet); // Release lock before UI operations + ui.horizontal(|ui| { + ui.label("Private Key (WIF):"); + ui.label(RichText::new(&wif).font(egui::FontId::monospace(12.0)).color(DashColors::warning_color(dark_mode))); + if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + ui.ctx().copy_text(wif); + self.display_message("Private key copied to clipboard", MessageType::Success); + } + }); + + ui.add_space(5.0); + ui.label(RichText::new("⚠️ Keep this private key secure! Anyone with access to it can spend these funds.") + .color(DashColors::warning_color(dark_mode)) + .italics()); + } + Err(e) => { + ui.label(RichText::new(format!("Error retrieving private key: {}", e)) + .color(DashColors::error_color(dark_mode))); + } + } + } else { + ui.label(RichText::new("Private key not found for this address") + .color(DashColors::error_color(dark_mode))); + } + } + } + }); + } else { + ui.vertical_centered(|ui| { + ui.add_space(50.0); + ui.label( + RichText::new("Asset lock not found") + .size(16.0) + .color(Color32::GRAY), + ); + }); + } + } + + fn check_message_expiration(&mut self) { + if let Some((_, _, timestamp)) = &self.message { + let now = Utc::now(); + let elapsed = now.signed_duration_since(*timestamp); + + if elapsed.num_seconds() >= 10 { + self.message = None; + } + } + } +} + +impl ScreenWithWalletUnlock for AssetLockDetailScreen { + 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 AssetLockDetailScreen { + fn ui(&mut self, ctx: &Context) -> AppAction { + self.check_message_expiration(); + + let wallet_name = self + .wallet + .as_ref() + .and_then(|w| w.read().ok()?.alias.clone()) + .unwrap_or_else(|| "Unknown Wallet".to_string()); + + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ( + "Wallets", + AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenWalletsBalances, + ), + ), + ( + &format!("{} / Asset Lock Details", wallet_name), + AppAction::None, + ), + ], + vec![], + ); + + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenWalletsBalances, + ); + + action |= island_central_panel(ctx, |ui| { + let inner_action = AppAction::None; + let dark_mode = ui.ctx().style().visuals.dark_mode; + + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.heading( + RichText::new("Asset Lock Information") + .color(DashColors::text_primary(dark_mode)) + .size(24.0), + ); + ui.add_space(10.0); + + self.render_asset_lock_info(ui); + }); + + // Display messages + if let Some((message, message_type, timestamp)) = &self.message { + let message_color = match message_type { + MessageType::Error => egui::Color32::DARK_RED, + MessageType::Info => DashColors::text_primary(dark_mode), + MessageType::Success => egui::Color32::DARK_GREEN, + }; + + ui.add_space(25.0); + ui.horizontal(|ui| { + ui.add_space(10.0); + + let now = Utc::now(); + let elapsed = now.signed_duration_since(*timestamp); + let remaining = (10 - elapsed.num_seconds()).max(0); + + let full_msg = format!("{} ({}s)", message, remaining); + ui.label(egui::RichText::new(full_msg).color(message_color)); + }); + ui.add_space(2.0); + } + + inner_action + }); + + action + } + + fn display_message(&mut self, message: &str, message_type: MessageType) { + self.message = Some((message.to_string(), message_type, Utc::now())); + } + + fn refresh_on_arrival(&mut self) {} + + fn refresh(&mut self) {} +} diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs new file mode 100644 index 000000000..ea4fce80a --- /dev/null +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -0,0 +1,530 @@ +use crate::app::AppAction; +use crate::backend_task::core::{CoreItem, CoreTask}; +use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; +use crate::context::AppContext; +use crate::model::wallet::Wallet; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::identities::funding_common::{WalletFundedScreenStep, generate_qr_code_image}; +use crate::ui::theme::DashColors; +use crate::ui::{MessageType, RootScreenType, ScreenLike}; +use chrono::{DateTime, Utc}; +use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dashcore_rpc::dashcore::{Address, OutPoint, TxOut}; +use dash_sdk::dpp::balances::credits::Credits; +use eframe::egui::{self, Context, Ui}; +use egui::{RichText, Vec2}; +use std::sync::{Arc, RwLock}; + +pub struct CreateAssetLockScreen { + pub wallet: Arc>, + selected_wallet: Option>>, + pub app_context: Arc, + message: Option<(String, MessageType, DateTime)>, + wallet_password: String, + show_password: bool, + error_message: Option, + + // Asset lock creation fields + step: Arc>, + amount_input: String, + amount_credits: Option, + funding_address: Option
, + funding_utxo: Option<(OutPoint, TxOut, Address)>, + core_has_funding_address: Option, + is_creating: bool, + asset_lock_tx_id: Option, +} + +impl CreateAssetLockScreen { + pub fn new(wallet: Arc>, app_context: &Arc) -> Self { + let selected_wallet = Some(wallet.clone()); + Self { + wallet, + selected_wallet, + app_context: app_context.clone(), + message: None, + wallet_password: String::new(), + show_password: false, + error_message: None, + step: Arc::new(RwLock::new(WalletFundedScreenStep::WaitingOnFunds)), + amount_input: "0.5".to_string(), // Default to 0.5 DASH + amount_credits: Some(50000000), // 0.5 DASH in credits + funding_address: None, + funding_utxo: None, + core_has_funding_address: None, + is_creating: false, + asset_lock_tx_id: None, + } + } + + fn render_amount_input(&mut self, ui: &mut Ui) { + let dark_mode = ui.ctx().style().visuals.dark_mode; + + ui.horizontal(|ui| { + ui.label(RichText::new("Amount (DASH):").color(DashColors::text_primary(dark_mode))); + + let response = ui.text_edit_singleline(&mut self.amount_input); + + if response.changed() { + // Parse the input as DASH and convert to credits + if let Ok(dash_amount) = self.amount_input.parse::() { + if dash_amount >= 0.0 { + let credits = (dash_amount * 100_000_000.0) as u64; + self.amount_credits = Some(credits); + } else { + self.amount_credits = None; + } + } else { + self.amount_credits = None; + } + } + }); + + ui.add_space(5.0); + + // Show amount in credits if valid + if let Some(credits) = self.amount_credits { + ui.label( + RichText::new(format!("= {} credits", credits)) + .size(12.0) + .color(DashColors::text_secondary(dark_mode)), + ); + } + } + + fn generate_funding_address(&mut self) -> Result<(), String> { + let mut wallet = self.wallet.write().unwrap(); + + // Generate a new asset lock funding address + let receive_address = + wallet.receive_address(self.app_context.network, false, Some(&self.app_context))?; + + // Import address to core if needed + if let Some(has_address) = self.core_has_funding_address { + if !has_address { + self.app_context + .core_client + .read() + .expect("Core client lock was poisoned") + .import_address( + &receive_address, + Some("Managed by Dash Evo Tool - Asset Lock"), + Some(false), + ) + .map_err(|e| e.to_string())?; + } + self.funding_address = Some(receive_address); + } else { + let info = self + .app_context + .core_client + .read() + .expect("Core client lock was poisoned") + .get_address_info(&receive_address) + .map_err(|e| e.to_string())?; + + if !(info.is_watchonly || info.is_mine) { + self.app_context + .core_client + .read() + .expect("Core client lock was poisoned") + .import_address( + &receive_address, + Some("Managed by Dash Evo Tool - Asset Lock"), + Some(false), + ) + .map_err(|e| e.to_string())?; + } + self.funding_address = Some(receive_address); + self.core_has_funding_address = Some(true); + } + + Ok(()) + } + + fn render_qr_code(&mut self, ui: &mut egui::Ui) -> Result<(), String> { + if self.funding_address.is_none() { + if let Err(e) = self.generate_funding_address() { + return Err(e); + } + } + + let address = self.funding_address.as_ref().unwrap(); + let amount = self.amount_input.parse::().unwrap_or(0.5); + let dash_uri = format!("dash:{}?amount={:.4}", address, amount); + + // Generate the QR code image + if let Ok(qr_image) = generate_qr_code_image(&dash_uri) { + let texture = ui + .ctx() + .load_texture("qr_code", qr_image, egui::TextureOptions::LINEAR); + ui.image((texture.id(), Vec2::new(200.0, 200.0))); + } else { + ui.label("Failed to generate QR code."); + } + + ui.add_space(10.0); + ui.label(&dash_uri); + ui.add_space(5.0); + + if ui.button("Copy Address").clicked() { + ui.ctx().copy_text(dash_uri.clone()); + self.display_message("Address copied to clipboard", MessageType::Success); + } + + Ok(()) + } + + fn check_message_expiration(&mut self) { + if let Some((_, _, timestamp)) = &self.message { + let now = Utc::now(); + let elapsed = now.signed_duration_since(*timestamp); + + if elapsed.num_seconds() >= 10 { + self.message = None; + } + } + } + + fn show_success(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + let dark_mode = ui.ctx().style().visuals.dark_mode; + + // Center the content vertically and horizontally + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading(RichText::new("🎉").size(48.0)); + ui.heading( + RichText::new("Success!") + .size(32.0) + .color(DashColors::success_color(dark_mode)), + ); + + ui.add_space(20.0); + + ui.label( + RichText::new("Asset lock created successfully!") + .size(18.0) + .color(DashColors::text_primary(dark_mode)), + ); + + ui.add_space(10.0); + + if let Some(tx_id) = &self.asset_lock_tx_id { + ui.horizontal(|ui| { + ui.label( + RichText::new("Transaction ID:") + .color(DashColors::text_secondary(dark_mode)), + ); + ui.label(RichText::new(tx_id).font(egui::FontId::monospace(12.0))); + if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + ui.ctx().copy_text(tx_id.clone()); + } + }); + } + + ui.add_space(30.0); + + // Display the "Back to Wallets" button + if ui + .button(RichText::new("Back to Wallets").size(16.0)) + .clicked() + { + action = AppAction::PopScreenAndRefresh; + } + }); + + action + } +} + +impl ScreenWithWalletUnlock for CreateAssetLockScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} + +impl ScreenLike for CreateAssetLockScreen { + fn ui(&mut self, ctx: &Context) -> AppAction { + self.check_message_expiration(); + + let wallet_name = self + .wallet + .read() + .ok() + .and_then(|w| w.alias.clone()) + .unwrap_or_else(|| "Unknown Wallet".to_string()); + + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ( + "Wallets", + AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenWalletsBalances, + ), + ), + ( + &format!("{} / Create Asset Lock", wallet_name), + AppAction::None, + ), + ], + vec![], + ); + + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenWalletsBalances, + ); + + action |= island_central_panel(ctx, |ui| { + let mut inner_action = AppAction::None; + let dark_mode = ui.ctx().style().visuals.dark_mode; + + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.heading( + RichText::new("Create Asset Lock") + .color(DashColors::text_primary(dark_mode)) + .size(24.0) + ); + ui.add_space(10.0); + + ui.label( + RichText::new("Follow these steps to create an asset lock") + .color(DashColors::text_secondary(dark_mode)) + ); + + ui.add_space(20.0); + + // Wallet unlock section + let (needs_unlock, unlocked) = self.render_wallet_unlock_if_needed(ui); + + if !needs_unlock || unlocked { + let step = *self.step.read().unwrap(); + + // Step 1: Amount selection + ui.heading("1. Select how much you would like to transfer?"); + ui.add_space(10.0); + + self.render_amount_input(ui); + ui.add_space(20.0); + + // Step 2: QR Code and address + let amount_valid = self.amount_input.parse::().map(|a| a > 0.0).unwrap_or(false); + if amount_valid { + let layout_action = ui.with_layout( + egui::Layout::top_down(egui::Align::Min).with_cross_align(egui::Align::Center), + |ui| { + if let Err(e) = self.render_qr_code(ui) { + self.error_message = Some(e); + } + + ui.add_space(20.0); + + if let Some(error_message) = self.error_message.as_ref() { + ui.colored_label(egui::Color32::DARK_RED, error_message); + ui.add_space(20.0); + } + + match step { + WalletFundedScreenStep::WaitingOnFunds => { + ui.heading("=> Waiting for funds. <="); + AppAction::None + } + WalletFundedScreenStep::FundsReceived => { + ui.heading("Funds received! Creating asset lock..."); + + // Trigger asset lock creation + if self.is_creating { + self.is_creating = false; + if let Some(credits) = self.amount_credits { + AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::CreateAssetLock(self.wallet.clone(), credits) + )) + } else { + AppAction::None + } + } else { + AppAction::None + } + } + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("=> Waiting for Core Chain to produce proof of asset lock. <="); + AppAction::None + } + WalletFundedScreenStep::Success => { + // Success screen will be shown below + AppAction::None + } + _ => AppAction::None + } + } + ); + + inner_action |= layout_action.inner; + } + + // Show success screen + if *self.step.read().unwrap() == WalletFundedScreenStep::Success { + inner_action |= self.show_success(ui); + } + } + }); + + // Display messages + if let Some((message, message_type, timestamp)) = &self.message { + let message_color = match message_type { + MessageType::Error => egui::Color32::DARK_RED, + MessageType::Info => DashColors::text_primary(dark_mode), + MessageType::Success => egui::Color32::DARK_GREEN, + }; + + ui.add_space(25.0); + ui.horizontal(|ui| { + ui.add_space(10.0); + + let now = Utc::now(); + let elapsed = now.signed_duration_since(*timestamp); + let remaining = (10 - elapsed.num_seconds()).max(0); + + let full_msg = format!("{} ({}s)", message, remaining); + ui.label(egui::RichText::new(full_msg).color(message_color)); + }); + ui.add_space(2.0); + } + + inner_action + }); + + action + } + + fn display_message(&mut self, message: &str, message_type: MessageType) { + self.message = Some((message.to_string(), message_type, Utc::now())); + } + + fn refresh_on_arrival(&mut self) { + self.is_creating = false; + } + + fn refresh(&mut self) {} + + fn display_task_result(&mut self, result: BackendTaskSuccessResult) { + let current_step = *self.step.read().unwrap(); + + match current_step { + WalletFundedScreenStep::WaitingOnFunds => { + if let BackendTaskSuccessResult::CoreItem( + CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), + ) = result + { + for utxo in outpoints_with_addresses { + let (_, _, address) = &utxo; + if let Some(funding_address) = &self.funding_address { + if funding_address == address { + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some(utxo); + drop(step); // Release the lock before creating new action + + // Refresh wallet to create the asset lock + self.is_creating = true; + return; + } + } + } + } + } + WalletFundedScreenStep::FundsReceived => { + // Asset lock creation was triggered + match &result { + BackendTaskSuccessResult::Message(msg) => { + if msg.contains("Asset lock transaction broadcast successfully") { + // Extract TX ID from message + if let Some(tx_id_start) = msg.find("TX ID: ") { + let tx_id = msg[tx_id_start + 7..].trim().to_string(); + self.asset_lock_tx_id = Some(tx_id); + } + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + drop(step); + self.display_message( + "Asset lock created successfully!", + MessageType::Success, + ); + } + } + BackendTaskSuccessResult::CoreItem( + CoreItem::ReceivedAvailableUTXOTransaction(tx, _), + ) => { + // This is the asset lock transaction from ZMQ + if tx.special_transaction_payload.is_some() { + self.asset_lock_tx_id = Some(tx.txid().to_string()); + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + drop(step); + self.display_message( + "Asset lock created successfully!", + MessageType::Success, + ); + } + } + _ => {} + } + } + WalletFundedScreenStep::WaitingForAssetLock => { + // Check if we received an asset lock transaction + if let BackendTaskSuccessResult::CoreItem( + CoreItem::ReceivedAvailableUTXOTransaction(tx, _), + ) = result + { + if tx.special_transaction_payload.is_some() { + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + drop(step); + self.display_message( + "Asset lock created successfully!", + MessageType::Success, + ); + } + } + } + _ => {} + } + + self.is_creating = false; + } +} diff --git a/src/ui/wallets/mod.rs b/src/ui/wallets/mod.rs index 8ced7a4dd..e71bfcca0 100644 --- a/src/ui/wallets/mod.rs +++ b/src/ui/wallets/mod.rs @@ -1,3 +1,5 @@ pub mod add_new_wallet_screen; +pub mod asset_lock_detail_screen; +pub mod create_asset_lock_screen; pub mod import_wallet_screen; pub mod wallets_screen; diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 5723a70af..57afff1f9 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -577,7 +577,16 @@ impl WalletsBalancesScreen { .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) .show(ui, |ui| { let dark_mode = ui.ctx().style().visuals.dark_mode; - ui.heading(RichText::new("Asset Locks").color(DashColors::text_primary(dark_mode))); + ui.horizontal(|ui| { + ui.heading(RichText::new("Asset Locks").color(DashColors::text_primary(dark_mode))); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button(RichText::new("Create Asset Lock").size(14.0)).clicked() { + app_action = AppAction::AddScreen( + ScreenType::CreateAssetLock(arc_wallet.clone()).create_screen(&self.app_context) + ); + } + }); + }); ui.add_space(10.0); if wallet.unused_asset_locks.is_empty() { @@ -607,6 +616,7 @@ impl WalletsBalancesScreen { .column(Column::initial(100.0)) // Amount (Duffs) .column(Column::initial(100.0)) // InstantLock status .column(Column::initial(100.0)) // Usable status + .column(Column::initial(120.0)) // Actions .header(30.0, |mut header| { header.col(|ui| { ui.label("Transaction ID"); @@ -623,9 +633,12 @@ impl WalletsBalancesScreen { header.col(|ui| { ui.label("Usable"); }); + header.col(|ui| { + ui.label("Actions"); + }); }) .body(|mut body| { - for (tx, address, amount, islock, proof) in &wallet.unused_asset_locks { + for (index, (tx, address, amount, islock, proof)) in wallet.unused_asset_locks.iter().enumerate() { body.row(25.0, |mut row| { row.col(|ui| { ui.label(tx.txid().to_string()); @@ -644,6 +657,16 @@ impl WalletsBalancesScreen { let status = if proof.is_some() { "Yes" } else { "No" }; ui.label(status); }); + row.col(|ui| { + if ui.button("View Full Info").clicked() { + app_action = AppAction::AddScreen( + ScreenType::AssetLockDetail( + wallet.seed_hash(), + index + ).create_screen(&self.app_context) + ); + } + }); }); } }); From b6da7fff1c81af0dfe081c314b65541b58527a74 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 28 Jul 2025 20:39:47 +0700 Subject: [PATCH 02/11] clippy fix --- src/ui/wallets/asset_lock_detail_screen.rs | 1 + src/ui/wallets/create_asset_lock_screen.rs | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/wallets/asset_lock_detail_screen.rs b/src/ui/wallets/asset_lock_detail_screen.rs index fa1dd417b..00320921a 100644 --- a/src/ui/wallets/asset_lock_detail_screen.rs +++ b/src/ui/wallets/asset_lock_detail_screen.rs @@ -53,6 +53,7 @@ impl AssetLockDetailScreen { } } + #[allow(clippy::type_complexity)] fn get_asset_lock_data( &self, ) -> Option<( diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index ea4fce80a..99bea112f 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -147,9 +147,7 @@ impl CreateAssetLockScreen { fn render_qr_code(&mut self, ui: &mut egui::Ui) -> Result<(), String> { if self.funding_address.is_none() { - if let Err(e) = self.generate_funding_address() { - return Err(e); - } + self.generate_funding_address()? } let address = self.funding_address.as_ref().unwrap(); From 2fa7aeb133a89207932d6b071f75b3b9d54cf784 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 4 Aug 2025 19:05:57 +0700 Subject: [PATCH 03/11] some fixes --- src/backend_task/core/create_asset_lock.rs | 71 ++++++- src/backend_task/core/mod.rs | 19 +- src/ui/wallets/create_asset_lock_screen.rs | 204 ++++++++++++++++++++- 3 files changed, 276 insertions(+), 18 deletions(-) diff --git a/src/backend_task/core/create_asset_lock.rs b/src/backend_task/core/create_asset_lock.rs index cfa5851de..d34b240e8 100644 --- a/src/backend_task/core/create_asset_lock.rs +++ b/src/backend_task/core/create_asset_lock.rs @@ -2,26 +2,85 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dpp::fee::Credits; use std::sync::{Arc, RwLock}; impl AppContext { - pub fn create_asset_lock( + pub fn create_registration_asset_lock( &self, wallet: Arc>, - amount: u64, // Amount in credits + amount: Credits, + allow_take_fee_from_amount: bool, + identity_index: u32, ) -> Result { // Create the asset lock transaction let (asset_lock_transaction, _private_key, _change_address, used_utxos) = { let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; - // Use identity index 0 for now (this could be made configurable) - let identity_index = 0u32; - wallet_guard.registration_asset_lock_transaction( self.network, amount, - true, // allow_take_fee_from_amount + allow_take_fee_from_amount, + identity_index, + Some(self), + )? + }; + + let tx_id = asset_lock_transaction.txid(); + + // Insert the transaction into waiting for finality + { + let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); + proofs.insert(tx_id, None); + } + + // Broadcast the transaction + self.core_client + .read() + .expect("Core client lock was poisoned") + .send_raw_transaction(&asset_lock_transaction) + .map_err(|e| format!("Failed to broadcast asset lock transaction: {}", e))?; + + // Update wallet UTXOs + { + let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; + wallet_guard.utxos.retain(|_, utxo_map| { + utxo_map.retain(|outpoint, _| !used_utxos.contains_key(outpoint)); + !utxo_map.is_empty() // Keep addresses that still have UTXOs + }); + + // Drop used UTXOs from database + for utxo in used_utxos.keys() { + self.db + .drop_utxo(utxo, &self.network.to_string()) + .map_err(|e| e.to_string())?; + } + } + + Ok(BackendTaskSuccessResult::Message(format!( + "Asset lock transaction broadcast successfully. TX ID: {}", + tx_id + ))) + } + + pub fn create_top_up_asset_lock( + &self, + wallet: Arc>, + amount: Credits, + allow_take_fee_from_amount: bool, + identity_index: u32, + top_up_index: u32, + ) -> Result { + // Create the asset lock transaction + let (asset_lock_transaction, _private_key, _change_address, used_utxos) = { + let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; + + wallet_guard.top_up_asset_lock_transaction( + self.network, + amount, + allow_take_fee_from_amount, identity_index, + top_up_index, Some(self), )? }; diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index cb53f0f81..78c01c616 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -10,6 +10,7 @@ use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dashcore_rpc::{Auth, Client}; use dash_sdk::dpp::dashcore::{Address, ChainLock, Network, OutPoint, Transaction, TxOut}; +use dash_sdk::dpp::fee::Credits; use std::path::PathBuf; use std::sync::{Arc, RwLock}; @@ -20,7 +21,8 @@ pub enum CoreTask { GetBestChainLocks, RefreshWalletInfo(Arc>), StartDashQT(Network, PathBuf, bool), - CreateAssetLock(Arc>, u64), // wallet, amount in credits + CreateRegistrationAssetLock(Arc>, Credits, u32), // wallet, amount in credits, identity index + CreateTopUpAssetLock(Arc>, Credits, u32, u32), // wallet, amount in credits, identity index, top up index } impl PartialEq for CoreTask { fn eq(&self, other: &Self) -> bool { @@ -37,8 +39,12 @@ impl PartialEq for CoreTask { CoreTask::StartDashQT(_, _, _) ) | ( - CoreTask::CreateAssetLock(_, _), - CoreTask::CreateAssetLock(_, _) + CoreTask::CreateRegistrationAssetLock(_, _, _), + CoreTask::CreateRegistrationAssetLock(_, _, _) + ) + | ( + CoreTask::CreateTopUpAssetLock(_, _, _, _), + CoreTask::CreateTopUpAssetLock(_, _, _, _) ) ) } @@ -119,9 +125,12 @@ impl AppContext { .start_dash_qt(network, custom_dash_qt, overwrite_dash_conf) .map_err(|e| e.to_string()) .map(|_| BackendTaskSuccessResult::None), - CoreTask::CreateAssetLock(wallet, amount) => self - .create_asset_lock(wallet, amount) + CoreTask::CreateRegistrationAssetLock(wallet, amount, identity_index) => self + .create_registration_asset_lock(wallet, amount, true, identity_index) .map_err(|e| format!("Error creating asset lock: {}", e)), + CoreTask::CreateTopUpAssetLock(wallet, amount, identity_index, top_up_index) => self + .create_top_up_asset_lock(wallet, amount, true, identity_index, top_up_index) + .map_err(|e| format!("Error creating top up asset lock: {}", e)), } } diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index 99bea112f..9d9357aa3 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -2,7 +2,9 @@ use crate::app::AppAction; use crate::backend_task::core::{CoreItem, CoreTask}; 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::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; @@ -18,6 +20,12 @@ use eframe::egui::{self, Context, Ui}; use egui::{RichText, Vec2}; use std::sync::{Arc, RwLock}; +#[derive(Debug, Clone, Copy, PartialEq)] +enum AssetLockPurpose { + Registration, + TopUp, +} + pub struct CreateAssetLockScreen { pub wallet: Arc>, selected_wallet: Option>>, @@ -30,12 +38,19 @@ pub struct CreateAssetLockScreen { // Asset lock creation fields step: Arc>, amount_input: String, + identity_index: u32, amount_credits: Option, funding_address: Option
, funding_utxo: Option<(OutPoint, TxOut, Address)>, core_has_funding_address: Option, is_creating: bool, asset_lock_tx_id: Option, + + // New fields for asset lock purpose flow + asset_lock_purpose: Option, + selected_identity: Option, + selected_identity_string: String, + top_up_index: u32, } impl CreateAssetLockScreen { @@ -51,12 +66,17 @@ impl CreateAssetLockScreen { error_message: None, step: Arc::new(RwLock::new(WalletFundedScreenStep::WaitingOnFunds)), amount_input: "0.5".to_string(), // Default to 0.5 DASH - amount_credits: Some(50000000), // 0.5 DASH in credits + identity_index: 0, + amount_credits: Some(50000000), // 0.5 DASH in credits funding_address: None, funding_utxo: None, core_has_funding_address: None, is_creating: false, asset_lock_tx_id: None, + asset_lock_purpose: None, + selected_identity: None, + selected_identity_string: String::new(), + top_up_index: 0, } } @@ -330,16 +350,169 @@ impl ScreenLike for CreateAssetLockScreen { let (needs_unlock, unlocked) = self.render_wallet_unlock_if_needed(ui); if !needs_unlock || unlocked { + // First, select the purpose of the asset lock + if self.asset_lock_purpose.is_none() { + ui.heading("Select Asset Lock Purpose"); + ui.add_space(10.0); + + ui.label( + RichText::new("What is the purpose of this asset lock?") + .color(DashColors::text_secondary(dark_mode)) + ); + ui.add_space(20.0); + + ui.horizontal(|ui| { + if ui.button(RichText::new("Registration").size(16.0)).clicked() { + self.asset_lock_purpose = Some(AssetLockPurpose::Registration); + } + + ui.add_space(20.0); + + if ui.button(RichText::new("Top Up").size(16.0)).clicked() { + self.asset_lock_purpose = Some(AssetLockPurpose::TopUp); + } + }); + + ui.add_space(20.0); + + // Show explanation + ui.group(|ui| { + ui.label(RichText::new("ℹ️ Information").strong()); + ui.add_space(5.0); + ui.label("• Registration: Create an asset lock for a new identity registration"); + ui.label("• Top Up: Add credits to an existing identity"); + }); + + return; + } + + // Show selected purpose + ui.horizontal(|ui| { + ui.label(RichText::new("Purpose:").strong()); + let purpose_text = match self.asset_lock_purpose { + Some(AssetLockPurpose::Registration) => "Registration", + Some(AssetLockPurpose::TopUp) => "Top Up", + None => "Not selected", + }; + ui.label(purpose_text); + + if ui.button("Change").clicked() { + self.asset_lock_purpose = None; + self.selected_identity = None; + self.selected_identity_string.clear(); + } + }); + ui.add_space(20.0); + + // For top up, select identity and indices + if self.asset_lock_purpose == Some(AssetLockPurpose::TopUp) { + ui.heading("1. Select Identity to Top Up"); + ui.add_space(10.0); + let identities = match self.app_context.load_local_qualified_identities() { + Ok(ids) => ids, + Err(e) => { + ui.label( + RichText::new(format!("Error loading identities: {}", e)) + .color(egui::Color32::RED) + ); + return; + } + }; + + if identities.is_empty() { + ui.label( + RichText::new("No identities found. Please create an identity first.") + .color(egui::Color32::from_rgb(255, 152, 0)) + ); + return; + } + + let identity_selector_response = ui.add(IdentitySelector::new( + "top_up_identity_selector", + &mut self.selected_identity_string, + &identities + ) + .selected_identity(&mut self.selected_identity).unwrap() + .label("Identity to top up:") + .width(300.0)); + + // Update identity index when identity selection changes + if identity_selector_response.changed() { + if let Some(selected) = &self.selected_identity { + if let Some(wallet_idx) = selected.wallet_index { + self.identity_index = wallet_idx; + } + } + } + + if self.selected_identity.is_none() { + return; + } + + ui.add_space(20.0); + + // Identity index input (for wallet key derivation) + ui.horizontal(|ui| { + ui.label(RichText::new("Identity Index:").color(DashColors::text_primary(dark_mode))); + let mut index_str = self.identity_index.to_string(); + if ui.text_edit_singleline(&mut index_str).changed() { + if let Ok(index) = index_str.parse::() { + self.identity_index = index; + } + } + }); + ui.label( + RichText::new("This is the wallet's key derivation index") + .size(12.0) + .color(DashColors::text_secondary(dark_mode)) + ); + + ui.add_space(10.0); + + // Top up index input + ui.horizontal(|ui| { + ui.label(RichText::new("Top Up Index:").color(DashColors::text_primary(dark_mode))); + let mut index_str = self.top_up_index.to_string(); + if ui.text_edit_singleline(&mut index_str).changed() { + if let Ok(index) = index_str.parse::() { + self.top_up_index = index; + } + } + }); + ui.label( + RichText::new("Sequential index for this specific top up") + .size(12.0) + .color(DashColors::text_secondary(dark_mode)) + ); + ui.add_space(20.0); + } else if self.asset_lock_purpose == Some(AssetLockPurpose::Registration) { + // Registration index input + ui.heading("1. Set Registration Index"); + ui.add_space(10.0); + + ui.horizontal(|ui| { + ui.label(RichText::new("Identity Index:").color(DashColors::text_primary(dark_mode))); + let mut index_str = self.identity_index.to_string(); + if ui.text_edit_singleline(&mut index_str).changed() { + if let Ok(index) = index_str.parse::() { + self.identity_index = index; + } + } + }); + ui.add_space(20.0); + } + let step = *self.step.read().unwrap(); - // Step 1: Amount selection - ui.heading("1. Select how much you would like to transfer?"); + // Step 2: Amount selection + let step_number = if self.asset_lock_purpose == Some(AssetLockPurpose::TopUp) { "2" } else { "2" }; + ui.heading(format!("{}. Select how much you would like to transfer?", step_number)); ui.add_space(10.0); self.render_amount_input(ui); ui.add_space(20.0); - // Step 2: QR Code and address + // Step 3: QR Code and address let amount_valid = self.amount_input.parse::().map(|a| a > 0.0).unwrap_or(false); if amount_valid { let layout_action = ui.with_layout( @@ -368,9 +541,24 @@ impl ScreenLike for CreateAssetLockScreen { if self.is_creating { self.is_creating = false; if let Some(credits) = self.amount_credits { - AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::CreateAssetLock(self.wallet.clone(), credits) - )) + match self.asset_lock_purpose { + Some(AssetLockPurpose::Registration) => { + AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::CreateRegistrationAssetLock(self.wallet.clone(), credits, self.identity_index) + )) + } + Some(AssetLockPurpose::TopUp) => { + if let Some(identity) = &self.selected_identity { + let identity_index = identity.wallet_index.unwrap_or(self.identity_index); + AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::CreateTopUpAssetLock(self.wallet.clone(), credits, identity_index, self.top_up_index) + )) + } else { + AppAction::None + } + } + None => AppAction::None + } } else { AppAction::None } @@ -398,6 +586,8 @@ impl ScreenLike for CreateAssetLockScreen { if *self.step.read().unwrap() == WalletFundedScreenStep::Success { inner_action |= self.show_success(ui); } + } else { + // Wallet needs to be unlocked } }); From dfe33fca89e51be061427ac92519f7b9e8dd8412 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 14:00:41 +0700 Subject: [PATCH 04/11] fix: ui cleanup and backend fixes --- src/backend_task/core/create_asset_lock.rs | 11 +- src/ui/wallets/asset_lock_detail_screen.rs | 12 +- src/ui/wallets/create_asset_lock_screen.rs | 507 ++++++++++++++------- src/ui/wallets/wallets_screen/mod.rs | 2 +- 4 files changed, 356 insertions(+), 176 deletions(-) diff --git a/src/backend_task/core/create_asset_lock.rs b/src/backend_task/core/create_asset_lock.rs index d34b240e8..94faf5379 100644 --- a/src/backend_task/core/create_asset_lock.rs +++ b/src/backend_task/core/create_asset_lock.rs @@ -2,6 +2,7 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; use dash_sdk::dpp::fee::Credits; use std::sync::{Arc, RwLock}; @@ -13,13 +14,16 @@ impl AppContext { allow_take_fee_from_amount: bool, identity_index: u32, ) -> Result { + // Convert credits to duffs (1 duff = 1000 credits) + let amount_duffs = amount / CREDITS_PER_DUFF; + // Create the asset lock transaction let (asset_lock_transaction, _private_key, _change_address, used_utxos) = { let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; wallet_guard.registration_asset_lock_transaction( self.network, - amount, + amount_duffs, allow_take_fee_from_amount, identity_index, Some(self), @@ -71,13 +75,16 @@ impl AppContext { identity_index: u32, top_up_index: u32, ) -> Result { + // Convert credits to duffs (1 duff = 1000 credits) + let amount_duffs = amount / CREDITS_PER_DUFF; + // Create the asset lock transaction let (asset_lock_transaction, _private_key, _change_address, used_utxos) = { let mut wallet_guard = wallet.write().map_err(|e| e.to_string())?; wallet_guard.top_up_asset_lock_transaction( self.network, - amount, + amount_duffs, allow_take_fee_from_amount, identity_index, top_up_index, diff --git a/src/ui/wallets/asset_lock_detail_screen.rs b/src/ui/wallets/asset_lock_detail_screen.rs index 00320921a..70cb3b197 100644 --- a/src/ui/wallets/asset_lock_detail_screen.rs +++ b/src/ui/wallets/asset_lock_detail_screen.rs @@ -183,7 +183,7 @@ impl AssetLockDetailScreen { ui.horizontal(|ui| { ui.label("Asset Lock Proof (hex):"); - if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + if ui.small_button("Copy").clicked() { ui.ctx().copy_text(proof_hex.clone()); self.display_message("Asset lock proof copied to clipboard", MessageType::Success); } @@ -219,21 +219,21 @@ impl AssetLockDetailScreen { if let Some(derivation_path) = wallet.known_addresses.get(&address).cloned() { drop(wallet); // Release the read lock before getting write lock let wallet = wallet_arc.write().unwrap(); - match wallet.private_key_at_derivation_path(&derivation_path) { + match wallet.private_key_at_derivation_path(&derivation_path, self.app_context.network) { Ok(private_key) => { let wif = private_key.to_wif(); drop(wallet); // Release lock before UI operations ui.horizontal(|ui| { ui.label("Private Key (WIF):"); ui.label(RichText::new(&wif).font(egui::FontId::monospace(12.0)).color(DashColors::warning_color(dark_mode))); - if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + if ui.small_button("Copy").clicked() { ui.ctx().copy_text(wif); self.display_message("Private key copied to clipboard", MessageType::Success); } }); ui.add_space(5.0); - ui.label(RichText::new("⚠️ Keep this private key secure! Anyone with access to it can spend these funds.") + ui.label(RichText::new("Warning: Keep this private key secure! Anyone with access to it can spend these funds.") .color(DashColors::warning_color(dark_mode)) .italics()); } @@ -301,6 +301,10 @@ impl ScreenWithWalletUnlock for AssetLockDetailScreen { fn error_message(&self) -> Option<&String> { self.error_message.as_ref() } + + fn app_context(&self) -> Arc { + self.app_context.clone() + } } impl ScreenLike for AssetLockDetailScreen { diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index 9d9357aa3..af8b6d039 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -9,7 +9,7 @@ use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::identities::funding_common::{WalletFundedScreenStep, generate_qr_code_image}; +use crate::ui::identities::funding_common::{self, WalletFundedScreenStep, generate_qr_code_image}; use crate::ui::theme::DashColors; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use chrono::{DateTime, Utc}; @@ -17,9 +17,12 @@ use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dashcore_rpc::dashcore::{Address, OutPoint, TxOut}; use dash_sdk::dpp::balances::credits::Credits; use eframe::egui::{self, Context, Ui}; -use egui::{RichText, Vec2}; +use egui::{Button, RichText, Vec2}; +use std::collections::HashSet; use std::sync::{Arc, RwLock}; +const MAX_IDENTITY_INDEX: u32 = 30; + #[derive(Debug, Clone, Copy, PartialEq)] enum AssetLockPurpose { Registration, @@ -51,11 +54,25 @@ pub struct CreateAssetLockScreen { selected_identity: Option, selected_identity_string: String, top_up_index: u32, + show_advanced_options: bool, } impl CreateAssetLockScreen { pub fn new(wallet: Arc>, app_context: &Arc) -> Self { let selected_wallet = Some(wallet.clone()); + + // Calculate next unused identity index + let identity_index = { + let wallet_guard = wallet.read().unwrap(); + wallet_guard + .identities + .keys() + .copied() + .max() + .map(|max| max + 1) + .unwrap_or(0) + }; + Self { wallet, selected_wallet, @@ -66,8 +83,8 @@ impl CreateAssetLockScreen { error_message: None, step: Arc::new(RwLock::new(WalletFundedScreenStep::WaitingOnFunds)), amount_input: "0.5".to_string(), // Default to 0.5 DASH - identity_index: 0, - amount_credits: Some(50000000), // 0.5 DASH in credits + identity_index, + amount_credits: Some(50_000_000_000), // 0.5 DASH in credits funding_address: None, funding_utxo: None, core_has_funding_address: None, @@ -77,6 +94,7 @@ impl CreateAssetLockScreen { selected_identity: None, selected_identity_string: String::new(), top_up_index: 0, + show_advanced_options: false, } } @@ -92,7 +110,7 @@ impl CreateAssetLockScreen { // Parse the input as DASH and convert to credits if let Ok(dash_amount) = self.amount_input.parse::() { if dash_amount >= 0.0 { - let credits = (dash_amount * 100_000_000.0) as u64; + let credits = (dash_amount * 100_000_000_000.0) as u64; self.amount_credits = Some(credits); } else { self.amount_credits = None; @@ -103,14 +121,15 @@ impl CreateAssetLockScreen { } }); - ui.add_space(5.0); - // Show amount in credits if valid if let Some(credits) = self.amount_credits { ui.label( - RichText::new(format!("= {} credits", credits)) - .size(12.0) - .color(DashColors::text_secondary(dark_mode)), + RichText::new(format!( + " = {} credits", + credits + )) + .size(12.0) + .color(DashColors::text_secondary(dark_mode)), ); } } @@ -207,53 +226,60 @@ impl CreateAssetLockScreen { } } - fn show_success(&self, ui: &mut Ui) -> AppAction { + fn show_success(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; - let dark_mode = ui.ctx().style().visuals.dark_mode; - // Center the content vertically and horizontally ui.vertical_centered(|ui| { - ui.add_space(50.0); + ui.add_space(100.0); - ui.heading(RichText::new("🎉").size(48.0)); - ui.heading( - RichText::new("Success!") - .size(32.0) - .color(DashColors::success_color(dark_mode)), - ); - - ui.add_space(20.0); - - ui.label( - RichText::new("Asset lock created successfully!") - .size(18.0) - .color(DashColors::text_primary(dark_mode)), - ); - - ui.add_space(10.0); + ui.heading("🎉"); + ui.heading("Asset Lock Created Successfully!"); if let Some(tx_id) = &self.asset_lock_tx_id { + ui.add_space(20.0); ui.horizontal(|ui| { - ui.label( - RichText::new("Transaction ID:") - .color(DashColors::text_secondary(dark_mode)), - ); + ui.label("Transaction ID:"); ui.label(RichText::new(tx_id).font(egui::FontId::monospace(12.0))); - if ui.button("📋").on_hover_text("Copy to clipboard").clicked() { + if ui.small_button("Copy").clicked() { ui.ctx().copy_text(tx_id.clone()); } }); } - ui.add_space(30.0); + ui.add_space(20.0); - // Display the "Back to Wallets" button - if ui - .button(RichText::new("Back to Wallets").size(16.0)) - .clicked() - { + if ui.button("Back to Wallets").clicked() { action = AppAction::PopScreenAndRefresh; } + if ui.button("Create Another").clicked() { + // Reset state for creating another asset lock + self.asset_lock_purpose = None; + self.selected_identity = None; + self.selected_identity_string.clear(); + // Recalculate next unused identity index + self.identity_index = { + let wallet_guard = self.wallet.read().unwrap(); + wallet_guard + .identities + .keys() + .copied() + .max() + .map(|max| max + 1) + .unwrap_or(0) + }; + self.top_up_index = 0; + self.amount_input = "0.5".to_string(); + self.amount_credits = Some(50_000_000_000); + self.funding_address = None; + self.funding_utxo = None; + self.core_has_funding_address = None; + self.asset_lock_tx_id = None; + self.error_message = None; + self.show_advanced_options = false; + *self.step.write().unwrap() = WalletFundedScreenStep::WaitingOnFunds; + } + + ui.add_space(100.0); }); action @@ -288,6 +314,10 @@ impl ScreenWithWalletUnlock for CreateAssetLockScreen { fn error_message(&self) -> Option<&String> { self.error_message.as_ref() } + + fn app_context(&self) -> Arc { + self.app_context.clone() + } } impl ScreenLike for CreateAssetLockScreen { @@ -311,10 +341,7 @@ impl ScreenLike for CreateAssetLockScreen { RootScreenType::RootScreenWalletsBalances, ), ), - ( - &format!("{} / Create Asset Lock", wallet_name), - AppAction::None, - ), + ("Create Asset Lock", AppAction::None), ], vec![], ); @@ -332,19 +359,31 @@ impl ScreenLike for CreateAssetLockScreen { egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { - ui.heading( - RichText::new("Create Asset Lock") - .color(DashColors::text_primary(dark_mode)) - .size(24.0) - ); - ui.add_space(10.0); + // Header with Back button and Advanced Options checkbox + ui.horizontal(|ui| { + ui.heading( + RichText::new("Create Asset Lock") + .color(DashColors::text_primary(dark_mode)) + .size(24.0) + ); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.checkbox(&mut self.show_advanced_options, "Advanced Options"); + }); + }); - ui.label( - RichText::new("Follow these steps to create an asset lock") - .color(DashColors::text_secondary(dark_mode)) - ); + // Show wallet name + ui.heading(RichText::new(format!("Wallet: {}", wallet_name)).color(DashColors::text_secondary(dark_mode))); - ui.add_space(20.0); + // Show success screen + if *self.step.read().unwrap() == WalletFundedScreenStep::Success { + inner_action |= self.show_success(ui); + return; + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); // Wallet unlock section let (needs_unlock, unlocked) = self.render_wallet_unlock_if_needed(ui); @@ -352,35 +391,30 @@ impl ScreenLike for CreateAssetLockScreen { if !needs_unlock || unlocked { // First, select the purpose of the asset lock if self.asset_lock_purpose.is_none() { - ui.heading("Select Asset Lock Purpose"); - ui.add_space(10.0); + ui.heading(RichText::new("Select Asset Lock Purpose").color(DashColors::text_primary(dark_mode))); - ui.label( - RichText::new("What is the purpose of this asset lock?") - .color(DashColors::text_secondary(dark_mode)) - ); - ui.add_space(20.0); + ui.add_space(10.0); ui.horizontal(|ui| { - if ui.button(RichText::new("Registration").size(16.0)).clicked() { + if ui.button("Registration").clicked() { self.asset_lock_purpose = Some(AssetLockPurpose::Registration); } - ui.add_space(20.0); + ui.add_space(5.0); - if ui.button(RichText::new("Top Up").size(16.0)).clicked() { + if ui.button("Top Up").clicked() { self.asset_lock_purpose = Some(AssetLockPurpose::TopUp); } }); - ui.add_space(20.0); + ui.add_space(10.0); // Show explanation ui.group(|ui| { - ui.label(RichText::new("ℹ️ Information").strong()); + ui.label(RichText::new("Information").strong().color(DashColors::text_primary(dark_mode))); ui.add_space(5.0); - ui.label("• Registration: Create an asset lock for a new identity registration"); - ui.label("• Top Up: Add credits to an existing identity"); + ui.label(RichText::new("• Registration: Create an asset lock for a new identity registration").color(DashColors::text_secondary(dark_mode))); + ui.label(RichText::new("• Top Up: Add credits to an existing identity").color(DashColors::text_secondary(dark_mode))); }); return; @@ -388,26 +422,30 @@ impl ScreenLike for CreateAssetLockScreen { // Show selected purpose ui.horizontal(|ui| { - ui.label(RichText::new("Purpose:").strong()); + ui.label(RichText::new("Purpose:").strong().color(DashColors::text_primary(dark_mode))); let purpose_text = match self.asset_lock_purpose { Some(AssetLockPurpose::Registration) => "Registration", Some(AssetLockPurpose::TopUp) => "Top Up", None => "Not selected", }; - ui.label(purpose_text); + ui.label(RichText::new(purpose_text).color(DashColors::text_secondary(dark_mode))); + }); - if ui.button("Change").clicked() { + // Only show Back button if a purpose has been selected + if self.asset_lock_purpose.is_some() { + if ui.button("Back").clicked() { self.asset_lock_purpose = None; self.selected_identity = None; self.selected_identity_string.clear(); } - }); - ui.add_space(20.0); + } - // For top up, select identity and indices + // For top up, select identity if self.asset_lock_purpose == Some(AssetLockPurpose::TopUp) { - ui.heading("1. Select Identity to Top Up"); ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + ui.heading(RichText::new("1. Select Identity to Top Up").color(DashColors::text_primary(dark_mode))); let identities = match self.app_context.load_local_qualified_identities() { Ok(ids) => ids, Err(e) => { @@ -436,12 +474,20 @@ impl ScreenLike for CreateAssetLockScreen { .label("Identity to top up:") .width(300.0)); - // Update identity index when identity selection changes + // Update identity index and top_up_index when identity selection changes if identity_selector_response.changed() { if let Some(selected) = &self.selected_identity { if let Some(wallet_idx) = selected.wallet_index { self.identity_index = wallet_idx; } + // Set top_up_index to next unused value + self.top_up_index = selected + .top_ups + .keys() + .max() + .cloned() + .map(|i| i + 1) + .unwrap_or(0); } } @@ -449,64 +495,166 @@ impl ScreenLike for CreateAssetLockScreen { return; } - ui.add_space(20.0); - - // Identity index input (for wallet key derivation) - ui.horizontal(|ui| { - ui.label(RichText::new("Identity Index:").color(DashColors::text_primary(dark_mode))); - let mut index_str = self.identity_index.to_string(); - if ui.text_edit_singleline(&mut index_str).changed() { - if let Ok(index) = index_str.parse::() { - self.identity_index = index; - } - } - }); - ui.label( - RichText::new("This is the wallet's key derivation index") - .size(12.0) - .color(DashColors::text_secondary(dark_mode)) - ); + if self.show_advanced_options { + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading(RichText::new("2. Index Selection").color(DashColors::text_primary(dark_mode))); + ui.add_space(10.0); + + // Get used identity indices from wallet + let wallet_guard = self.wallet.read().unwrap(); + let used_identity_indices: HashSet = wallet_guard.identities.keys().cloned().collect(); + drop(wallet_guard); + + // Get used top_up indices from selected identity + let used_top_up_indices: HashSet = self.selected_identity + .as_ref() + .map(|id| id.top_ups.keys().cloned().collect()) + .unwrap_or_default(); + + egui::Grid::new("top_up_advanced_options_grid") + .num_columns(2) + .spacing([10.0, 8.0]) + .show(ui, |ui| { + // Row 1: Identity Index + ui.label("Identity Index:"); + let selected_text = if used_identity_indices.contains(&self.identity_index) { + format!("{} (used)", self.identity_index) + } else { + format!("{}", self.identity_index) + }; + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { + egui::ComboBox::from_id_salt("top_up_identity_index") + .selected_text(selected_text) + .show_ui(ui, |ui| { + for i in 0..MAX_IDENTITY_INDEX { + let is_used = used_identity_indices.contains(&i); + let label = if is_used { + format!("{} (used)", i) + } else { + format!("{}", i) + }; + let is_selected = self.identity_index == i; + let enabled = !is_used || is_selected; + let response = ui.add_enabled(enabled, Button::selectable(is_selected, label)); + if response.clicked() && !is_used { + self.identity_index = i; + } + } + }); + }); + ui.end_row(); + + // Row 2: Top Up Index + ui.label("Top Up Index:"); + let selected_text = if used_top_up_indices.contains(&self.top_up_index) { + format!("{} (used)", self.top_up_index) + } else { + format!("{}", self.top_up_index) + }; + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { + egui::ComboBox::from_id_salt("top_up_index") + .selected_text(selected_text) + .show_ui(ui, |ui| { + for i in 0..MAX_IDENTITY_INDEX { + let is_used = used_top_up_indices.contains(&i); + let label = if is_used { + format!("{} (used)", i) + } else { + format!("{}", i) + }; + let is_selected = self.top_up_index == i; + let enabled = !is_used || is_selected; + let response = ui.add_enabled(enabled, Button::selectable(is_selected, label)); + if response.clicked() && !is_used { + self.top_up_index = i; + } + } + });}); + ui.end_row(); + }); + } + } else if self.asset_lock_purpose == Some(AssetLockPurpose::Registration) { - ui.add_space(10.0); + if self.show_advanced_options { + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading(RichText::new("1. Index Selection").color(DashColors::text_primary(dark_mode))); + ui.add_space(10.0); + + // Get used indices from wallet + let wallet_guard = self.wallet.read().unwrap(); + let used_indices: HashSet = wallet_guard.identities.keys().cloned().collect(); + drop(wallet_guard); + + egui::Grid::new("registration_advanced_options_grid") + .num_columns(2) + .spacing([10.0, 8.0]) + .show(ui, |ui| { + // Row 1: Identity Index + ui.label("Identity Index:"); + let selected_text = if used_indices.contains(&self.identity_index) { + format!("{} (used)", self.identity_index) + } else { + format!("{}", self.identity_index) + }; + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { + egui::ComboBox::from_id_salt("registration_identity_index") + .selected_text(selected_text) + .show_ui(ui, |ui| { + for i in 0..MAX_IDENTITY_INDEX { + let is_used = used_indices.contains(&i); + let label = if is_used { + format!("{} (used)", i) + } else { + format!("{}", i) + }; + let is_selected = self.identity_index == i; + let enabled = !is_used || is_selected; + let response = ui.add_enabled(enabled, Button::selectable(is_selected, label)); + if response.clicked() && !is_used { + self.identity_index = i; + } + } + }); + }); + ui.end_row(); + }); + } + } - // Top up index input - ui.horizontal(|ui| { - ui.label(RichText::new("Top Up Index:").color(DashColors::text_primary(dark_mode))); - let mut index_str = self.top_up_index.to_string(); - if ui.text_edit_singleline(&mut index_str).changed() { - if let Ok(index) = index_str.parse::() { - self.top_up_index = index; - } - } - }); - ui.label( - RichText::new("Sequential index for this specific top up") - .size(12.0) - .color(DashColors::text_secondary(dark_mode)) - ); - ui.add_space(20.0); - } else if self.asset_lock_purpose == Some(AssetLockPurpose::Registration) { - // Registration index input - ui.heading("1. Set Registration Index"); - ui.add_space(10.0); + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label(RichText::new("Identity Index:").color(DashColors::text_primary(dark_mode))); - let mut index_str = self.identity_index.to_string(); - if ui.text_edit_singleline(&mut index_str).changed() { - if let Ok(index) = index_str.parse::() { - self.identity_index = index; - } - } - }); - ui.add_space(20.0); + // Check if funds have arrived at the funding address + if let Some(utxo) = funding_common::capture_qr_funding_utxo_if_available( + &self.step, + self.selected_wallet.as_ref(), + self.funding_address.as_ref(), + ) { + self.funding_utxo = Some(utxo); } let step = *self.step.read().unwrap(); - // Step 2: Amount selection - let step_number = if self.asset_lock_purpose == Some(AssetLockPurpose::TopUp) { "2" } else { "2" }; - ui.heading(format!("{}. Select how much you would like to transfer?", step_number)); + // Request periodic repaints while waiting for funds + if step == WalletFundedScreenStep::WaitingOnFunds { + ui.ctx().request_repaint_after(std::time::Duration::from_secs(1)); + } + + // Amount selection step number depends on purpose and advanced options + let step_num = match (self.asset_lock_purpose, self.show_advanced_options) { + (Some(AssetLockPurpose::TopUp), true) => "3", + (Some(AssetLockPurpose::TopUp), false) => "2", + (Some(AssetLockPurpose::Registration), true) => "2", + _ => "1", + }; + ui.heading(RichText::new(format!("{}. Select how much you would like to transfer?", step_num)).color(DashColors::text_primary(dark_mode))); ui.add_space(10.0); self.render_amount_input(ui); @@ -531,43 +679,49 @@ impl ScreenLike for CreateAssetLockScreen { match step { WalletFundedScreenStep::WaitingOnFunds => { - ui.heading("=> Waiting for funds. <="); + ui.heading(RichText::new("Waiting for funds...").color(DashColors::text_primary(dark_mode))); AppAction::None } WalletFundedScreenStep::FundsReceived => { - ui.heading("Funds received! Creating asset lock..."); + ui.heading(RichText::new("Funds received! Creating asset lock...").color(DashColors::text_primary(dark_mode))); // Trigger asset lock creation - if self.is_creating { - self.is_creating = false; - if let Some(credits) = self.amount_credits { - match self.asset_lock_purpose { - Some(AssetLockPurpose::Registration) => { + if let Some(credits) = self.amount_credits { + // Transition to WaitingForAssetLock BEFORE dispatching to prevent duplicate dispatches + { + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForAssetLock; + } + + match self.asset_lock_purpose { + Some(AssetLockPurpose::Registration) => { + AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::CreateRegistrationAssetLock(self.wallet.clone(), credits, self.identity_index) + )) + } + Some(AssetLockPurpose::TopUp) => { + if let Some(identity) = &self.selected_identity { + let identity_index = identity.wallet_index.unwrap_or(self.identity_index); AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::CreateRegistrationAssetLock(self.wallet.clone(), credits, self.identity_index) + CoreTask::CreateTopUpAssetLock(self.wallet.clone(), credits, identity_index, self.top_up_index) )) + } else { + self.error_message = Some("No identity selected for top-up".to_string()); + AppAction::None } - Some(AssetLockPurpose::TopUp) => { - if let Some(identity) = &self.selected_identity { - let identity_index = identity.wallet_index.unwrap_or(self.identity_index); - AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::CreateTopUpAssetLock(self.wallet.clone(), credits, identity_index, self.top_up_index) - )) - } else { - AppAction::None - } - } - None => AppAction::None } - } else { - AppAction::None + None => { + self.error_message = Some("No purpose selected".to_string()); + AppAction::None + } } } else { + self.error_message = Some("No amount specified".to_string()); AppAction::None } } WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of asset lock. <="); + ui.heading(RichText::new("Waiting for Core Chain to produce proof of asset lock...").color(DashColors::text_primary(dark_mode))); AppAction::None } WalletFundedScreenStep::Success => { @@ -581,11 +735,6 @@ impl ScreenLike for CreateAssetLockScreen { inner_action |= layout_action.inner; } - - // Show success screen - if *self.step.read().unwrap() == WalletFundedScreenStep::Success { - inner_action |= self.show_success(ui); - } } else { // Wallet needs to be unlocked } @@ -694,20 +843,40 @@ impl ScreenLike for CreateAssetLockScreen { } } WalletFundedScreenStep::WaitingForAssetLock => { - // Check if we received an asset lock transaction - if let BackendTaskSuccessResult::CoreItem( - CoreItem::ReceivedAvailableUTXOTransaction(tx, _), - ) = result - { - if tx.special_transaction_payload.is_some() { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::Success; - drop(step); - self.display_message( - "Asset lock created successfully!", - MessageType::Success, - ); + match &result { + BackendTaskSuccessResult::Message(msg) => { + if msg.contains("Asset lock transaction broadcast successfully") { + // Extract TX ID from message + if let Some(tx_id_start) = msg.find("TX ID: ") { + let tx_id = msg[tx_id_start + 7..].trim().to_string(); + self.asset_lock_tx_id = Some(tx_id); + } + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + drop(step); + self.display_message( + "Asset lock created successfully!", + MessageType::Success, + ); + } + } + BackendTaskSuccessResult::CoreItem( + CoreItem::ReceivedAvailableUTXOTransaction(tx, _), + ) => { + // This is the asset lock transaction from ZMQ + if tx.special_transaction_payload.is_some() { + self.asset_lock_tx_id = Some(tx.txid().to_string()); + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + drop(step); + self.display_message( + "Asset lock created successfully!", + MessageType::Success, + ); + } } + _ => {} } } _ => {} diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 47918045c..6a958402f 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -1229,7 +1229,7 @@ impl WalletsBalancesScreen { ui.horizontal(|ui| { ui.heading(RichText::new("Asset Locks").color(DashColors::text_primary(dark_mode))); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button(RichText::new("Create Asset Lock").size(14.0)).clicked() { + if ui.button("Create Asset Lock").clicked() { app_action = AppAction::AddScreen( ScreenType::CreateAssetLock(arc_wallet.clone()).create_screen(&self.app_context) ); From e8ff3a620a2299d7d147d7cb9ed7d9ad54f7793b Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 14:03:53 +0700 Subject: [PATCH 05/11] fix: clippy --- src/ui/wallets/asset_lock_detail_screen.rs | 5 ++--- src/ui/wallets/create_asset_lock_screen.rs | 20 ++++++++------------ src/ui/wallets/wallets_screen/mod.rs | 5 ++--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/ui/wallets/asset_lock_detail_screen.rs b/src/ui/wallets/asset_lock_detail_screen.rs index 70cb3b197..b00f9aa3a 100644 --- a/src/ui/wallets/asset_lock_detail_screen.rs +++ b/src/ui/wallets/asset_lock_detail_screen.rs @@ -211,8 +211,8 @@ impl AssetLockDetailScreen { let (needs_unlock, unlocked) = self.render_wallet_unlock_if_needed(ui); - if !needs_unlock || unlocked { - if let Some(wallet_arc) = self.wallet.clone() { + if (!needs_unlock || unlocked) + && let Some(wallet_arc) = self.wallet.clone() { let wallet = wallet_arc.read().unwrap(); // Find the private key for this address @@ -247,7 +247,6 @@ impl AssetLockDetailScreen { .color(DashColors::error_color(dark_mode))); } } - } }); } else { ui.vertical_centered(|ui| { diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index af8b6d039..8eacd9cce 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -432,13 +432,12 @@ impl ScreenLike for CreateAssetLockScreen { }); // Only show Back button if a purpose has been selected - if self.asset_lock_purpose.is_some() { - if ui.button("Back").clicked() { + if self.asset_lock_purpose.is_some() + && ui.button("Back").clicked() { self.asset_lock_purpose = None; self.selected_identity = None; self.selected_identity_string.clear(); } - } // For top up, select identity if self.asset_lock_purpose == Some(AssetLockPurpose::TopUp) { @@ -475,8 +474,8 @@ impl ScreenLike for CreateAssetLockScreen { .width(300.0)); // Update identity index and top_up_index when identity selection changes - if identity_selector_response.changed() { - if let Some(selected) = &self.selected_identity { + if identity_selector_response.changed() + && let Some(selected) = &self.selected_identity { if let Some(wallet_idx) = selected.wallet_index { self.identity_index = wallet_idx; } @@ -489,7 +488,6 @@ impl ScreenLike for CreateAssetLockScreen { .map(|i| i + 1) .unwrap_or(0); } - } if self.selected_identity.is_none() { return; @@ -576,9 +574,9 @@ impl ScreenLike for CreateAssetLockScreen { ui.end_row(); }); } - } else if self.asset_lock_purpose == Some(AssetLockPurpose::Registration) { + } else if self.asset_lock_purpose == Some(AssetLockPurpose::Registration) - if self.show_advanced_options { + && self.show_advanced_options { ui.add_space(10.0); ui.separator(); ui.add_space(10.0); @@ -625,7 +623,6 @@ impl ScreenLike for CreateAssetLockScreen { ui.end_row(); }); } - } ui.add_space(10.0); ui.separator(); @@ -789,8 +786,8 @@ impl ScreenLike for CreateAssetLockScreen { { for utxo in outpoints_with_addresses { let (_, _, address) = &utxo; - if let Some(funding_address) = &self.funding_address { - if funding_address == address { + if let Some(funding_address) = &self.funding_address + && funding_address == address { let mut step = self.step.write().unwrap(); *step = WalletFundedScreenStep::FundsReceived; self.funding_utxo = Some(utxo); @@ -800,7 +797,6 @@ impl ScreenLike for CreateAssetLockScreen { self.is_creating = true; return; } - } } } } diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 6a958402f..b1084a90a 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -1332,11 +1332,10 @@ impl WalletsBalancesScreen { ).create_screen(&self.app_context) ); } - if proof.is_some() { - if ui.small_button("Fund").on_hover_text("Fund a Platform address with this asset lock").clicked() { + if proof.is_some() + && ui.small_button("Fund").on_hover_text("Fund a Platform address with this asset lock").clicked() { open_fund_dialog_for_idx = Some((index, platform_addresses.clone())); } - } }); }); } From b8fe461cf778cd3a58c75655ffa72dae2b6f2bbc Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 14:12:58 +0700 Subject: [PATCH 06/11] feat: tests --- src/ui/wallets/create_asset_lock_screen.rs | 229 +++++++++++++++++++++ tests/kittest/create_asset_lock_screen.rs | 57 +++++ tests/kittest/main.rs | 1 + 3 files changed, 287 insertions(+) create mode 100644 tests/kittest/create_asset_lock_screen.rs diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index 8eacd9cce..a56bcb22c 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -881,3 +881,232 @@ impl ScreenLike for CreateAssetLockScreen { self.is_creating = false; } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that DASH amount parsing correctly converts to credits + #[test] + fn test_dash_to_credits_conversion() { + // 1 DASH = 100_000_000_000 credits (10^11) + // Test various DASH amounts + + // 0.5 DASH = 50_000_000_000 credits + let dash_amount = 0.5f64; + let credits = (dash_amount * 100_000_000_000.0) as u64; + assert_eq!(credits, 50_000_000_000); + + // 1 DASH = 100_000_000_000 credits + let dash_amount = 1.0f64; + let credits = (dash_amount * 100_000_000_000.0) as u64; + assert_eq!(credits, 100_000_000_000); + + // 0.1 DASH = 10_000_000_000 credits + let dash_amount = 0.1f64; + let credits = (dash_amount * 100_000_000_000.0) as u64; + assert_eq!(credits, 10_000_000_000); + + // 10 DASH = 1_000_000_000_000 credits + let dash_amount = 10.0f64; + let credits = (dash_amount * 100_000_000_000.0) as u64; + assert_eq!(credits, 1_000_000_000_000); + } + + /// Test that invalid amounts are handled correctly + #[test] + fn test_invalid_amount_parsing() { + // Test that negative amounts don't produce valid credits + let dash_amount = -1.0f64; + let is_valid = dash_amount >= 0.0; + assert!(!is_valid); + + // Test that parsing invalid strings returns None + let invalid_input = "not_a_number"; + let parsed: Result = invalid_input.parse(); + assert!(parsed.is_err()); + + // Test empty string + let empty_input = ""; + let parsed: Result = empty_input.parse(); + assert!(parsed.is_err()); + } + + /// Test step number calculation based on purpose and advanced options + #[test] + fn test_step_number_calculation() { + // Helper function that mimics the step number calculation in the UI + fn calculate_step_num( + purpose: Option, + show_advanced_options: bool, + ) -> &'static str { + match (purpose, show_advanced_options) { + (Some(AssetLockPurpose::TopUp), true) => "3", + (Some(AssetLockPurpose::TopUp), false) => "2", + (Some(AssetLockPurpose::Registration), true) => "2", + _ => "1", + } + } + + // Top Up with advanced options: step 3 (1: identity selection, 2: index selection, 3: amount) + assert_eq!( + calculate_step_num(Some(AssetLockPurpose::TopUp), true), + "3" + ); + + // Top Up without advanced options: step 2 (1: identity selection, 2: amount) + assert_eq!( + calculate_step_num(Some(AssetLockPurpose::TopUp), false), + "2" + ); + + // Registration with advanced options: step 2 (1: index selection, 2: amount) + assert_eq!( + calculate_step_num(Some(AssetLockPurpose::Registration), true), + "2" + ); + + // Registration without advanced options: step 1 (1: amount) + assert_eq!( + calculate_step_num(Some(AssetLockPurpose::Registration), false), + "1" + ); + + // No purpose selected: step 1 + assert_eq!(calculate_step_num(None, false), "1"); + assert_eq!(calculate_step_num(None, true), "1"); + } + + /// Test next unused identity index calculation + #[test] + fn test_next_unused_identity_index() { + use std::collections::BTreeMap; + + // Helper function that mimics the next index calculation + fn calculate_next_identity_index(used_indices: &BTreeMap) -> u32 { + used_indices + .keys() + .copied() + .max() + .map(|max| max + 1) + .unwrap_or(0) + } + + // No used indices -> next is 0 + let empty: BTreeMap = BTreeMap::new(); + assert_eq!(calculate_next_identity_index(&empty), 0); + + // Used indices: 0 -> next is 1 + let mut used = BTreeMap::new(); + used.insert(0, ()); + assert_eq!(calculate_next_identity_index(&used), 1); + + // Used indices: 0, 1, 2 -> next is 3 + used.insert(1, ()); + used.insert(2, ()); + assert_eq!(calculate_next_identity_index(&used), 3); + + // Non-contiguous indices: 0, 5 -> next is 6 (not 1) + let mut non_contiguous = BTreeMap::new(); + non_contiguous.insert(0, ()); + non_contiguous.insert(5, ()); + assert_eq!(calculate_next_identity_index(&non_contiguous), 6); + } + + /// Test next unused top_up index calculation + #[test] + fn test_next_unused_top_up_index() { + use std::collections::BTreeMap; + + // Helper function that mimics the next top_up index calculation + fn calculate_next_top_up_index(used_indices: &BTreeMap) -> u32 { + used_indices + .keys() + .max() + .cloned() + .map(|i| i + 1) + .unwrap_or(0) + } + + // No used indices -> next is 0 + let empty: BTreeMap = BTreeMap::new(); + assert_eq!(calculate_next_top_up_index(&empty), 0); + + // Used indices: 0 -> next is 1 + let mut used = BTreeMap::new(); + used.insert(0, ()); + assert_eq!(calculate_next_top_up_index(&used), 1); + + // Used indices: 0, 1, 2 -> next is 3 + used.insert(1, ()); + used.insert(2, ()); + assert_eq!(calculate_next_top_up_index(&used), 3); + } + + /// Test AssetLockPurpose enum values + #[test] + fn test_asset_lock_purpose_values() { + let registration = AssetLockPurpose::Registration; + let top_up = AssetLockPurpose::TopUp; + + // Test equality + assert_eq!(registration, AssetLockPurpose::Registration); + assert_eq!(top_up, AssetLockPurpose::TopUp); + assert_ne!(registration, top_up); + + // Test copy semantics + let registration_copy = registration; + assert_eq!(registration, registration_copy); + } + + /// Test TX ID extraction from success message + #[test] + fn test_tx_id_extraction() { + let msg = "Asset lock transaction broadcast successfully. TX ID: abc123def456"; + + // Extract TX ID from message + let tx_id = if let Some(tx_id_start) = msg.find("TX ID: ") { + Some(msg[tx_id_start + 7..].trim().to_string()) + } else { + None + }; + + assert_eq!(tx_id, Some("abc123def456".to_string())); + + // Test message without TX ID + let msg_without_id = "Some other message"; + let no_tx_id = if let Some(tx_id_start) = msg_without_id.find("TX ID: ") { + Some(msg_without_id[tx_id_start + 7..].trim().to_string()) + } else { + None + }; + + assert_eq!(no_tx_id, None); + } + + /// Test MAX_IDENTITY_INDEX constant + #[test] + fn test_max_identity_index_constant() { + assert_eq!(MAX_IDENTITY_INDEX, 30); + + // Verify reasonable range for iteration + let indices: Vec = (0..MAX_IDENTITY_INDEX).collect(); + assert_eq!(indices.len(), 30); + assert_eq!(indices[0], 0); + assert_eq!(indices[29], 29); + } + + /// Test default amount values + #[test] + fn test_default_amount_values() { + // Default amount is 0.5 DASH + let default_amount_input = "0.5"; + let parsed_amount: f64 = default_amount_input.parse().unwrap(); + assert_eq!(parsed_amount, 0.5); + + // Default credits for 0.5 DASH + let default_credits = 50_000_000_000u64; + let calculated_credits = (parsed_amount * 100_000_000_000.0) as u64; + assert_eq!(calculated_credits, default_credits); + } +} diff --git a/tests/kittest/create_asset_lock_screen.rs b/tests/kittest/create_asset_lock_screen.rs new file mode 100644 index 000000000..be1d984f2 --- /dev/null +++ b/tests/kittest/create_asset_lock_screen.rs @@ -0,0 +1,57 @@ +use egui_kittest::Harness; + +/// Test that the create asset lock screen can be rendered +#[test] +fn test_create_asset_lock_screen_renders() { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + let _guard = rt.enter(); + + let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { + dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()).with_animations(false) + }); + + harness.set_size(egui::vec2(1024.0, 768.0)); + harness.run_steps(10); +} + +/// Test that the create asset lock screen handles window resize gracefully +#[test] +fn test_create_asset_lock_screen_resize() { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + let _guard = rt.enter(); + + let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { + dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()).with_animations(false) + }); + + // Test various window sizes + let sizes = [ + egui::vec2(800.0, 600.0), + egui::vec2(1200.0, 900.0), + egui::vec2(640.0, 480.0), + egui::vec2(1920.0, 1080.0), + ]; + + for size in sizes { + harness.set_size(size); + harness.run_steps(5); + } +} + +/// Test that the app remains responsive with multiple frame batches +#[test] +fn test_create_asset_lock_screen_frame_stability() { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + let _guard = rt.enter(); + + let mut harness = Harness::builder().with_max_steps(200).build_eframe(|ctx| { + dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()).with_animations(false) + }); + + harness.set_size(egui::vec2(1024.0, 768.0)); + + // Run multiple batches to test stability + for _ in 0..10 { + harness.run_steps(10); + } +} diff --git a/tests/kittest/main.rs b/tests/kittest/main.rs index e5cc0a94f..1562e7344 100644 --- a/tests/kittest/main.rs +++ b/tests/kittest/main.rs @@ -1,3 +1,4 @@ +mod create_asset_lock_screen; mod identities_screen; mod network_chooser; mod startup; From c43f38ad1414a25b6ccc82834b4d2e1aadb47d20 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 15:40:41 +0700 Subject: [PATCH 07/11] fix: remove identity index selector for top ups --- src/ui/wallets/create_asset_lock_screen.rs | 114 +++++++-------------- 1 file changed, 36 insertions(+), 78 deletions(-) diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index a56bcb22c..be1c4868f 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -473,13 +473,9 @@ impl ScreenLike for CreateAssetLockScreen { .label("Identity to top up:") .width(300.0)); - // Update identity index and top_up_index when identity selection changes + // Update top_up_index to next unused value when identity selection changes if identity_selector_response.changed() && let Some(selected) = &self.selected_identity { - if let Some(wallet_idx) = selected.wallet_index { - self.identity_index = wallet_idx; - } - // Set top_up_index to next unused value self.top_up_index = selected .top_ups .keys() @@ -498,81 +494,40 @@ impl ScreenLike for CreateAssetLockScreen { ui.separator(); ui.add_space(10.0); - ui.heading(RichText::new("2. Index Selection").color(DashColors::text_primary(dark_mode))); + ui.heading(RichText::new("2. Top Up Index Selection").color(DashColors::text_primary(dark_mode))); ui.add_space(10.0); - // Get used identity indices from wallet - let wallet_guard = self.wallet.read().unwrap(); - let used_identity_indices: HashSet = wallet_guard.identities.keys().cloned().collect(); - drop(wallet_guard); - // Get used top_up indices from selected identity let used_top_up_indices: HashSet = self.selected_identity .as_ref() .map(|id| id.top_ups.keys().cloned().collect()) .unwrap_or_default(); - egui::Grid::new("top_up_advanced_options_grid") - .num_columns(2) - .spacing([10.0, 8.0]) - .show(ui, |ui| { - // Row 1: Identity Index - ui.label("Identity Index:"); - let selected_text = if used_identity_indices.contains(&self.identity_index) { - format!("{} (used)", self.identity_index) - } else { - format!("{}", self.identity_index) - }; - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - egui::ComboBox::from_id_salt("top_up_identity_index") - .selected_text(selected_text) - .show_ui(ui, |ui| { - for i in 0..MAX_IDENTITY_INDEX { - let is_used = used_identity_indices.contains(&i); - let label = if is_used { - format!("{} (used)", i) - } else { - format!("{}", i) - }; - let is_selected = self.identity_index == i; - let enabled = !is_used || is_selected; - let response = ui.add_enabled(enabled, Button::selectable(is_selected, label)); - if response.clicked() && !is_used { - self.identity_index = i; - } - } - }); - }); - ui.end_row(); - - // Row 2: Top Up Index - ui.label("Top Up Index:"); - let selected_text = if used_top_up_indices.contains(&self.top_up_index) { - format!("{} (used)", self.top_up_index) - } else { - format!("{}", self.top_up_index) - }; - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - egui::ComboBox::from_id_salt("top_up_index") - .selected_text(selected_text) - .show_ui(ui, |ui| { - for i in 0..MAX_IDENTITY_INDEX { - let is_used = used_top_up_indices.contains(&i); - let label = if is_used { - format!("{} (used)", i) - } else { - format!("{}", i) - }; - let is_selected = self.top_up_index == i; - let enabled = !is_used || is_selected; - let response = ui.add_enabled(enabled, Button::selectable(is_selected, label)); - if response.clicked() && !is_used { - self.top_up_index = i; - } + ui.horizontal(|ui| { + ui.label("Top Up Index:"); + let selected_text = if used_top_up_indices.contains(&self.top_up_index) { + format!("{} (used)", self.top_up_index) + } else { + format!("{}", self.top_up_index) + }; + egui::ComboBox::from_id_salt("top_up_index") + .selected_text(selected_text) + .show_ui(ui, |ui| { + for i in 0..MAX_IDENTITY_INDEX { + let is_used = used_top_up_indices.contains(&i); + let label = if is_used { + format!("{} (used)", i) + } else { + format!("{}", i) + }; + let is_selected = self.top_up_index == i; + let response = ui.add_enabled(!is_used, Button::new(label).selected(is_selected)); + if response.clicked() { + self.top_up_index = i; } - });}); - ui.end_row(); - }); + } + }); + }); } } else if self.asset_lock_purpose == Some(AssetLockPurpose::Registration) @@ -612,9 +567,8 @@ impl ScreenLike for CreateAssetLockScreen { format!("{}", i) }; let is_selected = self.identity_index == i; - let enabled = !is_used || is_selected; - let response = ui.add_enabled(enabled, Button::selectable(is_selected, label)); - if response.clicked() && !is_used { + let response = ui.add_enabled(!is_used, Button::new(label).selected(is_selected)); + if response.clicked() { self.identity_index = i; } } @@ -698,10 +652,14 @@ impl ScreenLike for CreateAssetLockScreen { } Some(AssetLockPurpose::TopUp) => { if let Some(identity) = &self.selected_identity { - let identity_index = identity.wallet_index.unwrap_or(self.identity_index); - AppAction::BackendTask(BackendTask::CoreTask( - CoreTask::CreateTopUpAssetLock(self.wallet.clone(), credits, identity_index, self.top_up_index) - )) + if let Some(identity_index) = identity.wallet_index { + AppAction::BackendTask(BackendTask::CoreTask( + CoreTask::CreateTopUpAssetLock(self.wallet.clone(), credits, identity_index, self.top_up_index) + )) + } else { + self.error_message = Some("Selected identity has no wallet index".to_string()); + AppAction::None + } } else { self.error_message = Some("No identity selected for top-up".to_string()); AppAction::None From cdc1587f60578d8a3d31f5979116fa665e6a6e50 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 15:41:03 +0700 Subject: [PATCH 08/11] fmt --- src/ui/wallets/create_asset_lock_screen.rs | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index be1c4868f..cac31447c 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -745,16 +745,17 @@ impl ScreenLike for CreateAssetLockScreen { for utxo in outpoints_with_addresses { let (_, _, address) = &utxo; if let Some(funding_address) = &self.funding_address - && funding_address == address { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::FundsReceived; - self.funding_utxo = Some(utxo); - drop(step); // Release the lock before creating new action - - // Refresh wallet to create the asset lock - self.is_creating = true; - return; - } + && funding_address == address + { + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some(utxo); + drop(step); // Release the lock before creating new action + + // Refresh wallet to create the asset lock + self.is_creating = true; + return; + } } } } @@ -907,10 +908,7 @@ mod tests { } // Top Up with advanced options: step 3 (1: identity selection, 2: index selection, 3: amount) - assert_eq!( - calculate_step_num(Some(AssetLockPurpose::TopUp), true), - "3" - ); + assert_eq!(calculate_step_num(Some(AssetLockPurpose::TopUp), true), "3"); // Top Up without advanced options: step 2 (1: identity selection, 2: amount) assert_eq!( From 7305664d653b354d910c37f9dee8f6182f0aeced Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 15:53:36 +0700 Subject: [PATCH 09/11] refactor: use `AmountInput` component --- src/ui/wallets/create_asset_lock_screen.rs | 82 +++++++++------------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index cac31447c..80a3a2f52 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -2,8 +2,11 @@ use crate::app::AppAction; use crate::backend_task::core::{CoreItem, CoreTask}; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::amount_input::AmountInput; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; @@ -15,7 +18,6 @@ use crate::ui::{MessageType, RootScreenType, ScreenLike}; use chrono::{DateTime, Utc}; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dashcore_rpc::dashcore::{Address, OutPoint, TxOut}; -use dash_sdk::dpp::balances::credits::Credits; use eframe::egui::{self, Context, Ui}; use egui::{Button, RichText, Vec2}; use std::collections::HashSet; @@ -40,9 +42,8 @@ pub struct CreateAssetLockScreen { // Asset lock creation fields step: Arc>, - amount_input: String, + amount_input: Option, identity_index: u32, - amount_credits: Option, funding_address: Option
, funding_utxo: Option<(OutPoint, TxOut, Address)>, core_has_funding_address: Option, @@ -82,9 +83,12 @@ impl CreateAssetLockScreen { show_password: false, error_message: None, step: Arc::new(RwLock::new(WalletFundedScreenStep::WaitingOnFunds)), - amount_input: "0.5".to_string(), // Default to 0.5 DASH + amount_input: Some( + AmountInput::new(Amount::new_dash(0.5)) + .with_label("Amount (DASH):") + .with_min_amount(Some(1000)), // Minimum 0.00000001 DASH (1000 credits) + ), identity_index, - amount_credits: Some(50_000_000_000), // 0.5 DASH in credits funding_address: None, funding_utxo: None, core_has_funding_address: None, @@ -98,42 +102,6 @@ impl CreateAssetLockScreen { } } - fn render_amount_input(&mut self, ui: &mut Ui) { - let dark_mode = ui.ctx().style().visuals.dark_mode; - - ui.horizontal(|ui| { - ui.label(RichText::new("Amount (DASH):").color(DashColors::text_primary(dark_mode))); - - let response = ui.text_edit_singleline(&mut self.amount_input); - - if response.changed() { - // Parse the input as DASH and convert to credits - if let Ok(dash_amount) = self.amount_input.parse::() { - if dash_amount >= 0.0 { - let credits = (dash_amount * 100_000_000_000.0) as u64; - self.amount_credits = Some(credits); - } else { - self.amount_credits = None; - } - } else { - self.amount_credits = None; - } - } - }); - - // Show amount in credits if valid - if let Some(credits) = self.amount_credits { - ui.label( - RichText::new(format!( - " = {} credits", - credits - )) - .size(12.0) - .color(DashColors::text_secondary(dark_mode)), - ); - } - } - fn generate_funding_address(&mut self) -> Result<(), String> { let mut wallet = self.wallet.write().unwrap(); @@ -190,7 +158,12 @@ impl CreateAssetLockScreen { } let address = self.funding_address.as_ref().unwrap(); - let amount = self.amount_input.parse::().unwrap_or(0.5); + let amount = self + .amount_input + .as_ref() + .and_then(|ai| ai.current_value()) + .map(|a| a.to_f64()) + .unwrap_or(0.5); let dash_uri = format!("dash:{}?amount={:.4}", address, amount); // Generate the QR code image @@ -268,8 +241,12 @@ impl CreateAssetLockScreen { .unwrap_or(0) }; self.top_up_index = 0; - self.amount_input = "0.5".to_string(); - self.amount_credits = Some(50_000_000_000); + // Reset amount input to default 0.5 DASH + self.amount_input = Some( + AmountInput::new(Amount::new_dash(0.5)) + .with_label("Amount (DASH):") + .with_min_amount(Some(1000)), + ); self.funding_address = None; self.funding_utxo = None; self.core_has_funding_address = None; @@ -608,11 +585,16 @@ impl ScreenLike for CreateAssetLockScreen { ui.heading(RichText::new(format!("{}. Select how much you would like to transfer?", step_num)).color(DashColors::text_primary(dark_mode))); ui.add_space(10.0); - self.render_amount_input(ui); + // Show amount input using the component + let amount_response = self.amount_input.as_mut().map(|ai| ai.show(ui)); ui.add_space(20.0); // Step 3: QR Code and address - let amount_valid = self.amount_input.parse::().map(|a| a > 0.0).unwrap_or(false); + let amount_valid = amount_response + .as_ref() + .and_then(|r| r.inner.parsed_amount.as_ref()) + .map(|a| a.value() > 0) + .unwrap_or(false); if amount_valid { let layout_action = ui.with_layout( egui::Layout::top_down(egui::Align::Min).with_cross_align(egui::Align::Center), @@ -636,8 +618,12 @@ impl ScreenLike for CreateAssetLockScreen { WalletFundedScreenStep::FundsReceived => { ui.heading(RichText::new("Funds received! Creating asset lock...").color(DashColors::text_primary(dark_mode))); - // Trigger asset lock creation - if let Some(credits) = self.amount_credits { + // Trigger asset lock creation - get credits from the amount input + let credits = self.amount_input + .as_ref() + .and_then(|ai| ai.current_value()) + .map(|a| a.value()); + if let Some(credits) = credits { // Transition to WaitingForAssetLock BEFORE dispatching to prevent duplicate dispatches { let mut step = self.step.write().unwrap(); From bbf2e322426738772b4040d79ed72194ab730f4c Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 16:23:59 +0700 Subject: [PATCH 10/11] fixes --- src/ui/wallets/asset_lock_detail_screen.rs | 133 ++++++++++++++------- src/ui/wallets/create_asset_lock_screen.rs | 41 ++++--- 2 files changed, 118 insertions(+), 56 deletions(-) diff --git a/src/ui/wallets/asset_lock_detail_screen.rs b/src/ui/wallets/asset_lock_detail_screen.rs index b00f9aa3a..7e65e4395 100644 --- a/src/ui/wallets/asset_lock_detail_screen.rs +++ b/src/ui/wallets/asset_lock_detail_screen.rs @@ -24,6 +24,8 @@ pub struct AssetLockDetailScreen { wallet_password: String, show_password: bool, error_message: Option, + show_private_key_popup: bool, + private_key_wif: Option, } impl AssetLockDetailScreen { @@ -50,6 +52,8 @@ impl AssetLockDetailScreen { wallet_password: String::new(), show_password: false, error_message: None, + show_private_key_popup: false, + private_key_wif: None, } } @@ -218,30 +222,29 @@ impl AssetLockDetailScreen { // Find the private key for this address if let Some(derivation_path) = wallet.known_addresses.get(&address).cloned() { drop(wallet); // Release the read lock before getting write lock - let wallet = wallet_arc.write().unwrap(); - match wallet.private_key_at_derivation_path(&derivation_path, self.app_context.network) { - Ok(private_key) => { - let wif = private_key.to_wif(); - drop(wallet); // Release lock before UI operations - ui.horizontal(|ui| { - ui.label("Private Key (WIF):"); - ui.label(RichText::new(&wif).font(egui::FontId::monospace(12.0)).color(DashColors::warning_color(dark_mode))); - if ui.small_button("Copy").clicked() { - ui.ctx().copy_text(wif); - self.display_message("Private key copied to clipboard", MessageType::Success); - } - }); - ui.add_space(5.0); - ui.label(RichText::new("Warning: Keep this private key secure! Anyone with access to it can spend these funds.") - .color(DashColors::warning_color(dark_mode)) - .italics()); - } - Err(e) => { - ui.label(RichText::new(format!("Error retrieving private key: {}", e)) - .color(DashColors::error_color(dark_mode))); + ui.horizontal(|ui| { + ui.label("Private Key (WIF):"); + ui.label(RichText::new("••••••••••••••••••••").font(egui::FontId::monospace(12.0)).color(DashColors::text_secondary(dark_mode))); + if ui.small_button("View").clicked() { + // Retrieve the private key when View is clicked + let wallet = wallet_arc.write().unwrap(); + match wallet.private_key_at_derivation_path(&derivation_path, self.app_context.network) { + Ok(private_key) => { + self.private_key_wif = Some(private_key.to_wif()); + self.show_private_key_popup = true; + } + Err(e) => { + self.display_message(&format!("Error retrieving private key: {}", e), MessageType::Error); + } + } } - } + }); + + ui.add_space(5.0); + ui.label(RichText::new("Warning: Keep this private key secure! Anyone with access to it can spend these funds.") + .color(DashColors::warning_color(dark_mode)) + .italics()); } else { ui.label(RichText::new("Private key not found for this address") .color(DashColors::error_color(dark_mode))); @@ -310,12 +313,6 @@ impl ScreenLike for AssetLockDetailScreen { fn ui(&mut self, ctx: &Context) -> AppAction { self.check_message_expiration(); - let wallet_name = self - .wallet - .as_ref() - .and_then(|w| w.read().ok()?.alias.clone()) - .unwrap_or_else(|| "Unknown Wallet".to_string()); - let mut action = add_top_panel( ctx, &self.app_context, @@ -326,10 +323,7 @@ impl ScreenLike for AssetLockDetailScreen { RootScreenType::RootScreenWalletsBalances, ), ), - ( - &format!("{} / Asset Lock Details", wallet_name), - AppAction::None, - ), + ("Asset Lock Details", AppAction::None), ], vec![], ); @@ -341,19 +335,28 @@ impl ScreenLike for AssetLockDetailScreen { ); action |= island_central_panel(ctx, |ui| { - let inner_action = AppAction::None; + let mut inner_action = AppAction::None; let dark_mode = ui.ctx().style().visuals.dark_mode; + // Header with Back button (outside ScrollArea to avoid scrollbar overlap) + ui.horizontal(|ui| { + ui.heading( + RichText::new("Asset Lock Information") + .color(DashColors::text_primary(dark_mode)) + .size(24.0), + ); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Back").clicked() { + inner_action = AppAction::PopScreenAndRefresh; + } + }); + }); + ui.add_space(10.0); + egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { - ui.heading( - RichText::new("Asset Lock Information") - .color(DashColors::text_primary(dark_mode)) - .size(24.0), - ); - ui.add_space(10.0); - self.render_asset_lock_info(ui); }); @@ -382,6 +385,56 @@ impl ScreenLike for AssetLockDetailScreen { inner_action }); + // Private key popup + if self.show_private_key_popup { + // Draw dark overlay behind the popup + let screen_rect = ctx.screen_rect(); + let painter = ctx.layer_painter(egui::LayerId::new( + egui::Order::Background, + egui::Id::new("private_key_popup_overlay"), + )); + painter.rect_filled( + screen_rect, + 0.0, + egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), + ); + + egui::Window::new("Private Key") + .collapsible(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) + .show(ctx, |ui| { + ui.set_min_width(400.0); + + ui.add_space(10.0); + ui.label(RichText::new("⚠ Warning").color(Color32::from_rgb(255, 152, 0)).strong()); + ui.label("Keep this private key secure! Anyone with access to it can spend these funds."); + ui.add_space(15.0); + + ui.label("Private Key (WIF):"); + if let Some(wif) = self.private_key_wif.clone() { + ui.add(egui::TextEdit::multiline(&mut wif.as_str()) + .font(egui::FontId::monospace(12.0)) + .desired_width(f32::INFINITY) + .desired_rows(1)); + + ui.add_space(10.0); + + ui.horizontal(|ui| { + if ui.button("Copy").clicked() { + ui.ctx().copy_text(wif.clone()); + self.display_message("Private key copied to clipboard", MessageType::Success); + } + if ui.button("Close").clicked() { + self.show_private_key_popup = false; + self.private_key_wif = None; + } + }); + } + ui.add_space(10.0); + }); + } + action } diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index 80a3a2f52..ab3332882 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -333,24 +333,33 @@ impl ScreenLike for CreateAssetLockScreen { let mut inner_action = AppAction::None; let dark_mode = ui.ctx().style().visuals.dark_mode; + // Header with Back button and Advanced Options checkbox (outside ScrollArea) + ui.horizontal(|ui| { + ui.heading( + RichText::new("Create Asset Lock") + .color(DashColors::text_primary(dark_mode)) + .size(24.0), + ); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Back").clicked() { + inner_action = AppAction::PopScreenAndRefresh; + } + ui.add_space(10.0); + ui.checkbox(&mut self.show_advanced_options, "Advanced Options"); + }); + }); + + // Show wallet name + ui.label( + RichText::new(format!("Wallet: {}", wallet_name)) + .color(DashColors::text_secondary(dark_mode)), + ); + ui.add_space(10.0); + egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { - // Header with Back button and Advanced Options checkbox - ui.horizontal(|ui| { - ui.heading( - RichText::new("Create Asset Lock") - .color(DashColors::text_primary(dark_mode)) - .size(24.0) - ); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.checkbox(&mut self.show_advanced_options, "Advanced Options"); - }); - }); - - // Show wallet name - ui.heading(RichText::new(format!("Wallet: {}", wallet_name)).color(DashColors::text_secondary(dark_mode))); // Show success screen if *self.step.read().unwrap() == WalletFundedScreenStep::Success { @@ -410,7 +419,7 @@ impl ScreenLike for CreateAssetLockScreen { // Only show Back button if a purpose has been selected if self.asset_lock_purpose.is_some() - && ui.button("Back").clicked() { + && ui.button("Change Purpose").clicked() { self.asset_lock_purpose = None; self.selected_identity = None; self.selected_identity_string.clear(); From d0d36eabb600132bd996f6970908cbad8c15c0f1 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 22 Jan 2026 16:57:48 +0700 Subject: [PATCH 11/11] fix: clippy --- src/ui/wallets/create_asset_lock_screen.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/ui/wallets/create_asset_lock_screen.rs b/src/ui/wallets/create_asset_lock_screen.rs index ab3332882..c581f7e84 100644 --- a/src/ui/wallets/create_asset_lock_screen.rs +++ b/src/ui/wallets/create_asset_lock_screen.rs @@ -1016,21 +1016,17 @@ mod tests { let msg = "Asset lock transaction broadcast successfully. TX ID: abc123def456"; // Extract TX ID from message - let tx_id = if let Some(tx_id_start) = msg.find("TX ID: ") { - Some(msg[tx_id_start + 7..].trim().to_string()) - } else { - None - }; + let tx_id = msg + .find("TX ID: ") + .map(|tx_id_start| msg[tx_id_start + 7..].trim().to_string()); assert_eq!(tx_id, Some("abc123def456".to_string())); // Test message without TX ID let msg_without_id = "Some other message"; - let no_tx_id = if let Some(tx_id_start) = msg_without_id.find("TX ID: ") { - Some(msg_without_id[tx_id_start + 7..].trim().to_string()) - } else { - None - }; + let no_tx_id = msg_without_id + .find("TX ID: ") + .map(|tx_id_start| msg_without_id[tx_id_start + 7..].trim().to_string()); assert_eq!(no_tx_id, None); }