diff --git a/dash-spv/tests/dashd_sync/helpers.rs b/dash-spv/tests/dashd_sync/helpers.rs index c318224ce..98937c474 100644 --- a/dash-spv/tests/dashd_sync/helpers.rs +++ b/dash-spv/tests/dashd_sync/helpers.rs @@ -2,6 +2,7 @@ use dash_spv::network::NetworkEvent; use dash_spv::sync::{ProgressPercentage, SyncEvent, SyncProgress, SyncState}; use dash_spv::test_utils::DashCoreNode; use dashcore::Txid; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::transaction_checking::TransactionContext; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; @@ -62,8 +63,12 @@ pub(super) async fn count_wallet_transactions( ) -> usize { let wallet_read = wallet.read().await; let wallet_info = wallet_read.get_wallet_info(wallet_id).expect("Wallet info not found"); - let txids: HashSet<_> = - wallet_info.accounts().all_accounts().iter().flat_map(|a| a.transactions.keys()).collect(); + let txids: HashSet<_> = wallet_info + .accounts() + .all_accounts() + .iter() + .flat_map(|a| a.transactions().keys()) + .collect(); txids.len() } diff --git a/dash-spv/tests/dashd_sync/setup.rs b/dash-spv/tests/dashd_sync/setup.rs index 585509231..7daa736b5 100644 --- a/dash-spv/tests/dashd_sync/setup.rs +++ b/dash-spv/tests/dashd_sync/setup.rs @@ -12,6 +12,7 @@ use dash_spv::{ use dashcore::network::address::AddrV2Message; use dashcore::network::constants::ServiceFlags; use dashcore::Txid; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::managed_account_type::ManagedAccountType; use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; @@ -121,7 +122,7 @@ impl TestContext { let wallet_read = self.wallet.read().await; let wallet_info = wallet_read.get_wallet_info(&self.wallet_id).expect("Wallet info not found"); - wallet_info.accounts().all_accounts().iter().map(|a| a.transactions.len()).sum() + wallet_info.accounts().all_accounts().iter().map(|a| a.transactions().len()).sum() } /// Retrieves the spendable balance of the wallet. pub(super) async fn spendable_balance(&self) -> u64 { @@ -146,7 +147,7 @@ impl TestContext { let ManagedAccountType::Standard { external_addresses, .. - } = &account.managed_account_type + } = account.managed_account_type() else { panic!("Account 0 is not a Standard account type"); }; @@ -167,7 +168,7 @@ impl TestContext { .accounts() .all_accounts() .iter() - .any(|account| account.transactions.contains_key(txid)) + .any(|account| account.transactions().contains_key(txid)) || wallet_info.immature_transactions().iter().any(|tx| &tx.txid() == txid) } @@ -196,7 +197,7 @@ impl TestContext { let mut spv_txids = HashSet::new(); for managed_account in wallet_info.accounts().all_accounts() { - for txid in managed_account.transactions.keys() { + for txid in managed_account.transactions().keys() { spv_txids.insert(txid.to_string()); } } @@ -304,7 +305,7 @@ pub(super) async fn client_has_transaction( .accounts() .all_accounts() .iter() - .any(|account| account.transactions.contains_key(txid)) + .any(|account| account.transactions().contains_key(txid)) || wallet_info.immature_transactions().iter().any(|tx| &tx.txid() == txid) } diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index 728a24d6a..3ed280ad5 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -15,6 +15,7 @@ use key_wallet::account::ManagedAccountCollection; use key_wallet::managed_account::address_pool::{ AddressInfo, AddressPool, KeySource, PublicKeyType, }; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::AccountType; @@ -310,7 +311,7 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { external_addresses, .. - } = &managed_account.managed_account_type { + } = managed_account.managed_account_type() { external_addresses } else { (*error).set(FFIErrorCode::InvalidInput, "Account type does not have external address pool"); @@ -322,7 +323,7 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { internal_addresses, .. - } = &managed_account.managed_account_type { + } = managed_account.managed_account_type() { internal_addresses } else { (*error).set(FFIErrorCode::InvalidInput, "Account type does not have internal address pool"); @@ -331,7 +332,7 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( } FFIAddressPoolType::Single => { // Get the first (and only) address pool for non-standard accounts - let pools = managed_account.managed_account_type.address_pools(); + let pools = managed_account.managed_account_type().address_pools(); if pools.is_empty() { (*error).set(FFIErrorCode::InvalidInput, "Account has no address pools"); return false; @@ -395,7 +396,7 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { external_addresses, .. - } = &mut managed_account.managed_account_type { + } = managed_account.managed_account_type_mut() { external_addresses } else { (*error).set(FFIErrorCode::InvalidInput, "Account type does not have external address pool"); @@ -407,7 +408,7 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { internal_addresses, .. - } = &mut managed_account.managed_account_type { + } = managed_account.managed_account_type_mut() { internal_addresses } else { (*error).set(FFIErrorCode::InvalidInput, "Account type does not have internal address pool"); @@ -416,7 +417,7 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( } FFIAddressPoolType::Single => { // Get the first (and only) address pool for non-standard accounts - let pools = managed_account.managed_account_type.address_pools_mut(); + let pools = managed_account.managed_account_type_mut().address_pools_mut(); if pools.is_empty() { (*error).set(FFIErrorCode::InvalidInput, "Account has no address pools"); return false; @@ -482,7 +483,7 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { external_addresses, .. - } = &mut managed_account.managed_account_type { + } = managed_account.managed_account_type_mut() { { let current = external_addresses.highest_generated.unwrap_or(0); if target_index > current { @@ -502,7 +503,7 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( if let key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { internal_addresses, .. - } = &mut managed_account.managed_account_type { + } = managed_account.managed_account_type_mut() { { let current = internal_addresses.highest_generated.unwrap_or(0); if target_index > current { @@ -519,7 +520,7 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( } FFIAddressPoolType::Single => { // Get the first (and only) address pool for non-standard accounts - let mut pools = managed_account.managed_account_type.address_pools_mut(); + let mut pools = managed_account.managed_account_type_mut().address_pools_mut(); if pools.is_empty() { (*error).set(FFIErrorCode::InvalidInput, "Account has no address pools"); return false; diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index 1c47c95ee..c57d69b2e 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -21,6 +21,7 @@ use crate::wallet_manager::FFIWalletManager; use key_wallet::account::account_collection::{DashpayAccountKey, PlatformPaymentAccountKey}; use key_wallet::account::TransactionRecord; use key_wallet::managed_account::address_pool::AddressPool; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::managed_platform_account::ManagedPlatformAccount; use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::AccountType; @@ -497,7 +498,7 @@ pub unsafe extern "C" fn managed_core_account_get_network( } let account = &*account; - account.inner().network.into() + account.inner().network().into() } /// Get the parent wallet ID of a managed account @@ -536,7 +537,7 @@ pub unsafe extern "C" fn managed_core_account_get_account_type( let account = &*account; let managed_account = account.inner(); - let account_type_rust = managed_account.managed_account_type.to_account_type(); + let account_type_rust = managed_account.managed_account_type().to_account_type(); // Set the index if output pointer is provided if !index_out.is_null() { @@ -598,7 +599,7 @@ pub unsafe extern "C" fn managed_core_account_get_is_watch_only( } let account = &*account; - account.inner().is_watch_only + account.inner().is_watch_only() } /// Get the balance of a managed account @@ -617,7 +618,7 @@ pub unsafe extern "C" fn managed_core_account_get_balance( } let account = &*account; - let balance = &account.inner().balance; + let balance = account.inner().balance; *balance_out = crate::types::FFIBalance { confirmed: balance.confirmed(), @@ -644,7 +645,7 @@ pub unsafe extern "C" fn managed_core_account_get_transaction_count( } let account = &*account; - account.inner().transactions.len() as c_uint + account.inner().transactions().len() as c_uint } /// Get the number of UTXOs in a managed account @@ -951,7 +952,7 @@ pub unsafe extern "C" fn managed_core_account_get_transactions( } let account = &*account; - let transactions = &account.inner().transactions; + let transactions = account.inner().transactions(); if transactions.is_empty() { *transactions_out = std::ptr::null_mut(); @@ -1078,7 +1079,7 @@ pub unsafe extern "C" fn managed_core_account_get_index( } let account = &*account; - account.inner().managed_account_type.index_or_default() + account.inner().managed_account_type().index_or_default() } /// Get the external address pool from a managed account @@ -1102,7 +1103,7 @@ pub unsafe extern "C" fn managed_core_account_get_external_address_pool( let managed_account = account.inner(); // Get external address pool if this is a standard account - match &managed_account.managed_account_type { + match managed_account.managed_account_type() { key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { external_addresses, .. @@ -1138,7 +1139,7 @@ pub unsafe extern "C" fn managed_core_account_get_internal_address_pool( let managed_account = account.inner(); // Get internal address pool if this is a standard account - match &managed_account.managed_account_type { + match managed_account.managed_account_type() { key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { internal_addresses, .. @@ -1182,7 +1183,7 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( match pool_type { FFIAddressPoolType::External => { // Only standard accounts have external pools - match &managed_account.managed_account_type { + match managed_account.managed_account_type() { ManagedAccountType::Standard { external_addresses, .. @@ -1198,7 +1199,7 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( } FFIAddressPoolType::Internal => { // Only standard accounts have internal pools - match &managed_account.managed_account_type { + match managed_account.managed_account_type() { ManagedAccountType::Standard { internal_addresses, .. @@ -1214,7 +1215,7 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( } FFIAddressPoolType::Single => { // Get the single address pool for non-standard accounts - let pool_ref = match &managed_account.managed_account_type { + let pool_ref = match managed_account.managed_account_type() { ManagedAccountType::Standard { .. } => { @@ -1631,7 +1632,7 @@ mod tests { // Verify the account was created successfully let account = &*result.account; // Account should exist and be valid - assert!(!account.inner().is_watch_only); + assert!(!account.inner().is_watch_only()); // Clean up managed_core_account_free(result.account); diff --git a/key-wallet-ffi/src/managed_wallet.rs b/key-wallet-ffi/src/managed_wallet.rs index d1c9801e4..11d8de356 100644 --- a/key-wallet-ffi/src/managed_wallet.rs +++ b/key-wallet-ffi/src/managed_wallet.rs @@ -12,6 +12,7 @@ use crate::error::{FFIError, FFIErrorCode}; use crate::types::FFIWallet; use crate::{check_ptr, deref_ptr, deref_ptr_mut, unwrap_or_return}; use key_wallet::managed_account::address_pool::KeySource; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use std::ffi::c_void; @@ -170,7 +171,7 @@ pub unsafe extern "C" fn managed_wallet_get_bip_44_external_address_range( let addresses = if let key_wallet::account::ManagedAccountType::Standard { external_addresses, .. - } = &mut managed_account.managed_account_type + } = managed_account.managed_account_type_mut() { unwrap_or_return!( external_addresses.address_range(start_index, end_index, &key_source), @@ -250,7 +251,7 @@ pub unsafe extern "C" fn managed_wallet_get_bip_44_internal_address_range( let addresses = if let key_wallet::account::ManagedAccountType::Standard { internal_addresses, .. - } = &mut managed_account.managed_account_type + } = managed_account.managed_account_type_mut() { unwrap_or_return!( internal_addresses.address_range(start_index, end_index, &key_source), diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 33b846549..1eb1c28f6 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -16,6 +16,7 @@ use dashcore::{ consensus, hashes::Hash, sighash::SighashCache, EcdsaSighashType, Network, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, Txid, }; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ AssetLockFundingType, CreditOutputFunding, }; @@ -194,7 +195,7 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( HashMap::new(); // Collect from all address pools (receive, change, etc.) - for pool in managed_account.managed_account_type.address_pools() { + for pool in managed_account.managed_account_type().address_pools() { for addr_info in pool.addresses.values() { address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); } diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index 1f050e7c2..4b79a0a89 100644 --- a/key-wallet-manager/src/lib.rs +++ b/key-wallet-manager/src/lib.rs @@ -27,6 +27,7 @@ pub use wallet_interface::{BlockProcessingResult, MempoolTransactionResult, Wall use dashcore::blockdata::transaction::Transaction; use dashcore::prelude::CoreBlockHeight; use key_wallet::account::AccountCollection; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::transaction_record::TransactionRecord; use key_wallet::transaction_checking::TransactionContext; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 748b8ea76..f297054de 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -12,6 +12,7 @@ use crate::gap_limit::{ DEFAULT_SPECIAL_GAP_LIMIT, DIP17_GAP_LIMIT, }; use crate::managed_account::address_pool::{AddressPool, AddressPoolType}; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; use crate::managed_account::managed_platform_account::ManagedPlatformAccount; use crate::managed_account::ManagedCoreFundsAccount; @@ -72,7 +73,7 @@ macro_rules! get_by_account_type_match_impl { account_index, involved_addresses, } => $self.dashpay_receival_accounts.$values().find(|account| { - match &account.managed_account_type { + match account.managed_account_type() { ManagedAccountType::DashpayReceivingFunds { index, addresses, @@ -90,7 +91,7 @@ macro_rules! get_by_account_type_match_impl { account_index, involved_addresses, } => $self.dashpay_external_accounts.$values().find(|account| { - match &account.managed_account_type { + match account.managed_account_type() { ManagedAccountType::DashpayExternalAccount { index, addresses, @@ -269,7 +270,7 @@ impl ManagedAccountCollection { pub fn insert(&mut self, account: ManagedCoreFundsAccount) -> Result<(), crate::error::Error> { use crate::account::StandardAccountType; - match &account.managed_account_type { + match account.managed_account_type() { ManagedAccountType::Standard { index, standard_account_type, diff --git a/key-wallet/src/managed_account/managed_account_trait.rs b/key-wallet/src/managed_account/managed_account_trait.rs index 39ffb48fb..18e8eb04e 100644 --- a/key-wallet/src/managed_account/managed_account_trait.rs +++ b/key-wallet/src/managed_account/managed_account_trait.rs @@ -1,20 +1,35 @@ //! Trait for managed account functionality //! -//! This module defines the common interface for all managed account types. +//! Defines the shared interface implemented by every "core" managed account +//! type (funds-bearing or keys-only). All cross-cutting logic that does not +//! depend on funds bookkeeping (balance / UTXOs / spent outpoints) lives here +//! as default-method implementations so it is written exactly once. use std::collections::BTreeMap; use crate::account::TransactionRecord; +#[cfg(feature = "bls")] +use crate::derivation_bls_bip32::ExtendedBLSPubKey; +use crate::managed_account::address_pool; +#[cfg(any(feature = "bls", feature = "eddsa"))] +use crate::managed_account::address_pool::PublicKeyType; use crate::managed_account::managed_account_type::ManagedAccountType; -use crate::utxo::Utxo; -use crate::wallet::balance::WalletCoreBalance; +#[cfg(feature = "eddsa")] +use crate::AddressInfo; +use crate::ExtendedPubKey; use crate::Network; -use dashcore::blockdata::transaction::OutPoint; -use dashcore::Txid; +use dashcore::{Address, ScriptBuf, Txid}; -/// Common trait for all managed account types +/// Common trait for "core" managed account types — both funds-bearing +/// (`ManagedCoreFundsAccount`) and keys-only (`ManagedCoreKeysAccount`). +/// +/// Implementors only need to provide the small set of primitive accessors +/// listed under "Required" below. Everything else is defaulted in terms of +/// those accessors plus methods on the embedded [`ManagedAccountType`]. pub trait ManagedAccountTrait { - /// Get the managed account type + // ----- Required: primitive accessors ----- + + /// Get the managed account type (address pools + variant data) fn managed_account_type(&self) -> &ManagedAccountType; /// Get mutable managed account type @@ -26,23 +41,22 @@ pub trait ManagedAccountTrait { /// Check if this is a watch-only account fn is_watch_only(&self) -> bool; - /// Get balance - fn balance(&self) -> &WalletCoreBalance; - - /// Get mutable balance - fn balance_mut(&mut self) -> &mut WalletCoreBalance; - /// Get transactions fn transactions(&self) -> &BTreeMap; /// Get mutable transactions fn transactions_mut(&mut self) -> &mut BTreeMap; - /// Get UTXOs - fn utxos(&self) -> &BTreeMap; + /// Return the current monitor revision. + /// + /// Bumped whenever the monitored address set changes (e.g. new addresses + /// generated). Used to detect bloom-filter staleness. + fn monitor_revision(&self) -> u64; - /// Get mutable UTXOs - fn utxos_mut(&mut self) -> &mut BTreeMap; + /// Increment the monitor revision to signal that the monitored address set changed. + fn bump_monitor_revision(&mut self); + + // ----- Provided: defaults built on the primitives above ----- /// Get the account index fn index(&self) -> Option { @@ -53,4 +67,531 @@ pub trait ManagedAccountTrait { fn index_or_default(&self) -> u32 { self.managed_account_type().index_or_default() } + + /// Get the managed account type (alias for [`Self::managed_account_type`]) + fn managed_type(&self) -> &ManagedAccountType { + self.managed_account_type() + } + + /// Get the next unused receive address index for standard accounts + fn get_next_receive_address_index(&self) -> Option { + if let ManagedAccountType::Standard { + external_addresses, + .. + } = self.managed_account_type() + { + if let Some(addr) = external_addresses.unused_addresses().first() { + external_addresses.address_index(addr) + } else { + let stats = external_addresses.stats(); + Some(stats.highest_generated.map(|h| h + 1).unwrap_or(0)) + } + } else { + None + } + } + + /// Get the next unused change address index for standard accounts + fn get_next_change_address_index(&self) -> Option { + if let ManagedAccountType::Standard { + internal_addresses, + .. + } = self.managed_account_type() + { + if let Some(addr) = internal_addresses.unused_addresses().first() { + internal_addresses.address_index(addr) + } else { + let stats = internal_addresses.stats(); + Some(stats.highest_generated.map(|h| h + 1).unwrap_or(0)) + } + } else { + None + } + } + + /// Get the next unused address index for single-pool account types + fn get_next_address_index(&self) -> Option { + match self.managed_account_type() { + ManagedAccountType::Standard { + .. + } => self.get_next_receive_address_index(), + ManagedAccountType::CoinJoin { + addresses, + .. + } + | ManagedAccountType::IdentityRegistration { + addresses, + .. + } + | ManagedAccountType::IdentityTopUp { + addresses, + .. + } + | ManagedAccountType::IdentityTopUpNotBoundToIdentity { + addresses, + .. + } + | ManagedAccountType::IdentityInvitation { + addresses, + .. + } + | ManagedAccountType::AssetLockAddressTopUp { + addresses, + .. + } + | ManagedAccountType::AssetLockShieldedAddressTopUp { + addresses, + .. + } + | ManagedAccountType::ProviderVotingKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOwnerKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOperatorKeys { + addresses, + .. + } + | ManagedAccountType::ProviderPlatformKeys { + addresses, + .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. + } => { + addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) + } + } + } + + /// Mark an address as used + fn mark_address_used(&mut self, address: &Address) -> bool { + self.managed_account_type_mut().mark_address_used(address) + } + + /// Get all addresses from all pools + fn all_addresses(&self) -> Vec
{ + self.managed_account_type().all_addresses() + } + + /// Check if an address belongs to this account + fn contains_address(&self, address: &Address) -> bool { + self.managed_account_type().contains_address(address) + } + + /// Check if a script pub key belongs to this account + fn contains_script_pub_key(&self, script_pub_key: &ScriptBuf) -> bool { + self.managed_account_type().contains_script_pub_key(script_pub_key) + } + + /// Get address info for a given address + fn get_address_info(&self, address: &Address) -> Option { + self.managed_account_type().get_address_info(address) + } + + /// Generate the next address for non-standard (single-pool) account types. + /// + /// For Standard accounts, use `next_receive_address` / `next_change_address` + /// on the funds-bearing variant instead. + fn next_address( + &mut self, + account_xpub: Option<&ExtendedPubKey>, + add_to_state: bool, + ) -> Result { + match self.managed_account_type_mut() { + ManagedAccountType::Standard { + .. + } => Err("Standard accounts must use next_receive_address or next_change_address"), + ManagedAccountType::CoinJoin { + addresses, + .. + } + | ManagedAccountType::IdentityRegistration { + addresses, + .. + } + | ManagedAccountType::IdentityTopUp { + addresses, + .. + } + | ManagedAccountType::IdentityTopUpNotBoundToIdentity { + addresses, + .. + } + | ManagedAccountType::IdentityInvitation { + addresses, + .. + } + | ManagedAccountType::AssetLockAddressTopUp { + addresses, + .. + } + | ManagedAccountType::AssetLockShieldedAddressTopUp { + addresses, + .. + } + | ManagedAccountType::ProviderVotingKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOwnerKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOperatorKeys { + addresses, + .. + } + | ManagedAccountType::ProviderPlatformKeys { + addresses, + .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. + } => { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::Public(*xpub), + None => address_pool::KeySource::NoKeySource, + }; + + addresses.next_unused(&key_source, add_to_state).map_err(|e| match e { + crate::error::Error::NoKeySource => { + "No unused addresses available and no key source provided" + } + _ => "Failed to generate address", + }) + } + } + } + + /// Generate the next address with full info for non-standard account types. + fn next_address_with_info( + &mut self, + account_xpub: Option<&ExtendedPubKey>, + add_to_state: bool, + ) -> Result { + match self.managed_account_type_mut() { + ManagedAccountType::Standard { + .. + } => Err( + "Standard accounts must use next_receive_address_with_info or next_change_address_with_info", + ), + ManagedAccountType::CoinJoin { + addresses, + .. + } + | ManagedAccountType::IdentityRegistration { + addresses, + .. + } + | ManagedAccountType::IdentityTopUp { + addresses, + .. + } + | ManagedAccountType::IdentityTopUpNotBoundToIdentity { + addresses, + .. + } + | ManagedAccountType::IdentityInvitation { + addresses, + .. + } + | ManagedAccountType::AssetLockAddressTopUp { + addresses, + .. + } + | ManagedAccountType::AssetLockShieldedAddressTopUp { + addresses, + .. + } + | ManagedAccountType::ProviderVotingKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOwnerKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOperatorKeys { + addresses, + .. + } + | ManagedAccountType::ProviderPlatformKeys { + addresses, + .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. + } => { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::Public(*xpub), + None => address_pool::KeySource::NoKeySource, + }; + + addresses.next_unused_with_info(&key_source, add_to_state).map_err(|e| match e { + crate::error::Error::NoKeySource => { + "No unused addresses available and no key source provided" + } + _ => "Failed to generate address with info", + }) + } + } + } + + /// Generate the next BLS operator key (only for ProviderOperatorKeys accounts) + #[cfg(feature = "bls")] + fn next_bls_operator_key( + &mut self, + account_xpub: Option, + add_to_state: bool, + ) -> Result, &'static str> { + match self.managed_account_type_mut() { + ManagedAccountType::ProviderOperatorKeys { + addresses, + .. + } => { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::BLSPublic(xpub), + None => address_pool::KeySource::NoKeySource, + }; + + let info = addresses + .next_unused_with_info(&key_source, add_to_state) + .map_err(|_| "Failed to get next unused address")?; + + let Some(PublicKeyType::BLS(pub_key_bytes)) = info.public_key else { + return Err("Expected BLS public key but got different key type"); + }; + + addresses.mark_index_used(info.index); + + use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; + let public_key = PublicKey::::from_bytes_with_mode( + &pub_key_bytes, + SerializationFormat::Modern, + ) + .map_err(|_| "Failed to deserialize BLS public key")?; + + Ok(public_key) + } + _ => Err("This method only works for ProviderOperatorKeys accounts"), + } + } + + /// Generate the next EdDSA platform key (only for ProviderPlatformKeys accounts) + #[cfg(feature = "eddsa")] + fn next_eddsa_platform_key( + &mut self, + account_xpriv: crate::derivation_slip10::ExtendedEd25519PrivKey, + add_to_state: bool, + ) -> Result<(crate::derivation_slip10::VerifyingKey, AddressInfo), &'static str> { + match self.managed_account_type_mut() { + ManagedAccountType::ProviderPlatformKeys { + addresses, + .. + } => { + let key_source = address_pool::KeySource::EdDSAPrivate(account_xpriv); + + let info = addresses + .next_unused_with_info(&key_source, add_to_state) + .map_err(|_| "Failed to get next unused address")?; + + let Some(PublicKeyType::EdDSA(pub_key_bytes)) = info.public_key.clone() else { + return Err("Expected EdDSA public key but got different key type"); + }; + + addresses.mark_index_used(info.index); + + let verifying_key = crate::derivation_slip10::VerifyingKey::from_bytes( + &pub_key_bytes.try_into().map_err(|_| "Invalid EdDSA public key length")?, + ) + .map_err(|_| "Failed to deserialize EdDSA public key")?; + + Ok((verifying_key, info)) + } + _ => Err("This method only works for ProviderPlatformKeys accounts"), + } + } + + /// Consume the next unused address and derive its private key. + fn next_private_key( + &mut self, + root_xpriv: &crate::wallet::root_extended_keys::RootExtendedPrivKey, + network: Network, + ) -> Result<[u8; 32], &'static str> { + if matches!(self.managed_account_type(), ManagedAccountType::Standard { .. }) { + return Err("Standard accounts must use next_receive_address or next_change_address"); + } + + let mut pools = self.managed_account_type_mut().address_pools_mut(); + let pool = pools.first_mut().ok_or("Account has no address pool")?; + + let info = pool + .next_unused_with_info(&address_pool::KeySource::NoKeySource, false) + .map_err(|_| "No unused address available")?; + + pool.mark_index_used(info.index); + + let secp = secp256k1::Secp256k1::new(); + let root_ext_priv = root_xpriv.to_extended_priv_key(network); + let derived_xpriv = + root_ext_priv.derive_priv(&secp, &info.path).map_err(|_| "Key derivation failed")?; + + let mut private_key = [0u8; 32]; + private_key.copy_from_slice(&derived_xpriv.private_key[..]); + Ok(private_key) + } + + /// Peek at the next unused address's path and index without marking the index used. + fn peek_next_path(&mut self) -> Result<(crate::DerivationPath, u32), &'static str> { + if matches!(self.managed_account_type(), ManagedAccountType::Standard { .. }) { + return Err("Standard accounts must use next_receive_address or next_change_address"); + } + + let mut pools = self.managed_account_type_mut().address_pools_mut(); + let pool = pools.first_mut().ok_or("Account has no address pool")?; + + let info = pool + .next_unused_with_info(&address_pool::KeySource::NoKeySource, false) + .map_err(|_| "No unused address available")?; + + Ok((info.path, info.index)) + } + + /// Mark an index on the account's first address pool as used. + fn mark_first_pool_index_used(&mut self, index: u32) -> Result<(), &'static str> { + if matches!(self.managed_account_type(), ManagedAccountType::Standard { .. }) { + return Err("Standard accounts must use next_receive_address or next_change_address"); + } + + let mut pools = self.managed_account_type_mut().address_pools_mut(); + let pool = pools.first_mut().ok_or("Account has no address pool")?; + pool.mark_index_used(index); + Ok(()) + } + + /// Consume the next unused address and return only its derivation path. + fn next_path(&mut self) -> Result { + let (path, index) = self.peek_next_path()?; + self.mark_first_pool_index_used(index)?; + Ok(path) + } + + /// Get the derivation path for an address if it belongs to this account + fn address_derivation_path(&self, address: &Address) -> Option { + self.managed_account_type().get_address_derivation_path(address) + } + + /// Get total address count across all pools + fn total_address_count(&self) -> usize { + self.managed_account_type() + .address_pools() + .iter() + .map(|pool| pool.stats().total_generated as usize) + .sum() + } + + /// Get used address count across all pools + fn used_address_count(&self) -> usize { + self.managed_account_type() + .address_pools() + .iter() + .map(|pool| pool.stats().used_count as usize) + .sum() + } + + /// Get the gap limit for non-standard (single-pool) accounts + fn gap_limit(&self) -> Option { + match self.managed_account_type() { + ManagedAccountType::Standard { + .. + } => None, + ManagedAccountType::CoinJoin { + addresses, + .. + } + | ManagedAccountType::IdentityRegistration { + addresses, + .. + } + | ManagedAccountType::IdentityTopUp { + addresses, + .. + } + | ManagedAccountType::IdentityTopUpNotBoundToIdentity { + addresses, + .. + } + | ManagedAccountType::IdentityInvitation { + addresses, + .. + } + | ManagedAccountType::AssetLockAddressTopUp { + addresses, + .. + } + | ManagedAccountType::AssetLockShieldedAddressTopUp { + addresses, + .. + } + | ManagedAccountType::ProviderVotingKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOwnerKeys { + addresses, + .. + } + | ManagedAccountType::ProviderOperatorKeys { + addresses, + .. + } + | ManagedAccountType::ProviderPlatformKeys { + addresses, + .. + } + | ManagedAccountType::DashpayReceivingFunds { + addresses, + .. + } + | ManagedAccountType::DashpayExternalAccount { + addresses, + .. + } + | ManagedAccountType::PlatformPayment { + addresses, + .. + } => Some(addresses.gap_limit), + } + } } diff --git a/key-wallet/src/managed_account/managed_core_funds_account.rs b/key-wallet/src/managed_account/managed_core_funds_account.rs index 9d2f7b72e..88d3abc15 100644 --- a/key-wallet/src/managed_account/managed_core_funds_account.rs +++ b/key-wallet/src/managed_account/managed_core_funds_account.rs @@ -1,21 +1,24 @@ -//! Managed core funds account with mutable state including balance and UTXOs +//! Managed core funds account: keys-account state plus balance, UTXOs, and spent outpoints. //! -//! This module contains the mutable account state that changes during wallet operation, -//! kept separate from the immutable Account structure. Used for accounts that hold and -//! spend funds (Standard, CoinJoin, DashPay). +//! Composed of an inner [`ManagedCoreKeysAccount`] (which carries the address +//! pools, transactions, network, and monitor revision) plus the funds-specific +//! bookkeeping needed for accounts that hold and spend Dash directly +//! (Standard, CoinJoin, DashPay). +//! +//! Shared address-pool / key-derivation behavior is provided by +//! [`ManagedAccountTrait`] default methods; only the funds-specific pieces +//! (balance, UTXO updates, transaction recording, the Standard-account +//! receive/change paths) live here as inherent methods. #[cfg(feature = "bls")] use crate::account::BLSAccount; #[cfg(feature = "eddsa")] use crate::account::EdDSAAccount; -use crate::account::ManagedAccountTrait; use crate::account::TransactionRecord; -#[cfg(feature = "bls")] -use crate::derivation_bls_bip32::ExtendedBLSPubKey; use crate::managed_account::address_pool; -#[cfg(any(feature = "bls", feature = "eddsa"))] -use crate::managed_account::address_pool::PublicKeyType; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; +use crate::managed_account::managed_core_keys_account::ManagedCoreKeysAccount; use crate::managed_account::transaction_record::{ InputDetail, OutputDetail, OutputRole, TransactionDirection, }; @@ -23,280 +26,99 @@ use crate::transaction_checking::transaction_router::TransactionType; use crate::transaction_checking::{AccountMatch, TransactionContext}; use crate::utxo::Utxo; use crate::wallet::balance::WalletCoreBalance; -#[cfg(feature = "eddsa")] -use crate::AddressInfo; use crate::{ExtendedPubKey, Network}; use dashcore::blockdata::transaction::OutPoint; -use dashcore::{Address, ScriptBuf}; -use dashcore::{Transaction, Txid}; +use dashcore::{Address, Transaction, Txid}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::collections::{BTreeSet, HashSet}; -/// Managed core funds account with mutable state +/// Managed core funds account with mutable state including balance and UTXOs. /// -/// This struct contains the mutable state of an account including address pools, -/// metadata, balance, UTXO set, and transaction history. It is managed separately -/// from the immutable Account structure and is used for accounts that hold and -/// spend funds (Standard, CoinJoin, DashPay). +/// Wraps a [`ManagedCoreKeysAccount`] (the shared address-pool / transaction +/// state) and adds the funds-specific bookkeeping used by accounts that hold +/// and spend Dash directly (Standard, CoinJoin, DashPay). +/// +/// Most read/write surface comes from [`ManagedAccountTrait`] default methods +/// — which delegate to the inner keys account via the primitive accessors — +/// so this struct only carries the funds-specific inherent methods (transaction +/// recording, the Standard-account receive/change paths, etc.). The +/// funds-specific state (`balance`, `utxos`) is reachable as a public field +/// directly. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct ManagedCoreFundsAccount { - /// Account type with embedded address pools and index - pub managed_account_type: ManagedAccountType, - /// Network this account belongs to - pub network: Network, - /// Whether this is a watch-only account - pub is_watch_only: bool, + /// Shared keys-account state (address pools, transactions, network, + /// is_watch_only, monitor revision). + keys: ManagedCoreKeysAccount, /// Account balance information pub balance: WalletCoreBalance, - /// Transaction history for this account - pub transactions: BTreeMap, /// UTXO set for this account pub utxos: BTreeMap, /// Outpoints spent by recorded transactions. /// Rebuilt from `transactions` during deserialization. #[cfg_attr(feature = "serde", serde(skip_serializing))] spent_outpoints: HashSet, - /// Revision counter incremented when the monitored address set changes - /// (e.g. new addresses generated). Used to detect bloom filter staleness. - #[cfg_attr(feature = "serde", serde(skip_serializing))] - monitor_revision: u64, } impl ManagedCoreFundsAccount { - /// Create a new managed account + /// Create a new managed funds account pub fn new( managed_account_type: ManagedAccountType, network: Network, is_watch_only: bool, ) -> Self { Self { - managed_account_type, - network, - is_watch_only, + keys: ManagedCoreKeysAccount::new(managed_account_type, network, is_watch_only), balance: WalletCoreBalance::default(), - transactions: BTreeMap::new(), utxos: BTreeMap::new(), spent_outpoints: HashSet::new(), - monitor_revision: 0, } } - /// Return the current monitor revision. - pub fn monitor_revision(&self) -> u64 { - self.monitor_revision - } - - /// Increment the monitor revision to signal that the monitored address set changed. - pub fn bump_monitor_revision(&mut self) { - self.monitor_revision += 1; - } - - /// Check if an outpoint was spent by a previously recorded transaction. - fn is_outpoint_spent(&self, outpoint: &OutPoint) -> bool { - self.spent_outpoints.contains(outpoint) - } - - /// Create a ManagedAccount from an Account + /// Create a `ManagedCoreFundsAccount` from an [`Account`](super::super::Account). pub fn from_account(account: &super::super::Account) -> Self { - // Use the account's public key as the key source - let key_source = address_pool::KeySource::Public(account.account_xpub); - let managed_type = ManagedAccountType::from_account_type( - account.account_type, - account.network, - &key_source, - ) - .unwrap_or_else(|_| { - // Fallback: create without pre-generated addresses - let no_key_source = address_pool::KeySource::NoKeySource; - ManagedAccountType::from_account_type( - account.account_type, - account.network, - &no_key_source, - ) - .expect("Should succeed with NoKeySource") - }); - - Self::new(managed_type, account.network, account.is_watch_only) + Self::wrap(ManagedCoreKeysAccount::from_account(account)) } - /// Create a ManagedAccount from a BLS Account + /// Create a `ManagedCoreFundsAccount` from a [`BLSAccount`]. #[cfg(feature = "bls")] pub fn from_bls_account(account: &BLSAccount) -> Self { - // Use the BLS public key as the key source - let key_source = address_pool::KeySource::BLSPublic(account.bls_public_key.clone()); - let managed_type = ManagedAccountType::from_account_type( - account.account_type, - account.network, - &key_source, - ) - .unwrap_or_else(|_| { - // Fallback: create without pre-generated addresses - let no_key_source = address_pool::KeySource::NoKeySource; - ManagedAccountType::from_account_type( - account.account_type, - account.network, - &no_key_source, - ) - .expect("Should succeed with NoKeySource") - }); - - Self::new(managed_type, account.network, account.is_watch_only) + Self::wrap(ManagedCoreKeysAccount::from_bls_account(account)) } - /// Create a ManagedAccount from an EdDSA Account + /// Create a `ManagedCoreFundsAccount` from an [`EdDSAAccount`]. #[cfg(feature = "eddsa")] pub fn from_eddsa_account(account: &EdDSAAccount) -> Self { - // EdDSA requires hardened derivation, so we can't generate addresses without private key - let key_source = address_pool::KeySource::NoKeySource; - let managed_type = ManagedAccountType::from_account_type( - account.account_type, - account.network, - &key_source, - ) - .expect("Should succeed with NoKeySource"); - - Self::new(managed_type, account.network, account.is_watch_only) - } - - /// Get the account index - pub fn index(&self) -> Option { - self.managed_account_type.index() + Self::wrap(ManagedCoreKeysAccount::from_eddsa_account(account)) } - /// Get the account index or 0 if none exists - pub fn index_or_default(&self) -> u32 { - self.managed_account_type.index_or_default() - } - - /// Get the managed account type - pub fn managed_type(&self) -> &ManagedAccountType { - &self.managed_account_type - } - - /// Get the next unused receive address index for standard accounts - /// Note: This requires a key source which is not available in ManagedAccount - /// Address generation should be done through a method that has access to the Account's keys - pub fn get_next_receive_address_index(&self) -> Option { - // Only applicable for standard accounts - if let ManagedAccountType::Standard { - external_addresses, - .. - } = &self.managed_account_type - { - // Get the first unused address or the next index after the last used one - if let Some(addr) = external_addresses.unused_addresses().first() { - external_addresses.address_index(addr) - } else { - // If no unused addresses, return the next index based on stats - let stats = external_addresses.stats(); - Some(stats.highest_generated.map(|h| h + 1).unwrap_or(0)) - } - } else { - None + fn wrap(keys: ManagedCoreKeysAccount) -> Self { + Self { + keys, + balance: WalletCoreBalance::default(), + utxos: BTreeMap::new(), + spent_outpoints: HashSet::new(), } } - /// Get the next unused change address index for standard accounts - /// Note: This requires a key source which is not available in ManagedAccount - /// Address generation should be done through a method that has access to the Account's keys - pub fn get_next_change_address_index(&self) -> Option { - // Only applicable for standard accounts - if let ManagedAccountType::Standard { - internal_addresses, - .. - } = &self.managed_account_type - { - // Get the first unused address or the next index after the last used one - if let Some(addr) = internal_addresses.unused_addresses().first() { - internal_addresses.address_index(addr) - } else { - // If no unused addresses, return the next index based on stats - let stats = internal_addresses.stats(); - Some(stats.highest_generated.map(|h| h + 1).unwrap_or(0)) - } - } else { - None - } + /// Get a reference to the inner keys-account state. + pub fn keys(&self) -> &ManagedCoreKeysAccount { + &self.keys } - /// Get the next unused address index for single-pool account types - pub fn get_next_address_index(&self) -> Option { - match &self.managed_account_type { - ManagedAccountType::Standard { - .. - } => self.get_next_receive_address_index(), - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUp { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } => { - addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) - } - } + /// Get a mutable reference to the inner keys-account state. + pub fn keys_mut(&mut self) -> &mut ManagedCoreKeysAccount { + &mut self.keys } - /// Mark an address as used - pub fn mark_address_used(&mut self, address: &Address) -> bool { - // Use the account type's mark_address_used method - // The address pools already track gap limits internally - self.managed_account_type.mark_address_used(address) + /// Check if an outpoint was spent by a previously recorded transaction. + fn is_outpoint_spent(&self, outpoint: &OutPoint) -> bool { + self.spent_outpoints.contains(outpoint) } - /// Add new ones for received outputs, remove spent ones + /// Add new UTXOs for received outputs, remove spent ones. fn update_utxos( &mut self, tx: &Transaction, @@ -304,7 +126,7 @@ impl ManagedCoreFundsAccount { context: TransactionContext, ) { // Update UTXOs only for spendable account types - match &mut self.managed_account_type { + match self.keys.managed_account_type() { ManagedAccountType::Standard { .. } @@ -339,9 +161,11 @@ impl ManagedCoreFundsAccount { let txid = tx.txid(); let mut utxos_changed = false; + let network = self.keys.network(); + // Insert UTXOs for outputs paying to our addresses for (vout, output) in tx.output.iter().enumerate() { - if let Ok(addr) = Address::from_script(&output.script_pubkey, self.network) { + if let Ok(addr) = Address::from_script(&output.script_pubkey, network) { if involved_addrs.contains(&addr) { let outpoint = OutPoint { txid, @@ -402,7 +226,7 @@ impl ManagedCoreFundsAccount { } if utxos_changed { - self.monitor_revision += 1; + self.keys.bump_monitor_revision(); } } _ => {} @@ -418,13 +242,13 @@ impl ManagedCoreFundsAccount { context: TransactionContext, transaction_type: TransactionType, ) -> bool { - if !self.transactions.contains_key(&tx.txid()) { + if !self.keys.transactions().contains_key(&tx.txid()) { self.record_transaction(tx, account_match, context, transaction_type); return true; } let mut changed = false; - if let Some(tx_record) = self.transactions.get_mut(&tx.txid()) { + if let Some(tx_record) = self.keys.transactions_mut().get_mut(&tx.txid()) { debug_assert_eq!( tx_record.transaction_type, transaction_type, @@ -488,10 +312,11 @@ impl ManagedCoreFundsAccount { // the transaction still spent our funds even without matching UTXOs. let has_inputs = !input_details.is_empty() || account_match.sent > 0; + let network = self.keys.network(); let resolved_outputs: Vec> = tx .output .iter() - .map(|output| Address::from_script(&output.script_pubkey, self.network).ok()) + .map(|output| Address::from_script(&output.script_pubkey, network).ok()) .collect(); // Build output details — annotate every output with its role @@ -537,7 +362,7 @@ impl ManagedCoreFundsAccount { let tx_record = TransactionRecord::new( tx.clone(), - self.managed_account_type.to_account_type(), + self.keys.managed_account_type().to_account_type(), context.clone(), transaction_type, direction, @@ -547,7 +372,7 @@ impl ManagedCoreFundsAccount { ); let record = tx_record.clone(); - self.transactions.insert(tx.txid(), tx_record); + self.keys.transactions_mut().insert(tx.txid(), tx_record); self.update_utxos(tx, account_match, context); record @@ -604,42 +429,19 @@ impl ManagedCoreFundsAccount { self.balance = WalletCoreBalance::new(confirmed, unconfirmed, immature, locked); } - /// Get all addresses from all pools - pub fn all_addresses(&self) -> Vec
{ - self.managed_account_type.all_addresses() - } - - /// Check if an address belongs to this account - pub fn contains_address(&self, address: &Address) -> bool { - self.managed_account_type.contains_address(address) - } - - /// Check if a script pub key belongs to this account - pub fn contains_script_pub_key(&self, script_pub_key: &ScriptBuf) -> bool { - self.managed_account_type.contains_script_pub_key(script_pub_key) - } - - /// Get address info for a given address - pub fn get_address_info(&self, address: &Address) -> Option { - self.managed_account_type.get_address_info(address) - } - /// Generate the next receive address using the optionally provided extended public key - /// If no key is provided, can only return pre-generated unused addresses - /// This method derives a new address from the account's xpub but does not add it to the pool - /// The address must be added to the pool separately with proper tracking + /// If no key is provided, can only return pre-generated unused addresses. + /// Only valid for Standard accounts. pub fn next_receive_address( &mut self, account_xpub: Option<&ExtendedPubKey>, add_to_state: bool, ) -> Result { - // For standard accounts, use the address pool to get the next unused address if let ManagedAccountType::Standard { external_addresses, .. - } = &mut self.managed_account_type + } = self.keys.managed_account_type_mut() { - // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { Some(xpub) => address_pool::KeySource::Public(*xpub), None => address_pool::KeySource::NoKeySource, @@ -652,28 +454,25 @@ impl ManagedCoreFundsAccount { } _ => "Failed to generate receive address", })?; - self.monitor_revision += 1; + self.keys.bump_monitor_revision(); Ok(addr) } else { Err("Cannot generate receive address for non-standard account type") } } - /// Generate the next change address using the optionally provided extended public key - /// If no key is provided, can only return pre-generated unused addresses - /// This method uses the address pool to properly track and generate addresses + /// Generate the next change address using the optionally provided extended public key. + /// Only valid for Standard accounts. pub fn next_change_address( &mut self, account_xpub: Option<&ExtendedPubKey>, add_to_state: bool, ) -> Result { - // For standard accounts, use the address pool to get the next unused address if let ManagedAccountType::Standard { internal_addresses, .. - } = &mut self.managed_account_type + } = self.keys.managed_account_type_mut() { - // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { Some(xpub) => address_pool::KeySource::Public(*xpub), None => address_pool::KeySource::NoKeySource, @@ -686,31 +485,26 @@ impl ManagedCoreFundsAccount { } _ => "Failed to generate change address", })?; - self.monitor_revision += 1; + self.keys.bump_monitor_revision(); Ok(addr) } else { Err("Cannot generate change address for non-standard account type") } } - /// Generate multiple receive addresses at once using the optionally provided extended public key - /// - /// Returns the requested number of unused receive addresses, generating new ones if needed. - /// This is more efficient than calling `next_receive_address` multiple times. - /// If no key is provided, can only return pre-generated unused addresses. + /// Generate multiple receive addresses at once using the optionally provided extended public key. + /// Only valid for Standard accounts. pub fn next_receive_addresses( &mut self, account_xpub: Option<&ExtendedPubKey>, count: usize, add_to_state: bool, ) -> Result, String> { - // For standard accounts, use the address pool to get multiple unused addresses if let ManagedAccountType::Standard { external_addresses, .. - } = &mut self.managed_account_type + } = self.keys.managed_account_type_mut() { - // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { Some(xpub) => address_pool::KeySource::Public(*xpub), None => address_pool::KeySource::NoKeySource, @@ -736,24 +530,19 @@ impl ManagedCoreFundsAccount { } } - /// Generate multiple change addresses at once using the optionally provided extended public key - /// - /// Returns the requested number of unused change addresses, generating new ones if needed. - /// This is more efficient than calling `next_change_address` multiple times. - /// If no key is provided, can only return pre-generated unused addresses. + /// Generate multiple change addresses at once using the optionally provided extended public key. + /// Only valid for Standard accounts. pub fn next_change_addresses( &mut self, account_xpub: Option<&ExtendedPubKey>, count: usize, add_to_state: bool, ) -> Result, String> { - // For standard accounts, use the address pool to get multiple unused addresses if let ManagedAccountType::Standard { internal_addresses, .. - } = &mut self.managed_account_type + } = self.keys.managed_account_type_mut() { - // Create appropriate key source based on whether xpub is provided let key_source = match account_xpub { Some(xpub) => address_pool::KeySource::Public(*xpub), None => address_pool::KeySource::NoKeySource, @@ -779,409 +568,9 @@ impl ManagedCoreFundsAccount { } } - /// Generate the next address for non-standard accounts - /// This method is for special accounts like Identity, Provider accounts, etc. - /// Standard accounts (BIP44/BIP32) should use next_receive_address or next_change_address - pub fn next_address( - &mut self, - account_xpub: Option<&ExtendedPubKey>, - add_to_state: bool, - ) -> Result { - match &mut self.managed_account_type { - ManagedAccountType::Standard { - .. - } => Err("Standard accounts must use next_receive_address or next_change_address"), - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } => { - // Create appropriate key source based on whether xpub is provided - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::Public(*xpub), - None => address_pool::KeySource::NoKeySource, - }; - - addresses.next_unused(&key_source, add_to_state).map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address", - }) - } - ManagedAccountType::IdentityTopUp { - addresses, - .. - } => { - // Identity top-up has an address pool - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::Public(*xpub), - None => address_pool::KeySource::NoKeySource, - }; - - addresses.next_unused(&key_source, add_to_state).map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address", - }) - } - } - } - - /// Generate the next address with full info for non-standard accounts - /// This method is for special accounts like Identity, Provider accounts, etc. - /// Standard accounts (BIP44/BIP32) should use next_receive_address_with_info or next_change_address_with_info - pub fn next_address_with_info( - &mut self, - account_xpub: Option<&ExtendedPubKey>, - add_to_state: bool, - ) -> Result { - match &mut self.managed_account_type { - ManagedAccountType::Standard { - .. - } => Err("Standard accounts must use next_receive_address_with_info or next_change_address_with_info"), - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } => { - // Create appropriate key source based on whether xpub is provided - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::Public(*xpub), - None => address_pool::KeySource::NoKeySource, - }; - - addresses.next_unused_with_info(&key_source, add_to_state).map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address with info", - }) - } - ManagedAccountType::IdentityTopUp { - addresses, - .. - } => { - // Identity top-up has an address pool - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::Public(*xpub), - None => address_pool::KeySource::NoKeySource, - }; - - addresses.next_unused_with_info(&key_source, add_to_state).map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address with info", - }) - } - } - } - - /// Generate the next BLS operator key (only for ProviderOperatorKeys accounts) - /// Returns the BLS public key at the next unused index - #[cfg(feature = "bls")] - pub fn next_bls_operator_key( - &mut self, - account_xpub: Option, - add_to_state: bool, - ) -> Result, &'static str> { - match &mut self.managed_account_type { - ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } => { - // Create key source from the optional BLS public key - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::BLSPublic(xpub), - None => address_pool::KeySource::NoKeySource, - }; - - // Use next_unused_with_info to get the next address (handles caching and derivation) - let info = addresses - .next_unused_with_info(&key_source, add_to_state) - .map_err(|_| "Failed to get next unused address")?; - - // Extract the BLS public key from the address info - let Some(PublicKeyType::BLS(pub_key_bytes)) = info.public_key else { - return Err("Expected BLS public key but got different key type"); - }; - - // Mark as used - addresses.mark_index_used(info.index); - - // Convert bytes to BLS public key - use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; - let public_key = PublicKey::::from_bytes_with_mode( - &pub_key_bytes, - SerializationFormat::Modern, - ) - .map_err(|_| "Failed to deserialize BLS public key")?; - - Ok(public_key) - } - _ => Err("This method only works for ProviderOperatorKeys accounts"), - } - } - - /// Generate the next EdDSA platform key (only for ProviderPlatformKeys accounts) - /// Returns the Ed25519 public key and address info at the next unused index - #[cfg(feature = "eddsa")] - pub fn next_eddsa_platform_key( - &mut self, - account_xpriv: crate::derivation_slip10::ExtendedEd25519PrivKey, - add_to_state: bool, - ) -> Result<(crate::derivation_slip10::VerifyingKey, AddressInfo), &'static str> { - match &mut self.managed_account_type { - ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } => { - // Create key source from the EdDSA private key - let key_source = address_pool::KeySource::EdDSAPrivate(account_xpriv); - - // Use next_unused_with_info to get the next address (handles caching and derivation) - let info = addresses - .next_unused_with_info(&key_source, add_to_state) - .map_err(|_| "Failed to get next unused address")?; - - // Extract the EdDSA public key from the address info - let Some(PublicKeyType::EdDSA(pub_key_bytes)) = info.public_key.clone() else { - return Err("Expected EdDSA public key but got different key type"); - }; - - // Mark as used - addresses.mark_index_used(info.index); - - let verifying_key = crate::derivation_slip10::VerifyingKey::from_bytes( - &pub_key_bytes.try_into().map_err(|_| "Invalid EdDSA public key length")?, - ) - .map_err(|_| "Failed to deserialize EdDSA public key")?; - - Ok((verifying_key, info)) - } - _ => Err("This method only works for ProviderPlatformKeys accounts"), - } - } - - /// Consume the next unused address and derive its private key. - /// - /// Used for one-time keys (asset lock funding, identity registration, etc.). - /// The address is marked as used so subsequent calls return fresh keys. - /// - /// Only works for single-pool account types (not Standard accounts). - pub fn next_private_key( - &mut self, - root_xpriv: &crate::wallet::root_extended_keys::RootExtendedPrivKey, - network: Network, - ) -> Result<[u8; 32], &'static str> { - if matches!(self.managed_account_type, ManagedAccountType::Standard { .. }) { - return Err("Standard accounts must use next_receive_address or next_change_address"); - } - - let mut pools = self.managed_account_type.address_pools_mut(); - let pool = pools.first_mut().ok_or("Account has no address pool")?; - - let info = pool - .next_unused_with_info(&address_pool::KeySource::NoKeySource, false) - .map_err(|_| "No unused address available")?; - - pool.mark_index_used(info.index); - - let secp = secp256k1::Secp256k1::new(); - let root_ext_priv = root_xpriv.to_extended_priv_key(network); - let derived_xpriv = - root_ext_priv.derive_priv(&secp, &info.path).map_err(|_| "Key derivation failed")?; - - let mut private_key = [0u8; 32]; - private_key.copy_from_slice(&derived_xpriv.private_key[..]); - Ok(private_key) - } - - /// Peek at the next unused address's path and index **without** marking - /// the index used. - /// - /// Intended for two-phase flows where path consumption must not commit - /// until an external operation (e.g. an async signer request) has - /// succeeded. Pair with [`Self::mark_first_pool_index_used`] to - /// commit, or drop the result to leave the pool untouched. Calling - /// `peek_next_path` twice without committing in between returns the - /// same `(path, index)`. - /// - /// Only works for single-pool account types (not Standard accounts). - pub fn peek_next_path(&mut self) -> Result<(crate::DerivationPath, u32), &'static str> { - if matches!(self.managed_account_type, ManagedAccountType::Standard { .. }) { - return Err("Standard accounts must use next_receive_address or next_change_address"); - } - - let mut pools = self.managed_account_type.address_pools_mut(); - let pool = pools.first_mut().ok_or("Account has no address pool")?; - - let info = pool - .next_unused_with_info(&address_pool::KeySource::NoKeySource, false) - .map_err(|_| "No unused address available")?; - - Ok((info.path, info.index)) - } - - /// Mark an index on the account's first address pool as used. - /// - /// Commits what [`Self::peek_next_path`] returned. Accepts any index — - /// callers are responsible for passing back the index they peeked, not - /// an arbitrary one. - /// - /// Only works for single-pool account types (not Standard accounts). - pub fn mark_first_pool_index_used(&mut self, index: u32) -> Result<(), &'static str> { - if matches!(self.managed_account_type, ManagedAccountType::Standard { .. }) { - return Err("Standard accounts must use next_receive_address or next_change_address"); - } - - let mut pools = self.managed_account_type.address_pools_mut(); - let pool = pools.first_mut().ok_or("Account has no address pool")?; - pool.mark_index_used(index); - Ok(()) - } - - /// Consume the next unused address and return only its derivation path. - /// - /// Analogous to [`Self::next_private_key`] but does not require any - /// root extended private key: used when signing is delegated to an - /// external [`Signer`](crate::signer::Signer), which holds the keys - /// and only needs the path to produce signatures or public keys. - /// - /// Consumes the index immediately; callers that need to defer the - /// commit until after an external operation succeeds should use - /// [`Self::peek_next_path`] + [`Self::mark_first_pool_index_used`] - /// instead. - /// - /// Only works for single-pool account types (not Standard accounts). - pub fn next_path(&mut self) -> Result { - let (path, index) = self.peek_next_path()?; - self.mark_first_pool_index_used(index)?; - Ok(path) - } - - /// Get the derivation path for an address if it belongs to this account - pub fn address_derivation_path(&self, address: &Address) -> Option { - self.managed_account_type.get_address_derivation_path(address) - } - - /// Get total address count across all pools - pub fn total_address_count(&self) -> usize { - self.managed_account_type - .address_pools() - .iter() - .map(|pool| pool.stats().total_generated as usize) - .sum() - } - - /// Get used address count across all pools - pub fn used_address_count(&self) -> usize { - self.managed_account_type - .address_pools() - .iter() - .map(|pool| pool.stats().used_count as usize) - .sum() - } - /// Get the external gap limit for standard accounts pub fn external_gap_limit(&self) -> Option { - match &self.managed_account_type { + match self.keys.managed_account_type() { ManagedAccountType::Standard { external_addresses, .. @@ -1192,7 +581,7 @@ impl ManagedCoreFundsAccount { /// Get the internal gap limit for standard accounts pub fn internal_gap_limit(&self) -> Option { - match &self.managed_account_type { + match self.keys.managed_account_type() { ManagedAccountType::Standard { internal_addresses, .. @@ -1200,112 +589,39 @@ impl ManagedCoreFundsAccount { _ => None, } } - - /// Get the gap limit for non-standard (single-pool) accounts - pub fn gap_limit(&self) -> Option { - match &self.managed_account_type { - ManagedAccountType::Standard { - .. - } => None, - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUp { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } => Some(addresses.gap_limit), - } - } } impl ManagedAccountTrait for ManagedCoreFundsAccount { fn managed_account_type(&self) -> &ManagedAccountType { - &self.managed_account_type + self.keys.managed_account_type() } fn managed_account_type_mut(&mut self) -> &mut ManagedAccountType { - &mut self.managed_account_type + self.keys.managed_account_type_mut() } fn network(&self) -> Network { - self.network + self.keys.network() } fn is_watch_only(&self) -> bool { - self.is_watch_only - } - - fn balance(&self) -> &WalletCoreBalance { - &self.balance - } - - fn balance_mut(&mut self) -> &mut WalletCoreBalance { - &mut self.balance + self.keys.is_watch_only() } fn transactions(&self) -> &BTreeMap { - &self.transactions + self.keys.transactions() } fn transactions_mut(&mut self) -> &mut BTreeMap { - &mut self.transactions + self.keys.transactions_mut() } - fn utxos(&self) -> &BTreeMap { - &self.utxos + fn monitor_revision(&self) -> u64 { + self.keys.monitor_revision() } - fn utxos_mut(&mut self) -> &mut BTreeMap { - &mut self.utxos + fn bump_monitor_revision(&mut self) { + self.keys.bump_monitor_revision() } } @@ -1317,32 +633,26 @@ impl<'de> Deserialize<'de> for ManagedCoreFundsAccount { { #[derive(Deserialize)] struct Helper { - managed_account_type: ManagedAccountType, - network: Network, - is_watch_only: bool, + keys: ManagedCoreKeysAccount, balance: WalletCoreBalance, - transactions: BTreeMap, utxos: BTreeMap, } let helper = Helper::deserialize(deserializer)?; let spent_outpoints = helper - .transactions + .keys + .transactions() .values() .flat_map(|record| &record.transaction.input) .map(|input| input.previous_output) .collect(); Ok(ManagedCoreFundsAccount { - managed_account_type: helper.managed_account_type, - network: helper.network, - is_watch_only: helper.is_watch_only, + keys: helper.keys, balance: helper.balance, - transactions: helper.transactions, utxos: helper.utxos, spent_outpoints, - monitor_revision: 0, }) } } diff --git a/key-wallet/src/managed_account/managed_core_keys_account.rs b/key-wallet/src/managed_account/managed_core_keys_account.rs index 205522322..b9d26b632 100644 --- a/key-wallet/src/managed_account/managed_core_keys_account.rs +++ b/key-wallet/src/managed_account/managed_core_keys_account.rs @@ -11,38 +11,36 @@ use crate::account::BLSAccount; #[cfg(feature = "eddsa")] use crate::account::EdDSAAccount; use crate::account::TransactionRecord; -#[cfg(feature = "bls")] -use crate::derivation_bls_bip32::ExtendedBLSPubKey; use crate::managed_account::address_pool; -#[cfg(any(feature = "bls", feature = "eddsa"))] -use crate::managed_account::address_pool::PublicKeyType; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; -#[cfg(feature = "eddsa")] -use crate::AddressInfo; -use crate::{ExtendedPubKey, Network}; +use crate::Network; use dashcore::Txid; -use dashcore::{Address, ScriptBuf}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -/// Managed core keys account with mutable state but no funds tracking +/// Managed core keys account with mutable state but no funds tracking. +/// +/// Like [`crate::managed_account::ManagedCoreFundsAccount`] but without +/// `balance`, `utxos`, or `spent_outpoints`. Used for accounts that derive +/// special-purpose keys (identity registration, asset locks, masternode +/// provider keys) where per-account UTXO/balance bookkeeping is not +/// meaningful. /// -/// Like [`crate::managed_account::ManagedCoreFundsAccount`] but without `balance`, `utxos`, or -/// `spent_outpoints`. Used for accounts that derive special-purpose keys -/// (identity registration, asset locks, masternode provider keys) where -/// per-account UTXO/balance bookkeeping is not meaningful. +/// Most behavior comes from [`ManagedAccountTrait`] default methods; this +/// type only owns the primitive state. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ManagedCoreKeysAccount { /// Account type with embedded address pools and index - pub managed_account_type: ManagedAccountType, + managed_account_type: ManagedAccountType, /// Network this account belongs to - pub network: Network, + network: Network, /// Whether this is a watch-only account - pub is_watch_only: bool, + is_watch_only: bool, /// Transaction history for this account - pub transactions: BTreeMap, + transactions: BTreeMap, /// Revision counter incremented when the monitored address set changes /// (e.g. new addresses generated). Used to detect bloom filter staleness. #[cfg_attr(feature = "serde", serde(skip))] @@ -65,17 +63,7 @@ impl ManagedCoreKeysAccount { } } - /// Return the current monitor revision. - pub fn monitor_revision(&self) -> u64 { - self.monitor_revision - } - - /// Increment the monitor revision to signal that the monitored address set changed. - pub fn bump_monitor_revision(&mut self) { - self.monitor_revision += 1; - } - - /// Create a ManagedCoreKeysAccount from an Account + /// Create a `ManagedCoreKeysAccount` from an [`Account`](super::super::Account). pub fn from_account(account: &super::super::Account) -> Self { let key_source = address_pool::KeySource::Public(account.account_xpub); let managed_type = ManagedAccountType::from_account_type( @@ -96,7 +84,7 @@ impl ManagedCoreKeysAccount { Self::new(managed_type, account.network, account.is_watch_only) } - /// Create a ManagedCoreKeysAccount from a BLS Account + /// Create a `ManagedCoreKeysAccount` from a [`BLSAccount`]. #[cfg(feature = "bls")] pub fn from_bls_account(account: &BLSAccount) -> Self { let key_source = address_pool::KeySource::BLSPublic(account.bls_public_key.clone()); @@ -118,9 +106,10 @@ impl ManagedCoreKeysAccount { Self::new(managed_type, account.network, account.is_watch_only) } - /// Create a ManagedCoreKeysAccount from an EdDSA Account + /// Create a `ManagedCoreKeysAccount` from an [`EdDSAAccount`]. #[cfg(feature = "eddsa")] pub fn from_eddsa_account(account: &EdDSAAccount) -> Self { + // EdDSA requires hardened derivation, so we cannot generate addresses without the private key. let key_source = address_pool::KeySource::NoKeySource; let managed_type = ManagedAccountType::from_account_type( account.account_type, @@ -131,536 +120,38 @@ impl ManagedCoreKeysAccount { Self::new(managed_type, account.network, account.is_watch_only) } +} - /// Get the account index - pub fn index(&self) -> Option { - self.managed_account_type.index() - } - - /// Get the account index or 0 if none exists - pub fn index_or_default(&self) -> u32 { - self.managed_account_type.index_or_default() - } - - /// Get the managed account type - pub fn managed_type(&self) -> &ManagedAccountType { +impl ManagedAccountTrait for ManagedCoreKeysAccount { + fn managed_account_type(&self) -> &ManagedAccountType { &self.managed_account_type } - /// Get the next unused receive address index for standard accounts - pub fn get_next_receive_address_index(&self) -> Option { - if let ManagedAccountType::Standard { - external_addresses, - .. - } = &self.managed_account_type - { - if let Some(addr) = external_addresses.unused_addresses().first() { - external_addresses.address_index(addr) - } else { - let stats = external_addresses.stats(); - Some(stats.highest_generated.map(|h| h + 1).unwrap_or(0)) - } - } else { - None - } - } - - /// Get the next unused change address index for standard accounts - pub fn get_next_change_address_index(&self) -> Option { - if let ManagedAccountType::Standard { - internal_addresses, - .. - } = &self.managed_account_type - { - if let Some(addr) = internal_addresses.unused_addresses().first() { - internal_addresses.address_index(addr) - } else { - let stats = internal_addresses.stats(); - Some(stats.highest_generated.map(|h| h + 1).unwrap_or(0)) - } - } else { - None - } + fn managed_account_type_mut(&mut self) -> &mut ManagedAccountType { + &mut self.managed_account_type } - /// Get the next unused address index for single-pool account types - pub fn get_next_address_index(&self) -> Option { - match &self.managed_account_type { - ManagedAccountType::Standard { - .. - } => self.get_next_receive_address_index(), - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUp { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } => { - addresses.unused_addresses().first().and_then(|addr| addresses.address_index(addr)) - } - } + fn network(&self) -> Network { + self.network } - /// Mark an address as used - pub fn mark_address_used(&mut self, address: &Address) -> bool { - self.managed_account_type.mark_address_used(address) + fn is_watch_only(&self) -> bool { + self.is_watch_only } - /// Get all addresses from all pools - pub fn all_addresses(&self) -> Vec
{ - self.managed_account_type.all_addresses() + fn transactions(&self) -> &BTreeMap { + &self.transactions } - /// Check if an address belongs to this account - pub fn contains_address(&self, address: &Address) -> bool { - self.managed_account_type.contains_address(address) + fn transactions_mut(&mut self) -> &mut BTreeMap { + &mut self.transactions } - /// Check if a script pub key belongs to this account - pub fn contains_script_pub_key(&self, script_pub_key: &ScriptBuf) -> bool { - self.managed_account_type.contains_script_pub_key(script_pub_key) - } - - /// Get address info for a given address - pub fn get_address_info(&self, address: &Address) -> Option { - self.managed_account_type.get_address_info(address) - } - - /// Generate the next address for non-standard accounts - pub fn next_address( - &mut self, - account_xpub: Option<&ExtendedPubKey>, - add_to_state: bool, - ) -> Result { - match &mut self.managed_account_type { - ManagedAccountType::Standard { - .. - } => Err("Standard accounts must use next_receive_address or next_change_address"), - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } - | ManagedAccountType::IdentityTopUp { - addresses, - .. - } => { - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::Public(*xpub), - None => address_pool::KeySource::NoKeySource, - }; - - addresses.next_unused(&key_source, add_to_state).map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address", - }) - } - } - } - - /// Generate the next address with full info for non-standard accounts - pub fn next_address_with_info( - &mut self, - account_xpub: Option<&ExtendedPubKey>, - add_to_state: bool, - ) -> Result { - match &mut self.managed_account_type { - ManagedAccountType::Standard { - .. - } => Err("Standard accounts must use next_receive_address_with_info or next_change_address_with_info"), - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } - | ManagedAccountType::IdentityTopUp { - addresses, - .. - } => { - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::Public(*xpub), - None => address_pool::KeySource::NoKeySource, - }; - - addresses.next_unused_with_info(&key_source, add_to_state).map_err(|e| match e { - crate::error::Error::NoKeySource => { - "No unused addresses available and no key source provided" - } - _ => "Failed to generate address with info", - }) - } - } - } - - /// Generate the next BLS operator key (only for ProviderOperatorKeys accounts) - #[cfg(feature = "bls")] - pub fn next_bls_operator_key( - &mut self, - account_xpub: Option, - add_to_state: bool, - ) -> Result, &'static str> { - match &mut self.managed_account_type { - ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } => { - let key_source = match account_xpub { - Some(xpub) => address_pool::KeySource::BLSPublic(xpub), - None => address_pool::KeySource::NoKeySource, - }; - - let info = addresses - .next_unused_with_info(&key_source, add_to_state) - .map_err(|_| "Failed to get next unused address")?; - - let Some(PublicKeyType::BLS(pub_key_bytes)) = info.public_key else { - return Err("Expected BLS public key but got different key type"); - }; - - addresses.mark_index_used(info.index); - - use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; - let public_key = PublicKey::::from_bytes_with_mode( - &pub_key_bytes, - SerializationFormat::Modern, - ) - .map_err(|_| "Failed to deserialize BLS public key")?; - - Ok(public_key) - } - _ => Err("This method only works for ProviderOperatorKeys accounts"), - } - } - - /// Generate the next EdDSA platform key (only for ProviderPlatformKeys accounts) - #[cfg(feature = "eddsa")] - pub fn next_eddsa_platform_key( - &mut self, - account_xpriv: crate::derivation_slip10::ExtendedEd25519PrivKey, - add_to_state: bool, - ) -> Result<(crate::derivation_slip10::VerifyingKey, AddressInfo), &'static str> { - match &mut self.managed_account_type { - ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } => { - let key_source = address_pool::KeySource::EdDSAPrivate(account_xpriv); - - let info = addresses - .next_unused_with_info(&key_source, add_to_state) - .map_err(|_| "Failed to get next unused address")?; - - let Some(PublicKeyType::EdDSA(pub_key_bytes)) = info.public_key.clone() else { - return Err("Expected EdDSA public key but got different key type"); - }; - - addresses.mark_index_used(info.index); - - let verifying_key = crate::derivation_slip10::VerifyingKey::from_bytes( - &pub_key_bytes.try_into().map_err(|_| "Invalid EdDSA public key length")?, - ) - .map_err(|_| "Failed to deserialize EdDSA public key")?; - - Ok((verifying_key, info)) - } - _ => Err("This method only works for ProviderPlatformKeys accounts"), - } - } - - /// Consume the next unused address and derive its private key. - pub fn next_private_key( - &mut self, - root_xpriv: &crate::wallet::root_extended_keys::RootExtendedPrivKey, - network: Network, - ) -> Result<[u8; 32], &'static str> { - if matches!(self.managed_account_type, ManagedAccountType::Standard { .. }) { - return Err("Standard accounts must use next_receive_address or next_change_address"); - } - - let mut pools = self.managed_account_type.address_pools_mut(); - let pool = pools.first_mut().ok_or("Account has no address pool")?; - - let info = pool - .next_unused_with_info(&address_pool::KeySource::NoKeySource, false) - .map_err(|_| "No unused address available")?; - - pool.mark_index_used(info.index); - - let secp = secp256k1::Secp256k1::new(); - let root_ext_priv = root_xpriv.to_extended_priv_key(network); - let derived_xpriv = - root_ext_priv.derive_priv(&secp, &info.path).map_err(|_| "Key derivation failed")?; - - let mut private_key = [0u8; 32]; - private_key.copy_from_slice(&derived_xpriv.private_key[..]); - Ok(private_key) - } - - /// Peek at the next unused address's path and index without marking the index used. - pub fn peek_next_path(&mut self) -> Result<(crate::DerivationPath, u32), &'static str> { - if matches!(self.managed_account_type, ManagedAccountType::Standard { .. }) { - return Err("Standard accounts must use next_receive_address or next_change_address"); - } - - let mut pools = self.managed_account_type.address_pools_mut(); - let pool = pools.first_mut().ok_or("Account has no address pool")?; - - let info = pool - .next_unused_with_info(&address_pool::KeySource::NoKeySource, false) - .map_err(|_| "No unused address available")?; - - Ok((info.path, info.index)) - } - - /// Mark an index on the account's first address pool as used. - pub fn mark_first_pool_index_used(&mut self, index: u32) -> Result<(), &'static str> { - if matches!(self.managed_account_type, ManagedAccountType::Standard { .. }) { - return Err("Standard accounts must use next_receive_address or next_change_address"); - } - - let mut pools = self.managed_account_type.address_pools_mut(); - let pool = pools.first_mut().ok_or("Account has no address pool")?; - pool.mark_index_used(index); - Ok(()) - } - - /// Consume the next unused address and return only its derivation path. - pub fn next_path(&mut self) -> Result { - let (path, index) = self.peek_next_path()?; - self.mark_first_pool_index_used(index)?; - Ok(path) - } - - /// Get the derivation path for an address if it belongs to this account - pub fn address_derivation_path(&self, address: &Address) -> Option { - self.managed_account_type.get_address_derivation_path(address) - } - - /// Get total address count across all pools - pub fn total_address_count(&self) -> usize { - self.managed_account_type - .address_pools() - .iter() - .map(|pool| pool.stats().total_generated as usize) - .sum() - } - - /// Get used address count across all pools - pub fn used_address_count(&self) -> usize { - self.managed_account_type - .address_pools() - .iter() - .map(|pool| pool.stats().used_count as usize) - .sum() + fn monitor_revision(&self) -> u64 { + self.monitor_revision } - /// Get the gap limit for non-standard (single-pool) accounts - pub fn gap_limit(&self) -> Option { - match &self.managed_account_type { - ManagedAccountType::Standard { - .. - } => None, - ManagedAccountType::CoinJoin { - addresses, - .. - } - | ManagedAccountType::IdentityRegistration { - addresses, - .. - } - | ManagedAccountType::IdentityTopUp { - addresses, - .. - } - | ManagedAccountType::IdentityTopUpNotBoundToIdentity { - addresses, - .. - } - | ManagedAccountType::IdentityInvitation { - addresses, - .. - } - | ManagedAccountType::AssetLockAddressTopUp { - addresses, - .. - } - | ManagedAccountType::AssetLockShieldedAddressTopUp { - addresses, - .. - } - | ManagedAccountType::ProviderVotingKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOwnerKeys { - addresses, - .. - } - | ManagedAccountType::ProviderOperatorKeys { - addresses, - .. - } - | ManagedAccountType::ProviderPlatformKeys { - addresses, - .. - } - | ManagedAccountType::DashpayReceivingFunds { - addresses, - .. - } - | ManagedAccountType::DashpayExternalAccount { - addresses, - .. - } - | ManagedAccountType::PlatformPayment { - addresses, - .. - } => Some(addresses.gap_limit), - } + fn bump_monitor_revision(&mut self) { + self.monitor_revision += 1; } } diff --git a/key-wallet/src/test_utils/wallet.rs b/key-wallet/src/test_utils/wallet.rs index 7872abfe8..2a4756dd9 100644 --- a/key-wallet/src/test_utils/wallet.rs +++ b/key-wallet/src/test_utils/wallet.rs @@ -2,6 +2,7 @@ use dashcore::{Address, Network, Transaction, Txid}; use crate::{ account::{ManagedCoreFundsAccount, TransactionRecord}, + managed_account::managed_account_trait::ManagedAccountTrait, transaction_checking::{TransactionCheckResult, TransactionContext, WalletTransactionChecker}, wallet::{initialization::WalletAccountCreationOptions, ManagedWalletInfo}, ExtendedPubKey, Utxo, Wallet, @@ -61,7 +62,7 @@ impl TestWalletContext { /// Returns a transaction record by txid from the first BIP44 account. pub fn transaction(&self, txid: &Txid) -> &TransactionRecord { - self.bip44_account().transactions.get(txid).expect("Should have transaction") + self.bip44_account().transactions().get(txid).expect("Should have transaction") } /// Returns the first UTXO from the first BIP44 account. diff --git a/key-wallet/src/tests/balance_tests.rs b/key-wallet/src/tests/balance_tests.rs index e47de9066..ecb1f1481 100644 --- a/key-wallet/src/tests/balance_tests.rs +++ b/key-wallet/src/tests/balance_tests.rs @@ -21,10 +21,10 @@ fn test_balance_with_mixed_utxo_types() { account.utxos.insert(utxo3.outpoint, utxo3); wallet_info.accounts.insert(account).unwrap(); - assert_eq!(wallet_info.balance(), WalletCoreBalance::default()); + assert_eq!(wallet_info.balance, WalletCoreBalance::default()); wallet_info.update_last_processed_height(1100); let expected = WalletCoreBalance::new(10_100_000, 0, 20_000_000, 0); - assert_eq!(wallet_info.balance(), expected); + assert_eq!(wallet_info.balance, expected); } #[test] @@ -37,16 +37,16 @@ fn test_coinbase_maturity_boundary() { account.utxos.insert(utxo.outpoint, utxo); wallet_info.accounts.insert(account).unwrap(); - assert_eq!(wallet_info.balance(), WalletCoreBalance::default()); + assert_eq!(wallet_info.balance, WalletCoreBalance::default()); // 99 confirmations: immature wallet_info.update_last_processed_height(1099); let expected_immature = WalletCoreBalance::new(0, 0, 50_000_000, 0); - assert_eq!(wallet_info.balance(), expected_immature); + assert_eq!(wallet_info.balance, expected_immature); // 100 confirmations: mature wallet_info.update_last_processed_height(1100); let expected_mature = WalletCoreBalance::new(50_000_000, 0, 0, 0); - assert_eq!(wallet_info.balance(), expected_mature); + assert_eq!(wallet_info.balance, expected_mature); } #[test] @@ -59,10 +59,10 @@ fn test_locked_utxos_in_locked_balance() { account.utxos.insert(utxo.outpoint, utxo); wallet_info.accounts.insert(account).unwrap(); - assert_eq!(wallet_info.balance(), WalletCoreBalance::default()); + assert_eq!(wallet_info.balance, WalletCoreBalance::default()); wallet_info.update_last_processed_height(1100); let expected = WalletCoreBalance::new(0, 0, 0, 100_000); - assert_eq!(wallet_info.balance(), expected); + assert_eq!(wallet_info.balance, expected); } #[test] @@ -74,8 +74,8 @@ fn test_unconfirmed_utxos_in_unconfirmed_balance() { account.utxos.insert(utxo.outpoint, utxo); wallet_info.accounts.insert(account).unwrap(); - assert_eq!(wallet_info.balance(), WalletCoreBalance::default()); + assert_eq!(wallet_info.balance, WalletCoreBalance::default()); wallet_info.update_last_processed_height(1100); let expected = WalletCoreBalance::new(0, 100_000, 0, 0); - assert_eq!(wallet_info.balance(), expected); + assert_eq!(wallet_info.balance, expected); } diff --git a/key-wallet/src/tests/spent_outpoints_tests.rs b/key-wallet/src/tests/spent_outpoints_tests.rs index 931c01610..cf8215f58 100644 --- a/key-wallet/src/tests/spent_outpoints_tests.rs +++ b/key-wallet/src/tests/spent_outpoints_tests.rs @@ -4,6 +4,7 @@ use dashcore::blockdata::transaction::{OutPoint, Transaction}; use dashcore::{TxIn, Txid}; use crate::account::{AccountType, StandardAccountType, TransactionRecord}; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::transaction_record::TransactionDirection; use crate::managed_account::ManagedCoreFundsAccount; use crate::transaction_checking::{TransactionContext, TransactionType}; @@ -55,7 +56,7 @@ fn record_from_tx(tx: &Transaction) -> TransactionRecord { #[test] fn fresh_account_has_empty_spent_outpoints() { let account = ManagedCoreFundsAccount::dummy_bip44(); - assert!(account.transactions.is_empty()); + assert!(account.transactions().is_empty()); let probe = OutPoint::new(Txid::from([0xAA; 32]), 0); // Accessing spent_outpoints on a fresh account should not panic or misbehave. @@ -63,7 +64,7 @@ fn fresh_account_has_empty_spent_outpoints() { let json = serde_json::to_string(&account).unwrap(); let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); // No transactions, so spent_outpoints stays empty after round-trip. - assert!(deserialized.transactions.is_empty()); + assert!(deserialized.transactions().is_empty()); // Confirm the serialized form does not contain spent_outpoints. assert!(!json.contains("spent_outpoints")); let _ = probe; // used only for clarity of intent @@ -77,7 +78,7 @@ fn serde_round_trip_rebuilds_spent_outpoints() { let outpoint_b = OutPoint::new(Txid::from([0x02; 32]), 1); let tx = spending_tx(&[outpoint_a, outpoint_b]); let txid = tx.txid(); - account.transactions.insert(txid, record_from_tx(&tx)); + account.transactions_mut().insert(txid, record_from_tx(&tx)); // Serialize (spent_outpoints is skipped) let json = serde_json::to_string(&account).unwrap(); @@ -85,14 +86,14 @@ fn serde_round_trip_rebuilds_spent_outpoints() { // Deserialize: spent_outpoints should be rebuilt from transactions let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.transactions.len(), 1); + assert_eq!(deserialized.transactions().len(), 1); // Verify the rebuilt set by serializing again and comparing transactions // (spent_outpoints is private, so we test behavior through a second round-trip // to confirm stability) let json2 = serde_json::to_string(&deserialized).unwrap(); let deserialized2: ManagedCoreFundsAccount = serde_json::from_str(&json2).unwrap(); - assert_eq!(deserialized2.transactions.len(), 1); + assert_eq!(deserialized2.transactions().len(), 1); } #[test] @@ -102,19 +103,19 @@ fn receive_only_account_round_trips_correctly() { // Add a receive-only transaction (coinbase-like, no real spent outpoints) let tx = receive_only_tx(); let txid = tx.txid(); - account.transactions.insert(txid, record_from_tx(&tx)); + account.transactions_mut().insert(txid, record_from_tx(&tx)); - assert_eq!(account.transactions.len(), 1); + assert_eq!(account.transactions().len(), 1); // Round-trip should work without issues (no rebuild loop) let json = serde_json::to_string(&account).unwrap(); let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.transactions.len(), 1); + assert_eq!(deserialized.transactions().len(), 1); // A second round-trip should be stable let json2 = serde_json::to_string(&deserialized).unwrap(); let deserialized2: ManagedCoreFundsAccount = serde_json::from_str(&json2).unwrap(); - assert_eq!(deserialized2.transactions.len(), 1); + assert_eq!(deserialized2.transactions().len(), 1); } #[test] @@ -128,8 +129,8 @@ fn multiple_transactions_all_inputs_tracked_after_round_trip() { let tx1 = spending_tx(&[outpoint_1]); let tx2 = spending_tx(&[outpoint_2, outpoint_3]); - account.transactions.insert(tx1.txid(), record_from_tx(&tx1)); - account.transactions.insert(tx2.txid(), record_from_tx(&tx2)); + account.transactions_mut().insert(tx1.txid(), record_from_tx(&tx1)); + account.transactions_mut().insert(tx2.txid(), record_from_tx(&tx2)); let json = serde_json::to_string(&account).unwrap(); let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); @@ -137,7 +138,7 @@ fn multiple_transactions_all_inputs_tracked_after_round_trip() { // All three outpoints should be in the rebuilt spent set. // We verify by confirming the transaction inputs survived the round-trip. let all_spent: Vec = deserialized - .transactions + .transactions() .values() .flat_map(|r| &r.transaction.input) .map(|inp| inp.previous_output) diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 6ab764aca..36586fac5 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -8,6 +8,7 @@ use std::collections::BTreeMap; use super::transaction_router::AccountTypeToCheck; use crate::account::{ManagedAccountCollection, ManagedCoreFundsAccount}; use crate::managed_account::address_pool::{AddressInfo, PublicKeyType}; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; use crate::managed_account::transaction_record::TransactionRecord; use crate::Address; @@ -517,7 +518,7 @@ impl ManagedAccountCollection { impl ManagedCoreFundsAccount { /// Classify an address within this account pub fn classify_address(&self, address: &Address) -> AddressClassification { - match &self.managed_account_type { + match self.managed_account_type() { ManagedAccountType::Standard { external_addresses, internal_addresses, @@ -553,7 +554,7 @@ impl ManagedCoreFundsAccount { // Check if this script pubkey belongs to any address in this account if self.contains_script_pub_key(script_pubkey) { // Try to create an address from the script pubkey and get its info - if let Ok(address) = Address::from_script(script_pubkey, self.network) { + if let Ok(address) = Address::from_script(script_pubkey, self.network()) { return self.get_address_info(&address); } } @@ -594,7 +595,8 @@ impl ManagedCoreFundsAccount { if let Some(payout_info) = self.check_provider_payout(payout_script) { provider_payout_involved = true; // Classify the payout address - if let Ok(payout_address) = Address::from_script(payout_script, self.network) { + if let Ok(payout_address) = Address::from_script(payout_script, self.network()) + { match self.classify_address(&payout_address) { AddressClassification::External => { involved_receive_addresses.push(payout_info); @@ -614,7 +616,7 @@ impl ManagedCoreFundsAccount { // Check outputs (received) for output in &tx.output { if self.contains_script_pub_key(&output.script_pubkey) { - if let Ok(address) = Address::from_script(&output.script_pubkey, self.network) { + if let Ok(address) = Address::from_script(&output.script_pubkey, self.network()) { // Try to find the address info from the account if let Some(address_info) = self.get_address_info(&address) { // Use the new classification method @@ -666,7 +668,7 @@ impl ManagedCoreFundsAccount { || sent > 0; if has_addresses { - let account_type_match = match &self.managed_account_type { + let account_type_match = match self.managed_account_type() { ManagedAccountType::Standard { standard_account_type, .. @@ -799,7 +801,7 @@ impl ManagedCoreFundsAccount { for credit_output in &payload.credit_outputs { if self.contains_script_pub_key(&credit_output.script_pubkey) { if let Ok(address) = - Address::from_script(&credit_output.script_pubkey, self.network) + Address::from_script(&credit_output.script_pubkey, self.network()) { // Try to find the address info from the account if let Some(address_info) = self.get_address_info(&address) { @@ -812,7 +814,7 @@ impl ManagedCoreFundsAccount { if !involved_addresses.is_empty() { // Create the appropriate CoreAccountTypeMatch for identity accounts - let account_type_match = match &self.managed_account_type { + let account_type_match = match self.managed_account_type() { ManagedAccountType::IdentityRegistration { .. } => CoreAccountTypeMatch::IdentityRegistration { @@ -870,7 +872,7 @@ impl ManagedCoreFundsAccount { // Only check if this is a provider voting keys account if let ManagedAccountType::ProviderVotingKeys { addresses, - } = &self.managed_account_type + } = self.managed_account_type() { if let Some(payload) = &tx.special_transaction_payload { let voting_key_hash = match payload { @@ -915,7 +917,7 @@ impl ManagedCoreFundsAccount { // Only check if this is a provider owner keys account if let ManagedAccountType::ProviderOwnerKeys { addresses, - } = &self.managed_account_type + } = self.managed_account_type() { if let Some(payload) = &tx.special_transaction_payload { let owner_key_hash = match payload { @@ -955,7 +957,7 @@ impl ManagedCoreFundsAccount { // Only check if this is a provider voting keys account if let ManagedAccountType::ProviderOperatorKeys { addresses, - } = &self.managed_account_type + } = self.managed_account_type() { if let Some(payload) = &tx.special_transaction_payload { let operator_public_key = match payload { @@ -999,7 +1001,7 @@ impl ManagedCoreFundsAccount { // Only check if this is a provider voting keys account if let ManagedAccountType::ProviderPlatformKeys { addresses, - } = &self.managed_account_type + } = self.managed_account_type() { if let Some(payload) = &tx.special_transaction_payload { let platform_node_id = match payload { diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs index d918d8865..0ed906a49 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs @@ -1,6 +1,7 @@ //! Tests for coinbase transaction handling use super::helpers::test_addr; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, @@ -170,7 +171,7 @@ async fn test_update_state_flag_behavior() { let managed_account = managed_wallet_info .first_bip44_managed_account_mut() .expect("Failed to get first BIP44 managed account"); - (managed_account.balance.spendable(), managed_account.transactions.len()) + (managed_account.balance.spendable(), managed_account.transactions().len()) }; // Create a test transaction @@ -205,7 +206,7 @@ async fn test_update_state_flag_behavior() { "Balance should not change when update_state=false" ); assert_eq!( - managed_account.transactions.len(), + managed_account.transactions().len(), initial_tx_count, "Transaction count should not change when update_state=false" ); @@ -238,7 +239,7 @@ async fn test_update_state_flag_behavior() { println!( "After update_state=true: balance={}, tx_count={}", managed_account.balance.spendable(), - managed_account.transactions.len() + managed_account.transactions().len() ); } } diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs index 0f4491534..d7644b70c 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs @@ -2,6 +2,7 @@ use super::helpers::*; use crate::account::AccountType; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs index 2a62257d4..d9cb4c350 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs @@ -1,6 +1,7 @@ //! Tests for provider/masternode transaction handling use super::helpers::*; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs index 621025e86..a3dcfd2e5 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -3,6 +3,7 @@ use super::helpers::{test_addr, test_block_info}; use crate::account::{AccountType, StandardAccountType}; use crate::managed_account::address_pool::KeySource; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ @@ -183,7 +184,7 @@ async fn test_transaction_routing_to_coinjoin_account() { if let ManagedAccountType::CoinJoin { addresses, .. - } = &mut managed_account.managed_account_type + } = managed_account.managed_account_type_mut() { addresses.next_unused(&KeySource::Public(xpub), true).unwrap_or_else(|_| { // If that fails, generate a dummy address for testing diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index f01f8c6d1..163e50234 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -6,6 +6,7 @@ pub(crate) use super::account_checker::TransactionCheckResult; use super::transaction_context::TransactionContext; use super::transaction_router::TransactionRouter; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::managed_wallet_info::ManagedWalletInfo; use crate::{KeySource, Wallet}; @@ -71,7 +72,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { if let Some(account) = self.accounts.get_by_account_type_match(&account_match.account_type_match) { - if account.transactions.contains_key(&txid) { + if account.transactions().contains_key(&txid) { is_new = false; break; } @@ -89,7 +90,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { let already_confirmed = result.affected_accounts.iter().any(|am| { self.accounts .get_by_account_type_match(&am.account_type_match) - .and_then(|a| a.transactions.get(&txid)) + .and_then(|a| a.transactions().get(&txid)) .map_or(false, |r| r.is_confirmed()) }); if already_confirmed { @@ -107,9 +108,9 @@ impl WalletTransactionChecker for ManagedWalletInfo { else { continue; }; - if account.transactions.contains_key(&txid) { + if account.transactions().contains_key(&txid) { account.mark_utxos_instant_send(&txid); - if let Some(record) = account.transactions.get_mut(&txid) { + if let Some(record) = account.transactions_mut().get_mut(&txid) { record.update_context(context.clone()); result.updated_records.push(record.clone()); } @@ -150,10 +151,10 @@ impl WalletTransactionChecker for ManagedWalletInfo { result.new_records.push(record); result.state_modified = true; } else { - let existed_before = account.transactions.contains_key(&tx.txid()); + let existed_before = account.transactions().contains_key(&tx.txid()); if account.confirm_transaction(tx, &account_match, context.clone(), tx_type) { result.state_modified = true; - if let Some(record) = account.transactions.get(&tx.txid()) { + if let Some(record) = account.transactions().get(&tx.txid()) { if existed_before { result.updated_records.push(record.clone()); } else { @@ -176,7 +177,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { let key_source = KeySource::Public(xpub); let rev_before = result.new_addresses.len(); - for pool in account.managed_account_type.address_pools_mut() { + for pool in account.managed_account_type_mut().address_pools_mut() { match pool.maintain_gap_limit(&key_source) { Ok(addrs) => result.new_addresses.extend(addrs), Err(e) => { @@ -437,7 +438,7 @@ mod tests { let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert!( - managed_account.transactions.contains_key(&coinbase_tx.txid()), + managed_account.transactions().contains_key(&coinbase_tx.txid()), "Coinbase should be in regular transactions" ); @@ -452,7 +453,7 @@ mod tests { assert_eq!(immature_txs[0].txid(), coinbase_tx.txid()); // Immature balance should reflect the coinbase value - assert_eq!(managed_wallet.balance().immature(), 5_000_000_000); + assert_eq!(managed_wallet.balance.immature(), 5_000_000_000); // Spendable UTXOs should be empty (coinbase not mature) let last_processed_height = managed_wallet.last_processed_height(); @@ -539,7 +540,7 @@ mod tests { assert!(account.utxos.is_empty(), "Spent UTXO should be removed"); let record = account - .transactions + .transactions() .get(&spend_tx.txid()) .expect("Spend transaction should be recorded"); assert_eq!(record.net_amount, -(funding_value as i64)); @@ -597,7 +598,7 @@ mod tests { let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert!( - managed_account.transactions.contains_key(&coinbase_tx.txid()), + managed_account.transactions().contains_key(&coinbase_tx.txid()), "Coinbase should be in regular transactions" ); @@ -611,7 +612,7 @@ mod tests { assert_eq!(immature_txs.len(), 1, "Should have one immature transaction"); // Immature balance should reflect the coinbase value - assert_eq!(managed_wallet.balance().immature(), 5_000_000_000); + assert_eq!(managed_wallet.balance.immature(), 5_000_000_000); // Spendable UTXOs should be empty (coinbase not mature yet) let last_processed_height = managed_wallet.last_processed_height(); @@ -631,7 +632,7 @@ mod tests { let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert!( - managed_account.transactions.contains_key(&coinbase_tx.txid()), + managed_account.transactions().contains_key(&coinbase_tx.txid()), "Coinbase should still be in regular transactions" ); @@ -640,7 +641,7 @@ mod tests { assert!(immature_txs.is_empty(), "Matured coinbase should not be in immature transactions"); // Immature balance should now be zero - let immature_balance = managed_wallet.balance().immature(); + let immature_balance = managed_wallet.balance.immature(); assert_eq!(immature_balance, 0, "Immature balance should be zero after maturity"); // Spendable UTXOs should now contain the matured coinbase @@ -678,7 +679,7 @@ mod tests { managed_wallet.first_bip44_managed_account().expect("Should have managed account"); let stored_tx = - managed_account.transactions.get(&tx.txid()).expect("Should have stored transaction"); + managed_account.transactions().get(&tx.txid()).expect("Should have stored transaction"); assert_eq!( stored_tx.context, TransactionContext::Mempool, @@ -719,10 +720,10 @@ mod tests { let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert!( - managed_account.transactions.contains_key(&tx.txid()), + managed_account.transactions().contains_key(&tx.txid()), "Transaction should be stored" ); - let tx_count_before = managed_account.transactions.len(); + let tx_count_before = managed_account.transactions().len(); let total_tx_count_before = managed_wallet.metadata.total_transactions; assert_eq!( total_tx_count_before, 1, @@ -744,7 +745,7 @@ mod tests { let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert_eq!( - managed_account.transactions.len(), + managed_account.transactions().len(), tx_count_before, "Transaction count should not increase on rescan" ); @@ -826,7 +827,7 @@ mod tests { // Verify the transaction was stored let account = managed_wallet.first_bip44_managed_account().expect("Should have account"); assert!( - account.transactions.contains_key(&spend_tx.txid()), + account.transactions().contains_key(&spend_tx.txid()), "Spending tx should be stored" ); @@ -913,9 +914,9 @@ mod tests { // Stage 1: mempool (already done in setup). Mempool funds land // in the unconfirmed bucket but are spendable. - assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 200_000); - assert_eq!(ctx.managed_wallet.balance().confirmed(), 0); - assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance.unconfirmed(), 200_000); + assert_eq!(ctx.managed_wallet.balance.confirmed(), 0); + assert_eq!(ctx.managed_wallet.balance.spendable(), 200_000); assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); // Stage 2: IS lock @@ -926,8 +927,8 @@ mod tests { let result = ctx.check_transaction(&tx, TransactionContext::InstantSend(is_lock)).await; assert!(result.is_relevant); assert!(!result.is_new_transaction); - assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); - assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 0); + assert_eq!(ctx.managed_wallet.balance.spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance.unconfirmed(), 0); assert!(ctx.first_utxo().is_instantlocked); assert!(!ctx.first_utxo().is_confirmed); assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); @@ -947,7 +948,7 @@ mod tests { .await; assert!(result_dup.is_relevant); assert!(!result_dup.is_new_transaction); - assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance.spendable(), 200_000); // Stage 3: block confirmation let block_hash = BlockHash::from_slice(&[10u8; 32]).expect("hash"); @@ -958,24 +959,24 @@ mod tests { assert!(ctx.transaction(&txid).is_confirmed()); assert_eq!(ctx.transaction(&txid).height(), Some(1000)); assert!(ctx.first_utxo().is_confirmed); - assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance.spendable(), 200_000); // Stage 4: chain-locked block (rescan with stronger context) let cl_context = TransactionContext::InChainLockedBlock(BlockInfo::new(1000, block_hash, 1700000000)); let result = ctx.check_transaction(&tx, cl_context).await; assert!(!result.is_new_transaction); - assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance.spendable(), 200_000); assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); // Stage 5: late IS lock on already-confirmed tx should be ignored - let balance_before = ctx.managed_wallet.balance(); + let balance_before = ctx.managed_wallet.balance; let result = ctx .check_transaction(&tx, TransactionContext::InstantSend(InstantLock::default())) .await; assert!(result.is_relevant); assert!(!result.is_new_transaction); - assert_eq!(ctx.managed_wallet.balance().spendable(), balance_before.spendable()); + assert_eq!(ctx.managed_wallet.balance.spendable(), balance_before.spendable()); } /// Test that a new transaction arriving directly with IS context populates the dedup set @@ -995,7 +996,7 @@ mod tests { // Should be IS-locked and spendable immediately assert!(ctx.first_utxo().is_instantlocked); - assert_eq!(ctx.managed_wallet.balance().spendable(), 150_000); + assert_eq!(ctx.managed_wallet.balance.spendable(), 150_000); assert!(ctx.managed_wallet.instant_send_locks.contains(&txid)); // A follow-up IS lock should be a no-op @@ -1003,7 +1004,7 @@ mod tests { .check_transaction(&tx, TransactionContext::InstantSend(InstantLock::default())) .await; assert!(!result2.is_new_transaction); - assert_eq!(ctx.managed_wallet.balance().spendable(), 150_000); + assert_eq!(ctx.managed_wallet.balance.spendable(), 150_000); assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); } @@ -1079,9 +1080,9 @@ mod tests { let account1 = managed_wallet .bip44_managed_account_at_index_mut(1) .expect("Should have managed account 1"); - account1.transactions.remove(&txid); + account1.transactions_mut().remove(&txid); account1.utxos.clear(); - assert!(!account1.transactions.contains_key(&txid)); + assert!(!account1.transactions().contains_key(&txid)); assert!(account1.utxos.is_empty()); let is_result = managed_wallet @@ -1110,7 +1111,7 @@ mod tests { .bip44_managed_account_at_index(account_index) .expect("Should have account"); let record = account - .transactions + .transactions() .get(&txid) .expect("Both accounts should hold the record after IS backfill"); assert!(matches!(record.context, TransactionContext::InstantSend(_))); @@ -1136,9 +1137,9 @@ mod tests { .managed_wallet .first_bip44_managed_account_mut() .expect("Should have BIP44 account"); - assert!(account.transactions.contains_key(&txid)); - account.transactions.remove(&txid); - assert!(!account.transactions.contains_key(&txid)); + assert!(account.transactions().contains_key(&txid)); + account.transactions_mut().remove(&txid); + assert!(!account.transactions().contains_key(&txid)); // Now process the same tx as a block confirmation. // Since the wallet's `check_core_transaction` still sees no record, @@ -1186,9 +1187,9 @@ mod tests { .managed_wallet .first_bip44_managed_account_mut() .expect("Should have BIP44 account"); - account.transactions.remove(&txid); + account.transactions_mut().remove(&txid); account.utxos.clear(); - assert!(!account.transactions.contains_key(&txid)); + assert!(!account.transactions().contains_key(&txid)); assert!(account.utxos.is_empty()); // Call `confirm_transaction` directly — the backfill path should create the record @@ -1200,7 +1201,7 @@ mod tests { assert!(changed, "Should return true when backfilling a missing record"); // Verify the transaction was recorded with block context - let record = account.transactions.get(&txid).expect("Should have backfilled record"); + let record = account.transactions().get(&txid).expect("Should have backfilled record"); assert!(record.is_confirmed()); assert_eq!(record.height(), Some(600)); assert_eq!(record.block_info().unwrap().block_hash, block_hash); @@ -1226,8 +1227,8 @@ mod tests { .managed_wallet .first_bip44_managed_account_mut() .expect("Should have BIP44 account"); - assert!(account.transactions.contains_key(&txid)); - assert!(!account.transactions.get(&txid).unwrap().is_confirmed()); + assert!(account.transactions().contains_key(&txid)); + assert!(!account.transactions().get(&txid).unwrap().is_confirmed()); // Build a dummy AccountMatch for the confirm call let result = ctx.managed_wallet.accounts.check_transaction( @@ -1250,7 +1251,7 @@ mod tests { let changed = account.confirm_transaction(&tx, &account_match, block_context, tx_type); assert!(changed, "Should return true when confirming unconfirmed tx"); - let record = account.transactions.get(&txid).expect("Should have record"); + let record = account.transactions().get(&txid).expect("Should have record"); assert!(record.is_confirmed()); assert_eq!(record.height(), Some(700)); assert_eq!(record.block_info().unwrap().block_hash, block_hash); @@ -1651,7 +1652,7 @@ mod tests { let coinjoin_address = if let ManagedAccountType::CoinJoin { addresses, .. - } = &mut managed_account.managed_account_type + } = managed_account.managed_account_type_mut() { addresses.next_unused(&KeySource::Public(xpub), true).expect("coinjoin address") } else { @@ -1711,7 +1712,7 @@ mod tests { assert!(result.is_relevant, "CoinJoin tx should be relevant"); let account = managed_wallet.first_coinjoin_managed_account().expect("coinjoin account"); - let record = account.transactions.get(&tx.txid()).expect("should have record"); + let record = account.transactions().get(&tx.txid()).expect("should have record"); assert_eq!(record.direction, TransactionDirection::CoinJoin); assert_eq!(record.transaction_type, TransactionType::CoinJoin); assert!(record.input_details.is_empty(), "CoinJoin test has no funded UTXOs"); @@ -1741,8 +1742,8 @@ mod tests { 1_700_000_000, )); ctx.check_transaction(&funding_tx, block_context).await; - assert_eq!(ctx.managed_wallet.balance().confirmed(), funding_value); - assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 0); + assert_eq!(ctx.managed_wallet.balance.confirmed(), funding_value); + assert_eq!(ctx.managed_wallet.balance.unconfirmed(), 0); let change_address = ctx .managed_wallet @@ -1809,9 +1810,9 @@ mod tests { assert_eq!(change_utxo.txout.value, change_amount); // Account-level balance: change lives in `confirmed`, not `unconfirmed`. - assert_eq!(ctx.managed_wallet.balance().confirmed(), change_amount); - assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 0); - assert_eq!(ctx.managed_wallet.balance().spendable(), change_amount); + assert_eq!(ctx.managed_wallet.balance.confirmed(), change_amount); + assert_eq!(ctx.managed_wallet.balance.unconfirmed(), 0); + assert_eq!(ctx.managed_wallet.balance.spendable(), change_amount); } /// Sibling of `test_self_send_change_in_mempool_lands_in_confirmed_balance`: @@ -1832,7 +1833,7 @@ mod tests { let utxo = ctx.first_utxo(); assert!(!utxo.is_confirmed, "external mempool payment must stay unconfirmed"); assert!(!utxo.is_trusted, "external payment is not a self-send change"); - assert_eq!(ctx.managed_wallet.balance().confirmed(), 0); - assert_eq!(ctx.managed_wallet.balance().unconfirmed(), payment_value); + assert_eq!(ctx.managed_wallet.balance.confirmed(), 0); + assert_eq!(ctx.managed_wallet.balance.unconfirmed(), payment_value); } } diff --git a/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs b/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs index f4971479b..18e121188 100644 --- a/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs @@ -11,6 +11,7 @@ use secp256k1::PublicKey; use std::collections::HashMap; use std::fmt; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::ManagedCoreFundsAccount; use crate::signer::{Signer, SignerMethod}; use crate::wallet::managed_wallet_info::coin_selection::SelectionStrategy; @@ -223,7 +224,7 @@ impl ManagedWalletInfo { let utxos: Vec = funding_account.utxos.values().cloned().collect(); let mut address_to_path: HashMap = HashMap::new(); - for pool in funding_account.managed_account_type.address_pools() { + for pool in funding_account.managed_account_type().address_pools() { for addr_info in pool.addresses.values() { address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); } @@ -358,7 +359,7 @@ impl ManagedWalletInfo { let utxos: Vec = funding_account.utxos.values().cloned().collect(); let mut address_to_path: HashMap = HashMap::new(); - for pool in funding_account.managed_account_type.address_pools() { + for pool in funding_account.managed_account_type().address_pools() { for addr_info in pool.addresses.values() { address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); } diff --git a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs index 7169fbcb1..4a6ab17a9 100644 --- a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs +++ b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs @@ -10,6 +10,7 @@ use crate::account::EdDSAAccount; use crate::account::{Account, AccountType, ManagedCoreFundsAccount}; use crate::bip32::ExtendedPubKey; use crate::error::{Error, Result}; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::wallet::{Wallet, WalletType}; impl ManagedAccountOperations for ManagedWalletInfo { diff --git a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs index 51e5ebe24..0ef76c2ee 100644 --- a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs +++ b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs @@ -81,7 +81,7 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount self.accounts() .all_accounts() .iter() - .map(|acc| (acc.managed_account_type().to_account_type(), *acc.balance())) + .map(|acc| (acc.managed_account_type().to_account_type(), acc.balance)) .collect() } @@ -223,7 +223,7 @@ impl WalletInfoInterface for ManagedWalletInfo { let last_processed_height = self.last_processed_height(); for account in self.accounts.all_accounts_mut() { account.update_balance(last_processed_height); - balance += *account.balance(); + balance += account.balance; } self.balance = balance; } @@ -231,7 +231,7 @@ impl WalletInfoInterface for ManagedWalletInfo { fn transaction_history(&self) -> Vec<&TransactionRecord> { let mut transactions = Vec::new(); for account in self.accounts.all_accounts() { - transactions.extend(account.transactions.values()); + transactions.extend(account.transactions().values()); } transactions } @@ -259,7 +259,7 @@ impl WalletInfoInterface for ManagedWalletInfo { // Get the actual transactions let mut transactions = Vec::new(); for account in self.accounts.all_accounts() { - for (txid, record) in &account.transactions { + for (txid, record) in account.transactions() { if immature_txids.contains(txid) { transactions.push(record.transaction.clone()); } @@ -288,7 +288,7 @@ impl WalletInfoInterface for ManagedWalletInfo { } let mut matured = Vec::new(); for account in self.accounts.all_accounts() { - for record in account.transactions.values() { + for record in account.transactions().values() { if !record.transaction.is_coin_base() { continue; }