diff --git a/src/ui/wallets/wallets_screen/address_table.rs b/src/ui/wallets/wallets_screen/address_table.rs new file mode 100644 index 000000000..2e92c4af8 --- /dev/null +++ b/src/ui/wallets/wallets_screen/address_table.rs @@ -0,0 +1,398 @@ +use crate::app::AppAction; +use crate::model::wallet::{DerivationPathHelpers, DerivationPathReference}; +use crate::ui::wallets::account_summary::AccountCategory; +use crate::ui::{MessageType, ScreenLike}; +use dash_sdk::dashcore_rpc::dashcore::{Address, Network}; +use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; +use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath}; +use eframe::egui::{self, Ui}; +use egui_extras::{Column, TableBuilder}; + +use super::WalletsBalancesScreen; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum SortColumn { + Address, + Balance, + UTXOs, + TotalReceived, + Type, + Index, + DerivationPath, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum SortOrder { + Ascending, + Descending, +} + +pub(super) struct AddressData { + address: Address, + balance: u64, + /// Platform credits balance for Platform Payment addresses + platform_credits: u64, + utxo_count: usize, + total_received: u64, + address_type: String, + index: u32, + derivation_path: DerivationPath, + account_category: AccountCategory, + account_index: Option, +} + +impl AddressData { + /// Returns the address formatted for display. + /// Platform Payment addresses are shown in DIP-18 Bech32m format (e.g., tevo1...). + fn display_address(&self, network: Network) -> String { + if self.account_category == AccountCategory::PlatformPayment { + use dash_sdk::dpp::address_funds::PlatformAddress; + PlatformAddress::try_from(self.address.clone()) + .map(|pa| pa.to_bech32m_string(network)) + .unwrap_or_else(|_| self.address.to_string()) + } else { + self.address.to_string() + } + } +} + +impl WalletsBalancesScreen { + pub(super) fn toggle_sort(&mut self, column: SortColumn) { + if self.sort_column == column { + self.sort_order = match self.sort_order { + SortOrder::Ascending => SortOrder::Descending, + SortOrder::Descending => SortOrder::Ascending, + }; + } else { + self.sort_column = column; + self.sort_order = SortOrder::Ascending; + } + } + + #[allow(clippy::ptr_arg)] + fn sort_address_data(&self, data: &mut Vec) { + data.sort_by(|a, b| { + let order = match self.sort_column { + SortColumn::Address => a.address.cmp(&b.address), + SortColumn::Balance => a.balance.cmp(&b.balance), + SortColumn::UTXOs => a.utxo_count.cmp(&b.utxo_count), + SortColumn::TotalReceived => a.total_received.cmp(&b.total_received), + SortColumn::Type => a.address_type.cmp(&b.address_type), + SortColumn::Index => a.index.cmp(&b.index), + SortColumn::DerivationPath => a.derivation_path.cmp(&b.derivation_path), + }; + + if self.sort_order == SortOrder::Ascending { + order + } else { + order.reverse() + } + }); + } + + pub(super) fn categorize_path( + path: &DerivationPath, + reference: DerivationPathReference, + ) -> (AccountCategory, Option) { + let category = AccountCategory::from_reference(reference); + let index = match category { + AccountCategory::Bip44 | AccountCategory::Bip32 => path.bip44_account_index(), + _ => None, + }; + (category, index) + } + + pub(super) fn render_address_table(&mut self, ui: &mut Ui) -> AppAction { + let action = AppAction::None; + + // Move the data preparation into its own scope + let mut address_data = { + let wallet = self.selected_wallet.as_ref().unwrap().read().unwrap(); + + // Prepare data for the table + wallet + .known_addresses + .iter() + .map(|(address, derivation_path)| { + let utxo_info = wallet.utxos.get(address); + + let utxo_count = utxo_info.map(|outpoints| outpoints.len()).unwrap_or(0); + + // Get total received from the wallet (fetched from Core RPC) + let total_received = wallet + .address_total_received + .get(address) + .cloned() + .unwrap_or(0u64); + + let index = derivation_path + .into_iter() + .last() + .cloned() + .unwrap_or(ChildNumber::Normal { index: 0 }); + let index = match index { + ChildNumber::Normal { index } => index, + ChildNumber::Hardened { index } => index, + _ => 0, + }; + let address_type = + if derivation_path.is_bip44_external(self.app_context.network) { + "Funds".to_string() + } else if derivation_path.is_bip44_change(self.app_context.network) { + "Change".to_string() + } else if derivation_path.is_asset_lock_funding(self.app_context.network) { + "Identity Creation".to_string() + } else if derivation_path.is_platform_payment(self.app_context.network) { + "Platform".to_string() + } else { + "System".to_string() + }; + + let path_reference = wallet + .watched_addresses + .get(derivation_path) + .map(|info| info.path_reference) + .unwrap_or(DerivationPathReference::Unknown); + let (account_category, account_index) = + Self::categorize_path(derivation_path, path_reference); + + // Get Platform credits balance for Platform Payment addresses + // Use canonical lookup to handle potential Address key mismatches + let platform_credits = wallet + .get_platform_address_info(address) + .map(|info| info.balance) + .unwrap_or_default(); + + AddressData { + address: address.clone(), + balance: wallet + .address_balances + .get(address) + .cloned() + .unwrap_or_default(), + platform_credits, + utxo_count, + total_received, + address_type, + index, + derivation_path: derivation_path.clone(), + account_category, + account_index, + } + }) + .collect::>() + }; // The borrow of `wallet` ends here + + // Now you can use `self` mutably without conflict + // Sort the data + self.sort_address_data(&mut address_data); + + if let Some((category, index)) = self.selected_account.clone() { + address_data + .retain(|data| data.account_category == category && data.account_index == index); + } + + // Space allocation for UI elements is handled by the layout system + + // Render the table + TableBuilder::new(ui) + .id_salt("addresses_table") + .striped(false) + .resizable(true) + .vscroll(false) + .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) + .column(Column::auto()) // Address + .column(Column::initial(140.0)) // Balance + .column(Column::initial(70.0)) // UTXOs + .column(Column::initial(150.0)) // Total Received + .column(Column::initial(100.0)) // Type + .column(Column::initial(70.0)) // Index + .column(Column::initial(120.0)) // Derivation Path + .column(Column::initial(120.0)) // Actions + .header(30.0, |mut header| { + header.col(|ui| { + let label = if self.sort_column == SortColumn::Address { + match self.sort_order { + SortOrder::Ascending => "Address ^", + SortOrder::Descending => "Address v", + } + } else { + "Address" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::Address); + } + }); + header.col(|ui| { + let label = if self.sort_column == SortColumn::Balance { + match self.sort_order { + SortOrder::Ascending => "Balance (DASH) ^", + SortOrder::Descending => "Balance (DASH) v", + } + } else { + "Balance (DASH)" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::Balance); + } + }); + header.col(|ui| { + let label = if self.sort_column == SortColumn::UTXOs { + match self.sort_order { + SortOrder::Ascending => "UTXOs ^", + SortOrder::Descending => "UTXOs v", + } + } else { + "UTXOs" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::UTXOs); + } + }); + header.col(|ui| { + let label = if self.sort_column == SortColumn::TotalReceived { + match self.sort_order { + SortOrder::Ascending => "Total Received (DASH) ^", + SortOrder::Descending => "Total Received (DASH) v", + } + } else { + "Total Received (DASH)" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::TotalReceived); + } + }); + header.col(|ui| { + let label = if self.sort_column == SortColumn::Type { + match self.sort_order { + SortOrder::Ascending => "Type ^", + SortOrder::Descending => "Type v", + } + } else { + "Type" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::Type); + } + }); + header.col(|ui| { + let label = if self.sort_column == SortColumn::Index { + match self.sort_order { + SortOrder::Ascending => "Index ^", + SortOrder::Descending => "Index v", + } + } else { + "Index" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::Index); + } + }); + header.col(|ui| { + let label = if self.sort_column == SortColumn::DerivationPath { + match self.sort_order { + SortOrder::Ascending => "Full Path ^", + SortOrder::Descending => "Full Path v", + } + } else { + "Full Path" + }; + if ui.button(label).clicked() { + self.toggle_sort(SortColumn::DerivationPath); + } + }); + header.col(|ui| { + ui.label("Private Key"); + }); + }) + .body(|mut body| { + let network = self.app_context.network; + for data in &address_data { + body.row(25.0, |mut row| { + let is_key_only = data.account_category.is_key_only(); + let is_platform_payment = + data.account_category == AccountCategory::PlatformPayment; + + row.col(|ui| { + ui.label(data.display_address(network)); + }); + row.col(|ui| { + if is_key_only { + ui.label("N/A"); + } else if is_platform_payment { + // Platform credits: convert from credits to DASH + // Credits are in duffs * 1000, so divide by 1000 then by 1e8 + let dash_balance = + data.platform_credits as f64 / CREDITS_PER_DUFF as f64 / 1e8; + ui.label(format!("{:.8}", dash_balance)); + } else { + let dash_balance = data.balance as f64 * 1e-8; + ui.label(format!("{:.8}", dash_balance)); + } + }); + row.col(|ui| { + // Key-only addresses and Platform addresses don't hold UTXOs + if is_key_only || is_platform_payment { + ui.label("N/A"); + } else { + ui.label(format!("{}", data.utxo_count)); + } + }); + row.col(|ui| { + // These address types don't track historical received amounts + if is_key_only || is_platform_payment { + ui.label("N/A"); + } else { + let dash_received = data.total_received as f64 * 1e-8; + ui.label(format!("{:.8}", dash_received)); + } + }); + row.col(|ui| { + ui.label(&data.address_type); + }); + row.col(|ui| { + ui.label(format!("{}", data.index)); + }); + row.col(|ui| { + ui.label(format!("{}", data.derivation_path)); + }); + row.col(|ui| { + if ui.button("View Key").clicked() { + // Check if wallet is locked first + let wallet_locked = self + .selected_wallet + .as_ref() + .map(|w| { + w.read() + .map(|g| g.uses_password && !g.is_open()) + .unwrap_or(false) + }) + .unwrap_or(false); + + let display_address = data.display_address(network); + + if wallet_locked { + // Store pending info and show unlock popup + self.private_key_dialog.pending_derivation_path = + Some(data.derivation_path.clone()); + self.private_key_dialog.pending_address = Some(display_address); + self.wallet_unlock_popup.open(); + } else { + match self.derive_private_key_wif(&data.derivation_path) { + Ok(key) => { + self.private_key_dialog.is_open = true; + self.private_key_dialog.address = display_address; + self.private_key_dialog.private_key_wif = key; + self.private_key_dialog.show_key = false; + } + Err(err) => self.display_message(&err, MessageType::Error), + } + } + } + }); + }); + } + }); + action + } +} diff --git a/src/ui/wallets/wallets_screen/asset_locks.rs b/src/ui/wallets/wallets_screen/asset_locks.rs new file mode 100644 index 000000000..f7b152c10 --- /dev/null +++ b/src/ui/wallets/wallets_screen/asset_locks.rs @@ -0,0 +1,166 @@ +use crate::app::AppAction; +use crate::model::wallet::DerivationPathHelpers; +use crate::ui::ScreenType; +use crate::ui::theme::DashColors; +use eframe::egui::{self, Ui}; +use egui::{Color32, Frame, Margin, RichText}; +use egui_extras::{Column, TableBuilder}; + +use super::WalletsBalancesScreen; + +impl WalletsBalancesScreen { + pub(super) fn render_wallet_asset_locks(&mut self, ui: &mut Ui) -> AppAction { + let mut app_action = AppAction::None; + let mut open_fund_dialog_for_idx: Option<(usize, Vec<(String, u64)>)> = None; + let mut recover_asset_locks_clicked = false; + + if let Some(arc_wallet) = &self.selected_wallet { + let wallet = arc_wallet.read().unwrap(); + + let dark_mode = ui.ctx().style().visuals.dark_mode; + Frame::new() + .fill(DashColors::surface(dark_mode)) + .corner_radius(5.0) + .inner_margin(Margin::same(15)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .show(ui, |ui| { + let dark_mode = ui.ctx().style().visuals.dark_mode; + ui.horizontal(|ui| { + ui.heading(RichText::new("Asset Locks").color(DashColors::text_primary(dark_mode))); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Create Asset Lock").clicked() { + app_action = AppAction::AddScreen( + ScreenType::CreateAssetLock(arc_wallet.clone()).create_screen(&self.app_context) + ); + } + if ui.button("Search for Unused").on_hover_text("Scan Core wallet for untracked asset locks").clicked() { + recover_asset_locks_clicked = true; + } + }); + }); + ui.add_space(10.0); + + if wallet.unused_asset_locks.is_empty() { + ui.vertical_centered(|ui| { + ui.add_space(20.0); + ui.label(RichText::new("No asset locks found").color(Color32::GRAY).size(14.0)); + ui.add_space(10.0); + ui.label(RichText::new("Asset locks are special transactions that can be used to create identities or fund Platform addresses").color(Color32::GRAY).size(12.0)); + ui.add_space(20.0); + }); + } else { + // Collect Platform addresses for the fund dialog (using DIP-18 Bech32m format) + // Get from known_addresses where path is platform payment + let network = self.app_context.network; + let platform_addresses: Vec<(String, u64)> = wallet + .known_addresses + .iter() + .filter(|(_, path)| path.is_platform_payment(network)) + .filter_map(|(addr, _)| { + use dash_sdk::dpp::address_funds::PlatformAddress; + let balance = wallet + .get_platform_address_info(addr) + .map(|info| info.balance) + .unwrap_or(0); + PlatformAddress::try_from(addr.clone()) + .ok() + .map(|pa| (pa.to_bech32m_string(network), balance)) + }) + .collect(); + + egui::ScrollArea::both() + .id_salt("asset_locks_table") + .min_scrolled_height(200.0) + .show(ui, |ui| { + TableBuilder::new(ui) + .striped(false) + .resizable(true) + .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) + .column(Column::initial(200.0)) // Transaction ID + .column(Column::initial(100.0)) // Address + .column(Column::initial(100.0)) // Amount (Duffs) + .column(Column::initial(100.0)) // InstantLock status + .column(Column::initial(100.0)) // Usable status + .column(Column::initial(200.0)) // Actions + .header(30.0, |mut header| { + header.col(|ui| { + ui.label("Transaction ID"); + }); + header.col(|ui| { + ui.label("Address"); + }); + header.col(|ui| { + ui.label("Amount (Duffs)"); + }); + header.col(|ui| { + ui.label("InstantLock"); + }); + header.col(|ui| { + ui.label("Usable"); + }); + header.col(|ui| { + ui.label("Actions"); + }); + }) + .body(|mut body| { + for (index, (tx, address, amount, islock, proof)) in wallet.unused_asset_locks.iter().enumerate() { + body.row(25.0, |mut row| { + row.col(|ui| { + ui.label(tx.txid().to_string()); + }); + row.col(|ui| { + ui.label(address.to_string()); + }); + row.col(|ui| { + ui.label(format!("{}", amount)); + }); + row.col(|ui| { + let status = if islock.is_some() { "Yes" } else { "No" }; + ui.label(status); + }); + row.col(|ui| { + let status = if proof.is_some() { "Yes" } else { "No" }; + ui.label(status); + }); + row.col(|ui| { + if ui.small_button("View").on_hover_text("View full asset lock details").clicked() { + app_action = AppAction::AddScreen( + ScreenType::AssetLockDetail( + wallet.seed_hash(), + index + ).create_screen(&self.app_context) + ); + } + if proof.is_some() + && ui.small_button("Fund").on_hover_text("Fund a Platform address with this asset lock").clicked() { + open_fund_dialog_for_idx = Some((index, platform_addresses.clone())); + } + }); + }); + } + }); + }); + } + }); + } else { + ui.label("No wallet selected."); + } + + // Handle dialog opening outside the borrow + if let Some((idx, platform_addresses)) = open_fund_dialog_for_idx { + self.fund_platform_dialog.selected_asset_lock_index = Some(idx); + self.fund_platform_dialog.is_open = true; + self.fund_platform_dialog.platform_addresses = platform_addresses; + self.fund_platform_dialog.selected_platform_address = None; + self.fund_platform_dialog.status = None; + self.fund_platform_dialog.is_processing = false; + } + + // Handle recover asset locks button click - use custom action to check lock status + if recover_asset_locks_clicked { + app_action = AppAction::Custom("SearchAssetLocks".to_string()); + } + + app_action + } +} diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 3fb3ce019..4af4817ff 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -1,13 +1,14 @@ +mod address_table; +mod asset_locks; mod dialogs; +mod single_key_view; use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::BackendTask; use crate::backend_task::core::CoreTask; use crate::context::AppContext; use crate::model::amount::Amount; -use crate::model::wallet::{ - DerivationPathHelpers, DerivationPathReference, Wallet, WalletSeedHash, WalletTransaction, -}; +use crate::model::wallet::{Wallet, WalletSeedHash, WalletTransaction}; use crate::spv::CoreBackendMode; use crate::ui::components::component_trait::Component; use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; @@ -22,36 +23,19 @@ use crate::ui::wallets::account_summary::{ }; use crate::ui::{MessageType, RootScreenType, ScreenLike, ScreenType}; use chrono::{DateTime, Utc}; -use dash_sdk::dashcore_rpc::dashcore::{Address, Network}; +use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; -use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath}; use eframe::egui::{self, ComboBox, Context, Ui}; use egui::{Color32, Frame, Margin, RichText}; use egui_extras::{Column, TableBuilder}; use std::sync::{Arc, RwLock}; use crate::model::wallet::single_key::SingleKeyWallet; +use address_table::{SortColumn, SortOrder}; use dialogs::{ FundPlatformAddressDialogState, PrivateKeyDialogState, ReceiveDialogState, SendDialogState, }; -#[derive(Clone, Copy, PartialEq, Eq)] -enum SortColumn { - Address, - Balance, - UTXOs, - TotalReceived, - Type, - Index, - DerivationPath, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SortOrder { - Ascending, - Descending, -} - /// Refresh mode for dev mode dropdown - controls what gets refreshed #[derive(Clone, Copy, PartialEq, Eq, Default)] enum RefreshMode { @@ -131,36 +115,6 @@ pub struct WalletsBalancesScreen { refresh_mode: RefreshMode, } -// Define a struct to hold the address data -struct AddressData { - address: Address, - balance: u64, - /// Platform credits balance for Platform Payment addresses - platform_credits: u64, - utxo_count: usize, - total_received: u64, - address_type: String, - index: u32, - derivation_path: DerivationPath, - account_category: AccountCategory, - account_index: Option, -} - -impl AddressData { - /// Returns the address formatted for display. - /// Platform Payment addresses are shown in DIP-18 Bech32m format (e.g., tevo1...). - fn display_address(&self, network: Network) -> String { - if self.account_category == AccountCategory::PlatformPayment { - use dash_sdk::dpp::address_funds::PlatformAddress; - PlatformAddress::try_from(self.address.clone()) - .map(|pa| pa.to_bech32m_string(network)) - .unwrap_or_else(|_| self.address.to_string()) - } else { - self.address.to_string() - } - } -} - impl WalletsBalancesScreen { pub fn new(app_context: &Arc) -> Self { // Try to restore previously selected wallet from AppContext @@ -363,39 +317,6 @@ impl WalletsBalancesScreen { } } - fn toggle_sort(&mut self, column: SortColumn) { - if self.sort_column == column { - self.sort_order = match self.sort_order { - SortOrder::Ascending => SortOrder::Descending, - SortOrder::Descending => SortOrder::Ascending, - }; - } else { - self.sort_column = column; - self.sort_order = SortOrder::Ascending; - } - } - - #[allow(clippy::ptr_arg)] - fn sort_address_data(&self, data: &mut Vec) { - data.sort_by(|a, b| { - let order = match self.sort_column { - SortColumn::Address => a.address.cmp(&b.address), - SortColumn::Balance => a.balance.cmp(&b.balance), - SortColumn::UTXOs => a.utxo_count.cmp(&b.utxo_count), - SortColumn::TotalReceived => a.total_received.cmp(&b.total_received), - SortColumn::Type => a.address_type.cmp(&b.address_type), - SortColumn::Index => a.index.cmp(&b.index), - SortColumn::DerivationPath => a.derivation_path.cmp(&b.derivation_path), - }; - - if self.sort_order == SortOrder::Ascending { - order - } else { - order.reverse() - } - }); - } - fn render_wallet_selection(&mut self, ui: &mut Ui) -> AppAction { let action = AppAction::None; @@ -664,300 +585,6 @@ impl WalletsBalancesScreen { action } - fn render_address_table(&mut self, ui: &mut Ui) -> AppAction { - let action = AppAction::None; - - // Move the data preparation into its own scope - let mut address_data = { - let wallet = self.selected_wallet.as_ref().unwrap().read().unwrap(); - - // Prepare data for the table - wallet - .known_addresses - .iter() - .map(|(address, derivation_path)| { - let utxo_info = wallet.utxos.get(address); - - let utxo_count = utxo_info.map(|outpoints| outpoints.len()).unwrap_or(0); - - // Get total received from the wallet (fetched from Core RPC) - let total_received = wallet - .address_total_received - .get(address) - .cloned() - .unwrap_or(0u64); - - let index = derivation_path - .into_iter() - .last() - .cloned() - .unwrap_or(ChildNumber::Normal { index: 0 }); - let index = match index { - ChildNumber::Normal { index } => index, - ChildNumber::Hardened { index } => index, - _ => 0, - }; - let address_type = - if derivation_path.is_bip44_external(self.app_context.network) { - "Funds".to_string() - } else if derivation_path.is_bip44_change(self.app_context.network) { - "Change".to_string() - } else if derivation_path.is_asset_lock_funding(self.app_context.network) { - "Identity Creation".to_string() - } else if derivation_path.is_platform_payment(self.app_context.network) { - "Platform".to_string() - } else { - "System".to_string() - }; - - let path_reference = wallet - .watched_addresses - .get(derivation_path) - .map(|info| info.path_reference) - .unwrap_or(DerivationPathReference::Unknown); - let (account_category, account_index) = - Self::categorize_path(derivation_path, path_reference); - - // Get Platform credits balance for Platform Payment addresses - // Use canonical lookup to handle potential Address key mismatches - let platform_credits = wallet - .get_platform_address_info(address) - .map(|info| info.balance) - .unwrap_or_default(); - - AddressData { - address: address.clone(), - balance: wallet - .address_balances - .get(address) - .cloned() - .unwrap_or_default(), - platform_credits, - utxo_count, - total_received, - address_type, - index, - derivation_path: derivation_path.clone(), - account_category, - account_index, - } - }) - .collect::>() - }; // The borrow of `wallet` ends here - - // Now you can use `self` mutably without conflict - // Sort the data - self.sort_address_data(&mut address_data); - - if let Some((category, index)) = self.selected_account.clone() { - address_data - .retain(|data| data.account_category == category && data.account_index == index); - } - - // Space allocation for UI elements is handled by the layout system - - // Render the table - TableBuilder::new(ui) - .id_salt("addresses_table") - .striped(false) - .resizable(true) - .vscroll(false) - .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::auto()) // Address - .column(Column::initial(140.0)) // Balance - .column(Column::initial(70.0)) // UTXOs - .column(Column::initial(150.0)) // Total Received - .column(Column::initial(100.0)) // Type - .column(Column::initial(70.0)) // Index - .column(Column::initial(120.0)) // Derivation Path - .column(Column::initial(120.0)) // Actions - .header(30.0, |mut header| { - header.col(|ui| { - let label = if self.sort_column == SortColumn::Address { - match self.sort_order { - SortOrder::Ascending => "Address ^", - SortOrder::Descending => "Address v", - } - } else { - "Address" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::Address); - } - }); - header.col(|ui| { - let label = if self.sort_column == SortColumn::Balance { - match self.sort_order { - SortOrder::Ascending => "Balance (DASH) ^", - SortOrder::Descending => "Balance (DASH) v", - } - } else { - "Balance (DASH)" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::Balance); - } - }); - header.col(|ui| { - let label = if self.sort_column == SortColumn::UTXOs { - match self.sort_order { - SortOrder::Ascending => "UTXOs ^", - SortOrder::Descending => "UTXOs v", - } - } else { - "UTXOs" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::UTXOs); - } - }); - header.col(|ui| { - let label = if self.sort_column == SortColumn::TotalReceived { - match self.sort_order { - SortOrder::Ascending => "Total Received (DASH) ^", - SortOrder::Descending => "Total Received (DASH) v", - } - } else { - "Total Received (DASH)" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::TotalReceived); - } - }); - header.col(|ui| { - let label = if self.sort_column == SortColumn::Type { - match self.sort_order { - SortOrder::Ascending => "Type ^", - SortOrder::Descending => "Type v", - } - } else { - "Type" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::Type); - } - }); - header.col(|ui| { - let label = if self.sort_column == SortColumn::Index { - match self.sort_order { - SortOrder::Ascending => "Index ^", - SortOrder::Descending => "Index v", - } - } else { - "Index" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::Index); - } - }); - header.col(|ui| { - let label = if self.sort_column == SortColumn::DerivationPath { - match self.sort_order { - SortOrder::Ascending => "Full Path ^", - SortOrder::Descending => "Full Path v", - } - } else { - "Full Path" - }; - if ui.button(label).clicked() { - self.toggle_sort(SortColumn::DerivationPath); - } - }); - header.col(|ui| { - ui.label("Private Key"); - }); - }) - .body(|mut body| { - let network = self.app_context.network; - for data in &address_data { - body.row(25.0, |mut row| { - let is_key_only = data.account_category.is_key_only(); - let is_platform_payment = - data.account_category == AccountCategory::PlatformPayment; - - row.col(|ui| { - ui.label(data.display_address(network)); - }); - row.col(|ui| { - if is_key_only { - ui.label("N/A"); - } else if is_platform_payment { - // Platform credits: convert from credits to DASH - // Credits are in duffs * 1000, so divide by 1000 then by 1e8 - let dash_balance = - data.platform_credits as f64 / CREDITS_PER_DUFF as f64 / 1e8; - ui.label(format!("{:.8}", dash_balance)); - } else { - let dash_balance = data.balance as f64 * 1e-8; - ui.label(format!("{:.8}", dash_balance)); - } - }); - row.col(|ui| { - // Key-only addresses and Platform addresses don't hold UTXOs - if is_key_only || is_platform_payment { - ui.label("N/A"); - } else { - ui.label(format!("{}", data.utxo_count)); - } - }); - row.col(|ui| { - // These address types don't track historical received amounts - if is_key_only || is_platform_payment { - ui.label("N/A"); - } else { - let dash_received = data.total_received as f64 * 1e-8; - ui.label(format!("{:.8}", dash_received)); - } - }); - row.col(|ui| { - ui.label(&data.address_type); - }); - row.col(|ui| { - ui.label(format!("{}", data.index)); - }); - row.col(|ui| { - ui.label(format!("{}", data.derivation_path)); - }); - row.col(|ui| { - if ui.button("View Key").clicked() { - // Check if wallet is locked first - let wallet_locked = self - .selected_wallet - .as_ref() - .map(|w| { - w.read() - .map(|g| g.uses_password && !g.is_open()) - .unwrap_or(false) - }) - .unwrap_or(false); - - let display_address = data.display_address(network); - - if wallet_locked { - // Store pending info and show unlock popup - self.private_key_dialog.pending_derivation_path = - Some(data.derivation_path.clone()); - self.private_key_dialog.pending_address = Some(display_address); - self.wallet_unlock_popup.open(); - } else { - match self.derive_private_key_wif(&data.derivation_path) { - Ok(key) => { - self.private_key_dialog.is_open = true; - self.private_key_dialog.address = display_address; - self.private_key_dialog.private_key_wif = key; - self.private_key_dialog.show_key = false; - } - Err(err) => self.display_message(&err, MessageType::Error), - } - } - } - }); - }); - } - }); - action - } - fn render_bottom_options(&mut self, ui: &mut Ui) { let wallet_is_open = self .selected_wallet @@ -1085,161 +712,6 @@ impl WalletsBalancesScreen { } } - fn render_wallet_asset_locks(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut open_fund_dialog_for_idx: Option<(usize, Vec<(String, u64)>)> = None; - let mut recover_asset_locks_clicked = false; - - if let Some(arc_wallet) = &self.selected_wallet { - let wallet = arc_wallet.read().unwrap(); - - let dark_mode = ui.ctx().style().visuals.dark_mode; - Frame::new() - .fill(DashColors::surface(dark_mode)) - .corner_radius(5.0) - .inner_margin(Margin::same(15)) - .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) - .show(ui, |ui| { - let dark_mode = ui.ctx().style().visuals.dark_mode; - ui.horizontal(|ui| { - ui.heading(RichText::new("Asset Locks").color(DashColors::text_primary(dark_mode))); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("Create Asset Lock").clicked() { - app_action = AppAction::AddScreen( - ScreenType::CreateAssetLock(arc_wallet.clone()).create_screen(&self.app_context) - ); - } - if ui.button("Search for Unused").on_hover_text("Scan Core wallet for untracked asset locks").clicked() { - recover_asset_locks_clicked = true; - } - }); - }); - ui.add_space(10.0); - - if wallet.unused_asset_locks.is_empty() { - ui.vertical_centered(|ui| { - ui.add_space(20.0); - ui.label(RichText::new("No asset locks found").color(Color32::GRAY).size(14.0)); - ui.add_space(10.0); - ui.label(RichText::new("Asset locks are special transactions that can be used to create identities or fund Platform addresses").color(Color32::GRAY).size(12.0)); - ui.add_space(20.0); - }); - } else { - // Collect Platform addresses for the fund dialog (using DIP-18 Bech32m format) - // Get from known_addresses where path is platform payment - let network = self.app_context.network; - let platform_addresses: Vec<(String, u64)> = wallet - .known_addresses - .iter() - .filter(|(_, path)| path.is_platform_payment(network)) - .filter_map(|(addr, _)| { - use dash_sdk::dpp::address_funds::PlatformAddress; - let balance = wallet - .get_platform_address_info(addr) - .map(|info| info.balance) - .unwrap_or(0); - PlatformAddress::try_from(addr.clone()) - .ok() - .map(|pa| (pa.to_bech32m_string(network), balance)) - }) - .collect(); - - egui::ScrollArea::both() - .id_salt("asset_locks_table") - .min_scrolled_height(200.0) - .show(ui, |ui| { - TableBuilder::new(ui) - .striped(false) - .resizable(true) - .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::initial(200.0)) // Transaction ID - .column(Column::initial(100.0)) // Address - .column(Column::initial(100.0)) // Amount (Duffs) - .column(Column::initial(100.0)) // InstantLock status - .column(Column::initial(100.0)) // Usable status - .column(Column::initial(200.0)) // Actions - .header(30.0, |mut header| { - header.col(|ui| { - ui.label("Transaction ID"); - }); - header.col(|ui| { - ui.label("Address"); - }); - header.col(|ui| { - ui.label("Amount (Duffs)"); - }); - header.col(|ui| { - ui.label("InstantLock"); - }); - header.col(|ui| { - ui.label("Usable"); - }); - header.col(|ui| { - ui.label("Actions"); - }); - }) - .body(|mut body| { - for (index, (tx, address, amount, islock, proof)) in wallet.unused_asset_locks.iter().enumerate() { - body.row(25.0, |mut row| { - row.col(|ui| { - ui.label(tx.txid().to_string()); - }); - row.col(|ui| { - ui.label(address.to_string()); - }); - row.col(|ui| { - ui.label(format!("{}", amount)); - }); - row.col(|ui| { - let status = if islock.is_some() { "Yes" } else { "No" }; - ui.label(status); - }); - row.col(|ui| { - let status = if proof.is_some() { "Yes" } else { "No" }; - ui.label(status); - }); - row.col(|ui| { - if ui.small_button("View").on_hover_text("View full asset lock details").clicked() { - app_action = AppAction::AddScreen( - ScreenType::AssetLockDetail( - wallet.seed_hash(), - index - ).create_screen(&self.app_context) - ); - } - if proof.is_some() - && ui.small_button("Fund").on_hover_text("Fund a Platform address with this asset lock").clicked() { - open_fund_dialog_for_idx = Some((index, platform_addresses.clone())); - } - }); - }); - } - }); - }); - } - }); - } else { - ui.label("No wallet selected."); - } - - // Handle dialog opening outside the borrow - if let Some((idx, platform_addresses)) = open_fund_dialog_for_idx { - self.fund_platform_dialog.selected_asset_lock_index = Some(idx); - self.fund_platform_dialog.is_open = true; - self.fund_platform_dialog.platform_addresses = platform_addresses; - self.fund_platform_dialog.selected_platform_address = None; - self.fund_platform_dialog.status = None; - self.fund_platform_dialog.is_processing = false; - } - - // Handle recover asset locks button click - use custom action to check lock status - if recover_asset_locks_clicked { - app_action = AppAction::Custom("SearchAssetLocks".to_string()); - } - - app_action - } - fn render_no_wallets_view(&self, ui: &mut Ui) { // Optionally put everything in a framed "card"-like container Frame::group(ui.style()) @@ -1705,18 +1177,6 @@ impl WalletsBalancesScreen { action } - fn categorize_path( - path: &DerivationPath, - reference: DerivationPathReference, - ) -> (AccountCategory, Option) { - let category = AccountCategory::from_reference(reference); - let index = match category { - AccountCategory::Bip44 | AccountCategory::Bip32 => path.bip44_account_index(), - _ => None, - }; - (category, index) - } - fn ensure_account_selection(&mut self, summaries: &[AccountSummary]) { if summaries.is_empty() { self.selected_account = None; @@ -1767,181 +1227,6 @@ impl WalletsBalancesScreen { } } - /// Render the detail view for a selected single key wallet - fn render_single_key_wallet_view(&mut self, ui: &mut Ui, dark_mode: bool) -> AppAction { - let mut action = AppAction::None; - - let wallet_arc = match &self.selected_single_key_wallet { - Some(w) => w.clone(), - None => return action, - }; - - let wallet = wallet_arc.read().unwrap(); - let address = wallet.address.to_string(); - let alias = wallet - .alias - .clone() - .unwrap_or_else(|| "Unnamed Key".to_string()); - let balance_duffs = wallet.total_balance_duffs(); - let balance_dash = balance_duffs as f64 * 1e-8; - let utxo_count = wallet.utxos.len(); - let utxos: Vec<_> = wallet.utxos.iter().map(|(o, t)| (*o, t.clone())).collect(); - drop(wallet); - - let text_color = DashColors::text_primary(dark_mode); - - Frame::group(ui.style()) - .fill(DashColors::surface(dark_mode)) - .inner_margin(Margin::symmetric(16, 16)) - .show(ui, |ui| { - ui.vertical(|ui| { - ui.heading(RichText::new(&alias).strong().color(text_color)); - ui.add_space(10.0); - - // Balance info - ui.label(RichText::new(format!("Balance: {:.8} DASH", balance_dash))); - ui.add_space(10.0); - - // Action buttons for SK wallet - ui.horizontal(|ui| { - if ui - .button(RichText::new("Send").color(text_color).strong()) - .clicked() - { - action = AppAction::AddScreen( - crate::ui::ScreenType::SingleKeyWalletSendScreen( - wallet_arc.clone(), - ) - .create_screen(&self.app_context), - ); - } - - if ui - .button(RichText::new("Receive").color(text_color)) - .clicked() - { - self.receive_dialog.core_addresses = - vec![(address.clone(), balance_duffs)]; - self.receive_dialog.selected_core_index = 0; - self.receive_dialog.is_open = true; - } - }); - ui.add_space(15.0); - - // UTXOs section - ui.separator(); - ui.add_space(10.0); - ui.heading(RichText::new(format!("UTXOs ({})", utxo_count)).color(text_color)); - ui.add_space(10.0); - - if utxos.is_empty() { - ui.label("No UTXOs available. Click 'Refresh' to load UTXOs from Core."); - } else { - const UTXOS_PER_PAGE: usize = 50; - let total_pages = utxo_count.div_ceil(UTXOS_PER_PAGE); - - // Ensure current page is valid - if self.utxo_page >= total_pages { - self.utxo_page = total_pages.saturating_sub(1); - } - - let start_idx = self.utxo_page * UTXOS_PER_PAGE; - let utxos_page: Vec<_> = - utxos.iter().skip(start_idx).take(UTXOS_PER_PAGE).collect(); - - // Pagination controls - if total_pages > 1 { - ui.horizontal(|ui| { - if ui - .add_enabled(self.utxo_page > 0, egui::Button::new("<< First")) - .clicked() - { - self.utxo_page = 0; - } - if ui - .add_enabled(self.utxo_page > 0, egui::Button::new("< Prev")) - .clicked() - { - self.utxo_page = self.utxo_page.saturating_sub(1); - } - - ui.label(format!( - "Page {} of {} ({}-{} of {})", - self.utxo_page + 1, - total_pages, - start_idx + 1, - (start_idx + utxos_page.len()).min(utxo_count), - utxo_count - )); - - if ui - .add_enabled( - self.utxo_page < total_pages - 1, - egui::Button::new("Next >"), - ) - .clicked() - { - self.utxo_page += 1; - } - if ui - .add_enabled( - self.utxo_page < total_pages - 1, - egui::Button::new("Last >>"), - ) - .clicked() - { - self.utxo_page = total_pages - 1; - } - }); - ui.add_space(10.0); - } - - egui::ScrollArea::vertical() - .max_height(300.0) - .show(ui, |ui| { - for (outpoint, tx_out) in utxos_page { - Frame::group(ui.style()) - .fill(DashColors::surface(dark_mode).gamma_multiply(0.9)) - .inner_margin(Margin::symmetric(10, 8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label("TxID:"); - ui.label( - RichText::new(format!( - "{}:{}", - outpoint.txid, outpoint.vout - )) - .monospace() - .size(11.0) - .color(text_color), - ); - }); - ui.horizontal(|ui| { - ui.label("Amount:"); - ui.label( - RichText::new(format!( - "{:.8} DASH", - tx_out.value as f64 * 1e-8 - )) - .strong() - .color(text_color), - ); - }); - }); - }); - }); - ui.add_space(5.0); - } - }); - } - }); - }); - - action - } - /// Creates the appropriate refresh action based on the current refresh mode fn create_refresh_action(&self, wallet_arc: &Arc>) -> AppAction { self.create_refresh_action_for_mode(wallet_arc, self.refresh_mode) diff --git a/src/ui/wallets/wallets_screen/single_key_view.rs b/src/ui/wallets/wallets_screen/single_key_view.rs new file mode 100644 index 000000000..7fc8768dc --- /dev/null +++ b/src/ui/wallets/wallets_screen/single_key_view.rs @@ -0,0 +1,186 @@ +use crate::app::AppAction; +use crate::ui::ScreenType; +use crate::ui::theme::DashColors; +use eframe::egui; +use egui::{Frame, Margin, RichText, Ui}; + +use super::WalletsBalancesScreen; + +impl WalletsBalancesScreen { + /// Render the detail view for a selected single key wallet + pub(super) fn render_single_key_wallet_view( + &mut self, + ui: &mut Ui, + dark_mode: bool, + ) -> AppAction { + let mut action = AppAction::None; + + let wallet_arc = match &self.selected_single_key_wallet { + Some(w) => w.clone(), + None => return action, + }; + + let wallet = wallet_arc.read().unwrap(); + let address = wallet.address.to_string(); + let alias = wallet + .alias + .clone() + .unwrap_or_else(|| "Unnamed Key".to_string()); + let balance_duffs = wallet.total_balance_duffs(); + let balance_dash = balance_duffs as f64 * 1e-8; + let utxo_count = wallet.utxos.len(); + let utxos: Vec<_> = wallet.utxos.iter().map(|(o, t)| (*o, t.clone())).collect(); + drop(wallet); + + let text_color = DashColors::text_primary(dark_mode); + + Frame::group(ui.style()) + .fill(DashColors::surface(dark_mode)) + .inner_margin(Margin::symmetric(16, 16)) + .show(ui, |ui| { + ui.vertical(|ui| { + ui.heading(RichText::new(&alias).strong().color(text_color)); + ui.add_space(10.0); + + // Balance info + ui.label(RichText::new(format!("Balance: {:.8} DASH", balance_dash))); + ui.add_space(10.0); + + // Action buttons for SK wallet + ui.horizontal(|ui| { + if ui + .button(RichText::new("Send").color(text_color).strong()) + .clicked() + { + action = AppAction::AddScreen( + ScreenType::SingleKeyWalletSendScreen(wallet_arc.clone()) + .create_screen(&self.app_context), + ); + } + + if ui + .button(RichText::new("Receive").color(text_color)) + .clicked() + { + self.receive_dialog.core_addresses = + vec![(address.clone(), balance_duffs)]; + self.receive_dialog.selected_core_index = 0; + self.receive_dialog.is_open = true; + } + }); + ui.add_space(15.0); + + // UTXOs section + ui.separator(); + ui.add_space(10.0); + ui.heading(RichText::new(format!("UTXOs ({})", utxo_count)).color(text_color)); + ui.add_space(10.0); + + if utxos.is_empty() { + ui.label("No UTXOs available. Click 'Refresh' to load UTXOs from Core."); + } else { + const UTXOS_PER_PAGE: usize = 50; + let total_pages = utxo_count.div_ceil(UTXOS_PER_PAGE); + + // Ensure current page is valid + if self.utxo_page >= total_pages { + self.utxo_page = total_pages.saturating_sub(1); + } + + let start_idx = self.utxo_page * UTXOS_PER_PAGE; + let utxos_page: Vec<_> = + utxos.iter().skip(start_idx).take(UTXOS_PER_PAGE).collect(); + + // Pagination controls + if total_pages > 1 { + ui.horizontal(|ui| { + if ui + .add_enabled(self.utxo_page > 0, egui::Button::new("<< First")) + .clicked() + { + self.utxo_page = 0; + } + if ui + .add_enabled(self.utxo_page > 0, egui::Button::new("< Prev")) + .clicked() + { + self.utxo_page = self.utxo_page.saturating_sub(1); + } + + ui.label(format!( + "Page {} of {} ({}-{} of {})", + self.utxo_page + 1, + total_pages, + start_idx + 1, + (start_idx + utxos_page.len()).min(utxo_count), + utxo_count + )); + + if ui + .add_enabled( + self.utxo_page < total_pages - 1, + egui::Button::new("Next >"), + ) + .clicked() + { + self.utxo_page += 1; + } + if ui + .add_enabled( + self.utxo_page < total_pages - 1, + egui::Button::new("Last >>"), + ) + .clicked() + { + self.utxo_page = total_pages - 1; + } + }); + ui.add_space(10.0); + } + + egui::ScrollArea::vertical() + .max_height(300.0) + .show(ui, |ui| { + for (outpoint, tx_out) in utxos_page { + Frame::group(ui.style()) + .fill(DashColors::surface(dark_mode).gamma_multiply(0.9)) + .inner_margin(Margin::symmetric(10, 8)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label("TxID:"); + ui.label( + RichText::new(format!( + "{}:{}", + outpoint.txid, outpoint.vout + )) + .monospace() + .size(11.0) + .color(text_color), + ); + }); + ui.horizontal(|ui| { + ui.label("Amount:"); + ui.label( + RichText::new(format!( + "{:.8} DASH", + tx_out.value as f64 * 1e-8 + )) + .strong() + .color(text_color), + ); + }); + }); + }); + }); + ui.add_space(5.0); + } + }); + } + }); + }); + + action + } +}