diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index 2136b401b..ef8d18296 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -457,8 +457,8 @@ impl AppContext { let client = match Client::new(&addr, Auth::CookieFile(cookie_path.clone())) { Ok(client) => client, Err(_) => { - tracing::debug!( - "Failed to authenticate using .cookie file at {:?}, falling back to user/pass", + tracing::trace!( + "Cookie auth unavailable at {:?}, using user/pass", cookie_path ); match Client::new( diff --git a/src/backend_task/shielded/bundle.rs b/src/backend_task/shielded/bundle.rs index 6e63e8f50..a0fffbe71 100644 --- a/src/backend_task/shielded/bundle.rs +++ b/src/backend_task/shielded/bundle.rs @@ -3,7 +3,7 @@ use crate::context::AppContext; use crate::context::shielded::get_proving_key; use crate::model::fee_estimation::{format_credits_as_dash, shielded_fee_for_actions}; use crate::model::wallet::WalletSeedHash; -use crate::model::wallet::shielded::ShieldedWalletState; +use crate::model::wallet::shielded::{ShieldedNote, ShieldedWalletState}; use dash_sdk::dpp::address_funds::{ AddressFundsFeeStrategy, AddressFundsFeeStrategyStep, OrchardAddress, PlatformAddress, }; @@ -13,12 +13,15 @@ use dash_sdk::dpp::shielded::builder::{ OrchardProver, SpendableNote, build_shield_transition, build_shielded_transfer_transition, build_shielded_withdrawal_transition, build_unshield_transition, }; +use dash_sdk::dpp::state_transition::proof_result::StateTransitionProofResult; use dash_sdk::dpp::version::PlatformVersion; use dash_sdk::dpp::withdrawal::Pooling; -use dash_sdk::grovedb_commitment_tree::{Nullifier, PaymentAddress, ProvingKey}; +use dash_sdk::grovedb_commitment_tree::{ + Anchor, ClientPersistentCommitmentTree, Nullifier, PaymentAddress, ProvingKey, +}; use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use std::collections::BTreeMap; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, MutexGuard}; /// Wrapper around a cached `ProvingKey` that implements `OrchardProver`. struct CachedProver { @@ -98,7 +101,7 @@ pub fn build_shield_credit( let recipient_addr = payment_address_to_orchard(recipient_payment_address)?; let wallet_arc = { - let wallets = app_context.wallets.read().unwrap(); + let wallets = app_context.wallets.read()?; wallets .get(seed_hash) .cloned() @@ -111,7 +114,8 @@ pub fn build_shield_credit( let fee_strategy: AddressFundsFeeStrategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)]; - let wallet = wallet_arc.read().unwrap(); + let wallet = wallet_arc.read()?; + // memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo build_shield_transition( &recipient_addr, amount, @@ -148,7 +152,7 @@ pub async fn shield_credits( let recipient_addr = payment_address_to_orchard(recipient_payment_address)?; let wallet_arc = { - let wallets = app_context.wallets.read().unwrap(); + let wallets = app_context.wallets.read()?; wallets .get(seed_hash) .cloned() @@ -158,7 +162,7 @@ pub async fn shield_credits( let nonce: u32 = if let Some(n) = nonce_override { n } else { - let wallet = wallet_arc.read().unwrap(); + let wallet = wallet_arc.read()?; wallet .platform_address_info .iter() @@ -187,11 +191,12 @@ pub async fn shield_credits( ); if let Some(s) = &stage { - *s.lock().unwrap() = ShieldStage::BuildingProof { nonce }; + *s.lock()? = ShieldStage::BuildingProof { nonce }; } let state_transition = { - let wallet = wallet_arc.read().unwrap(); + let wallet = wallet_arc.read()?; + // memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo build_shield_transition( &recipient_addr, amount, @@ -207,7 +212,7 @@ pub async fn shield_credits( }; if let Some(s) = &stage { - *s.lock().unwrap() = ShieldStage::Broadcasting; + *s.lock()? = ShieldStage::Broadcasting; } tracing::trace!("Shield credits: state transition built, broadcasting..."); @@ -217,8 +222,16 @@ pub async fn shield_credits( .await .map_err(shielded_broadcast_error)?; + state_transition + .wait_for_response::(&sdk, None) + .await + .map_err(|e| { + tracing::warn!("Shield credits broadcast succeeded but confirmation wait failed: {e}"); + }) + .ok(); + tracing::info!( - "Shield credits broadcast succeeded: {} — balance will update after the next block is mined and notes are synced", + "Shield credits broadcast succeeded: {}", format_credits_as_dash(amount), ); @@ -269,35 +282,13 @@ pub async fn shielded_transfer( let spent_nullifiers: Vec = spendable_notes.iter().map(|n| n.nullifier).collect(); let (spends, anchor) = { - let tree = shielded_state.commitment_tree.lock().unwrap(); - let spends = spendable_notes - .iter() - .map(|note| { - let merkle_path = tree - .witness(note.position, 0) - .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { - detail: e.to_string(), - })? - .ok_or(TaskError::ShieldedMerkleWitnessUnavailable { - detail: "No Merkle path available for note".into(), - })?; - Ok(SpendableNote { - note: note.note, - merkle_path, - }) - }) - .collect::, TaskError>>()?; - - let anchor = tree - .anchor() - .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { - detail: e.to_string(), - })?; - (spends, anchor) + let tree = shielded_state.commitment_tree.lock()?; + extract_spends_and_anchor(&tree, &spendable_notes)? }; let change_addr = payment_address_to_orchard(&shielded_state.keys.default_address)?; + // memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo let state_transition = build_shielded_transfer_transition( spends, &recipient_addr, @@ -320,8 +311,18 @@ pub async fn shielded_transfer( .await .map_err(shielded_broadcast_error)?; + state_transition + .wait_for_response::(&sdk, None) + .await + .map_err(|e| { + tracing::warn!( + "Shielded transfer broadcast succeeded but confirmation wait failed: {e}" + ); + }) + .ok(); + tracing::info!( - "Shielded transfer broadcast succeeded: {} nullifiers created, change={} — balance will update after the next block is mined and notes are synced", + "Shielded transfer broadcast succeeded: {} nullifiers created, change={}", spent_nullifiers.len(), change_amount > 0, ); @@ -367,35 +368,13 @@ pub async fn unshield_credits( let spent_nullifiers: Vec = spendable_notes.iter().map(|n| n.nullifier).collect(); let (spends, anchor) = { - let tree = shielded_state.commitment_tree.lock().unwrap(); - let spends = spendable_notes - .iter() - .map(|note| { - let merkle_path = tree - .witness(note.position, 0) - .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { - detail: e.to_string(), - })? - .ok_or(TaskError::ShieldedMerkleWitnessUnavailable { - detail: "No Merkle path available for note".into(), - })?; - Ok(SpendableNote { - note: note.note, - merkle_path, - }) - }) - .collect::, TaskError>>()?; - - let anchor = tree - .anchor() - .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { - detail: e.to_string(), - })?; - (spends, anchor) + let tree = shielded_state.commitment_tree.lock()?; + extract_spends_and_anchor(&tree, &spendable_notes)? }; let change_addr = payment_address_to_orchard(&shielded_state.keys.default_address)?; + // memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo let state_transition = build_unshield_transition( spends, to_platform_address, @@ -418,8 +397,18 @@ pub async fn unshield_credits( .await .map_err(shielded_broadcast_error)?; + state_transition + .wait_for_response::(&sdk, None) + .await + .map_err(|e| { + tracing::warn!( + "Unshield credits broadcast succeeded but confirmation wait failed: {e}" + ); + }) + .ok(); + tracing::info!( - "Unshield credits broadcast succeeded: {} nullifiers created, change={} — balance will update after the next block is mined and notes are synced", + "Unshield credits broadcast succeeded: {} nullifiers created, change={}", spent_nullifiers.len(), change_amount > 0, ); @@ -444,7 +433,6 @@ pub async fn shield_from_asset_lock( use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::dpp::shielded::builder::build_shield_from_asset_lock_transition; - use dash_sdk::platform::transition::broadcast::BroadcastStateTransition; use std::time::Duration; let proving_key = crate::context::shielded::get_proving_key(); @@ -457,7 +445,7 @@ pub async fn shield_from_asset_lock( // Step 1: Create the asset lock transaction let (asset_lock_transaction, asset_lock_private_key, _asset_lock_address, used_utxos) = { let wallet_arc = { - let wallets = app_context.wallets.read().unwrap(); + let wallets = app_context.wallets.read()?; wallets .get(seed_hash) .cloned() @@ -501,10 +489,7 @@ pub async fn shield_from_asset_lock( // Step 2: Register this transaction as waiting for finality { - let mut proofs = app_context - .transactions_waiting_for_finality - .lock() - .unwrap(); + let mut proofs = app_context.transactions_waiting_for_finality.lock()?; proofs.insert(tx_id, None); } @@ -520,7 +505,7 @@ pub async fn shield_from_asset_lock( // Step 4: Remove used UTXOs from wallet { let wallet_arc = { - let wallets = app_context.wallets.read().unwrap(); + let wallets = app_context.wallets.read()?; wallets .get(seed_hash) .cloned() @@ -573,7 +558,7 @@ pub async fn shield_from_asset_lock( return Err(TaskError::ShieldedAssetLockTimeout); } _ = tokio::time::sleep(Duration::from_millis(200)) => { - let proofs = app_context.transactions_waiting_for_finality.lock().unwrap(); + let proofs = app_context.transactions_waiting_for_finality.lock()?; if let Some(Some(proof)) = proofs.get(&tx_id) { asset_lock_proof = proof.clone(); break; @@ -584,10 +569,7 @@ pub async fn shield_from_asset_lock( // Step 6: Clean up the finality tracking { - let mut proofs = app_context - .transactions_waiting_for_finality - .lock() - .unwrap(); + let mut proofs = app_context.transactions_waiting_for_finality.lock()?; proofs.remove(&tx_id); } @@ -611,6 +593,7 @@ pub async fn shield_from_asset_lock( shield_amount_credits, ); + // memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo let state_transition = build_shield_from_asset_lock_transition( &recipient, shield_amount_credits, @@ -629,8 +612,18 @@ pub async fn shield_from_asset_lock( .await .map_err(shielded_broadcast_error)?; + state_transition + .wait_for_response::(&sdk, None) + .await + .map_err(|e| { + tracing::warn!( + "Shield from asset lock broadcast succeeded but confirmation wait failed: {e}" + ); + }) + .ok(); + tracing::info!( - "Shield from asset lock broadcast succeeded: {} — balance will update after the next block is mined and notes are synced", + "Shield from asset lock broadcast succeeded: {}", format_credits_as_dash(shield_amount_credits), ); @@ -677,35 +670,13 @@ pub async fn shielded_withdrawal( let spent_nullifiers: Vec = spendable_notes.iter().map(|n| n.nullifier).collect(); let (spends, anchor) = { - let tree = shielded_state.commitment_tree.lock().unwrap(); - let spends = spendable_notes - .iter() - .map(|note| { - let merkle_path = tree - .witness(note.position, 0) - .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { - detail: e.to_string(), - })? - .ok_or(TaskError::ShieldedMerkleWitnessUnavailable { - detail: "No Merkle path available for note".into(), - })?; - Ok(SpendableNote { - note: note.note, - merkle_path, - }) - }) - .collect::, TaskError>>()?; - - let anchor = tree - .anchor() - .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { - detail: e.to_string(), - })?; - (spends, anchor) + let tree = shielded_state.commitment_tree.lock()?; + extract_spends_and_anchor(&tree, &spendable_notes)? }; let change_addr = payment_address_to_orchard(&shielded_state.keys.default_address)?; + // memo: 36-byte structured memo (4-byte type tag + 32-byte payload); all zeros = empty memo let state_transition = build_shielded_withdrawal_transition( spends, amount, @@ -730,8 +701,18 @@ pub async fn shielded_withdrawal( .await .map_err(shielded_broadcast_error)?; + state_transition + .wait_for_response::(&sdk, None) + .await + .map_err(|e| { + tracing::warn!( + "Shielded withdrawal broadcast succeeded but confirmation wait failed: {e}" + ); + }) + .ok(); + tracing::info!( - "Shielded withdrawal broadcast succeeded: {} nullifiers created, change={} — balance will update after the next block is mined and notes are synced", + "Shielded withdrawal broadcast succeeded: {} nullifiers created, change={}", spent_nullifiers.len(), change_amount > 0, ); @@ -833,6 +814,40 @@ fn select_notes_for_amount( Ok((selected, accumulated)) } +/// Extract spendable notes with Merkle witnesses and the tree anchor. +/// +/// Locks the commitment tree, computes a Merkle path for each selected note, +/// and returns them alongside the current tree anchor for proof construction. +fn extract_spends_and_anchor( + tree: &MutexGuard<'_, ClientPersistentCommitmentTree>, + notes: &[&ShieldedNote], +) -> Result<(Vec, Anchor), TaskError> { + let spends = notes + .iter() + .map(|note| { + let merkle_path = tree + .witness(note.position, 0) + .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { + detail: e.to_string(), + })? + .ok_or(TaskError::ShieldedMerkleWitnessUnavailable { + detail: "No Merkle path available for note".into(), + })?; + Ok(SpendableNote { + note: note.note, + merkle_path, + }) + }) + .collect::, TaskError>>()?; + + let anchor = tree + .anchor() + .map_err(|e| TaskError::ShieldedMerkleWitnessUnavailable { + detail: e.to_string(), + })?; + Ok((spends, anchor)) +} + /// Convert a PaymentAddress to an OrchardAddress for the builder functions. fn payment_address_to_orchard(addr: &PaymentAddress) -> Result { let raw = addr.to_raw_address_bytes(); diff --git a/src/context/mod.rs b/src/context/mod.rs index 0eba4c4f3..c6e82c2e2 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -605,8 +605,8 @@ impl AppContext { if let Ok(client) = Client::new(url, Auth::CookieFile(cookie_path.clone())) { return Ok(client); } - tracing::debug!( - "Failed to authenticate using .cookie file at {:?}, falling back to user/pass", + tracing::trace!( + "Cookie auth unavailable at {:?}, using user/pass", cookie_path, ); } diff --git a/src/context/shielded.rs b/src/context/shielded.rs index 68a217a47..f61c05d7a 100644 --- a/src/context/shielded.rs +++ b/src/context/shielded.rs @@ -107,14 +107,14 @@ impl AppContext { seed_hash: &WalletSeedHash, from_address: &dash_sdk::dpp::address_funds::PlatformAddress, ) { - let wallets = self.wallets.read().unwrap(); + let wallets = self.wallets.read().unwrap_or_else(|e| e.into_inner()); let wallet_arc = match wallets.get(seed_hash) { Some(w) => w.clone(), None => return, }; drop(wallets); - let mut wallet = wallet_arc.write().unwrap(); + let mut wallet = wallet_arc.write().unwrap_or_else(|e| e.into_inner()); // Find the matching entry (platform_address_info is keyed by core Address) let mut found: Option<(dash_sdk::dpp::dashcore::Address, u64, u32)> = None; for (core_addr, info) in wallet.platform_address_info.iter_mut() { @@ -150,14 +150,14 @@ impl AppContext { from_address: &dash_sdk::dpp::address_funds::PlatformAddress, nonce: u32, ) { - let wallets = self.wallets.read().unwrap(); + let wallets = self.wallets.read().unwrap_or_else(|e| e.into_inner()); let wallet_arc = match wallets.get(seed_hash) { Some(w) => w.clone(), None => return, }; drop(wallets); - let mut wallet = wallet_arc.write().unwrap(); + let mut wallet = wallet_arc.write().unwrap_or_else(|e| e.into_inner()); for (core_addr, info) in wallet.platform_address_info.iter_mut() { if let Ok(pa) = dash_sdk::dpp::address_funds::PlatformAddress::try_from(core_addr.clone()) @@ -201,7 +201,10 @@ impl AppContext { &self, seed_hash: &WalletSeedHash, ) -> Option { - let states = self.shielded_states.lock().unwrap(); + let states = self + .shielded_states + .lock() + .unwrap_or_else(|e| e.into_inner()); states.get(seed_hash).map(|s| s.keys.default_address) } @@ -212,7 +215,7 @@ impl AppContext { ) -> Result { // Check if already initialized { - let states = self.shielded_states.lock().unwrap(); + let states = self.shielded_states.lock()?; if states.contains_key(&seed_hash) { let balance = states .get(&seed_hash) @@ -224,9 +227,9 @@ impl AppContext { // Get the wallet seed let seed_bytes = { - let wallets = self.wallets.read().unwrap(); + let wallets = self.wallets.read()?; let wallet_arc = wallets.get(&seed_hash).ok_or(TaskError::WalletNotFound)?; - let wallet = wallet_arc.read().unwrap(); + let wallet = wallet_arc.read()?; match &wallet.wallet_seed { crate::model::wallet::WalletSeed::Open(open) => open.seed, crate::model::wallet::WalletSeed::Closed(_) => { @@ -326,7 +329,7 @@ impl AppContext { let balance = state.shielded_balance; - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.insert(seed_hash, state); Ok(BackendTaskSuccessResult::ShieldedInitialized { seed_hash, balance }) @@ -339,7 +342,7 @@ impl AppContext { ) -> Result { // Take the state temporarily for the async operation let mut state = { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.remove(&seed_hash).ok_or(TaskError::WalletNotFound)? }; @@ -357,7 +360,7 @@ impl AppContext { // Put state back { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.insert(seed_hash, state); } @@ -382,7 +385,7 @@ impl AppContext { nonce_override: Option, ) -> Result { let default_address = { - let states = self.shielded_states.lock().unwrap(); + let states = self.shielded_states.lock()?; let state = states.get(&seed_hash).ok_or(TaskError::WalletNotFound)?; state.keys.default_address }; @@ -531,7 +534,7 @@ impl AppContext { operation: impl AsyncFn(&ShieldedWalletState) -> Result, TaskError>, ) -> Result, TaskError> { let mut state = { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.remove(seed_hash).ok_or(TaskError::WalletNotFound)? }; @@ -591,7 +594,7 @@ impl AppContext { } { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.insert(*seed_hash, state); } @@ -610,7 +613,7 @@ impl AppContext { source_address: Option, ) -> Result { let state_ref = { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.remove(&seed_hash).ok_or(TaskError::WalletNotFound)? }; @@ -625,7 +628,7 @@ impl AppContext { // Always put state back { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.insert(seed_hash, state_ref); } @@ -642,7 +645,7 @@ impl AppContext { seed_hash: WalletSeedHash, ) -> Result { let mut state = { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.remove(&seed_hash).ok_or(TaskError::WalletNotFound)? }; @@ -660,7 +663,7 @@ impl AppContext { // Put state back { - let mut states = self.shielded_states.lock().unwrap(); + let mut states = self.shielded_states.lock()?; states.insert(seed_hash, state); } diff --git a/src/context/wallet_lifecycle.rs b/src/context/wallet_lifecycle.rs index 97c15209d..98009c7d7 100644 --- a/src/context/wallet_lifecycle.rs +++ b/src/context/wallet_lifecycle.rs @@ -137,11 +137,21 @@ impl AppContext { } pub fn bootstrap_wallet_addresses(&self, wallet: &Arc>) { - if let Ok(mut guard) = wallet.write() - && guard.known_addresses.is_empty() - { - tracing::info!(wallet = %hex::encode(guard.seed_hash()), "Bootstrapping wallet addresses"); - guard.bootstrap_known_addresses(self); + if let Ok(mut guard) = wallet.write() { + // Bootstrap when no addresses exist (fresh wallet) or when + // platform payment addresses haven't been derived yet (wallet + // created with only a Core address via new_from_seed). + // INTENTIONAL(CODE-006): Bootstrap checks only PlatformPayment address type. + // Other platform address types may trigger redundant re-derivation, but + // bootstrap_known_addresses() is idempotent so this is safe. + let has_platform_addresses = guard.watched_addresses.values().any(|info| { + info.path_reference + == crate::model::wallet::DerivationPathReference::PlatformPayment + }); + if guard.known_addresses.is_empty() || !has_platform_addresses { + tracing::info!(wallet = %hex::encode(guard.seed_hash()), "Bootstrapping wallet addresses"); + guard.bootstrap_known_addresses(self); + } } } diff --git a/src/mcp/tools/identity.rs b/src/mcp/tools/identity.rs index 771bf553b..2dc21a096 100644 --- a/src/mcp/tools/identity.rs +++ b/src/mcp/tools/identity.rs @@ -460,15 +460,18 @@ impl AsyncTool for IdentityCreditsWithdraw { let qi = resolve::qualified_identity(&ctx, ¶m.identity_id)?; - let core_address: dash_sdk::dashcore_rpc::dashcore::Address< - dash_sdk::dashcore_rpc::dashcore::address::NetworkUnchecked, - > = param + let core_address = param .to_address - .parse() - .map_err(|e| McpToolError::InvalidParam { - message: format!("Invalid Core address: {e}"), + .parse::>() + .map_err(|_| McpToolError::InvalidParam { + message: "The Core address is invalid.".to_owned(), + })? + .require_network(ctx.network()) + .map_err(|_| McpToolError::InvalidParam { + message: "The Core address does not match the active network.".to_owned(), })?; - let core_address = core_address.assume_checked(); let task = BackendTask::IdentityTask(IdentityTask::WithdrawFromIdentity( qi, diff --git a/src/mcp/tools/shielded.rs b/src/mcp/tools/shielded.rs index 2cbb538a5..6f70a7b47 100644 --- a/src/mcp/tools/shielded.rs +++ b/src/mcp/tools/shielded.rs @@ -29,6 +29,8 @@ pub struct ShieldFromCoreParams { pub amount_duffs: u64, /// Expected network (required for destructive operations) pub network: String, + /// Optional Core address to fund from (restricts UTXO selection to this address) + pub source_address: Option, } #[derive(Serialize, schemars::JsonSchema)] @@ -81,10 +83,29 @@ impl AsyncTool for ShieldedShieldFromCore { let seed_hash = resolve::wallet(&ctx, ¶m.wallet_id)?; resolve::ensure_spv_synced(&ctx).await?; + let source_address = param + .source_address + .map(|addr_str| { + resolve::validate_address(&addr_str)?; + addr_str + .parse::>() + .map_err(|_| McpToolError::InvalidParam { + message: "The source Core address is invalid.".to_owned(), + })? + .require_network(ctx.network()) + .map_err(|_| McpToolError::InvalidParam { + message: "The source Core address does not match the active network." + .to_owned(), + }) + }) + .transpose()?; + let task = BackendTask::ShieldedTask(ShieldedTask::ShieldFromAssetLock { seed_hash, amount_duffs: param.amount_duffs, - source_address: None, + source_address, }); let result = dispatch_task(&ctx, task) @@ -494,15 +515,18 @@ impl AsyncTool for ShieldedWithdrawTool { // (withdrawal is queued on Platform and settles after confirmation) let seed_hash = resolve::wallet(&ctx, ¶m.wallet_id)?; - let core_address: dash_sdk::dashcore_rpc::dashcore::Address< - dash_sdk::dashcore_rpc::dashcore::address::NetworkUnchecked, - > = param + let core_address = param .to_address - .parse() - .map_err(|e| McpToolError::InvalidParam { - message: format!("Invalid Core address: {e}"), + .parse::>() + .map_err(|_| McpToolError::InvalidParam { + message: "The Core address is invalid.".to_owned(), + })? + .require_network(ctx.network()) + .map_err(|_| McpToolError::InvalidParam { + message: "The Core address does not match the active network.".to_owned(), })?; - let core_address = core_address.assume_checked(); let task = BackendTask::ShieldedTask(ShieldedTask::ShieldedWithdrawal { seed_hash, diff --git a/src/ui/components/address_input.rs b/src/ui/components/address_input.rs index 0c9e51ca2..89ba2cb1a 100644 --- a/src/ui/components/address_input.rs +++ b/src/ui/components/address_input.rs @@ -211,14 +211,21 @@ impl AddressInput { self } - /// Provide wallet data for Core and Platform autocomplete. + /// Provide wallet data for **Core and Platform** autocomplete only. + /// + /// This extracts BIP44 (Core) addresses from `known_addresses` and + /// PlatformPayment addresses from `watched_addresses`. It does NOT + /// extract identities or shielded addresses — those live outside the + /// `Wallet` struct and must be added separately: + /// + /// - **Identities**: call [`with_identities()`] with `QualifiedIdentity` + /// data from `AppContext::load_local_qualified_identities()`. + /// - **Shielded**: call [`with_shielded_balance()`] with the address + /// string from `AppContext::shielded_states`. /// /// Entries are extracted immediately (read lock acquired once per wallet). /// Skips gracefully if a wallet lock is poisoned. /// When more than one wallet is provided, entries are prefixed with the wallet alias. - // TODO: Once shielded state is moved from AppContext::shielded_states into - // Wallet, extract shielded addresses here automatically (like Core and - // Platform) instead of requiring callers to call with_shielded_balance(). pub fn with_wallets(mut self, wallets: &[Arc>]) -> Self { let multi = wallets.len() > 1; for wallet in wallets { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 08b6cd90a..e460c6c97 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1172,9 +1172,9 @@ impl ScreenLike for Screen { Screen::DashPayQRGeneratorScreen(_) => {} Screen::DashPayProfileSearchScreen(screen) => screen.refresh(), // Shielded screens - Screen::ShieldScreen(_) => {} - Screen::ShieldedSendScreen(_) => {} - Screen::UnshieldCreditsScreen(_) => {} + Screen::ShieldScreen(screen) => screen.refresh(), + Screen::ShieldedSendScreen(screen) => screen.refresh(), + Screen::UnshieldCreditsScreen(screen) => screen.refresh(), } } @@ -1242,9 +1242,9 @@ impl ScreenLike for Screen { Screen::DashPayQRGeneratorScreen(_) => {} Screen::DashPayProfileSearchScreen(screen) => screen.refresh_on_arrival(), // Shielded screens - Screen::ShieldScreen(_) => {} - Screen::ShieldedSendScreen(_) => {} - Screen::UnshieldCreditsScreen(_) => {} + Screen::ShieldScreen(screen) => screen.refresh_on_arrival(), + Screen::ShieldedSendScreen(screen) => screen.refresh_on_arrival(), + Screen::UnshieldCreditsScreen(screen) => screen.refresh_on_arrival(), } } diff --git a/src/ui/wallets/send_screen.rs b/src/ui/wallets/send_screen.rs index 8eb497d31..a21f904dc 100644 --- a/src/ui/wallets/send_screen.rs +++ b/src/ui/wallets/send_screen.rs @@ -628,7 +628,9 @@ impl WalletSendScreen { fn get_shielded_balance(&self) -> Option<(WalletSeedHash, u64)> { let seed_hash = self.selected_wallet_seed_hash?; // Try in-memory state first (most accurate, reflects optimistic spend marks) - let states = self.app_context.shielded_states.lock().unwrap(); + let Ok(states) = self.app_context.shielded_states.lock() else { + return None; + }; if let Some(state) = states.get(&seed_hash) { let balance = state.shielded_balance; return if balance > 0 { @@ -1510,13 +1512,16 @@ impl WalletSendScreen { ))) } - /// Shield credits from Platform address to shielded pool (Platform -> Shielded). + /// Shield credits from Platform address(es) to shielded pool (Platform -> Shielded). + /// + /// When the requested amount exceeds a single address balance, multiple + /// addresses are used — one `ShieldCredits` task per address, dispatched + /// sequentially. fn send_platform_to_shielded( &mut self, seed_hash: WalletSeedHash, addresses: Vec<(PlatformAddress, Address, u64)>, ) -> Result { - // Shielding from Platform always deposits into the wallet's own shielded pool. if !matches!( &self.validated_destination, Some(ValidatedAddress::Shielded(_)) @@ -1533,31 +1538,75 @@ impl WalletSendScreen { return Err("Amount must be greater than 0".to_string()); } - // Select the highest-balance platform address as the source - let (from_address, from_balance) = addresses - .iter() - .max_by_key(|(_, _, balance)| *balance) - .map(|(platform_addr, _, balance)| (*platform_addr, *balance)) - .ok_or_else(|| "No platform addresses available".to_string())?; + // Sort addresses by balance descending (greedy allocation) + let mut sorted_addrs = addresses; + sorted_addrs.sort_by(|a, b| b.2.cmp(&a.2)); + + let total_available: u64 = sorted_addrs.iter().map(|(_, _, b)| b).sum(); + if amount_credits > total_available { + return Err(format!( + "Insufficient platform balance. Need {} but total available is {}.", + format_credits_as_dash(amount_credits), + format_credits_as_dash(total_available) + )); + } + + // Allocate amount across addresses (highest balance first), reserving + // per-operation fee headroom so each address can cover its own shield fee. + // Apply the network fee multiplier for consistency with ShieldScreen. + let base_fee = crate::model::fee_estimation::shielded_fee_for_actions( + 2, + dash_sdk::dpp::version::PlatformVersion::latest(), + ); + let multiplier = self.app_context.fee_multiplier_permille().max(1000); + let per_op_fee = base_fee.saturating_mul(multiplier) / 1000; + let mut remaining = amount_credits; + let mut tasks: Vec = Vec::new(); + for (platform_addr, _, balance) in &sorted_addrs { + if remaining == 0 { + break; + } + let available = balance.saturating_sub(per_op_fee); + if available == 0 { + continue; + } + let spend = remaining.min(available); + tasks.push(BackendTask::ShieldedTask( + crate::backend_task::shielded::ShieldedTask::ShieldCredits { + seed_hash, + amount: spend, + from_address: *platform_addr, + nonce_override: None, + }, + )); + remaining -= spend; + } - // Check that the selected source address has sufficient balance - if amount_credits > from_balance { + // Reject if allocation could not cover the full amount after fee deductions + if tasks.is_empty() { + return Err( + "Insufficient platform balance after fees. No address has enough to cover the shield operation fee." + .to_string(), + ); + } + if remaining > 0 { + let max_sendable = amount_credits.saturating_sub(remaining); return Err(format!( - "Insufficient platform balance. Need {} but highest address has {}", + "Insufficient platform balance after fees. Need {} but only {} is available after estimated shield fees.", format_credits_as_dash(amount_credits), - format_credits_as_dash(from_balance) + format_credits_as_dash(max_sendable), )); } self.mark_sending(); - Ok(AppAction::BackendTask(BackendTask::ShieldedTask( - crate::backend_task::shielded::ShieldedTask::ShieldCredits { - seed_hash, - amount: amount_credits, - from_address, - nonce_override: None, - }, - ))) + if tasks.len() == 1 { + Ok(AppAction::BackendTask(tasks.into_iter().next().unwrap())) + } else { + Ok(AppAction::BackendTasks( + tasks, + crate::app::BackendTasksExecutionMode::Sequential, + )) + } } /// Top up an identity from Platform addresses (Platform -> Identity). @@ -1749,9 +1798,11 @@ impl WalletSendScreen { .and_then(|v| v.as_identity_id().copied()) .ok_or_else(|| "Invalid identity ID".to_string())?; + // Prevent self-send (same identity as source and destination) if to_identity_id == qualified_identity.identity.id() { return Err( - "Cannot transfer to the same identity. Choose a different destination.".to_string(), + "You cannot send credits to the same identity. Please choose a different destination." + .to_string(), ); } @@ -1897,11 +1948,13 @@ impl WalletSendScreen { ui.horizontal(|ui| { let mut selected = is_identity_selected; if ui.radio_value(&mut selected, true, "").changed() && selected { - if let Some(identity) = self - .selected_identity - .clone() - .or_else(|| identities.first().cloned()) - { + if let Some(identity) = self.selected_identity.clone().or_else(|| { + // Default to the identity with the highest balance + identities + .iter() + .max_by_key(|qi| qi.identity.balance()) + .cloned() + }) { self.selected_source = Some(SourceSelection::Identity(Box::new(identity.clone()))); self.selected_identity = Some(identity); @@ -2066,71 +2119,99 @@ impl WalletSendScreen { fn render_destination_input(&mut self, ui: &mut Ui) { let developer_mode = self.app_context.is_developer_mode(); - let addr_input = self.address_input.get_or_insert_with(|| { - let allowed_kinds = match &self.selected_source { - Some(SourceSelection::CoreWallet) => { - let mut kinds = vec![AddressKind::Core, AddressKind::Platform]; - if developer_mode { - kinds.push(AddressKind::Shielded); + // Pre-load data outside the closure to avoid double-borrow of self. + // Filter out the source identity (if any) to prevent self-sends. + let source_identity_id = if let Some(SourceSelection::Identity(qi)) = &self.selected_source + { + Some(qi.identity.id()) + } else { + None + }; + // Only load identities and shielded state when building a new AddressInput + // (get_or_insert_with fires once). Avoids per-frame DB queries. + let addr_input = if self.address_input.is_some() { + self.address_input.as_mut().unwrap() + } else { + let loaded_identities: Vec<_> = self + .get_loaded_identities() + .into_iter() + .filter(|qi| Some(qi.identity.id()) != source_identity_id) + .collect(); + let shielded_info: Option<(String, u64)> = + self.selected_wallet_seed_hash.and_then(|sh| { + let states = self.app_context.shielded_states.lock().ok()?; + let state = states.get(&sh)?; + use dash_sdk::dpp::address_funds::OrchardAddress; + let raw = state.keys.default_address.to_raw_address_bytes(); + let addr = OrchardAddress::from_raw_bytes(&raw).ok()?; + Some(( + addr.to_bech32m_string(self.app_context.network), + state.shielded_balance, + )) + }); + self.address_input.get_or_insert_with(|| { + let allowed_kinds = match &self.selected_source { + Some(SourceSelection::CoreWallet) => { + let mut kinds = vec![AddressKind::Core, AddressKind::Platform]; + if developer_mode { + kinds.push(AddressKind::Shielded); + } + kinds.push(AddressKind::Identity); + kinds } - kinds.push(AddressKind::Identity); - kinds - } - Some(SourceSelection::PlatformAddresses(_)) => { - let mut kinds = vec![AddressKind::Platform, AddressKind::Core]; - if developer_mode { - kinds.push(AddressKind::Shielded); + Some(SourceSelection::PlatformAddresses(_)) => { + let mut kinds = vec![AddressKind::Platform, AddressKind::Core]; + if developer_mode { + kinds.push(AddressKind::Shielded); + } + kinds.push(AddressKind::Identity); + kinds + } + Some(SourceSelection::Identity(_)) => { + vec![ + AddressKind::Core, + AddressKind::Platform, + AddressKind::Identity, + ] + } + Some(SourceSelection::Shielded(..)) => { + vec![ + AddressKind::Shielded, + AddressKind::Platform, + AddressKind::Core, + ] + } + None => AddressKind::ALL.to_vec(), + }; + + let mut builder = AddressInput::new(self.app_context.network) + .with_label("Send to") + .with_hint_text("Enter address (X.../y.../dash1.../tdash1...)") + .with_address_kinds(&allowed_kinds) + .with_exclude_change(true); + + // Provide all wallet addresses for autocomplete + if let Ok(wallets_guard) = self.app_context.wallets.read() { + let all_wallets: Vec>> = + wallets_guard.values().cloned().collect(); + if !all_wallets.is_empty() { + builder = builder.with_wallets(&all_wallets); } - kinds.push(AddressKind::Identity); - kinds - } - Some(SourceSelection::Identity(_)) => { - vec![ - AddressKind::Core, - AddressKind::Platform, - AddressKind::Identity, - ] - } - Some(SourceSelection::Shielded(..)) => { - vec![ - AddressKind::Shielded, - AddressKind::Platform, - AddressKind::Core, - ] } - None => AddressKind::ALL.to_vec(), - }; - let mut builder = AddressInput::new(self.app_context.network) - .with_label("Send to") - .with_hint_text("Enter address (X.../y.../dash1.../tdash1...)") - .with_address_kinds(&allowed_kinds) - .with_exclude_change(true); - - // Provide all wallet addresses for autocomplete - if let Ok(wallets_guard) = self.app_context.wallets.read() { - let all_wallets: Vec>> = - wallets_guard.values().cloned().collect(); - if !all_wallets.is_empty() { - builder = builder.with_wallets(&all_wallets); + // Add identities for autocomplete (searchable by alias/DPNS name) + if !loaded_identities.is_empty() { + builder = builder.with_identities(&loaded_identities); } - } - // Add shielded address for autocomplete (if wallet has shielded state) - if let Some(seed_hash) = self.selected_wallet_seed_hash - && let Ok(states) = self.app_context.shielded_states.lock() - && let Some(state) = states.get(&seed_hash) - { - use dash_sdk::dpp::address_funds::OrchardAddress; - let raw = state.keys.default_address.to_raw_address_bytes(); - if let Ok(orchard_addr) = OrchardAddress::from_raw_bytes(&raw) { - let addr_str = orchard_addr.to_bech32m_string(self.app_context.network); - builder = builder.with_shielded_balance(addr_str, state.shielded_balance); + // Add shielded address for autocomplete (if wallet has shielded state) + if let Some((addr_str, balance)) = &shielded_info { + builder = builder.with_shielded_balance(addr_str.clone(), *balance); } - } - builder - }); + builder + }) + }; let resp = addr_input.show(ui); resp.inner.update(&mut self.validated_destination); diff --git a/src/ui/wallets/shield_screen.rs b/src/ui/wallets/shield_screen.rs index 63961b3fb..452bde06b 100644 --- a/src/ui/wallets/shield_screen.rs +++ b/src/ui/wallets/shield_screen.rs @@ -91,11 +91,15 @@ pub struct ShieldScreen { batch_amount: Option, /// Frozen platform address for the current batch. batch_address: Option, + // Cached wallet data to avoid per-frame RwLock reads (CODE-007) + cached_base_nonce: Option, + cached_platform_balance: Option, + cached_core_balance: Option, } impl ShieldScreen { pub fn new(seed_hash: WalletSeedHash, app_context: &Arc) -> Self { - Self { + let mut screen = Self { app_context: app_context.clone(), seed_hash, address_input: None, @@ -115,7 +119,12 @@ impl ShieldScreen { json_preview: None, batch_amount: None, batch_address: None, - } + cached_base_nonce: None, + cached_platform_balance: None, + cached_core_balance: None, + }; + screen.refresh_cached_balances(); + screen } /// Reset the address and amount inputs — called when AppContext switches network. @@ -124,6 +133,9 @@ impl ShieldScreen { self.validated_source = None; self.amount_input = None; self.amount = None; + self.cached_base_nonce = None; + self.cached_platform_balance = None; + self.cached_core_balance = None; } fn parse_repeat_count(&self) -> u32 { @@ -141,58 +153,67 @@ impl ShieldScreen { .and_then(|v| v.as_platform().copied()) } - /// Read the current nonce for the selected platform address from the wallet. + /// Refresh cached wallet data (balance, nonce) from the RwLock-protected wallet. + fn refresh_cached_balances(&mut self) { + // Clone the wallet Arc while holding the wallets map read lock, then + // drop the map lock before acquiring the per-wallet lock to avoid + // lock-order deadlocks with code that holds a wallet lock and needs + // wallets write access. + let wallet_arc = self + .app_context + .wallets + .read() + .ok() + .and_then(|w| w.get(&self.seed_hash).cloned()); + let Some(wallet_arc) = wallet_arc else { + return; + }; + let wallet_guard = wallet_arc.read().ok(); + + if let Some(wallet) = &wallet_guard { + // Platform nonce and balance for selected address + if let Some(from_address) = self.selected_platform_address() { + let info = wallet + .platform_address_info + .iter() + .find_map(|(addr, info)| { + let platform_addr = PlatformAddress::try_from(addr.clone()).ok()?; + (platform_addr == from_address).then_some(info) + }); + self.cached_base_nonce = info.map(|i| i.nonce); + self.cached_platform_balance = info.map(|i| i.balance); + } else { + self.cached_base_nonce = None; + self.cached_platform_balance = None; + } + + // Core balance + if let Some(addr) = self.validated_source.as_ref().and_then(|v| v.as_core()) { + self.cached_core_balance = + Some(wallet.address_balances.get(addr).copied().unwrap_or(0)); + } else { + self.cached_core_balance = Some(wallet.total_balance_duffs()); + } + } else { + self.cached_base_nonce = None; + self.cached_platform_balance = None; + self.cached_core_balance = Some(0); + } + } + + /// Return the cached nonce for the selected platform address. fn read_base_nonce(&self) -> Option { - let from_address = self.selected_platform_address()?; - let wallets = self.app_context.wallets.read().unwrap(); - let wallet_arc = wallets.get(&self.seed_hash)?; - let wallet = wallet_arc.read().unwrap(); - wallet - .platform_address_info - .iter() - .find_map(|(addr, info)| { - let platform_addr = PlatformAddress::try_from(addr.clone()).ok()?; - if platform_addr == from_address { - Some(info.nonce) - } else { - None - } - }) + self.cached_base_nonce } - /// Read the current balance (in credits) for the selected platform address. + /// Return the cached balance (credits) for the selected platform address. fn read_platform_balance(&self) -> Option { - let from_address = self.selected_platform_address()?; - let wallets = self.app_context.wallets.read().unwrap(); - let wallet_arc = wallets.get(&self.seed_hash)?; - let wallet = wallet_arc.read().unwrap(); - wallet - .platform_address_info - .iter() - .find_map(|(addr, info)| { - let platform_addr = PlatformAddress::try_from(addr.clone()).ok()?; - if platform_addr == from_address { - Some(info.balance) - } else { - None - } - }) + self.cached_platform_balance } - /// Read the core wallet balance in duffs. + /// Return the cached core wallet balance in duffs. fn read_core_balance_duffs(&self) -> u64 { - let wallets = self.app_context.wallets.read().unwrap(); - let Some(wallet_arc) = wallets.get(&self.seed_hash) else { - return 0; - }; - let wallet = wallet_arc.read().unwrap(); - // If a specific Core address is selected, return its individual balance - // so the max-amount display matches the funds actually available for this address. - if let Some(addr) = self.validated_source.as_ref().and_then(|v| v.as_core()) { - wallet.address_balances.get(addr).copied().unwrap_or(0) - } else { - wallet.total_balance_duffs() - } + self.cached_core_balance.unwrap_or(0) } /// Build a single ShieldCredits task with optional nonce override. @@ -704,6 +725,7 @@ impl ScreenLike for ShieldScreen { // Reset amount input when source changes (different balance constraints) self.amount_input = None; self.amount = None; + self.refresh_cached_balances(); } ui.add_space(5.0); @@ -763,9 +785,11 @@ impl ScreenLike for ShieldScreen { if self.validated_source.is_some() { let max_credits = match source_kind { Some(AddressKind::Platform) => { - let fee_headroom = - shielded_fee_for_actions(2, PlatformVersion::latest()) - .saturating_mul(2); + let base_fee = + shielded_fee_for_actions(2, PlatformVersion::latest()); + let multiplier = + self.app_context.fee_multiplier_permille().max(1000); + let fee_headroom = base_fee.saturating_mul(multiplier) / 1000; self.read_platform_balance() .map(|b| b.saturating_sub(fee_headroom)) } @@ -841,7 +865,11 @@ impl ScreenLike for ShieldScreen { // Buttons (only when not busy and source is selected) if !is_busy && self.status == Status::NotStarted && self.validated_source.is_some() { - let can_confirm = self.amount.as_ref().map(|a| a.value()).is_some(); + let can_confirm = self + .amount + .as_ref() + .map(|a| a.value()) + .is_some_and(|v| v > 0); ui.horizontal(|ui| { let button_label = match source_kind { @@ -971,7 +999,12 @@ impl ScreenLike for ShieldScreen { action } + fn refresh_on_arrival(&mut self) { + self.refresh_cached_balances(); + } + fn display_task_result(&mut self, result: BackendTaskSuccessResult) { + self.refresh_cached_balances(); let ctx = self.app_context.egui_ctx().clone(); match result { BackendTaskSuccessResult::ShieldedCreditsShielded { seed_hash, amount } diff --git a/src/ui/wallets/shielded_send_screen.rs b/src/ui/wallets/shielded_send_screen.rs index cf6b4263f..43bc7b82e 100644 --- a/src/ui/wallets/shielded_send_screen.rs +++ b/src/ui/wallets/shielded_send_screen.rs @@ -41,13 +41,12 @@ pub struct ShieldedSendScreen { impl ShieldedSendScreen { pub fn new(seed_hash: WalletSeedHash, app_context: &Arc) -> Self { - let max_balance = { - let states = app_context.shielded_states.lock().unwrap(); - states - .get(&seed_hash) - .map(|s| s.shielded_balance) - .unwrap_or(0) - }; + let max_balance = app_context + .shielded_states + .lock() + .ok() + .and_then(|states| states.get(&seed_hash).map(|s| s.shielded_balance)) + .unwrap_or(0); Self { app_context: app_context.clone(), @@ -89,6 +88,14 @@ impl ShieldedSendScreen { } impl ScreenLike for ShieldedSendScreen { + fn refresh_on_arrival(&mut self) { + if let Ok(states) = self.app_context.shielded_states.lock() + && let Some(state) = states.get(&self.seed_hash) + { + self.max_balance = state.shielded_balance; + } + } + fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = self .pending_refresh_task diff --git a/src/ui/wallets/unshield_credits_screen.rs b/src/ui/wallets/unshield_credits_screen.rs index eafb8183a..baf766eda 100644 --- a/src/ui/wallets/unshield_credits_screen.rs +++ b/src/ui/wallets/unshield_credits_screen.rs @@ -12,6 +12,7 @@ use crate::ui::components::component_trait::Component; 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::theme::DashColors; use crate::ui::{MessageType, RootScreenType, ScreenLike}; use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; use eframe::egui::{self, Context}; @@ -50,13 +51,12 @@ impl UnshieldCreditsScreen { } pub fn new(seed_hash: WalletSeedHash, app_context: &Arc) -> Self { - let max_balance = { - let states = app_context.shielded_states.lock().unwrap(); - states - .get(&seed_hash) - .map(|s| s.shielded_balance) - .unwrap_or(0) - }; + let max_balance = app_context + .shielded_states + .lock() + .ok() + .and_then(|states| states.get(&seed_hash).map(|s| s.shielded_balance)) + .unwrap_or(0); Self { app_context: app_context.clone(), @@ -76,6 +76,14 @@ impl UnshieldCreditsScreen { } impl ScreenLike for UnshieldCreditsScreen { + fn refresh_on_arrival(&mut self) { + if let Ok(states) = self.app_context.shielded_states.lock() + && let Some(state) = states.get(&self.seed_hash) + { + self.max_balance = state.shielded_balance; + } + } + fn ui(&mut self, ctx: &Context) -> AppAction { let mut action = self .pending_refresh_task @@ -114,13 +122,15 @@ impl ScreenLike for UnshieldCreditsScreen { )); ui.add_space(15.0); + let dark_mode = ui.ctx().style().visuals.dark_mode; + // Error/success messages if let Some(err) = &self.error_message { - ui.colored_label(Color32::from_rgb(255, 100, 100), err); + ui.colored_label(DashColors::error_color(dark_mode), err); ui.add_space(5.0); } if let Some(msg) = &self.success_message { - ui.colored_label(Color32::DARK_GREEN, msg); + ui.colored_label(DashColors::success_color(dark_mode), msg); if self.balance_update_pending { ui.add_space(8.0); ui.label( @@ -157,13 +167,13 @@ impl ScreenLike for UnshieldCreditsScreen { match self.validated_destination.as_ref().map(|v| v.kind()) { Some(AddressKind::Platform) => { ui.colored_label( - Color32::DARK_GREEN, + DashColors::success_color(dark_mode), "Platform address — credits will be moved to this platform address", ); } Some(AddressKind::Core) => { ui.colored_label( - Color32::DARK_GREEN, + DashColors::success_color(dark_mode), "Core address — credits will be withdrawn as DASH to this address", ); }