diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index aee36f4b7..789cffe04 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -12,6 +12,7 @@ mod withdraw_from_identity; use super::BackendTaskSuccessResult; use crate::app::TaskResult; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_identity::encrypted_key_storage::{KeyStorage, WalletDerivationPath}; use crate::model::qualified_identity::qualified_identity_public_key::QualifiedIdentityPublicKey; use crate::model::qualified_identity::{IdentityType, PrivateKeyTarget, QualifiedIdentity}; @@ -21,7 +22,6 @@ use dash_sdk::dashcore_rpc::dashcore::bip32::DerivationPath; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey, TxOut}; use dash_sdk::dpp::ProtocolError; -use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{OutPoint, Transaction}; use dash_sdk::dpp::fee::Credits; @@ -197,14 +197,15 @@ pub type TopUpIndex = u32; pub enum RegisterIdentityFundingMethod { UseAssetLock(Address, Box, Box), FundWithUtxo(OutPoint, TxOut, Address, IdentityIndex), - FundWithWallet(Duffs, IdentityIndex), + FundWithWallet(Amount, IdentityIndex), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum TopUpIdentityFundingMethod { UseAssetLock(Address, Box, Box), + // QR code mathod FundWithUtxo(OutPoint, TxOut, Address, IdentityIndex, TopUpIndex), - FundWithWallet(Duffs, IdentityIndex, TopUpIndex), + FundWithWallet(Amount, IdentityIndex, TopUpIndex), } #[derive(Debug, Clone)] diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index 57dc94c04..e5665d75f 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -174,7 +174,7 @@ impl AppContext { wallet_id = wallet.seed_hash(); match wallet.registration_asset_lock_transaction( sdk.network, - amount, + amount.dash_to_duffs().expect("amount should be in DASH"), true, identity_index, Some(self), @@ -193,7 +193,7 @@ impl AppContext { .map_err(|e| e.to_string())?; wallet.registration_asset_lock_transaction( sdk.network, - amount, + amount.dash_to_duffs().expect("amount should be in DASH"), true, identity_index, Some(self), diff --git a/src/backend_task/identity/top_up_identity.rs b/src/backend_task/identity/top_up_identity.rs index b2bb94808..124cb45ed 100644 --- a/src/backend_task/identity/top_up_identity.rs +++ b/src/backend_task/identity/top_up_identity.rs @@ -2,6 +2,7 @@ use crate::app::TaskResult; use crate::backend_task::BackendTaskSuccessResult; use crate::backend_task::identity::{IdentityTopUpInfo, TopUpIdentityFundingMethod}; use crate::context::AppContext; +use crate::model::amount::Amount; use dash_sdk::Error; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dpp::ProtocolError; @@ -92,7 +93,7 @@ impl AppContext { let mut wallet = wallet.write().unwrap(); match wallet.top_up_asset_lock_transaction( sdk.network, - amount, + amount.dash_to_duffs().expect("amount should be in DASH"), true, identity_index, top_up_index, @@ -112,7 +113,7 @@ impl AppContext { .map_err(|e| e.to_string())?; wallet.top_up_asset_lock_transaction( sdk.network, - amount, + amount.dash_to_duffs().expect("amount should be in DASH"), true, identity_index, top_up_index, @@ -242,7 +243,7 @@ impl AppContext { asset_lock_proof, asset_lock_proof_private_key, tx_id, - Some((tx_out.value, top_up_index)), + Some((Amount::dash_from_duffs(tx_out.value), top_up_index)), ) } }; @@ -326,7 +327,7 @@ impl AppContext { .insert_top_up( qualified_identity.identity.id().as_bytes(), top_up_index, - amount, + amount.dash_to_duffs().expect("amount should be in DASH"), ) .map_err(|e| e.to_string())?; } diff --git a/src/backend_task/system_task/mod.rs b/src/backend_task/system_task/mod.rs index d7a6383d2..2999b43fb 100644 --- a/src/backend_task/system_task/mod.rs +++ b/src/backend_task/system_task/mod.rs @@ -49,7 +49,7 @@ impl AppContext { theme_mode: ThemeMode, ) -> Result { let _guard = self.invalidate_settings_cache(); - + self.db .update_theme_preference(theme_mode) .map_err(|e| e.to_string())?; diff --git a/src/database/top_ups.rs b/src/database/top_ups.rs index a1ab9654c..6e368e06d 100644 --- a/src/database/top_ups.rs +++ b/src/database/top_ups.rs @@ -1,4 +1,5 @@ use crate::database::Database; +use dash_sdk::dpp::balances::credits::Duffs; use rusqlite::{OptionalExtension, params}; impl Database { @@ -34,7 +35,7 @@ impl Database { &self, identity_id: &[u8], top_up_index: u32, - amount: u64, + amount: Duffs, ) -> rusqlite::Result<()> { self.execute( "INSERT INTO top_up (identity_id, top_up_index, amount) VALUES (?, ?, ?)", diff --git a/src/model/wallet/asset_lock_transaction.rs b/src/model/wallet/asset_lock_transaction.rs index 1e3265a06..20bdf9e85 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -1,6 +1,7 @@ use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; +use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::psbt::serialize::Serialize; use dash_sdk::dpp::dashcore::secp256k1::Message; use dash_sdk::dpp::dashcore::sighash::SighashCache; @@ -16,7 +17,7 @@ impl Wallet { pub fn registration_asset_lock_transaction( &mut self, network: Network, - amount: u64, + amount: Duffs, allow_take_fee_from_amount: bool, identity_index: u32, register_addresses: Option<&AppContext>, @@ -47,7 +48,7 @@ impl Wallet { pub fn top_up_asset_lock_transaction( &mut self, network: Network, - amount: u64, + amount_duffs: Duffs, allow_take_fee_from_amount: bool, identity_index: u32, top_up_index: u32, @@ -69,7 +70,7 @@ impl Wallet { )?; self.asset_lock_transaction_from_private_key( network, - amount, + amount_duffs, allow_take_fee_from_amount, private_key, register_addresses, @@ -80,7 +81,7 @@ impl Wallet { fn asset_lock_transaction_from_private_key( &mut self, network: Network, - amount: u64, + amount_duffs: Duffs, allow_take_fee_from_amount: bool, private_key: PrivateKey, register_addresses: Option<&AppContext>, @@ -99,8 +100,15 @@ impl Wallet { let one_time_key_hash = asset_lock_public_key.pubkey_hash(); let fee = 3_000; + tracing::debug!(wallet=?self.alias, + "Creating asset lock transaction with amount: {}, fee: {}, allow_take_fee_from_amount: {}", + amount_duffs, + fee, + allow_take_fee_from_amount + ); + let (utxos, change_option) = self - .take_unspent_utxos_for(amount, fee, allow_take_fee_from_amount) + .take_unspent_utxos_for(amount_duffs, fee, allow_take_fee_from_amount) .ok_or("take_unspent_utxos_for() returned None".to_string())?; let actual_amount = if change_option.is_none() && allow_take_fee_from_amount { @@ -109,7 +117,7 @@ impl Wallet { let total_input_value: u64 = utxos.iter().map(|(_, (tx_out, _))| tx_out.value).sum(); total_input_value - fee } else { - amount + amount_duffs }; let payload_output = TxOut { diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 5e3e5b912..87d075f87 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -263,6 +263,7 @@ impl Wallet { !self.unused_asset_locks.is_empty() } + /// Returns the maximum balance of the wallet by summing up all UTXOs. pub fn max_balance(&self) -> u64 { self.utxos .values() diff --git a/src/ui/components/amount_input.rs b/src/ui/components/amount_input.rs index 8ef235d44..898b40f92 100644 --- a/src/ui/components/amount_input.rs +++ b/src/ui/components/amount_input.rs @@ -118,7 +118,7 @@ impl AmountInput { show_max_button: false, desired_width: None, show_validation_errors: true, // Default to showing validation errors - changed: false, + changed: true, // Start as changed to force initial validation } } @@ -135,13 +135,36 @@ impl AmountInput { self.decimal_places } + /// Update decimal places used to render values. + /// + /// Value displayed in the input is not changed, but the actual [Amount] + /// will be multiplied or divided by 10^(difference of decimal places). + /// + /// ## Example + /// + /// The input contains `12.34` and decimal places is set to 3. + /// It will be interpreted as `12.340` when parsed (credits value `12_340`). + /// + /// + /// If you change the decimal places from 3 to 5: + /// + /// * The input will still display `12.34` (unchanged) + /// * The next time the input is parsed, it will generate `12.34000` + /// (credits value `1_234_000`). + pub fn set_decimal_places(&mut self, decimal_places: u8) -> &mut Self { + self.decimal_places = decimal_places; + self.changed = true; + + self + } + /// Gets the unit name this input is configured for. pub fn unit_name(&self) -> Option<&str> { self.unit_name.as_deref() } /// Sets the label for the input field. - pub fn label>(mut self, label: T) -> Self { + pub fn with_label>(mut self, label: T) -> Self { self.label = Some(label.into()); self } @@ -154,7 +177,7 @@ impl AmountInput { } /// Sets the hint text for the input field. - pub fn hint_text>(mut self, hint_text: T) -> Self { + pub fn with_hint_text>(mut self, hint_text: T) -> Self { self.hint_text = Some(hint_text.into()); self } @@ -167,7 +190,7 @@ impl AmountInput { /// Sets the maximum amount allowed. If provided, a "Max" button will be shown /// when `show_max_button` is true. - pub fn max_amount(mut self, max_amount: Option) -> Self { + pub fn with_max_amount(mut self, max_amount: Option) -> Self { self.max_amount = max_amount; self } @@ -181,7 +204,7 @@ impl AmountInput { /// Sets the minimum amount allowed. Defaults to 1 (must be greater than zero). /// Set to Some(0) to allow zero amounts, or None to disable minimum validation. - pub fn min_amount(mut self, min_amount: Option) -> Self { + pub fn with_min_amount(mut self, min_amount: Option) -> Self { self.min_amount = min_amount; self } @@ -193,7 +216,7 @@ impl AmountInput { } /// Whether to show a "Max" button that sets the amount to the maximum. - pub fn max_button(mut self, show: bool) -> Self { + pub fn with_max_button(mut self, show: bool) -> Self { self.show_max_button = show; self } @@ -205,7 +228,7 @@ impl AmountInput { } /// Sets the desired width of the input field. - pub fn desired_width(mut self, width: f32) -> Self { + pub fn with_desired_width(mut self, width: f32) -> Self { self.desired_width = Some(width); self } @@ -217,7 +240,7 @@ impl AmountInput { } /// Controls whether validation errors are displayed as a label within the component. - pub fn show_validation_errors(mut self, show: bool) -> Self { + pub fn with_validation_errors_display(mut self, show: bool) -> Self { self.show_validation_errors = show; self } @@ -343,6 +366,15 @@ impl Component for AmountInput { fn show(&mut self, ui: &mut Ui) -> InnerResponse { AmountInput::show_internal(self, ui) } + + fn current_value(&self) -> Option { + // Validate the current amount string and return the parsed amount + match self.validate_amount() { + Ok(Some(amount)) => Some(amount), + Ok(None) => None, // Empty input + Err(_) => None, // Invalid input returns None + } + } } #[cfg(test)] @@ -382,15 +414,15 @@ mod tests { assert_eq!(input.min_amount, Some(1)); // Custom minimum - let input = AmountInput::new(Amount::new(0, 8)).min_amount(Some(1000)); + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(1000)); assert_eq!(input.min_amount, Some(1000)); // Allow zero - let input = AmountInput::new(Amount::new(0, 8)).min_amount(Some(0)); + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(0)); assert_eq!(input.min_amount, Some(0)); // No minimum - let input = AmountInput::new(Amount::new(0, 8)).min_amount(None); + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(None); assert_eq!(input.min_amount, None); } @@ -478,8 +510,8 @@ mod tests { fn test_min_max_validation() { let amount = Amount::new(0, 2); let mut input = AmountInput::new(amount) - .min_amount(Some(100)) // Minimum 1.00 - .max_amount(Some(10000)); // Maximum 100.00 + .with_min_amount(Some(100)) // Minimum 1.00 + .with_max_amount(Some(10000)); // Maximum 100.00 // Test amount below minimum input.amount_str = "0.50".to_string(); // 50 (below min of 100) diff --git a/src/ui/components/component_trait.rs b/src/ui/components/component_trait.rs index 199d87e2c..6a5feb28e 100644 --- a/src/ui/components/component_trait.rs +++ b/src/ui/components/component_trait.rs @@ -92,4 +92,12 @@ pub trait Component { /// An [`InnerResponse`] containing the component's response data in [`InnerResponse::inner`] field. /// [`InnerResponse::inner`] should implement [`ComponentResponse`] trait. fn show(&mut self, ui: &mut Ui) -> InnerResponse; + + /// Returns the current value of the component. + /// + /// Note that only valid values should be returned here. + /// If the component value is invalid, this should return `None`. + /// + /// See [`ComponentResponse::current_value`] for more details. + fn current_value(&self) -> Option; } diff --git a/src/ui/components/funding_widget.rs b/src/ui/components/funding_widget.rs new file mode 100644 index 000000000..1b86669f9 --- /dev/null +++ b/src/ui/components/funding_widget.rs @@ -0,0 +1,1173 @@ +use crate::context::AppContext; +use crate::model::amount::Amount; +use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::{Component, ComponentResponse}; +use crate::ui::identities::add_new_identity_screen::FundingMethod; +use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; +use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dashcore_rpc::dashcore::{Address, OutPoint, TxOut}; +use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; +use dash_sdk::dpp::dashcore::Transaction; +use dash_sdk::dpp::prelude::AssetLockProof; +use eframe::epaint::TextureHandle; +use egui::{Color32, ComboBox, InnerResponse, Ui}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +/// We assume a standard fee for funding transactions +pub const FEE_DUFFS: u64 = 10_000; + +/// Funding method for the funding widget - doesn't require identity indices +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FundingWidgetMethod { + UseAssetLock(Address, Box, Box), + FundWithUtxo(OutPoint, TxOut, Address), + FundWithWallet(Amount), +} + +impl FundingWidgetMethod { + /// Convert to RegisterIdentityFundingMethod with the provided identity_index + pub fn to_register_identity_funding_method( + self, + identity_index: u32, + ) -> crate::backend_task::identity::RegisterIdentityFundingMethod { + use crate::backend_task::identity::RegisterIdentityFundingMethod; + + match self { + FundingWidgetMethod::UseAssetLock(address, proof, transaction) => { + RegisterIdentityFundingMethod::UseAssetLock(address, proof, transaction) + } + FundingWidgetMethod::FundWithUtxo(outpoint, tx_out, address) => { + RegisterIdentityFundingMethod::FundWithUtxo( + outpoint, + tx_out, + address, + identity_index, + ) + } + FundingWidgetMethod::FundWithWallet(amount) => { + RegisterIdentityFundingMethod::FundWithWallet(amount, identity_index) + } + } + } +} + +/// Response from the funding widget containing all state changes and actions +#[derive(Debug, Clone, Default)] +pub struct FundingWidgetResponse { + /// Wallet selection changed + pub wallet_changed: Option>>, + /// Funding method changed and has sufficient funds + pub funding_method_changed: Option, + /// Funding amount changed + pub amount_changed: Option, + /// Address generated or changed. + /// + /// This field is populated when a new address is generated or an existing address is updated. + /// Address changes can be triggered by user actions (e.g., selecting a different funding method) + /// or by system events (e.g., generating a new address for a funding transaction). + pub address_changed: Option
, + /// Asset lock selected (Transaction, AssetLockProof, Address) + pub asset_lock_selected: Option<(Transaction, AssetLockProof, Address)>, + /// Error occurred + pub error: Option, + /// Whether the currently selected funding method has sufficient funds + pub funding_secured: Option, +} + +impl FundingWidgetResponse { + /// Check if any changes occurred + pub fn has_changes(&self) -> bool { + self.wallet_changed.is_some() + || self.funding_method_changed.is_some() + || self.amount_changed.is_some() + || self.address_changed.is_some() + || self.asset_lock_selected.is_some() + || self.error.is_some() + || self.funding_secured.is_some() + } + + /// Return true when the funding has been secured (funding method is selected and has sufficient funds) + pub fn funded(&self) -> bool { + self.funding_secured.is_some() + } + + /// Call a function when the funding has been secured + pub fn on_funded(self, mut f: impl FnMut(&FundingWidgetResponse)) -> Self { + if self.funded() { + f(&self) + }; + self + } +} + +impl ComponentResponse for FundingWidgetResponse { + type DomainType = FundingWidgetMethod; + + fn has_changed(&self) -> bool { + self.has_changes() + } + + fn is_valid(&self) -> bool { + self.error.is_none() + } + + fn changed_value(&self) -> &Option { + &self.funding_secured + } + + fn error_message(&self) -> Option<&str> { + self.error.as_deref() + } +} + +/// Funding widget state +pub struct FundingWidget { + app_context: Arc, + + // Configuration + predefined_wallet: Option>>, + predefined_address: Option
, + show_max_button: bool, + show_qr_code: bool, + show_copy_button: bool, + ignore_existing_utxos: bool, + + // Internal state (private, managed by component) + selected_wallet: Option>>, + funding_method: FundingMethod, + amount_input: Option, + current_funding_amount: Option, + funding_address: Option
, + selected_asset_lock: Option<(Transaction, AssetLockProof, Address)>, + core_has_funding_address: Option, + copied_to_clipboard: Option>, + /// Track existing UTXOs at the time the address was set up + /// This is used when ignore_existing_utxos is true to filter out pre-existing UTXOs + existing_utxos_snapshot: Option>, +} + +impl FundingWidget { + /// Create a new FundingWidget with default configuration + pub fn new(app_context: Arc) -> Self { + let default_amount = Amount::new_dash(0.5); // 0.5 DASH + + Self { + app_context, + predefined_wallet: None, + predefined_address: None, + show_max_button: true, + show_qr_code: true, + show_copy_button: true, + ignore_existing_utxos: true, + selected_wallet: None, + funding_method: FundingMethod::NoSelection, + amount_input: None, + current_funding_amount: Some(default_amount), + funding_address: None, + selected_asset_lock: None, + core_has_funding_address: None, + copied_to_clipboard: None, + existing_utxos_snapshot: None, + } + } + + /// Set a predefined wallet to use. If not set, user can select from available wallets + pub fn with_wallet(mut self, wallet: Arc>) -> Self { + self.predefined_wallet = Some(wallet.clone()); + self.selected_wallet = Some(wallet); + self + } + + /// Set a predefined address to use. If not set, a new address will be generated + pub fn with_address(mut self, address: Address) -> Self { + self.predefined_address = Some(address.clone()); + self.funding_address = Some(address); + // When address is predefined, force QR code method + self.funding_method = FundingMethod::AddressWithQRCode; + self + } + + /// Set the default funding amount + pub fn with_default_amount(mut self, amount: Amount) -> Self { + self.current_funding_amount = Some(amount); + // Reset amount_input to ensure it gets recreated with new amount + self.amount_input = None; + self + } + + /// Set whether to show the "Max" button for wallet balance funding + pub fn with_max_button(mut self, show: bool) -> Self { + self.show_max_button = show; + self + } + + /// Set whether to show QR code + pub fn with_qr_code(mut self, show: bool) -> Self { + self.show_qr_code = show; + self + } + + /// Set whether to show copy button + pub fn with_copy_button(mut self, show: bool) -> Self { + self.show_copy_button = show; + self + } + + /// Set whether to ignore existing UTXOs when using QR code method. + /// This is useful for wallet top-up scenarios where the user wants to send NEW funds + /// even if there are already sufficient funds at the address. + pub fn with_ignore_existing_utxos(mut self, ignore: bool) -> Self { + self.ignore_existing_utxos = ignore; + self + } + + /// Set the default funding amount (mutable reference version) + pub fn set_default_amount(&mut self, amount: Amount) -> &mut Self { + self.current_funding_amount = Some(amount); + // Reset amount_input to ensure it gets recreated with new amount + self.amount_input = None; + self + } + + /// Set whether to show the "Max" button (mutable reference version) + pub fn set_show_max_button(&mut self, show: bool) -> &mut Self { + self.show_max_button = show; + self + } + + /// Get the current selected wallet + pub fn selected_wallet(&self) -> Option<&Arc>> { + self.selected_wallet.as_ref() + } + + /// Get the current funding method + pub fn funding_method(&self) -> FundingMethod { + self.funding_method + } + + /// Get the current funding amount + pub fn funding_amount(&self) -> Option<&Amount> { + self.current_funding_amount.as_ref() + } + + /// Get the current funding address + pub fn funding_address(&self) -> Option<&Address> { + self.funding_address.as_ref() + } + + /// Get the currently selected asset lock + pub fn selected_asset_lock(&self) -> Option<&(Transaction, AssetLockProof, Address)> { + self.selected_asset_lock.as_ref() + } + + /// Reset wallet-dependent settings when wallet changes + fn reset_wallet_dependent_settings(&mut self, response: &mut FundingWidgetResponse) { + // Only reset if address is not predefined + if self.predefined_address.is_none() { + self.funding_address = None; + response.address_changed = None; // Clear any previous address + } + + self.funding_method = FundingMethod::NoSelection; + self.selected_asset_lock = None; + self.core_has_funding_address = None; + self.copied_to_clipboard = None; + self.existing_utxos_snapshot = None; + + // Set response fields to notify about the resets + response.funding_method_changed = Some(FundingMethod::NoSelection); + response.asset_lock_selected = None; // Clear any previous asset lock selection + response.funding_secured = None; // Clear any previous funding method readiness + } + + /// Check if the currently selected funding method has sufficient funds for the specified amount + /// Returns Some(FundingWidgetMethod) if ready, None if not ready + fn check_funding_method_readiness(&self) -> Option { + let Some(wallet) = &self.selected_wallet else { + return None; + }; + + let Some(amount) = &self.current_funding_amount else { + return None; + }; + + if amount.value() == 0 { + return None; + } + + match self.funding_method { + FundingMethod::UseUnusedAssetLock => { + let wallet = wallet.read().unwrap(); + if wallet.has_unused_asset_lock() && self.selected_asset_lock.is_some() { + if let Some((transaction, asset_lock_proof, address)) = + &self.selected_asset_lock + { + // Check if the selected asset lock has sufficient funds + if let Some((_, _, lock_amount, _, _)) = wallet + .unused_asset_locks + .iter() + .find(|(_, addr, _, _, _)| addr == address) + { + if *lock_amount + >= amount.dash_to_duffs().expect("amount should be in DASH") + { + Some(FundingWidgetMethod::UseAssetLock( + address.clone(), + Box::new(asset_lock_proof.clone()), + Box::new(transaction.clone()), + )) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + } + } + FundingMethod::UseWalletBalance => { + if self.check_wallet_balance_sufficient(amount) { + Some(FundingWidgetMethod::FundWithWallet(amount.clone())) + } else { + None + } + } + FundingMethod::AddressWithQRCode => { + if self.funding_address.is_some() { + // Try to get UTXO information + if let Some((outpoint, tx_out, addr)) = self.get_funding_utxo() { + Some(FundingWidgetMethod::FundWithUtxo(outpoint, tx_out, addr)) + } else { + None + } + } else { + None + } + } + FundingMethod::NoSelection => None, + } + } + + /// Check if the wallet balance has an UTXO that is sufficient for the specified amount + fn check_wallet_balance_sufficient(&self, required_amount: &Amount) -> bool { + let Some(wallet_guard) = &self.selected_wallet else { + return false; + }; + + let wallet = wallet_guard.read().unwrap(); + let max_balance_duffs = wallet.max_balance(); + + max_balance_duffs + >= required_amount + .dash_to_duffs() + .expect("amount should be in DASH") + + FEE_DUFFS + } + + fn render_wallet_selection( + &mut self, + ui: &mut Ui, + response: &mut FundingWidgetResponse, + ) -> bool { + // If wallet is predefined, don't show selection + if self.predefined_wallet.is_some() { + return false; + } + + if !self + .app_context + .has_wallet + .load(std::sync::atomic::Ordering::Relaxed) + { + ui.label("No wallets available."); + return false; + } + + let wallets = self.app_context.wallets.read().unwrap(); + if wallets.len() <= 1 { + // Auto-select the only wallet if available + if let Some(wallet) = wallets.values().next() { + if self.selected_wallet.is_none() { + let wallet_clone = wallet.clone(); + // Drop wallets before modifying self + drop(wallets); + self.selected_wallet = Some(wallet_clone.clone()); + self.reset_wallet_dependent_settings(response); + response.wallet_changed = Some(wallet_clone); + } + } + return false; + } + + // Multiple wallets - show selection + let selected_wallet_alias = self + .selected_wallet + .as_ref() + .and_then(|wallet| { + let wallet_guard = wallet.read().ok()?; + let alias = wallet_guard + .alias + .clone() + .unwrap_or_else(|| "Unnamed Wallet".to_string()); + let balance = wallet_guard.max_balance() as f64 * 1e-8; // Convert to DASH + Some(format!("{} ({:.8} DASH)", alias, balance)) + }) + .unwrap_or_else(|| "Select Wallet".to_string()); + + ui.label("Select Wallet:"); + + let mut wallet_selected = None; + ComboBox::from_id_salt("funding_widget_wallet_selection") + .selected_text(selected_wallet_alias) + .show_ui(ui, |ui| { + for wallet in wallets.values() { + let wallet_guard = wallet.read().ok(); + let (wallet_alias, wallet_balance_dash) = + if let Some(wallet_guard) = wallet_guard { + let alias = wallet_guard + .alias + .clone() + .unwrap_or_else(|| "Unnamed Wallet".to_string()); + let balance_dash = wallet_guard.max_balance() as f64 * 1e-8; + (alias, balance_dash) + } else { + ("Unnamed Wallet".to_string(), 0.0) + }; + + let display_text = + format!("{} ({:.4} DASH)", wallet_alias, wallet_balance_dash); + + let is_selected = self + .selected_wallet + .as_ref() + .is_some_and(|selected| Arc::ptr_eq(selected, wallet)); + + if ui.selectable_label(is_selected, display_text).clicked() { + wallet_selected = Some(wallet.clone()); + } + } + }); + // Drop wallets borrow before accessing self mutably + drop(wallets); + + if let Some(wallet) = wallet_selected { + self.selected_wallet = Some(wallet.clone()); + self.reset_wallet_dependent_settings(response); + response.wallet_changed = Some(wallet); + } + + true + } + + fn render_funding_method_selection( + &mut self, + ui: &mut Ui, + response: &mut FundingWidgetResponse, + ) { + let Some(selected_wallet) = self.selected_wallet.clone() else { + return; + }; + + // If address is predefined, only show QR code method and don't allow changing + if self.predefined_address.is_some() { + ui.label("Funding Method:"); + ui.label("Address with QR Code (predefined address)"); + return; + } + + ui.label("Funding Method:"); + + let mut method_changed = None; + ComboBox::from_id_salt("funding_widget_method_selection") + .selected_text(format!("{}", self.funding_method)) + .show_ui(ui, |ui| { + let mut temp_method = self.funding_method; + if ui + .selectable_value( + &mut temp_method, + FundingMethod::NoSelection, + "Please select funding method", + ) + .changed() + { + method_changed = Some(temp_method); + } + + let (has_unused_asset_lock, has_balance) = { + let wallet = selected_wallet.read().unwrap(); + (wallet.has_unused_asset_lock(), wallet.has_balance()) + }; + + // Use Unused Asset Lock option + ui.add_enabled_ui(has_unused_asset_lock, |ui| { + let response = ui.selectable_value( + &mut temp_method, + FundingMethod::UseUnusedAssetLock, + "Use Unused Asset Lock (recommended)", + ); + if response.changed() { + method_changed = Some(temp_method); + } + if !has_unused_asset_lock { + response.on_disabled_hover_text( + "This wallet has no unused asset locks available", + ); + } + }); + + // Use Wallet Balance option + ui.add_enabled_ui(has_balance, |ui| { + let response = ui.selectable_value( + &mut temp_method, + FundingMethod::UseWalletBalance, + "Use Wallet Balance", + ); + if response.changed() { + method_changed = Some(temp_method); + } + if !has_balance { + response.on_disabled_hover_text("This wallet has no available balance"); + } + }); + + // Address with QR Code option (always available) + if ui + .selectable_value( + &mut temp_method, + FundingMethod::AddressWithQRCode, + "Address with QR Code", + ) + .changed() + { + method_changed = Some(temp_method); + } + }); + + if let Some(new_method) = method_changed { + self.funding_method = new_method; + // Reset asset lock when method changes + self.selected_asset_lock = None; + + // Clear existing UTXOs snapshot; even if we switch to QR code method, + // we want to capture the current state of UTXOs for future checks + self.existing_utxos_snapshot = None; + + // Only set max amount when switching to wallet balance + if new_method == FundingMethod::UseWalletBalance + && self.current_funding_amount.is_none() + { + if let Some(wallet) = &self.selected_wallet { + let wallet = wallet.read().unwrap(); + + let max_amount_duffs = wallet.max_balance().saturating_sub(FEE_DUFFS); + self.current_funding_amount = Some(Amount::dash_from_duffs(max_amount_duffs)); + } + }; + + // Reset amount_input to recreate it when needed + self.amount_input = None; + + // Always report the method change, readiness will be checked separately + response.funding_method_changed = Some(new_method); + } + } + + fn render_amount_input(&mut self, ui: &mut Ui, response: &mut FundingWidgetResponse) { + // Initialize the AmountInput component if not already created + let default_amount = self + .current_funding_amount + .clone() + .unwrap_or_else(|| Amount::new_dash(0.5)); // 0.5 DASH default + + let amount_input = self.amount_input.get_or_insert_with(|| { + AmountInput::new(default_amount) + .with_label("Amount:") + .with_hint_text("Enter amount (e.g., 0.1234)") + .with_validation_errors_display(true) + .with_max_button( + self.show_max_button && self.funding_method == FundingMethod::UseWalletBalance, + ) + }); + + // Update max amount for wallet balance method + if self.show_max_button && self.funding_method == FundingMethod::UseWalletBalance { + if let Some(wallet) = &self.selected_wallet { + let wallet = wallet.read().unwrap(); + let wallet_balance = wallet.max_balance(); + + let max_amount = if wallet_balance > FEE_DUFFS { + Some((wallet.max_balance() - FEE_DUFFS) * CREDITS_PER_DUFF) + } else { + None + }; + amount_input.set_max_amount(max_amount); + } + } else { + amount_input.set_max_amount(None); + } + + // Show the AmountInput component + let amount_response = amount_input.show(ui); + + // Handle the response + if amount_response + .inner + .update(&mut self.current_funding_amount) + { + if let Some(amount) = &self.current_funding_amount { + response.amount_changed = Some(amount.to_string()); + } else { + response.amount_changed = Some("".to_string()); + } + } + } + + fn render_funding_status_hints(&self, ui: &mut Ui) { + if !ui.is_enabled() { + return; + } + if self.funding_method == FundingMethod::NoSelection { + return; + }; + + // Only show hints if we have a valid amount and a funding method selected + let Some(amount) = &self.current_funding_amount else { + // display error if amount is invalid + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(255, 165, 0), // Orange color + "⚠ Invalid funding amount. Please enter a valid number.", + ); + return; + }; + + if amount.value() == 0 { + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(255, 165, 0), // Orange color + "⚠ Funding amount must be greater than zero.", + ); + return; + } + + // Use the centralized readiness check to determine if we should show hints + let is_ready = self.check_funding_method_readiness().is_some(); + + if is_ready { + // Show positive feedback when funding method is ready + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(34, 139, 34), // Forest green + match self.funding_method { + FundingMethod::UseUnusedAssetLock => { + "✅ Selected asset lock has sufficient funds and is ready to use" + } + FundingMethod::UseWalletBalance => { + "✅ Wallet has sufficient balance for this transaction" + } + FundingMethod::AddressWithQRCode => { + "✅ Address has received sufficient funds and is ready to use" + } + FundingMethod::NoSelection => "", // This shouldn't happen when is_ready is true + }, + ); + } else if !is_ready { + match self.funding_method { + FundingMethod::UseWalletBalance => { + if let Some(wallet) = &self.selected_wallet { + let wallet = wallet.read().unwrap(); + let available_balance = wallet.max_balance() as f64 * 1e-8; + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(255, 165, 0), // Orange color + format!( + "⚠ Insufficient wallet balance. Available: {:.8} DASH, Required: {}", + available_balance, amount + ) + ); + } + } + FundingMethod::UseUnusedAssetLock => { + if let Some(wallet) = &self.selected_wallet { + let wallet = wallet.read().unwrap(); + if !wallet.has_unused_asset_lock() { + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(255, 165, 0), // Orange color + "⚠ No unused asset locks available in this wallet", + ); + } else if self.selected_asset_lock.is_none() { + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(100, 149, 237), // Cornflower blue + "ℹ Please select an asset lock from the list", + ); + } else { + // Asset lock is selected but funding method is not ready, + // so the selected asset lock must have insufficient funds + if let Some((transaction, _proof, address)) = &self.selected_asset_lock + { + if let Some((_, _, lock_amount, _, _)) = wallet + .unused_asset_locks + .iter() + .find(|(tx, addr, _, _, _)| { + addr == address && tx == transaction + }) + { + let available_amount = *lock_amount as f64 * 1e-8; + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(255, 165, 0), // Orange color + format!( + "⚠ Selected asset lock has insufficient funds. Available: {:.8} DASH, Required: {:.8}", + available_amount, amount + ) + ); + } + } + } + } + } + FundingMethod::AddressWithQRCode => { + // Check if we have a UTXO for this address + let utxo = self.get_funding_utxo(); + + if utxo.is_none() { + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb(100, 149, 237), // Cornflower blue + format!("ℹ Waiting for {} to be sent to the address above", amount), + ); + ui.add_space(3.0); + ui.colored_label( + Color32::from_rgb(100, 149, 237), // Cornflower blue + "💡 Important: The funds must be sent in a single transaction", + ); + } + } + FundingMethod::NoSelection => {} + } + } + } + + fn ensure_funding_address( + &mut self, + response: &mut FundingWidgetResponse, + force_new: bool, + ) -> Result { + // Use predefined address if available and not forcing new + if let Some(address) = &self.predefined_address { + if !force_new { + return Ok(address.clone()); + } + } + + // Generate new address if needed or forced + if self.funding_address.is_none() || force_new { + let Some(wallet_guard) = self.selected_wallet.as_ref() else { + return Err("No wallet selected".to_string()); + }; + + let receive_address = { + let mut wallet = wallet_guard.write().map_err(|e| e.to_string())?; + // Always generate a new address when force_new is true + wallet.receive_address( + self.app_context.network, + force_new, + 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"), + Some(false), + ) + .map_err(|e| e.to_string())?; + } + } 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"), + Some(false), + ) + .map_err(|e| e.to_string())?; + } + self.core_has_funding_address = Some(true); + } + + // Store the address and emit event + self.funding_address = Some(receive_address.clone()); + response.address_changed = Some(receive_address.clone()); + + // If ignore_existing_utxos is enabled, capture current UTXOs as "existing" + // so we can filter them out later when checking for new funds + if self.ignore_existing_utxos { + self.capture_existing_utxos(&receive_address); + } + } + + Ok(self + .funding_address + .as_ref() + .ok_or_else(|| "No funding address available".to_string())? + .clone()) + } + + /// Capture existing UTXOs for the given address to track what was already there + /// before we started waiting for new funds + fn capture_existing_utxos(&mut self, address: &Address) { + if let Some(wallet_guard) = &self.selected_wallet { + let wallet = wallet_guard.read().unwrap(); + if let Some(utxos) = wallet.utxos.get(address) { + // Store a snapshot of existing UTXOs + self.existing_utxos_snapshot = Some(utxos.clone()); + } else { + // No existing UTXOs for this address + self.existing_utxos_snapshot = Some(HashMap::new()); + } + } + } + + /// Ensure existing UTXOs are captured when we have both wallet and address available + /// and ignore_existing_utxos is enabled + fn ensure_existing_utxos_captured(&mut self) { + if !self.ignore_existing_utxos + || self.funding_method != FundingMethod::AddressWithQRCode + || self.selected_wallet.is_none() + || self.funding_address.is_none() + || self.existing_utxos_snapshot.is_some() + { + return; + } + + let address = self.funding_address.as_ref().unwrap().clone(); + self.capture_existing_utxos(&address); + } + + fn render_qr_code( + &mut self, + ui: &mut Ui, + response: &mut FundingWidgetResponse, + ) -> Result<(), String> { + if !self.show_qr_code { + return Ok(()); + } + + // error displayed in `render_funding_status_hints` + let amount = if let Some(amount) = &self.current_funding_amount { + if amount.value() == 0 { + self.render_funding_status_hints(ui); + return Ok(()); + } + amount.clone() + } else { + self.render_funding_status_hints(ui); + return Ok(()); + }; + + let address = self.ensure_funding_address(response, false)?; + let pay_uri = format!("{}?amount={}", address.to_qr_uri(), amount.to_f64()); + + // Generate the QR code image + if let Ok(qr_image) = generate_qr_code_image(&pay_uri) { + let texture: TextureHandle = ui.ctx().load_texture( + "funding_widget_qr_code", + qr_image, + egui::TextureOptions::LINEAR, + ); + ui.vertical_centered(|ui| { + ui.image(&texture); + }); + } else { + ui.vertical_centered(|ui| { + ui.label("Failed to generate QR code."); + }); + } + + ui.add_space(10.0); + ui.vertical_centered(|ui| { + ui.label(&pay_uri); + + // Show buttons if needed + let show_copy = self.show_copy_button; + let show_new_address = self.predefined_address.is_none(); // Hide when address is predefined + + if show_copy || show_new_address { + ui.add_space(5.0); + + // Use horizontal layout with manual centering for proper alignment + ui.horizontal(|ui| { + // Calculate available width and center the buttons manually + let available_width = ui.available_width(); + + // Estimate button widths (approximate values for centering calculation) + let copy_button_width = if show_copy { 100.0 } else { 0.0 }; + let new_address_button_width = if show_new_address { 100.0 } else { 0.0 }; + let spacing_width = if show_copy && show_new_address { + 10.0 + } else { + 0.0 + }; + let total_content_width = + copy_button_width + new_address_button_width + spacing_width; + + // Add left padding to center the content + let left_padding = (available_width - total_content_width).max(0.0) / 2.0; + ui.add_space(left_padding); + + if show_copy && ui.button("Copy Address").clicked() { + self.copied_to_clipboard = Some(copy_to_clipboard(&pay_uri).err()); + } + + if show_copy && show_new_address { + ui.add_space(10.0); + } + + if show_new_address && ui.button("New Address").clicked() { + // Generate a new address + if let Ok(_new_address) = self.ensure_funding_address(response, true) { + // The address has been updated, no additional action needed + } else { + response.error = Some("Failed to generate new address".to_string()); + } + } + }); + } + + if let Some(error) = &self.copied_to_clipboard { + ui.add_space(5.0); + if let Some(error) = error { + ui.label(format!("Failed to copy to clipboard: {}", error)); + } else { + ui.label("Address copied to clipboard."); + } + } + + // Show funding status for QR code method + self.render_funding_status_hints(ui); + }); + Ok(()) + } + + fn render_asset_lock_selection(&mut self, ui: &mut Ui, response: &mut FundingWidgetResponse) { + let Some(selected_wallet) = self.selected_wallet.clone() else { + ui.label("No wallet selected."); + return; + }; + + let wallet = selected_wallet.read().unwrap(); + + if wallet.unused_asset_locks.is_empty() { + ui.label("No unused asset locks available."); + return; + } + + ui.heading("Select an unused asset lock:"); + + // Track the index of the currently selected asset lock (if any) + let selected_index = self.selected_asset_lock.as_ref().and_then(|(_, proof, _)| { + wallet + .unused_asset_locks + .iter() + .position(|(_, _, _, _, p)| p.as_ref() == Some(proof)) + }); + + // Display the asset locks in a scrollable area + egui::ScrollArea::vertical().show(ui, |ui| { + for (index, (tx, address, amount_duffs, islock, proof)) in + wallet.unused_asset_locks.iter().enumerate() + { + ui.horizontal(|ui| { + let tx_id = tx.txid().to_string(); + let lock_amount = Amount::dash_from_duffs(*amount_duffs); + let is_locked = if islock.is_some() { "Yes" } else { "No" }; + + // Display asset lock information with "Selected" if this one is selected + let selected_text = if Some(index) == selected_index { + " (Selected)" + } else { + "" + }; + + ui.label(format!( + "TxID: {}, Address: {}, Amount: {}, InstantLock: {}{}", + tx_id, address, lock_amount, is_locked, selected_text + )); + + // Button to select this asset lock + if ui.button("Select").clicked() { + // Update the selected asset lock + let selected_lock = ( + tx.clone(), + proof.clone().expect("Asset lock proof is required"), + address.clone(), + ); + self.selected_asset_lock = Some(selected_lock.clone()); + response.asset_lock_selected = Some(selected_lock); + + // Update amount with asset lock amount + self.current_funding_amount = Some(lock_amount); + // Reset amount_input to recreate it with the asset lock amount + self.amount_input = None; + response.amount_changed = + self.current_funding_amount.as_ref().map(|a| a.to_string()); + + // Update readiness status immediately after selection + response.funding_secured = self.check_funding_method_readiness(); + + // Request repaint to update hints immediately + ui.ctx().request_repaint(); + } + }); + + ui.add_space(5.0); // Add space between each entry + } + }); + + // Show validation message for selected asset lock + self.render_funding_status_hints(ui); + } +} + +impl Component for FundingWidget { + type DomainType = FundingWidgetMethod; + type Response = FundingWidgetResponse; + + fn show(&mut self, ui: &mut Ui) -> InnerResponse { + let mut response = FundingWidgetResponse::default(); + + let ui_response = ui.vertical(|ui| { + // Wallet selection (if not predefined) + if self.render_wallet_selection(ui, &mut response) { + ui.add_space(10.0); + } + + // Only show funding options if wallet is selected + if self.selected_wallet.is_some() { + // Ensure existing UTXOs are captured when we have both wallet and address + // and ignore_existing_utxos is enabled + self.ensure_existing_utxos_captured(); + + // Funding method selection + self.render_funding_method_selection(ui, &mut response); + ui.add_space(10.0); + + // Amount input + if self.funding_method != FundingMethod::NoSelection { + // for asset locks, we just use asset lock amount + if self.funding_method != FundingMethod::UseUnusedAssetLock { + self.render_amount_input(ui, &mut response); + ui.add_space(10.0); + } + + // Asset lock selection for UseUnusedAssetLock method + match self.funding_method { + FundingMethod::UseUnusedAssetLock => { + self.render_asset_lock_selection(ui, &mut response); + ui.add_space(10.0); + } + + // QR code for address-based funding + FundingMethod::AddressWithQRCode => { + if let Err(e) = self.render_qr_code(ui, &mut response) { + response.error = Some(e); + } + } + FundingMethod::UseWalletBalance => { + // No additional UI for UseWalletBalance, just show hints + self.render_funding_status_hints(ui); + } + FundingMethod::NoSelection => { + ui.label("Please select a funding method."); + } + } + } + } else if self.predefined_wallet.is_none() { + ui.label("Please select a wallet to continue."); + } + + // Check if the current funding method is ready (has sufficient funds) + response.funding_secured = self.check_funding_method_readiness(); + }); + + InnerResponse::new(response, ui_response.response) + } + + fn current_value(&self) -> Option { + self.check_funding_method_readiness() + } +} + +impl FundingWidget { + /// Get UTXO information for AddressWithQRCode funding method + /// Returns (OutPoint, TxOut, Address) if a suitable UTXO is found + /// + /// Returns None if: + /// - Funding method is not AddressWithQRCode + /// - Funding address is not set + /// - Funding amount is invalid or <= 0 + /// - No suitable UTXO found that meets the required amount + /// - No wallet is selected + /// - No UTXOs available for the funding address + /// - Existing UTXOs snapshot contains the UTXO already + pub fn get_funding_utxo(&self) -> Option<(OutPoint, TxOut, Address)> { + if self.funding_method != FundingMethod::AddressWithQRCode { + return None; + } + + let funding_address = self.funding_address.as_ref()?; + let amount = self.current_funding_amount.as_ref()?; + + if amount.value() == 0 { + return None; + } + + let wallet_guard = self.selected_wallet.as_ref()?; + let wallet = wallet_guard.read().unwrap(); + let required_amount_duffs = amount.dash_to_duffs().expect("amount should be in DASH"); + + // we don't use existing UTXOs snapshot if ignore_existing_utxos is enabled; when it's disabled, existing_utxos will be None + let existing_utxos = self.existing_utxos_snapshot.as_ref(); + + // Get UTXOs for the address + if let Some(utxos) = wallet.utxos.get(funding_address) { + utxos + .iter() + .find(|utxo| { + // enough value and NOT on the existing UTXOs list + utxo.1.value >= required_amount_duffs + && !existing_utxos.is_some_and(|snapshot| snapshot.contains_key(utxo.0)) + }) + .map(|utxo| (*utxo.0, utxo.1.clone(), funding_address.clone())) + } else { + None + } + } +} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 94c579e59..91d17bb2f 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -3,6 +3,7 @@ pub mod component_trait; pub mod contract_chooser_panel; pub mod dpns_subscreen_chooser_panel; pub mod entropy_grid; +pub mod funding_widget; pub mod identity_selector; pub mod left_panel; pub mod left_wallet_panel; diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs deleted file mode 100644 index 628dd333f..000000000 --- a/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::app::AppAction; -use crate::ui::identities::add_new_identity_screen::{ - AddNewIdentityScreen, FundingMethod, WalletFundedScreenStep, -}; -use egui::{Color32, Ui}; - -impl AddNewIdentityScreen { - fn render_choose_funding_asset_lock(&mut self, ui: &mut egui::Ui) { - // Ensure a wallet is selected - let Some(selected_wallet) = self.selected_wallet.clone() else { - ui.label("No wallet selected."); - return; - }; - - // Read the wallet to access unused asset locks - let wallet = selected_wallet.read().unwrap(); - - if wallet.unused_asset_locks.is_empty() { - ui.label("No unused asset locks available."); - return; - } - - ui.heading("Select an unused asset lock:"); - - // Track the index of the currently selected asset lock (if any) - let selected_index = self.funding_asset_lock.as_ref().and_then(|(_, proof, _)| { - wallet - .unused_asset_locks - .iter() - .position(|(_, _, _, _, p)| p.as_ref() == Some(proof)) - }); - - // Display the asset locks in a scrollable area - egui::ScrollArea::vertical().show(ui, |ui| { - for (index, (tx, address, amount, islock, proof)) in - wallet.unused_asset_locks.iter().enumerate() - { - ui.horizontal(|ui| { - let tx_id = tx.txid().to_string(); - let lock_amount = *amount as f64 * 1e-8; // Convert to DASH - let is_locked = if islock.is_some() { "Yes" } else { "No" }; - - // Display asset lock information with "Selected" if this one is selected - let selected_text = if Some(index) == selected_index { - " (Selected)" - } else { - "" - }; - - ui.label(format!( - "TxID: {}, Address: {}, Amount: {:.8} DASH, InstantLock: {}{}", - tx_id, address, lock_amount, is_locked, selected_text - )); - - // Button to select this asset lock - if ui.button("Select").clicked() { - // Update the selected asset lock - self.funding_asset_lock = Some(( - tx.clone(), - proof.clone().expect("Asset lock proof is required"), - address.clone(), - )); - - // Update the step to ready to create identity - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ReadyToCreate; - } - }); - - ui.add_space(5.0); // Add space between each entry - } - }); - } - - pub fn render_ui_by_using_unused_asset_lock( - &mut self, - ui: &mut Ui, - step_number: u32, - ) -> AppAction { - let mut action = AppAction::None; - - // Extract the step from the RwLock to minimize borrow scope - let step = *self.step.read().unwrap(); - - ui.heading( - format!( - "{}. Choose the unused asset lock that you would like to use.", - step_number - ) - .as_str(), - ); - ui.add_space(10.0); - self.render_choose_funding_asset_lock(ui); - - if ui.button("Create Identity").clicked() { - self.error_message = None; - action |= self.register_identity_clicked(FundingMethod::UseUnusedAssetLock); - } - - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); - ui.add_space(20.0); - } - - ui.vertical_centered(|ui| { - match step { - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - _ => {} - } - }); - - ui.add_space(40.0); - action - } -} diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs deleted file mode 100644 index fe6c25daf..000000000 --- a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::app::AppAction; -use crate::ui::identities::add_new_identity_screen::{ - AddNewIdentityScreen, FundingMethod, WalletFundedScreenStep, -}; -use egui::{Color32, RichText, Ui}; - -impl AddNewIdentityScreen { - fn show_wallet_balance(&self, ui: &mut egui::Ui) { - if let Some(selected_wallet) = &self.selected_wallet { - let wallet = selected_wallet.read().unwrap(); // Read lock on the wallet - - let total_balance: u64 = wallet.max_balance(); // Sum up all the balances - - let dash_balance = total_balance as f64 * 1e-8; // Convert to DASH units - - ui.horizontal(|ui| { - ui.label(format!("Wallet Balance: {:.8} DASH", dash_balance)); - }); - } else { - ui.label("No wallet selected"); - } - } - - pub fn render_ui_by_using_unused_balance( - &mut self, - ui: &mut Ui, - step_number: u32, - ) -> AppAction { - let mut action = AppAction::None; - - ui.add_space(10.0); - ui.heading(format!( - "{}. How much of your wallet balance would you like to transfer?", - step_number - )); - - ui.add_space(10.0); - self.show_wallet_balance(ui); - ui.add_space(5.0); - - self.render_funding_amount_input(ui); - - // Extract the step from the RwLock to minimize borrow scope - let step = *self.step.read().unwrap(); - - let Ok(_) = self.funding_amount.parse::() else { - return action; - }; - - let button = egui::Button::new(RichText::new("Create Identity").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .frame(true) - .corner_radius(3.0); - if ui.add(button).clicked() { - self.error_message = None; - action = self.register_identity_clicked(FundingMethod::UseWalletBalance); - } - - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); - ui.add_space(20.0); - } - - ui.vertical_centered(|ui| { - match step { - WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - _ => {} - } - }); - - ui.add_space(40.0); - action - } -} diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs deleted file mode 100644 index 14b905714..000000000 --- a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::app::AppAction; -use crate::backend_task::BackendTask; -use crate::backend_task::identity::{ - IdentityRegistrationInfo, IdentityTask, RegisterIdentityFundingMethod, -}; -use crate::ui::identities::add_new_identity_screen::{ - AddNewIdentityScreen, WalletFundedScreenStep, -}; -use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; -use dash_sdk::dashcore_rpc::RpcApi; -use eframe::epaint::TextureHandle; -use egui::{Color32, Ui}; -use std::sync::Arc; - -impl AddNewIdentityScreen { - fn render_qr_code(&mut self, ui: &mut egui::Ui, amount: f64) -> Result<(), String> { - let (address, _should_check_balance) = { - // Scope the write lock to ensure it's dropped before calling `start_balance_check`. - - if let Some(wallet_guard) = self.selected_wallet.as_ref() { - // Get the receive address - if self.funding_address.is_none() { - let mut wallet = wallet_guard.write().unwrap(); - let receive_address = wallet.receive_address( - self.app_context.network, - false, - Some(&self.app_context), - )?; - - if let Some(has_address) = self.core_has_funding_address { - if !has_address { - self.app_context - .core_client - .read() - .expect("Core client lock was poisoned") - .import_address( - &receive_address, - Some("Managed by Dash Evo Tool"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - self.funding_address = Some(receive_address); - } else { - let info = self - .app_context - .core_client - .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"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - self.funding_address = Some(receive_address); - self.core_has_funding_address = Some(true); - } - - // Extract the address to return it outside this scope - (self.funding_address.as_ref().unwrap().clone(), true) - } else { - (self.funding_address.as_ref().unwrap().clone(), false) - } - } else { - return Err("No wallet selected".to_string()); - } - }; - - // if should_check_balance { - // // Now `address` is available, and all previous borrows are dropped. - // self.start_balance_check(&address, ui.ctx()); - // } - - let pay_uri = format!("{}?amount={:.4}", address.to_qr_uri(), amount); - - // Generate the QR code image - if let Ok(qr_image) = generate_qr_code_image(&pay_uri) { - let texture: TextureHandle = - ui.ctx() - .load_texture("qr_code", qr_image, egui::TextureOptions::LINEAR); - ui.image(&texture); - } else { - ui.label("Failed to generate QR code."); - } - - ui.add_space(10.0); - - ui.label(&pay_uri); - ui.add_space(5.0); - - if ui.button("Copy Address").clicked() { - if let Err(e) = copy_to_clipboard(pay_uri.as_str()) { - self.copied_to_clipboard = Some(Some(e)); - } else { - self.copied_to_clipboard = Some(None); - } - } - - if let Some(error) = self.copied_to_clipboard.as_ref() { - ui.add_space(5.0); - if let Some(error) = error { - ui.label(format!("Failed to copy to clipboard: {}", error)); - } else { - ui.label("Address copied to clipboard."); - } - } - - Ok(()) - } - - pub fn render_ui_by_wallet_qr_code(&mut self, ui: &mut Ui, step_number: u32) -> AppAction { - // Extract the step from the RwLock to minimize borrow scope - let step = *self.step.read().unwrap(); - - ui.add_space(10.0); - - ui.heading( - format!( - "{}. Select how much you would like to transfer?", - step_number - ) - .as_str(), - ); - - ui.add_space(8.0); - - self.render_funding_amount_input(ui); - - let Ok(amount_dash) = self.funding_amount.parse::() else { - return AppAction::None; - }; - - if amount_dash <= 0.0 { - return AppAction::None; - } - - let response = 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, amount_dash) { - self.error_message = Some(e); - } - - ui.add_space(20.0); - - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); - ui.add_space(20.0); - } - - match step { - WalletFundedScreenStep::ChooseFundingMethod => {} - WalletFundedScreenStep::WaitingOnFunds => { - ui.heading("=> Waiting for funds. <="); - } - WalletFundedScreenStep::FundsReceived => { - let Some(selected_wallet) = &self.selected_wallet else { - return AppAction::None; - }; - if let Some((utxo, tx_out, address)) = self.funding_utxo.clone() { - let identity_input = IdentityRegistrationInfo { - alias_input: self.alias_input.clone(), - keys: self.identity_keys.clone(), - wallet: Arc::clone(selected_wallet), // Clone the Arc reference - wallet_identity_index: self.identity_id_number, - identity_funding_method: RegisterIdentityFundingMethod::FundWithUtxo( - utxo, - tx_out, - address, - self.identity_id_number, - ), - }; - - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingForAssetLock; - - // Create the backend task to register the identity - return AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::RegisterIdentity(identity_input), - )) - } - } - WalletFundedScreenStep::ReadyToCreate => {} - WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - } - AppAction::None - }); - - ui.add_space(40.0); - - response.inner - } -} diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index 5de98ea6c..b0428783d 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -1,16 +1,13 @@ -mod by_using_unused_asset_lock; -mod by_using_unused_balance; -mod by_wallet_qr_code; mod success_screen; use crate::app::AppAction; use crate::backend_task::core::CoreItem; -use crate::backend_task::identity::{ - IdentityKeys, IdentityRegistrationInfo, IdentityTask, RegisterIdentityFundingMethod, -}; +use crate::backend_task::identity::{IdentityKeys, IdentityRegistrationInfo, IdentityTask}; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::funding_widget::FundingWidget; 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; @@ -21,17 +18,16 @@ use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dashcore_rpc::dashcore::transaction::special_transaction::TransactionPayload; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::secp256k1::hashes::hex::DisplayHex; -use dash_sdk::dpp::dashcore::{OutPoint, PrivateKey, Transaction, TxOut}; +use dash_sdk::dpp::dashcore::{PrivateKey, Transaction}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::platform::Identifier; use eframe::egui::Context; use egui::ahash::HashSet; -use egui::{Button, Color32, ComboBox, ScrollArea, Ui}; +use egui::{Button, Color32, ComboBox, ScrollArea}; use std::cmp::PartialEq; use std::fmt; -use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] @@ -59,14 +55,11 @@ pub struct AddNewIdentityScreen { step: Arc>, funding_asset_lock: Option<(Transaction, AssetLockProof, Address)>, selected_wallet: Option>>, - core_has_funding_address: Option, funding_address: Option
, funding_method: Arc>, funding_amount: String, funding_amount_exact: Option, - funding_utxo: Option<(OutPoint, TxOut, Address)>, alias_input: String, - copied_to_clipboard: Option>, identity_keys: IdentityKeys, error_message: Option, show_password: bool, @@ -75,34 +68,25 @@ pub struct AddNewIdentityScreen { in_key_selection_advanced_mode: bool, pub app_context: Arc, successful_qualified_identity_id: Option, + funding_widget: Option, } impl AddNewIdentityScreen { pub fn new(app_context: &Arc) -> Self { - let mut selected_wallet = None; + // Initialize funding widget immediately + let funding_widget = FundingWidget::new(app_context.clone()) + .with_default_amount(crate::model::amount::Amount::new_dash(0.5)); // 0.5 DASH - if app_context.has_wallet.load(Ordering::Relaxed) { - let wallets = &app_context.wallets.read().unwrap(); - if let Some(wallet) = wallets.values().next() { - // Automatically select the only available wallet - selected_wallet = Some(wallet.clone()); - } - } - - let mut created = Self { - identity_id_number: 0, // updated later + Self { + identity_id_number: 0, // updated later when wallet is selected step: Arc::new(RwLock::new(WalletFundedScreenStep::ChooseFundingMethod)), funding_asset_lock: None, - selected_wallet: None, // updated later - core_has_funding_address: None, + selected_wallet: None, // will be set by funding widget funding_address: None, funding_method: Arc::new(RwLock::new(FundingMethod::NoSelection)), funding_amount: "0.5".to_string(), funding_amount_exact: None, - funding_utxo: None, alias_input: String::new(), - copied_to_clipboard: None, - // updated later identity_keys: IdentityKeys { master_private_key: None, master_private_key_type: KeyType::ECDSA_HASH160, @@ -115,13 +99,8 @@ impl AddNewIdentityScreen { in_key_selection_advanced_mode: false, app_context: app_context.clone(), successful_qualified_identity_id: None, - }; - - if let Some(wallet) = selected_wallet { - created.update_wallet(wallet); - }; - - created + funding_widget: Some(funding_widget), + } } /// Ensure that identity keys are correctly set up and generated. @@ -336,76 +315,6 @@ impl AddNewIdentityScreen { // false // } - fn render_wallet_selection(&mut self, ui: &mut Ui) -> bool { - let mut selected_wallet = None; - let rendered = if self.app_context.has_wallet.load(Ordering::Relaxed) { - let wallets = &self.app_context.wallets.read().unwrap(); - if wallets.len() > 1 { - // Retrieve the alias of the currently selected wallet, if any - let selected_wallet_alias = self - .selected_wallet - .as_ref() - .and_then(|wallet| wallet.read().ok()?.alias.clone()) - .unwrap_or_else(|| "Select".to_string()); - - ui.heading( - "1. Choose the wallet to use in which this identities keys will come from.", - ); - - // Display the ComboBox for wallet selection - ComboBox::from_id_salt("select_wallet") - .selected_text(selected_wallet_alias) - .show_ui(ui, |ui| { - for wallet in wallets.values() { - let wallet_alias = wallet - .read() - .ok() - .and_then(|w| w.alias.clone()) - .unwrap_or_else(|| "Unnamed Wallet".to_string()); - - let is_selected = self - .selected_wallet - .as_ref() - .is_some_and(|selected| Arc::ptr_eq(selected, wallet)); - - if ui.selectable_label(is_selected, wallet_alias).clicked() { - // Update the selected wallet - selected_wallet = Some(wallet.clone()); - // Reset the funding address - self.funding_address = None; - // Reset the funding asset lock - self.funding_asset_lock = None; - // Reset the funding UTXO - self.funding_utxo = None; - // Reset the copied to clipboard state - self.copied_to_clipboard = None; - // Reset the step to choose funding method - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ChooseFundingMethod; - } - } - }); - true - } else if let Some(wallet) = wallets.values().next() { - if self.selected_wallet.is_none() { - // Automatically select the only available wallet - selected_wallet = Some(wallet.clone()); - } - false - } else { - false - } - } else { - false - }; - - if let Some(wallet) = selected_wallet { - self.update_wallet(wallet); - } - - rendered - } - /// Update selected wallet and trigger all dependent actions, like updating identity keys /// and identity index. /// @@ -441,81 +350,6 @@ impl AddNewIdentityScreen { .unwrap_or_default() } - fn render_funding_method(&mut self, ui: &mut egui::Ui) { - let Some(selected_wallet) = self.selected_wallet.clone() else { - return; - }; - let funding_method_arc = self.funding_method.clone(); - let mut funding_method = funding_method_arc.write().unwrap(); // Write lock on funding_method - - ComboBox::from_id_salt("funding_method") - .selected_text(format!("{}", *funding_method)) - .show_ui(ui, |ui| { - if ui - .selectable_value( - &mut *funding_method, - FundingMethod::NoSelection, - "Please select funding method", - ) - .changed() - { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ChooseFundingMethod; - self.funding_amount = "0.5".to_string(); - } - - let (has_unused_asset_lock, has_balance) = { - let wallet = selected_wallet.read().unwrap(); - (wallet.has_unused_asset_lock(), wallet.has_balance()) - }; - - if has_unused_asset_lock - && ui - .selectable_value( - &mut *funding_method, - FundingMethod::UseUnusedAssetLock, - "Use Unused Evo Funding Locks (recommended)", - ) - .changed() - { - self.ensure_correct_identity_keys() - .expect("failed to initialize keys"); - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ReadyToCreate; - self.funding_amount = "0.5".to_string(); - } - if has_balance - && ui - .selectable_value( - &mut *funding_method, - FundingMethod::UseWalletBalance, - "Use Wallet Balance", - ) - .changed() - { - if let Some(wallet) = &self.selected_wallet { - let wallet = wallet.read().unwrap(); - let max_amount = wallet.max_balance(); - self.funding_amount = format!("{:.4}", max_amount as f64 * 1e-8); - } - let mut step = self.step.write().unwrap(); // Write lock on step - *step = WalletFundedScreenStep::ReadyToCreate; - } - if ui - .selectable_value( - &mut *funding_method, - FundingMethod::AddressWithQRCode, - "Address with QR Code", - ) - .changed() - { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingOnFunds; - self.funding_amount = "0.5".to_string(); - } - }); - } - // Function to render the key selection mode (Default or Advanced) fn render_key_selection(&mut self, ui: &mut egui::Ui) { // Provide the selection toggle for Default or Advanced mode @@ -638,118 +472,58 @@ impl AddNewIdentityScreen { } } - fn register_identity_clicked(&mut self, funding_method: FundingMethod) -> AppAction { + fn register_identity_clicked( + &mut self, + funding_widget_response: &crate::ui::components::funding_widget::FundingWidgetResponse, + ) -> AppAction { let Some(selected_wallet) = &self.selected_wallet else { return AppAction::None; }; if self.identity_keys.master_private_key.is_none() { return AppAction::None; }; - match funding_method { - FundingMethod::UseUnusedAssetLock => { - if let Some((tx, funding_asset_lock, address)) = self.funding_asset_lock.clone() { - let identity_input = IdentityRegistrationInfo { - alias_input: self.alias_input.clone(), - keys: self.identity_keys.clone(), - wallet: Arc::clone(selected_wallet), // Clone the Arc reference - wallet_identity_index: self.identity_id_number, - identity_funding_method: RegisterIdentityFundingMethod::UseAssetLock( - address, - Box::new(funding_asset_lock), - Box::new(tx), - ), - }; - - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; - - AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::RegisterIdentity(identity_input), - )) - } else { - AppAction::None - } - } - FundingMethod::UseWalletBalance => { - // Parse the funding amount or fall back to the default value - let amount = self.funding_amount_exact.unwrap_or_else(|| { - (self.funding_amount.parse::().unwrap_or(0.0) * 1e8) as u64 - }); - - if amount == 0 { - return AppAction::None; - } - - let seed = selected_wallet.read().unwrap().wallet_seed.clone(); - tracing::debug!(selected_wallet = ?selected_wallet,?seed, "funding with wallet balance"); - let identity_input = IdentityRegistrationInfo { - alias_input: self.alias_input.clone(), - keys: self.identity_keys.clone(), - wallet: Arc::clone(selected_wallet), // Clone the Arc reference - wallet_identity_index: self.identity_id_number, - identity_funding_method: RegisterIdentityFundingMethod::FundWithWallet( - amount, - self.identity_id_number, - ), - }; - - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingForAssetLock; - - // Create the backend task to register the identity - AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::RegisterIdentity( - identity_input, - ))) - } - _ => AppAction::None, - } - } - - fn render_funding_amount_input(&mut self, ui: &mut egui::Ui) { - let funding_method = self.funding_method.read().unwrap(); - ui.horizontal(|ui| { - ui.label("Amount (DASH):"); - - // Render the text input field for the funding amount - let amount_input = ui - .add( - egui::TextEdit::singleline(&mut self.funding_amount) - .hint_text("Enter amount (e.g., 0.1234)") - .desired_width(100.0), - ) - .lost_focus(); - - let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); - - if amount_input && enter_pressed { - // Optional: Validate the input when Enter is pressed - if self.funding_amount.parse::().is_err() { - ui.label("Invalid amount. Please enter a valid number."); - } - } + // reset error message + self.error_message = None; + + // Process the funding method + if let Some(funding_method) = &funding_widget_response.funding_secured { + let register_identity_funding_method = funding_method + .clone() + .to_register_identity_funding_method(self.identity_id_number); + + let identity_input = IdentityRegistrationInfo { + alias_input: self.alias_input.clone(), + keys: self.identity_keys.clone(), + wallet: Arc::clone(selected_wallet), + wallet_identity_index: self.identity_id_number, + identity_funding_method: register_identity_funding_method, + }; - // Check if the funding method is `UseWalletBalance` - if *funding_method == FundingMethod::UseWalletBalance { - // Safely access the selected wallet - if let Some(wallet) = &self.selected_wallet { - let wallet = wallet.read().unwrap(); // Read lock on the wallet - if ui.button("Max").clicked() { - let max_amount = wallet.max_balance(); - self.funding_amount = format!("{:.4}", max_amount as f64 * 1e-8); - self.funding_amount_exact = Some(max_amount); - } + // Set the appropriate step based on funding method + let mut step = self.step.write().unwrap(); + *step = match funding_method { + crate::ui::components::funding_widget::FundingWidgetMethod::UseAssetLock( + _, + _, + _, + ) => WalletFundedScreenStep::WaitingForPlatformAcceptance, + crate::ui::components::funding_widget::FundingWidgetMethod::FundWithWallet(_) => { + WalletFundedScreenStep::WaitingForAssetLock } - } - - if self.funding_amount.parse::().is_err() - || self.funding_amount.parse::().unwrap_or_default() <= 0.0 - { - ui.colored_label(Color32::DARK_RED, "Invalid amount"); - } - }); + crate::ui::components::funding_widget::FundingWidgetMethod::FundWithUtxo( + _, + _, + _, + ) => WalletFundedScreenStep::WaitingForAssetLock, + }; - ui.add_space(10.0); + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::RegisterIdentity( + identity_input, + ))) + } else { + AppAction::None + } } /// Update existing identity keys based on the current wallet and identity index. @@ -883,31 +657,20 @@ impl ScreenLike for AddNewIdentityScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { if message_type == MessageType::Error { self.error_message = Some(format!("Error registering identity: {}", message)); + let mut step = self.step.write().unwrap(); + if *step == WalletFundedScreenStep::WaitingForAssetLock { + // Funding rejected, reset to funding method selection + *step = WalletFundedScreenStep::ChooseFundingMethod; + } } else { self.error_message = Some(message.to_string()); } } + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { let mut step = self.step.write().unwrap(); match *step { WalletFundedScreenStep::ChooseFundingMethod => {} - WalletFundedScreenStep::WaitingOnFunds => { - if let Some(funding_address) = self.funding_address.as_ref() { - if let BackendTaskSuccessResult::CoreItem( - CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), - ) = backend_task_success_result - { - for (outpoint, tx_out, address) in outpoints_with_addresses { - if funding_address == &address { - *step = WalletFundedScreenStep::FundsReceived; - self.funding_utxo = Some((outpoint, tx_out, address)) - } - } - } - } - } - WalletFundedScreenStep::FundsReceived => {} - WalletFundedScreenStep::ReadyToCreate => {} WalletFundedScreenStep::WaitingForAssetLock => { if let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(tx, _), @@ -965,6 +728,8 @@ impl ScreenLike for AddNewIdentityScreen { action |= island_central_panel(ctx, |ui| { let mut inner_action = AppAction::None; + // Ensure we use whole window width + ui.set_width(ui.available_width()); ScrollArea::vertical().show(ui, |ui| { let step = {*self.step.read().unwrap()}; if step == WalletFundedScreenStep::Success { @@ -975,125 +740,190 @@ impl ScreenLike for AddNewIdentityScreen { ui.heading("Follow these steps to create your identity!"); ui.add_space(15.0); - let mut step_number = 1; + // Step 1: Show FundingWidget with wallet selection + ui.heading("1. Select wallet and provide funding details"); + ui.add_space(10.0); - if self.render_wallet_selection(ui) { - // We had more than 1 wallet - step_number += 1; - } + // Track funding readiness + let mut funding_ready = false; + let mut funding_widget_response: Option = None; + + // Render the funding widget first - it handles wallet selection + if let Some(ref mut widget) = self.funding_widget { + // Disable balance checks if operation is in progress + let step = {*self.step.read().unwrap()}; + let enabled = step == WalletFundedScreenStep::ChooseFundingMethod; + let response_data = ui.add_enabled_ui(enabled, |ui|{ + widget.show(ui).inner + }).inner; + + // Store the response for later use + funding_widget_response = Some(response_data.clone()); + + // Handle wallet changes from the funding widget + if let Some(wallet) = response_data.wallet_changed { + self.update_wallet(wallet); + // Clear funding asset lock when wallet changes + self.funding_asset_lock = None; + } - if self.selected_wallet.is_none() { - return; - }; + if let Some(method) = response_data.funding_method_changed { + let mut funding_method_guard = self.funding_method.write().unwrap(); + *funding_method_guard = method; + // Clear funding asset lock when method changes + self.funding_asset_lock = None; + } - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + if let Some(amount) = response_data.amount_changed { + self.funding_amount = amount.clone(); + self.funding_amount_exact = amount.parse::().ok().map(|f| { + (f * 1e8) as u64 // Convert to Duffs + }); + } - if needed_unlock { - if just_unlocked { - // Select wallet will properly update all dependencies - self.update_wallet(self.selected_wallet.clone().expect("we just checked selected_wallet set above")); - } else { - return; + if let Some(address) = response_data.address_changed { + self.funding_address = Some(address); } - } - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); + if let Some(asset_lock) = response_data.asset_lock_selected { + self.funding_asset_lock = Some(asset_lock); + } - // Display the heading with an info icon that shows a tooltip on hover - ui.horizontal(|ui| { - let wallet_guard = self.selected_wallet.as_ref().unwrap(); - let wallet = wallet_guard.read().unwrap(); - if wallet.identities.is_empty() { - ui.heading(format!( - "{}. Choose an identity index. Leave this 0 if this is your first identity for this wallet.", - step_number - )); - } else { - ui.heading(format!( - "{}. Choose an identity index. Leaving this {} is recommended.", - step_number, - self.next_identity_id(), - )); + if let Some(error) = response_data.error { + self.error_message = Some(error); } + // Get funding readiness from the widget response + funding_ready = response_data.funding_secured.is_some() || step!= WalletFundedScreenStep::ChooseFundingMethod; + } - // Create info icon button with tooltip - let response = crate::ui::helpers::info_icon_button(ui, "The identity index is an internal reference within the wallet. The wallet's seed phrase can always be used to recover any identity, including this one, by using the same index."); + // Don't proceed if funding is not ready + if funding_ready { + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); - // Check if the label was clicked - if response.clicked() { - self.show_pop_up_info = Some("The identity index is an internal reference within the wallet. The wallet’s seed phrase can always be used to recover any identity, including this one, by using the same index.".to_string()); + if needed_unlock { + if just_unlocked { + // Select wallet will properly update all dependencies + self.update_wallet(self.selected_wallet.clone().expect("we just checked selected_wallet set above")); + } else { + return; + } } - }); - step_number += 1; + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); - ui.add_space(8.0); + // Step 2: Identity index selection + ui.horizontal(|ui| { + let wallet_guard = self.selected_wallet.as_ref().unwrap(); + let wallet = wallet_guard.read().unwrap(); + if wallet.identities.is_empty() { + ui.heading("2. Choose an identity index. Leave this 0 if this is your first identity for this wallet."); + } else { + ui.heading(format!( + "2. Choose an identity index. Leaving this {} is recommended.", + self.next_identity_id(), + )); + } - self.render_identity_index_input(ui); - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); + // Create info icon button with tooltip + let response = crate::ui::helpers::info_icon_button(ui, "The identity index is an internal reference within the wallet. The wallet's seed phrase can always be used to recover any identity, including this one, by using the same index."); - // Display the heading with an info icon that shows a tooltip on hover - ui.horizontal(|ui| { - ui.heading(format!( - "{}. Choose what keys you want to add to this new identity.", - step_number - )); + // Check if the label was clicked + if response.clicked() { + self.show_pop_up_info = Some("The identity index is an internal reference within the wallet. The wallet’s seed phrase can always be used to recover any identity, including this one, by using the same index.".to_string()); + } + }); - // Create info icon button with tooltip - let response = crate::ui::helpers::info_icon_button(ui, "Keys allow an identity to perform actions on the Blockchain. They are contained in your wallet and allow you to prove that the action you are making is really coming from yourself."); + ui.add_space(8.0); - // Check if the label was clicked - if response.clicked() { - self.show_pop_up_info = Some("Keys allow an identity to perform actions on the Blockchain. They are contained in your wallet and allow you to prove that the action you are making is really coming from yourself.".to_string()); - } - }); + self.render_identity_index_input(ui); - step_number += 1; + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); - ui.add_space(8.0); + // Step 3: Key selection + ui.horizontal(|ui| { + ui.heading("3. Choose what keys you want to add to this new identity."); - self.render_key_selection(ui); + // Create info icon button with tooltip + let response = crate::ui::helpers::info_icon_button(ui, "Keys allow an identity to perform actions on the Blockchain. They are contained in your wallet and allow you to prove that the action you are making is really coming from yourself."); - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); + // Check if the label was clicked + if response.clicked() { + self.show_pop_up_info = Some("Keys allow an identity to perform actions on the Blockchain. They are contained in your wallet and allow you to prove that the action you are making is really coming from yourself.".to_string()); + } + }); - ui.heading( - format!("{}. Choose your funding method.", step_number).as_str() - ); - step_number += 1; + ui.add_space(8.0); - ui.add_space(10.0); - self.render_funding_method(ui); - ui.add_space(10.0); - ui.separator(); + self.render_key_selection(ui); - // Extract the funding method from the RwLock to minimize borrow scope - let funding_method = *self.funding_method.read().unwrap(); + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); - if funding_method == FundingMethod::NoSelection { - return; - } + // Step 4: Identity alias and registration + ui.heading("4. Final details and registration"); + ui.add_space(10.0); + + // Add alias input + ui.horizontal(|ui| { + ui.label("Identity Alias (Optional):"); + ui.add( + egui::TextEdit::singleline(&mut self.alias_input) + .hint_text("Enter a friendly name for this identity") + .desired_width(200.0), + ); + }); + + if let Some(ref widget_response) = funding_widget_response { + ui.add_space(15.0); + ui.separator(); + ui.add_space(10.0); + + let step = self.step.read().unwrap().to_owned(); + + let enabled= !matches!(step, + WalletFundedScreenStep::WaitingForAssetLock | + WalletFundedScreenStep::WaitingForPlatformAcceptance); + + ui.add_enabled_ui(enabled,|ui|{ + if ui.button("Register Identity").clicked() { + inner_action |= self.register_identity_clicked(widget_response); + } + }).response.on_disabled_hover_text(format!("Registration in progress, please wait until it is finished. Current step: {step}")); + } + } // end of if funding_ready - match funding_method { - FundingMethod::NoSelection => (), - FundingMethod::UseUnusedAssetLock => { - inner_action |= self.render_ui_by_using_unused_asset_lock(ui, step_number); - }, - FundingMethod::UseWalletBalance => { - inner_action |= self.render_ui_by_using_unused_balance(ui, step_number); - }, - FundingMethod::AddressWithQRCode => { - inner_action |= self.render_ui_by_wallet_qr_code(ui, step_number) - }, + // Show error message if any + if let Some(error_message) = self.error_message.as_ref() { + ui.add_space(10.0); + ui.colored_label(Color32::DARK_RED, error_message); } + + // Show step status + let step = *self.step.read().unwrap(); + ui.add_space(20.0); + ui.vertical_centered(|ui| match step { + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("=> Creating asset lock transaction <="); + } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("=> Waiting for Platform acknowledgement <="); + ui.add_space(10.0); + ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector to change the method and use those funds to complete the process."); + } + WalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + _ => {} + }); }); + inner_action }); diff --git a/src/ui/identities/funding_common.rs b/src/ui/identities/funding_common.rs index 4dd6802d9..8bd5b7c9d 100644 --- a/src/ui/identities/funding_common.rs +++ b/src/ui/identities/funding_common.rs @@ -1,20 +1,32 @@ +use std::fmt::Display; + use arboard::Clipboard; use eframe::epaint::{Color32, ColorImage}; use egui::Vec2; use image::Luma; use qrcode::QrCode; -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Debug)] pub enum WalletFundedScreenStep { ChooseFundingMethod, - WaitingOnFunds, - FundsReceived, - ReadyToCreate, WaitingForAssetLock, WaitingForPlatformAcceptance, Success, } +impl Display for WalletFundedScreenStep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WalletFundedScreenStep::ChooseFundingMethod => write!(f, "Choose Funding Method"), + WalletFundedScreenStep::WaitingForAssetLock => write!(f, "Waiting for Asset Lock"), + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + write!(f, "Waiting for Platform Acceptance") + } + WalletFundedScreenStep::Success => write!(f, "Success"), + } + } +} + // Function to generate a QR code image from the address pub fn generate_qr_code_image(pay_uri: &str) -> Result { // Generate the QR code 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/identities/top_up_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs deleted file mode 100644 index ccb420274..000000000 --- a/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::app::AppAction; -use crate::ui::identities::add_new_identity_screen::FundingMethod; -use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; -use egui::{Color32, RichText, Ui}; - -impl TopUpIdentityScreen { - fn render_choose_funding_asset_lock(&mut self, ui: &mut egui::Ui) { - // Ensure a wallet is selected - let Some(selected_wallet) = self.wallet.clone() else { - ui.label("No wallet selected."); - return; - }; - - // Read the wallet to access unused asset locks - let wallet = selected_wallet.read().unwrap(); - - if wallet.unused_asset_locks.is_empty() { - ui.label("No unused asset locks available."); - return; - } - - ui.heading("Select an unused asset lock:"); - - // Track the index of the currently selected asset lock (if any) - let selected_index = self.funding_asset_lock.as_ref().and_then(|(_, proof, _)| { - wallet - .unused_asset_locks - .iter() - .position(|(_, _, _, _, p)| p.as_ref() == Some(proof)) - }); - - // Display the asset locks in a scrollable area - egui::ScrollArea::vertical().show(ui, |ui| { - for (index, (tx, address, amount, islock, proof)) in - wallet.unused_asset_locks.iter().enumerate() - { - ui.horizontal(|ui| { - let tx_id = tx.txid().to_string(); - let lock_amount = *amount as f64 * 1e-8; // Convert to DASH - let is_locked = if islock.is_some() { "Yes" } else { "No" }; - - // Display asset lock information with "Selected" if this one is selected - let selected_text = if Some(index) == selected_index { - " (Selected)" - } else { - "" - }; - - ui.label(format!( - "TxID: {}, Address: {}, Amount: {:.8} DASH, InstantLock: {}{}", - tx_id, address, lock_amount, is_locked, selected_text - )); - - // Button to select this asset lock - if ui.button("Select").clicked() { - // Update the selected asset lock - self.funding_asset_lock = Some(( - tx.clone(), - proof.clone().expect("Asset lock proof is required"), - address.clone(), - )); - - // Update the step to ready to create identity - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ReadyToCreate; - } - }); - - ui.add_space(5.0); // Add space between each entry - } - }); - } - - pub fn render_ui_by_using_unused_asset_lock( - &mut self, - ui: &mut Ui, - step_number: u32, - ) -> AppAction { - let mut action = AppAction::None; - - // Extract the step from the RwLock to minimize borrow scope - let step = *self.step.read().unwrap(); - - ui.heading( - format!( - "{}. Choose the unused asset lock that you would like to use.", - step_number - ) - .as_str(), - ); - ui.add_space(10.0); - self.render_choose_funding_asset_lock(ui); - ui.add_space(10.0); - - // Top up button - let mut new_style = (**ui.style()).clone(); - new_style.spacing.button_padding = egui::vec2(10.0, 5.0); - ui.set_style(new_style); - let button = egui::Button::new(RichText::new("Top Up Identity").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .frame(true) - .corner_radius(3.0); - if ui.add(button).clicked() { - self.error_message = None; - action |= self.top_up_identity_clicked(FundingMethod::UseUnusedAssetLock); - } - - ui.add_space(20.0); - - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); - ui.add_space(20.0); - } - - ui.vertical_centered(|ui| match step { - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - _ => {} - }); - - ui.add_space(40.0); - action - } -} diff --git a/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs deleted file mode 100644 index 762f4a051..000000000 --- a/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::app::AppAction; -use crate::ui::identities::add_new_identity_screen::FundingMethod; -use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; -use egui::{Color32, RichText, Ui}; - -impl TopUpIdentityScreen { - fn show_wallet_balance(&self, ui: &mut egui::Ui) { - if let Some(selected_wallet) = &self.wallet { - let wallet = selected_wallet.read().unwrap(); // Read lock on the wallet - - let total_balance: u64 = wallet.max_balance(); // Sum up all the balances - - let dash_balance = total_balance as f64 * 1e-8; // Convert to DASH units - - ui.horizontal(|ui| { - ui.label(format!("Wallet Balance: {:.8} DASH", dash_balance)); - }); - } else { - ui.label("No wallet selected"); - } - } - - pub fn render_ui_by_using_unused_balance( - &mut self, - ui: &mut Ui, - step_number: u32, - ) -> AppAction { - let mut action = AppAction::None; - - ui.heading(format!( - "{}. How much of your wallet balance would you like to transfer?", - step_number - )); - - ui.add_space(10.0); - self.show_wallet_balance(ui); - ui.add_space(5.0); - - self.top_up_funding_amount_input(ui); - - // Extract the step from the RwLock to minimize borrow scope - let step = *self.step.read().unwrap(); - - let Ok(_) = self.funding_amount.parse::() else { - return action; - }; - - // Top up button - let mut new_style = (**ui.style()).clone(); - new_style.spacing.button_padding = egui::vec2(10.0, 5.0); - ui.set_style(new_style); - let button = egui::Button::new(RichText::new("Top Up Identity").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .frame(true) - .corner_radius(3.0); - if ui.add(button).clicked() { - self.error_message = None; - action = self.top_up_identity_clicked(FundingMethod::UseWalletBalance); - } - - ui.add_space(20.0); - - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); - ui.add_space(20.0); - } - - ui.vertical_centered(|ui| { - match step { - WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - _ => {} - }; - }); - - ui.add_space(40.0); - action - } -} diff --git a/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs deleted file mode 100644 index 4ee0b1575..000000000 --- a/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::app::AppAction; -use crate::backend_task::BackendTask; -use crate::backend_task::identity::{IdentityTask, IdentityTopUpInfo, TopUpIdentityFundingMethod}; -use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; -use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; -use dash_sdk::dashcore_rpc::RpcApi; -use eframe::epaint::TextureHandle; -use egui::{Color32, Ui}; -use std::sync::Arc; - -impl TopUpIdentityScreen { - fn render_qr_code(&mut self, ui: &mut egui::Ui, amount: f64) -> Result<(), String> { - let address = { - if let Some(wallet_guard) = self.wallet.as_ref() { - // Get the receive address from the selected wallet - if self.funding_address.is_none() { - let mut wallet = wallet_guard.write().unwrap(); - let receive_address = wallet.receive_address( - self.app_context.network, - false, - Some(&self.app_context), - )?; - - // Import address to Core if needed for monitoring - let core_client = self - .app_context - .core_client - .read() - .map_err(|_| "Core client lock was poisoned".to_string())?; - - let info = core_client - .get_address_info(&receive_address) - .map_err(|e| e.to_string())?; - - if !(info.is_watchonly || info.is_mine) { - core_client - .import_address( - &receive_address, - Some("Managed by Dash Evo Tool"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - - drop(core_client); - - self.funding_address = Some(receive_address.clone()); - receive_address - } else { - self.funding_address.as_ref().unwrap().clone() - } - } else { - return Err("No wallet selected".to_string()); - } - }; - - let pay_uri = format!("{}?amount={:.4}", address.to_qr_uri(), amount); - - // Generate the QR code image - if let Ok(qr_image) = generate_qr_code_image(&pay_uri) { - let texture: TextureHandle = - ui.ctx() - .load_texture("qr_code", qr_image, egui::TextureOptions::LINEAR); - ui.image(&texture); - } else { - ui.label("Failed to generate QR code."); - } - - ui.add_space(15.0); - - ui.label(&pay_uri); - ui.add_space(5.0); - - if ui.button("Copy Address").clicked() { - if let Err(e) = copy_to_clipboard(pay_uri.as_str()) { - self.copied_to_clipboard = Some(Some(e)); - } else { - self.copied_to_clipboard = Some(None); - } - } - - if let Some(error) = self.copied_to_clipboard.as_ref() { - ui.add_space(5.0); - if let Some(error) = error { - ui.label(format!("Failed to copy to clipboard: {}", error)); - } else { - ui.label("Address copied to clipboard."); - } - } - - Ok(()) - } - - pub fn render_ui_by_wallet_qr_code(&mut self, ui: &mut Ui, step_number: u32) -> AppAction { - // Extract the step from the RwLock to minimize borrow scope - let step = *self.step.read().unwrap(); - - ui.heading( - format!( - "{}. Select how much you would like to transfer?", - step_number - ) - .as_str(), - ); - - ui.add_space(8.0); - - self.top_up_funding_amount_input(ui); - - let response = ui.vertical_centered(|ui| { - // Only try to render QR code if we have a valid amount - if let Ok(amount_dash) = self.funding_amount.parse::() { - if amount_dash > 0.0 { - if let Err(e) = self.render_qr_code(ui, amount_dash) { - self.error_message = Some(e); - } - } else { - ui.label("Please enter an amount greater than 0"); - } - } else if !self.funding_amount.is_empty() { - ui.label("Please enter a valid amount"); - } - - ui.add_space(20.0); - - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); - ui.add_space(20.0); - } - - match step { - WalletFundedScreenStep::ChooseFundingMethod => {} - WalletFundedScreenStep::WaitingOnFunds => { - ui.heading("=> Waiting for funds. <="); - } - WalletFundedScreenStep::FundsReceived => { - let Some(selected_wallet) = &self.wallet else { - return AppAction::None; - }; - if let Some((utxo, tx_out, address)) = self.funding_utxo.clone() { - let wallet_index = self.identity.wallet_index.unwrap_or(u32::MAX >> 1); - let top_up_index = self - .identity - .top_ups - .keys() - .max() - .cloned() - .map(|i| i + 1) - .unwrap_or_default(); - let identity_input = IdentityTopUpInfo { - qualified_identity: self.identity.clone(), - wallet: Arc::clone(selected_wallet), - identity_funding_method: TopUpIdentityFundingMethod::FundWithUtxo( - utxo, - tx_out, - address, - wallet_index, - top_up_index, - ), - }; - - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingForAssetLock; - - return AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::TopUpIdentity(identity_input), - )); - } - } - WalletFundedScreenStep::ReadyToCreate => {} - WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - } - AppAction::None - }); - - ui.add_space(40.0); - - response.inner - } -} diff --git a/src/ui/identities/top_up_identity_screen/mod.rs b/src/ui/identities/top_up_identity_screen/mod.rs index d3e7878a4..0d1c0b87f 100644 --- a/src/ui/identities/top_up_identity_screen/mod.rs +++ b/src/ui/identities/top_up_identity_screen/mod.rs @@ -1,6 +1,3 @@ -mod by_using_unused_asset_lock; -mod by_using_unused_balance; -mod by_wallet_qr_code; mod success_screen; use crate::app::AppAction; @@ -10,44 +7,34 @@ 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::Component; +use crate::ui::components::funding_widget::{FundingWidget, FundingWidgetMethod}; 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::add_new_identity_screen::FundingMethod; use crate::ui::identities::funding_common::WalletFundedScreenStep; use crate::ui::{MessageType, ScreenLike}; use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dashcore_rpc::dashcore::transaction::special_transaction::TransactionPayload; -use dash_sdk::dpp::balances::credits::Duffs; -use dash_sdk::dpp::dashcore::{OutPoint, Transaction, TxOut}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; -use dash_sdk::dpp::prelude::AssetLockProof; use eframe::egui::Context; -use egui::{ComboBox, ScrollArea, Ui}; -use std::sync::atomic::Ordering; +use egui::{Button, Color32, ScrollArea}; use std::sync::{Arc, RwLock}; -const WALLET_SELECTION_TOOLTIP: &str = "This wallet will provide the address for receiving funds \ -and create the asset lock transaction to top up your identity."; - pub struct TopUpIdentityScreen { pub identity: QualifiedIdentity, step: Arc>, - funding_asset_lock: Option<(Transaction, AssetLockProof, Address)>, wallet: Option>>, - funding_address: Option
, - funding_method: Arc>, - funding_amount: String, - funding_amount_exact: Option, - funding_utxo: Option<(OutPoint, TxOut, Address)>, - copied_to_clipboard: Option>, error_message: Option, show_password: bool, wallet_password: String, show_pop_up_info: Option, pub app_context: Arc, + funding_widget: Option, + funding_amount: String, + funding: Option, } impl TopUpIdentityScreen { @@ -55,225 +42,47 @@ impl TopUpIdentityScreen { Self { identity: qualified_identity, step: Arc::new(RwLock::new(WalletFundedScreenStep::ChooseFundingMethod)), - funding_asset_lock: None, wallet: None, - funding_address: None, - funding_method: Arc::new(RwLock::new(FundingMethod::NoSelection)), funding_amount: "".to_string(), - funding_amount_exact: None, - funding_utxo: None, - copied_to_clipboard: None, + funding: None, error_message: None, show_password: false, wallet_password: "".to_string(), show_pop_up_info: None, app_context: app_context.clone(), + funding_widget: None, } } - fn render_wallet_selection(&mut self, ui: &mut Ui) -> bool { - if self.app_context.has_wallet.load(Ordering::Relaxed) { - let wallets = self.app_context.wallets.read().unwrap(); - if wallets.len() > 1 { - // Get the current funding method - let funding_method = *self.funding_method.read().unwrap(); - - // Retrieve the alias of the currently selected wallet, if any - let selected_wallet_alias = self - .wallet - .as_ref() - .and_then(|wallet| wallet.read().ok()?.alias.clone()) - .unwrap_or_else(|| "Select".to_string()); - - // Display the ComboBox for wallet selection - ComboBox::from_id_salt("select_wallet") - .selected_text(selected_wallet_alias) - .show_ui(ui, |ui| { - for wallet in wallets.values() { - let (wallet_alias, has_required_resources) = { - let wallet_read = wallet.read().unwrap(); - let alias = wallet_read - .alias - .clone() - .unwrap_or_else(|| "Unnamed Wallet".to_string()); - - let has_resources = match funding_method { - FundingMethod::UseWalletBalance => wallet_read.has_balance(), - FundingMethod::UseUnusedAssetLock => { - wallet_read.has_unused_asset_lock() - } - _ => true, - }; - - (alias, has_resources) - }; - - let is_selected = self - .wallet - .as_ref() - .is_some_and(|selected| Arc::ptr_eq(selected, wallet)); - - ui.add_enabled_ui(has_required_resources, |ui| { - if ui.selectable_label(is_selected, wallet_alias).clicked() { - // Update the selected wallet from app_context - self.wallet = Some(wallet.clone()); - // Reset the funding address - self.funding_address = None; - // Reset the funding asset lock - self.funding_asset_lock = None; - // Reset the funding UTXO - self.funding_utxo = None; - // Reset the copied to clipboard state - self.copied_to_clipboard = None; - // Reset the step to choose funding method - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ChooseFundingMethod; - } - }); - } - }); - true - } else if let Some(wallet) = wallets.values().next() { - if self.wallet.is_none() { - // Get the current funding method - let funding_method = *self.funding_method.read().unwrap(); - - // Check if the wallet has the required resources - let has_required_resources = { - let wallet_read = wallet.read().unwrap(); - match funding_method { - FundingMethod::UseWalletBalance => wallet_read.has_balance(), - FundingMethod::UseUnusedAssetLock => { - wallet_read.has_unused_asset_lock() - } - _ => true, - } - }; - - if has_required_resources { - // Automatically select the only available wallet from app_context - self.wallet = Some(wallet.clone()); - } - } - false - } else { - false - } - } else { - false - } - } - - fn render_funding_method(&mut self, ui: &mut egui::Ui) { - let funding_method_arc = self.funding_method.clone(); - let mut funding_method = funding_method_arc.write().unwrap(); - - // Check if any wallet has unused asset locks or balance - let (has_any_unused_asset_lock, has_any_balance) = { - let wallets = self.app_context.wallets.read().unwrap(); - let mut has_unused_asset_lock = false; - let mut has_balance = false; - - for wallet in wallets.values() { - let wallet = wallet.read().unwrap(); - if wallet.has_unused_asset_lock() { - has_unused_asset_lock = true; - } - if wallet.has_balance() { - has_balance = true; - } - if has_unused_asset_lock && has_balance { - break; // No need to check further - } - } - - (has_unused_asset_lock, has_balance) - }; - - ComboBox::from_id_salt("funding_method") - .selected_text(format!("{}", *funding_method)) - .show_ui(ui, |ui| { - ui.selectable_value( - &mut *funding_method, - FundingMethod::NoSelection, - "Please select funding method", - ); - - ui.add_enabled_ui(has_any_unused_asset_lock, |ui| { - if ui - .selectable_value( - &mut *funding_method, - FundingMethod::UseUnusedAssetLock, - "Use Unused Asset Locks", - ) - .changed() - { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ReadyToCreate; - } - }); - - ui.add_enabled_ui(has_any_balance, |ui| { - if ui - .selectable_value( - &mut *funding_method, - FundingMethod::UseWalletBalance, - "Use Wallet Balance", - ) - .changed() - { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::ReadyToCreate; - } - }); - - if ui - .selectable_value( - &mut *funding_method, - FundingMethod::AddressWithQRCode, - "Address with QR Code", - ) - .changed() - { - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingOnFunds; - } - }); - } - - fn top_up_identity_clicked(&mut self, funding_method: FundingMethod) -> AppAction { + fn top_up_identity_clicked(&mut self, funding_method: FundingWidgetMethod) -> AppAction { let Some(selected_wallet) = &self.wallet else { return AppAction::None; }; + // reset error message + self.error_message = None; + + // Process the funding method match funding_method { - FundingMethod::UseUnusedAssetLock => { - if let Some((tx, funding_asset_lock, address)) = self.funding_asset_lock.clone() { - let identity_input = IdentityTopUpInfo { - qualified_identity: self.identity.clone(), - wallet: Arc::clone(selected_wallet), - identity_funding_method: TopUpIdentityFundingMethod::UseAssetLock( - address, - Box::new(funding_asset_lock), - Box::new(tx), - ), - }; - - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; - - AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::TopUpIdentity( - identity_input, - ))) - } else { - AppAction::None - } - } - FundingMethod::UseWalletBalance => { - // Parse the funding amount or fall back to the default value - let amount = self.funding_amount_exact.unwrap_or_else(|| { - (self.funding_amount.parse::().unwrap_or(0.0) * 1e8) as u64 - }); + FundingWidgetMethod::UseAssetLock(address, funding_asset_lock, tx) => { + let txid = tx.txid().to_hex(); + let identity_input = IdentityTopUpInfo { + qualified_identity: self.identity.clone(), + wallet: Arc::clone(selected_wallet), + identity_funding_method: TopUpIdentityFundingMethod::UseAssetLock( + address, + funding_asset_lock, + tx, + ), + }; + tracing::debug!("Using asset lock for identity top-up: {:?}", txid); + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::TopUpIdentity( + identity_input, + ))) + } + FundingWidgetMethod::FundWithWallet(amount) => { if amount == 0 { return AppAction::None; } @@ -301,34 +110,33 @@ impl TopUpIdentityScreen { identity_input, ))) } - _ => AppAction::None, - } - } - - fn top_up_funding_amount_input(&mut self, ui: &mut egui::Ui) { - ui.horizontal(|ui| { - ui.label("Amount (DASH):"); - - // Render the text input field for the funding amount - let amount_input = ui - .add(egui::TextEdit::singleline(&mut self.funding_amount).desired_width(100.0)) - .lost_focus(); - - self.funding_amount_exact = self.funding_amount.parse::().ok().map(|f| { - (f * 1e8) as u64 // Convert the amount to Duffs - }); + FundingWidgetMethod::FundWithUtxo(outpoint, tx_out, address) => { + let identity_input = IdentityTopUpInfo { + qualified_identity: self.identity.clone(), + wallet: Arc::clone(selected_wallet), + identity_funding_method: TopUpIdentityFundingMethod::FundWithUtxo( + outpoint, + tx_out, + address, + self.identity.wallet_index.unwrap_or(u32::MAX >> 1), + self.identity + .top_ups + .keys() + .max() + .cloned() + .map(|i| i + 1) + .unwrap_or_default(), + ), + }; - let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForAssetLock; - if amount_input && enter_pressed { - // Optional: Validate the input when Enter is pressed - if self.funding_amount.parse::().is_err() { - ui.label("Invalid amount. Please enter a valid number."); - } + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::TopUpIdentity( + identity_input, + ))) } - }); - - ui.add_space(10.0); + } } } @@ -366,6 +174,11 @@ impl ScreenLike for TopUpIdentityScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { if message_type == MessageType::Error { self.error_message = Some(format!("Error topping up identity: {}", message)); + let mut step = self.step.write().unwrap(); + if *step == WalletFundedScreenStep::WaitingForAssetLock { + // Funding rejected, reset to funding method selection + *step = WalletFundedScreenStep::ChooseFundingMethod; + } } else { self.error_message = Some(message.to_string()); } @@ -374,23 +187,6 @@ impl ScreenLike for TopUpIdentityScreen { let mut step = self.step.write().unwrap(); match *step { WalletFundedScreenStep::ChooseFundingMethod => {} - WalletFundedScreenStep::WaitingOnFunds => { - if let Some(funding_address) = self.funding_address.as_ref() { - if let BackendTaskSuccessResult::CoreItem( - CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), - ) = backend_task_success_result - { - for (outpoint, tx_out, address) in outpoints_with_addresses { - if funding_address == &address { - *step = WalletFundedScreenStep::FundsReceived; - self.funding_utxo = Some((outpoint, tx_out, address)) - } - } - } - } - } - WalletFundedScreenStep::FundsReceived => {} - WalletFundedScreenStep::ReadyToCreate => {} WalletFundedScreenStep::WaitingForAssetLock => { if let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(tx, _), @@ -483,69 +279,102 @@ impl ScreenLike for TopUpIdentityScreen { ui.heading("Follow these steps to top up your identity:"); ui.add_space(15.0); - let mut step_number = 1; - ui.heading(format!("{}. Choose your funding method.", step_number).as_str()); - step_number += 1; - ui.add_space(10.0); + let step_number = 1; - self.render_funding_method(ui); + // Initialize funding widget if needed + if self.funding_widget.is_none() { + let mut widget = FundingWidget::new(self.app_context.clone()) + .with_default_amount(crate::model::amount::Amount::new_dash(0.5)); // 0.5 DASH - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); + // Set wallet if already selected + if let Some(wallet) = &self.wallet { + widget = widget.with_wallet(wallet.clone()); + } - // Extract the funding method from the RwLock to minimize borrow scope - let funding_method = *self.funding_method.read().unwrap(); - if funding_method == FundingMethod::NoSelection { - return; + self.funding_widget = Some(widget); + + // Initialize funding_amount_exact with the default amount + self.funding_amount = "0.5".to_string(); } - if funding_method == FundingMethod::UseWalletBalance - || funding_method == FundingMethod::UseUnusedAssetLock - || funding_method == FundingMethod::AddressWithQRCode - { - ui.horizontal(|ui| { - ui.heading(format!( - "{}. Choose the wallet to use to top up this identity.", - step_number - )); - ui.add_space(10.0); - - // Add info icon with hover tooltip - crate::ui::helpers::info_icon_button(ui, WALLET_SELECTION_TOOLTIP); - }); - step_number += 1; + // Render the funding widget + if let Some(ref mut widget) = self.funding_widget { + // Disable balance checks if operation is in progress + let step = {*self.step.read().unwrap()}; + let enabled = step == WalletFundedScreenStep::ChooseFundingMethod; + ui.heading(format!("{}. Configure your top-up", step_number)); ui.add_space(10.0); - self.render_wallet_selection(ui); - - if self.wallet.is_none() { - return; - }; + let response_data = ui.add_enabled_ui(enabled, |ui| { + widget.show(ui).inner + }).inner; - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + // Handle widget responses + if let Some(wallet) = &response_data.wallet_changed { + self.wallet = Some(wallet.clone()); + // Clear funding asset lock when wallet changes + self.funding = None; + } - if needed_unlock && !just_unlocked { - return; + if let Some(method) = &response_data.funding_secured { + self.funding = Some(method.clone()); } - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - } + if let Some(amount) = &response_data.amount_changed { + self.funding_amount = amount.clone(); + } - match funding_method { - FundingMethod::NoSelection => (), - FundingMethod::UseUnusedAssetLock => { - inner_action |= self.render_ui_by_using_unused_asset_lock(ui, step_number); + if let Some(error) = &response_data.error { + self.error_message = Some(error.clone()); } - FundingMethod::UseWalletBalance => { - inner_action |= self.render_ui_by_using_unused_balance(ui, step_number); + + let funding_secured = response_data.funded() || step!= WalletFundedScreenStep::ChooseFundingMethod; + + if funding_secured { + if let Some(funding_method) = self.funding.clone() { + // for FundWithUtxo (eg. qr code scan), we don't need to show the confirmation + // button, as the funding is already secured. + if step== WalletFundedScreenStep::ChooseFundingMethod && matches!(funding_method, FundingWidgetMethod::FundWithUtxo(_, _, _)) { + inner_action |= self.top_up_identity_clicked(funding_method); + } else { + ui.add_space(15.0); + ui.separator(); + ui.add_space(10.0); + + let btn = Button::new("Top Up Identity"); + // top up button is enabled only if we are in the ChooseFundingMethod step + let top_up_enabled = matches!(step, WalletFundedScreenStep::ChooseFundingMethod); + if ui.add_enabled(top_up_enabled, btn).clicked() { + inner_action |= self.top_up_identity_clicked(funding_method); + } + } + } } - FundingMethod::AddressWithQRCode => { - inner_action |= self.render_ui_by_wallet_qr_code(ui, step_number) + + // Show error message if any + if let Some(error_message) = self.error_message.as_ref() { + ui.add_space(10.0); + ui.colored_label(Color32::DARK_RED, error_message); } + + // Show step status + let step = *self.step.read().unwrap(); + ui.add_space(20.0); + ui.vertical_centered(|ui| match step { + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("=> Creating asset lock transaction <="); + } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("=> Waiting for Platform acknowledgement <="); + ui.add_space(10.0); + ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); + } + WalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + _ => {} + }); } }); diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 4240337b1..2a0d330f5 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -115,9 +115,9 @@ impl TransferScreen { let amount_input = self.amount_input.get_or_insert_with(|| { AmountInput::new(Amount::new_dash(0.0)) - .label("Amount:") - .max_button(true) - .max_amount(Some(max_amount_credits)) + .with_label("Amount:") + .with_max_button(true) + .with_max_amount(Some(max_amount_credits)) }); // Check if input should be disabled when operation is in progress diff --git a/src/ui/identities/withdraw_screen.rs b/src/ui/identities/withdraw_screen.rs index 041daa70e..f03e4301b 100644 --- a/src/ui/identities/withdraw_screen.rs +++ b/src/ui/identities/withdraw_screen.rs @@ -32,6 +32,9 @@ use super::get_selected_wallet; use super::keys::add_key_screen::AddKeyScreen; use super::keys::key_info_screen::KeyInfoScreen; +/// Fee in credits for the withdrawal transaction +const WITHDRAWAL_FEE_IN_CREDITS: Credits = 1_000_000_000; + #[derive(PartialEq)] pub enum WithdrawFromIdentityStatus { NotStarted, @@ -46,7 +49,7 @@ pub struct WithdrawalScreen { withdrawal_address: String, withdrawal_amount: Option, withdrawal_amount_input: Option, - max_amount: u64, + max_amount_credits: Credits, pub app_context: Arc, confirmation_popup: bool, withdraw_from_identity_status: WithdrawFromIdentityStatus, @@ -75,7 +78,7 @@ impl WithdrawalScreen { withdrawal_address: String::new(), withdrawal_amount: None, withdrawal_amount_input: None, - max_amount, + max_amount_credits: max_amount, app_context: app_context.clone(), confirmation_popup: false, withdraw_from_identity_status: WithdrawFromIdentityStatus::NotStarted, @@ -99,20 +102,22 @@ impl WithdrawalScreen { } fn render_amount_input(&mut self, ui: &mut Ui) { - let max_amount_minus_fee = (self.max_amount as f64 / 100_000_000_000.0 - 0.0001).max(0.0); - let max_amount_credits = (max_amount_minus_fee * 100_000_000_000.0) as u64; + let max_amount_credits = self + .max_amount_credits + .saturating_sub(WITHDRAWAL_FEE_IN_CREDITS); // Lazy initialization with basic configuration let amount_input = self.withdrawal_amount_input.get_or_insert_with(|| { AmountInput::new(Amount::new_dash(0.0)) - .label("Amount:") - .max_button(true) + .with_label("Amount:") + .with_max_button(true) }); // Check if input should be disabled when operation is in progress let enabled = match self.withdraw_from_identity_status { WithdrawFromIdentityStatus::WaitingForResult(_) | WithdrawFromIdentityStatus::Complete => false, + WithdrawFromIdentityStatus::NotStarted | WithdrawFromIdentityStatus::ErrorMessage(_) => { amount_input.set_max_amount(Some(max_amount_credits)); @@ -289,7 +294,7 @@ impl ScreenLike for WithdrawalScreen { .into_iter() .find(|identity| identity.identity.id() == self.identity.identity.id()) .unwrap(); - self.max_amount = self.identity.identity.balance(); + self.max_amount_credits = self.identity.identity.balance(); } /// Renders the UI components for the withdrawal screen diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index 6b6db3fd3..a7e3a02b2 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -523,10 +523,9 @@ impl TokensScreen { .token_configuration .conventions() .plural_form_by_language_code_or_default("en"); - let reward_amount = Amount::new( - explanation.total_amount, - decimal_places, - ).with_unit_name(unit_name); + let reward_amount = + Amount::new(explanation.total_amount, decimal_places) + .with_unit_name(unit_name); ui.label(format!("Total Estimated Rewards: {}", reward_amount)); ui.separator(); diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index e35d5b736..e8a01d1d8 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -120,8 +120,8 @@ impl TransferTokensScreen { .as_ref() .unwrap_or(&Amount::from(&self.identity_token_balance)), ) - .label("Amount:") - .max_button(true), + .with_label("Amount:") + .with_max_button(true), ); self.amount_input diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index edfa21969..ca5f6c136 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -3,6 +3,8 @@ use crate::backend_task::BackendTask; use crate::backend_task::core::CoreTask; use crate::context::AppContext; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::funding_widget::FundingWidget; 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; @@ -46,6 +48,8 @@ pub struct WalletsBalancesScreen { refreshing: bool, show_rename_dialog: bool, rename_input: String, + // Funding widget for top-up + funding_widget: Option, wallet_password: String, show_password: bool, error_message: Option, @@ -138,6 +142,7 @@ impl WalletsBalancesScreen { refreshing: false, show_rename_dialog: false, rename_input: String::new(), + funding_widget: None, wallet_password: String::new(), show_password: false, error_message: None, @@ -437,7 +442,8 @@ impl WalletsBalancesScreen { .column(Column::initial(150.0)) // Total Received .column(Column::initial(100.0)) // Type .column(Column::initial(60.0)) // Index - .column(Column::remainder()) // Derivation Path + .column(Column::initial(150.0)) // Derivation Path + .column(Column::remainder()) // Top-up Action .header(30.0, |mut header| { header.col(|ui| { let label = if self.sort_column == SortColumn::Address { @@ -530,6 +536,9 @@ impl WalletsBalancesScreen { self.toggle_sort(SortColumn::DerivationPath); } }); + header.col(|ui| { + ui.label("Actions"); + }); }) .body(|mut body| { for data in &address_data { @@ -557,6 +566,16 @@ impl WalletsBalancesScreen { row.col(|ui| { ui.label(format!("{}", data.derivation_path)); }); + row.col(|ui| { + if data.address_type.eq("Funds") + && ui + .button("💰") + .on_hover_text("Top-up this address") + .clicked() + { + self.init_address_topup_widget(data.address.clone()); + } + }); }); } }); @@ -747,6 +766,88 @@ impl WalletsBalancesScreen { }); } + fn render_top_up_modal(&mut self, ui: &mut Ui) { + if self.funding_widget.is_none() { + return; + }; + + let ctx = ui.ctx(); + let screen_rect = ctx.screen_rect(); + let max_height = screen_rect.height() * 0.9; // 90% of screen height + + let mut open = true; + + egui::Window::new("💰 Top-up Address") + .collapsible(false) + .resizable(true) + .max_height(max_height) + .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)) + .open(&mut open) + .show(ctx, |ui| { + self.render_modal_content(ui); + }); + + // Handle close button click + if !open { + self.close_funding_modal(); + } + } + + /// Render the content inside the top-up modal + fn render_modal_content(&mut self, ui: &mut Ui) { + egui::ScrollArea::vertical() + .auto_shrink([true; 2]) + .show(ui, |ui| { + ui.vertical(|ui| { + // Render the widget and get response + let funding_widget = self.funding_widget.as_mut().expect("Checked above"); + let response_data = funding_widget.show(ui).inner.on_funded(|_| { + self.close_funding_modal(); + }); + + if let Some(e) = response_data.error { + self.display_message( + &format!("Funding widget error: {}", e), + MessageType::Error, + ); + } + + ui.add_space(15.0); + ui.separator(); + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button(RichText::new("Close").size(14.0)).clicked() { + self.close_funding_modal(); + } + }); + }); + }); + }); + } + + /// Initialize funding widget for address top-up + fn init_address_topup_widget(&mut self, address: Address) { + let mut widget = FundingWidget::new(self.app_context.clone()) + .with_address(address) + .with_default_amount(crate::model::amount::Amount::new_dash(0.1)) // 0.1 DASH + .with_qr_code(true) + .with_copy_button(true) + .with_max_button(false) // Disable max button for address top-up + .with_ignore_existing_utxos(true); // Enable ignore existing UTXOs for top-up + + if let Some(wallet) = &self.selected_wallet { + widget = widget.with_wallet(wallet.clone()); + } + + self.funding_widget = Some(widget); + } + + /// Close the funding modal and reset state + fn close_funding_modal(&mut self) { + self.funding_widget = None; + } + fn dismiss_message(&mut self) { self.message = None; } @@ -913,6 +1014,7 @@ impl ScreenLike for WalletsBalancesScreen { ui.add_space(10.0); self.render_bottom_options(ui); + self.render_top_up_modal(ui); } else { ui.vertical_centered(|ui| { ui.add_space(50.0);