diff --git a/src/context/wallet_lifecycle.rs b/src/context/wallet_lifecycle.rs index 537cb5689..4450c604a 100644 --- a/src/context/wallet_lifecycle.rs +++ b/src/context/wallet_lifecycle.rs @@ -406,6 +406,14 @@ impl AppContext { } } + if derivation_path.is_bip32() { + return (DerivationPathReference::BIP32, default_type); + } + + if derivation_path.is_bip44(self.network) { + return (DerivationPathReference::BIP44, default_type); + } + (default_ref, default_type) } diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 69cae397f..fe6cf613f 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -112,16 +112,20 @@ pub trait DerivationPathHelpers { ) -> DerivationPath; } +pub(crate) fn is_bip44_path(path: &DerivationPath, network: Network) -> bool { + let coin_type = match network { + Network::Dash => 5, + _ => 1, + }; + let components = path.as_ref(); + components.len() >= 4 + && components[0] == ChildNumber::Hardened { index: 44 } + && components[1] == ChildNumber::Hardened { index: coin_type } +} + impl DerivationPathHelpers for DerivationPath { fn is_bip44(&self, network: Network) -> bool { - let coin_type = match network { - Network::Dash => 5, - _ => 1, - }; - let components = self.as_ref(); - components.len() >= 4 - && components[0] == ChildNumber::Hardened { index: 44 } - && components[1] == ChildNumber::Hardened { index: coin_type } + is_bip44_path(self, network) } fn is_bip44_external(&self, network: Network) -> bool { diff --git a/src/ui/wallets/account_summary.rs b/src/ui/wallets/account_summary.rs index bb984c229..bccd98be6 100644 --- a/src/ui/wallets/account_summary.rs +++ b/src/ui/wallets/account_summary.rs @@ -1,6 +1,8 @@ use std::collections::BTreeMap; use dash_sdk::dpp::balances::credits::Credits; +use dash_sdk::dpp::dashcore::Network; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; use crate::model::wallet::{DerivationPathHelpers, DerivationPathReference, Wallet}; @@ -51,9 +53,10 @@ impl AccountCategory { pub fn label(&self, index: Option) -> String { match self { - AccountCategory::Bip44 => match index.unwrap_or(0) { - 0 => "Main Account".to_string(), - idx => format!("BIP44 Account #{}", idx), + AccountCategory::Bip44 => match index { + Some(0) => "Main Account".to_string(), + Some(idx) => format!("BIP44 Account #{}", idx), + None => "BIP44 Account".to_string(), }, AccountCategory::Bip32 => match index { Some(idx) if idx > 0 => format!("Legacy BIP32 Account #{}", idx), @@ -150,6 +153,29 @@ impl AccountCategory { } } +pub(crate) fn categorize_account_path( + path: &DerivationPath, + network: Network, + reference: DerivationPathReference, +) -> (AccountCategory, Option) { + // Derivation path shape is authoritative over stored metadata. + // This prevents stale/misclassified references from surfacing wrong account labels. + let category = if path.is_bip32() { + AccountCategory::Bip32 + } else if path.is_bip44(network) { + AccountCategory::Bip44 + } else { + AccountCategory::from_reference(reference) + }; + + let index = match category { + AccountCategory::Bip44 | AccountCategory::Bip32 => path.bip44_account_index(), + _ => None, + }; + + (category, index) +} + #[derive(Clone, Debug)] pub struct AccountSummary { pub category: AccountCategory, @@ -199,15 +225,11 @@ impl AccountSummaryBuilder { } } -pub fn collect_account_summaries(wallet: &Wallet) -> Vec { +pub fn collect_account_summaries(wallet: &Wallet, network: Network) -> Vec { let mut builders: BTreeMap = BTreeMap::new(); for (path, info) in &wallet.watched_addresses { - let category = AccountCategory::from_reference(info.path_reference); - let index = match category { - AccountCategory::Bip44 | AccountCategory::Bip32 => path.bip44_account_index(), - _ => None, - }; + let (category, index) = categorize_account_path(path, network, info.path_reference); let balance = wallet .address_balances @@ -243,3 +265,59 @@ pub fn collect_account_summaries(wallet: &Wallet) -> Vec { summaries } + +#[cfg(test)] +mod tests { + use super::*; + use dash_sdk::dpp::key_wallet::bip32::ChildNumber; + + #[test] + fn bip44_without_account_index_is_not_main_account() { + assert_eq!(AccountCategory::Bip44.label(None), "BIP44 Account"); + } + + #[test] + fn legacy_path_overrides_incorrect_bip44_reference() { + let path = DerivationPath::from(vec![ + ChildNumber::Hardened { index: 0 }, + ChildNumber::Normal { index: 1 }, + ChildNumber::Normal { index: 3 }, + ]); + + let (category, index) = + categorize_account_path(&path, Network::Testnet, DerivationPathReference::BIP44); + assert_eq!(category, AccountCategory::Bip32); + assert_eq!(index, None); + } + + #[test] + fn bip44_path_overrides_incorrect_bip32_reference() { + let path = DerivationPath::from(vec![ + ChildNumber::Hardened { index: 44 }, + ChildNumber::Hardened { index: 1 }, + ChildNumber::Hardened { index: 0 }, + ChildNumber::Normal { index: 0 }, + ChildNumber::Normal { index: 1 }, + ]); + + let (category, index) = + categorize_account_path(&path, Network::Testnet, DerivationPathReference::BIP32); + assert_eq!(category, AccountCategory::Bip44); + assert_eq!(index, Some(0)); + } + + #[test] + fn bip44_requires_matching_coin_type_for_network() { + let path = DerivationPath::from(vec![ + ChildNumber::Hardened { index: 44 }, + ChildNumber::Hardened { index: 5 }, + ChildNumber::Hardened { index: 0 }, + ChildNumber::Normal { index: 0 }, + ChildNumber::Normal { index: 1 }, + ]); + + let (category, _) = + categorize_account_path(&path, Network::Testnet, DerivationPathReference::Unknown); + assert_ne!(category, AccountCategory::Bip44); + } +} diff --git a/src/ui/wallets/wallets_screen/address_table.rs b/src/ui/wallets/wallets_screen/address_table.rs index 766f382e1..af4d51ea2 100644 --- a/src/ui/wallets/wallets_screen/address_table.rs +++ b/src/ui/wallets/wallets_screen/address_table.rs @@ -1,6 +1,6 @@ use crate::app::AppAction; use crate::model::wallet::{DerivationPathHelpers, DerivationPathReference}; -use crate::ui::wallets::account_summary::AccountCategory; +use crate::ui::wallets::account_summary::{AccountCategory, categorize_account_path}; use crate::ui::{MessageType, ScreenLike}; use dash_sdk::dashcore_rpc::dashcore::{Address, Network}; use dash_sdk::dpp::balances::credits::CREDITS_PER_DUFF; @@ -93,13 +93,9 @@ impl WalletsBalancesScreen { pub(super) fn categorize_path( path: &DerivationPath, reference: DerivationPathReference, + network: Network, ) -> (AccountCategory, Option) { - let category = AccountCategory::from_reference(reference); - let index = match category { - AccountCategory::Bip44 | AccountCategory::Bip32 => path.bip44_account_index(), - _ => None, - }; - (category, index) + categorize_account_path(path, network, reference) } pub(super) fn render_address_table(&mut self, ui: &mut Ui) -> AppAction { @@ -153,8 +149,11 @@ impl WalletsBalancesScreen { .get(derivation_path) .map(|info| info.path_reference) .unwrap_or(DerivationPathReference::Unknown); - let (account_category, account_index) = - Self::categorize_path(derivation_path, path_reference); + let (account_category, account_index) = Self::categorize_path( + derivation_path, + path_reference, + self.app_context.network, + ); // Get Platform credits balance for Platform Payment addresses // Use canonical lookup to handle potential Address key mismatches diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index fa9550ebb..a05815612 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -596,7 +596,7 @@ impl WalletsBalancesScreen { .selected_account .as_ref() .is_some_and(|(category, index)| { - *category == AccountCategory::Bip44 && index.unwrap_or(0) == 0 + *category == AccountCategory::Bip44 && *index == Some(0) }); if wallet_is_open && is_main_account { @@ -1133,7 +1133,7 @@ impl WalletsBalancesScreen { let summaries = { let wallet = wallet_arc.read().unwrap(); self.render_wallet_overview(ui, &wallet); - collect_account_summaries(&wallet) + collect_account_summaries(&wallet, self.app_context.network) }; self.ensure_account_selection(&summaries);