From fa537d175dc84d44ac74c828c5ef4f38509dfaa1 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 18 Feb 2026 20:09:20 -0600 Subject: [PATCH 1/2] fix(wallets): prevent duplicate main account labels --- src/context/wallet_lifecycle.rs | 8 ++ src/ui/wallets/account_summary.rs | 81 +++++++++++++++++-- .../wallets/wallets_screen/address_table.rs | 9 +-- src/ui/wallets/wallets_screen/mod.rs | 2 +- 4 files changed, 84 insertions(+), 16 deletions(-) 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/ui/wallets/account_summary.rs b/src/ui/wallets/account_summary.rs index bb984c229..cd5a66ce2 100644 --- a/src/ui/wallets/account_summary.rs +++ b/src/ui/wallets/account_summary.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use dash_sdk::dpp::balances::credits::Credits; +use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath}; use crate::model::wallet::{DerivationPathHelpers, DerivationPathReference, Wallet}; @@ -51,9 +52,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 +152,35 @@ impl AccountCategory { } } +fn looks_like_bip44(path: &DerivationPath) -> bool { + matches!( + path.as_ref().first(), + Some(ChildNumber::Hardened { index: 44 }) + ) +} + +pub(crate) fn categorize_account_path( + path: &DerivationPath, + 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 looks_like_bip44(path) { + 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, @@ -203,11 +234,7 @@ pub fn collect_account_summaries(wallet: &Wallet) -> 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, info.path_reference); let balance = wallet .address_balances @@ -243,3 +270,41 @@ pub fn collect_account_summaries(wallet: &Wallet) -> Vec { summaries } + +#[cfg(test)] +mod tests { + use super::*; + + #[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, 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, DerivationPathReference::BIP32); + assert_eq!(category, AccountCategory::Bip44); + assert_eq!(index, Some(0)); + } +} diff --git a/src/ui/wallets/wallets_screen/address_table.rs b/src/ui/wallets/wallets_screen/address_table.rs index 766f382e1..aaea2a679 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; @@ -94,12 +94,7 @@ impl WalletsBalancesScreen { 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) + categorize_account_path(path, reference) } pub(super) fn render_address_table(&mut self, ui: &mut Ui) -> AppAction { diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index fa9550ebb..dc9c2305c 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 { From ee51e6f34bdad791b21ee64d43175bf3273f49a9 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 18 Feb 2026 20:20:56 -0600 Subject: [PATCH 2/2] fix(wallets): align BIP44 validation between lifecycle and UI (CodeRabbit) --- src/model/wallet/mod.rs | 20 ++++++---- src/ui/wallets/account_summary.rs | 39 ++++++++++++------- .../wallets/wallets_screen/address_table.rs | 10 +++-- src/ui/wallets/wallets_screen/mod.rs | 2 +- 4 files changed, 46 insertions(+), 25 deletions(-) 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 cd5a66ce2..bccd98be6 100644 --- a/src/ui/wallets/account_summary.rs +++ b/src/ui/wallets/account_summary.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use dash_sdk::dpp::balances::credits::Credits; -use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath}; +use dash_sdk::dpp::dashcore::Network; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; use crate::model::wallet::{DerivationPathHelpers, DerivationPathReference, Wallet}; @@ -152,22 +153,16 @@ impl AccountCategory { } } -fn looks_like_bip44(path: &DerivationPath) -> bool { - matches!( - path.as_ref().first(), - Some(ChildNumber::Hardened { index: 44 }) - ) -} - 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 looks_like_bip44(path) { + } else if path.is_bip44(network) { AccountCategory::Bip44 } else { AccountCategory::from_reference(reference) @@ -230,11 +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, index) = categorize_account_path(path, info.path_reference); + let (category, index) = categorize_account_path(path, network, info.path_reference); let balance = wallet .address_balances @@ -274,6 +269,7 @@ pub fn collect_account_summaries(wallet: &Wallet) -> Vec { #[cfg(test)] mod tests { use super::*; + use dash_sdk::dpp::key_wallet::bip32::ChildNumber; #[test] fn bip44_without_account_index_is_not_main_account() { @@ -288,7 +284,8 @@ mod tests { ChildNumber::Normal { index: 3 }, ]); - let (category, index) = categorize_account_path(&path, DerivationPathReference::BIP44); + let (category, index) = + categorize_account_path(&path, Network::Testnet, DerivationPathReference::BIP44); assert_eq!(category, AccountCategory::Bip32); assert_eq!(index, None); } @@ -303,8 +300,24 @@ mod tests { ChildNumber::Normal { index: 1 }, ]); - let (category, index) = categorize_account_path(&path, DerivationPathReference::BIP32); + 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 aaea2a679..af4d51ea2 100644 --- a/src/ui/wallets/wallets_screen/address_table.rs +++ b/src/ui/wallets/wallets_screen/address_table.rs @@ -93,8 +93,9 @@ impl WalletsBalancesScreen { pub(super) fn categorize_path( path: &DerivationPath, reference: DerivationPathReference, + network: Network, ) -> (AccountCategory, Option) { - categorize_account_path(path, reference) + categorize_account_path(path, network, reference) } pub(super) fn render_address_table(&mut self, ui: &mut Ui) -> AppAction { @@ -148,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 dc9c2305c..a05815612 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -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);