diff --git a/dash-spv/src/sync/mempool/sync_manager.rs b/dash-spv/src/sync/mempool/sync_manager.rs index aef25554f..8038d8247 100644 --- a/dash-spv/src/sync/mempool/sync_manager.rs +++ b/dash-spv/src/sync/mempool/sync_manager.rs @@ -123,7 +123,7 @@ impl SyncManager for MempoolManager { // Rebuild bloom filter if the wallet's monitored set has changed. // // We poll the revision counter rather than using push-based wallet events - // for simplicity: the revision lives on `ManagedCoreAccount` and auto-bumps + // for simplicity: the revision lives on `ManagedCoreFundsAccount` and auto-bumps // on address generation and UTXO mutations, giving us a single source of // truth without needing event emission after every wallet operation. // Adding a push-based approach would require a new `select!` branch in the diff --git a/dash-spv/tests/dashd_sync/helpers.rs b/dash-spv/tests/dashd_sync/helpers.rs index 739eb1a1d..5f3162292 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; @@ -67,8 +68,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 fcde7c022..3ed280ad5 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -15,14 +15,15 @@ use key_wallet::account::ManagedAccountCollection; use key_wallet::managed_account::address_pool::{ AddressInfo, AddressPool, KeySource, PublicKeyType, }; -use key_wallet::managed_account::ManagedCoreAccount; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; +use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::AccountType; // Helper functions to get managed accounts by type fn get_managed_account_by_type<'a>( collection: &'a ManagedAccountCollection, account_type: &AccountType, -) -> Option<&'a ManagedCoreAccount> { +) -> Option<&'a ManagedCoreFundsAccount> { match account_type { AccountType::Standard { index, @@ -75,7 +76,7 @@ fn get_managed_account_by_type<'a>( fn get_managed_account_by_type_mut<'a>( collection: &'a mut ManagedAccountCollection, account_type: &AccountType, -) -> Option<&'a mut ManagedCoreAccount> { +) -> Option<&'a mut ManagedCoreFundsAccount> { match account_type { AccountType::Standard { index, @@ -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 38b54fe5a..c57d69b2e 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -21,26 +21,27 @@ 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::ManagedCoreAccount; +use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::AccountType; /// Opaque managed account handle that wraps ManagedAccount pub struct FFIManagedCoreAccount { /// The underlying managed account - pub(crate) account: Arc, + pub(crate) account: Arc, } impl FFIManagedCoreAccount { /// Create a new FFI managed account handle - pub fn new(account: &ManagedCoreAccount) -> Self { + pub fn new(account: &ManagedCoreFundsAccount) -> Self { FFIManagedCoreAccount { account: Arc::new(account.clone()), } } /// Get a reference to the inner managed account - pub fn inner(&self) -> &ManagedCoreAccount { + pub fn inner(&self) -> &ManagedCoreFundsAccount { self.account.as_ref() } } @@ -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 dc155298c..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), @@ -554,7 +555,7 @@ mod tests { #[test] fn test_comprehensive_address_generation() { use key_wallet::account::{ - ManagedAccountCollection, ManagedCoreAccount, StandardAccountType, + ManagedAccountCollection, ManagedCoreFundsAccount, StandardAccountType, }; use key_wallet::bip32::DerivationPath; use key_wallet::managed_account::address_pool::{AddressPool, AddressPoolType}; @@ -608,7 +609,7 @@ mod tests { ) .expect("Failed to create internal pool"); - let managed_account = ManagedCoreAccount::new( + let managed_account = ManagedCoreFundsAccount::new( ManagedAccountType::Standard { index: 0, standard_account_type: StandardAccountType::BIP44Account, 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-ffi/src/utxo_tests.rs b/key-wallet-ffi/src/utxo_tests.rs index 6b8fc35cc..05cd85bde 100644 --- a/key-wallet-ffi/src/utxo_tests.rs +++ b/key-wallet-ffi/src/utxo_tests.rs @@ -175,7 +175,7 @@ mod utxo_tests { use dashcore::blockdata::script::ScriptBuf; use dashcore::{Address, OutPoint, TxOut, Txid}; use key_wallet::account::account_type::StandardAccountType; - use key_wallet::managed_account::ManagedCoreAccount; + use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::utxo::Utxo; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::Network; @@ -189,7 +189,7 @@ mod utxo_tests { let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [1u8; 32]); // Create a BIP44 account with UTXOs - let mut bip44_account = ManagedCoreAccount::new( + let mut bip44_account = ManagedCoreFundsAccount::new( ManagedAccountType::Standard { index: 0, standard_account_type: StandardAccountType::BIP44Account, @@ -280,7 +280,7 @@ mod utxo_tests { fn test_managed_wallet_get_utxos_multiple_accounts() { use crate::managed_wallet::FFIManagedWalletInfo; use key_wallet::account::account_type::StandardAccountType; - use key_wallet::managed_account::ManagedCoreAccount; + use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::Network; @@ -292,7 +292,7 @@ mod utxo_tests { let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [2u8; 32]); // Create BIP44 account with 2 UTXOs - let mut bip44_account = ManagedCoreAccount::new( + let mut bip44_account = ManagedCoreFundsAccount::new( ManagedAccountType::Standard { index: 0, standard_account_type: StandardAccountType::BIP44Account, @@ -318,7 +318,7 @@ mod utxo_tests { managed_info.accounts.insert(bip44_account).unwrap(); // Create BIP32 account with 1 UTXO - let mut bip32_account = ManagedCoreAccount::new( + let mut bip32_account = ManagedCoreFundsAccount::new( ManagedAccountType::Standard { index: 0, standard_account_type: StandardAccountType::BIP32Account, @@ -344,7 +344,7 @@ mod utxo_tests { managed_info.accounts.insert(bip32_account).unwrap(); // Create CoinJoin account with 2 UTXOs - let mut coinjoin_account = ManagedCoreAccount::new( + let mut coinjoin_account = ManagedCoreFundsAccount::new( ManagedAccountType::CoinJoin { index: 0, addresses: key_wallet::managed_account::address_pool::AddressPoolBuilder::default() @@ -383,7 +383,7 @@ mod utxo_tests { fn test_managed_wallet_get_utxos() { use crate::managed_wallet::FFIManagedWalletInfo; use key_wallet::account::account_type::StandardAccountType; - use key_wallet::managed_account::ManagedCoreAccount; + use key_wallet::managed_account::ManagedCoreFundsAccount; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::Network; @@ -396,7 +396,7 @@ mod utxo_tests { let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [3u8; 32]); // Add a UTXO to Testnet account - let mut testnet_account = ManagedCoreAccount::new( + let mut testnet_account = ManagedCoreFundsAccount::new( ManagedAccountType::Standard { index: 0, standard_account_type: StandardAccountType::BIP44Account, diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index 99b042507..f607b6d65 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/CLAUDE.md b/key-wallet/CLAUDE.md index 54eed21f3..662825e88 100644 --- a/key-wallet/CLAUDE.md +++ b/key-wallet/CLAUDE.md @@ -58,7 +58,7 @@ Account (Immutable) ├── account_xpub: ExtendedPubKey └── is_watch_only: bool -ManagedCoreAccount (Mutable) +ManagedCoreFundsAccount (Mutable) ├── account_type: ManagedAccountType (contains address pools) ├── metadata: AccountMetadata ├── balance: WalletCoreBalance @@ -202,7 +202,7 @@ Tests are organized by functionality: let account_xpriv = derive_account_key(master, account_type)?; let account_xpub = ExtendedPubKey::from_priv(&secp, &account_xpriv); let account = Account::new(wallet_id, account_type, account_xpub, network)?; -let managed_account = ManagedCoreAccount::from_account(&account); +let managed_account = ManagedCoreFundsAccount::from_account(&account); ``` ### 2. Address Generation Pattern diff --git a/key-wallet/IMPLEMENTATION_SUMMARY.md b/key-wallet/IMPLEMENTATION_SUMMARY.md index 8e4454774..07b2bb206 100644 --- a/key-wallet/IMPLEMENTATION_SUMMARY.md +++ b/key-wallet/IMPLEMENTATION_SUMMARY.md @@ -180,10 +180,10 @@ let account = wallet.create_account( )?; // Wrap in a managed account for mutable operations -let mut managed = ManagedCoreAccount::from_account(&account); +let mut managed = ManagedCoreFundsAccount::from_account(&account); // Get a receive address -let address = managed.get_next_receive_address()?; +let address = managed.next_receive_address(Some(&account.account_xpub), true)?; ``` ## Dependencies diff --git a/key-wallet/README.md b/key-wallet/README.md index cd2f9785e..8e0d3a880 100644 --- a/key-wallet/README.md +++ b/key-wallet/README.md @@ -94,7 +94,7 @@ println!("Wallet ID: {:?}", hex::encode(wallet.wallet_id)); ```rust use key_wallet::account::{Account, AccountType, StandardAccountType}; -use key_wallet::managed_account::ManagedCoreAccount; +use key_wallet::managed_account::ManagedCoreFundsAccount; // Create a standard BIP44 account let account = wallet.create_account( @@ -106,7 +106,7 @@ let account = wallet.create_account( )?; // Convert to managed account for mutable operations -let mut managed_account = ManagedCoreAccount::from_account(&account); +let mut managed_account = ManagedCoreFundsAccount::from_account(&account); // Generate receive addresses let addresses = managed_account.generate_receive_addresses(10)?; @@ -218,7 +218,7 @@ if result.is_relevant { - `Wallet`: Main wallet structure managing accounts and keys - `Account`: Individual account within a wallet -- `ManagedCoreAccount`: Mutable account with address pools and metadata +- `ManagedCoreFundsAccount`: Mutable account with address pools and metadata - `Mnemonic`: BIP39 mnemonic phrase handling - `ExtendedPrivKey`/`ExtendedPubKey`: BIP32 extended keys - `DerivationPath`: HD wallet derivation paths @@ -240,7 +240,7 @@ if result.is_relevant { ### From v0.39 to v0.40 -- Account structure split into immutable `Account` and mutable `ManagedCoreAccount` +- Account structure split into immutable `Account` and mutable `ManagedCoreFundsAccount` - New transaction checking system with optimized routing - Enhanced address pool management with pre-generation support - Improved gap limit tracking with staged discovery diff --git a/key-wallet/src/account/mod.rs b/key-wallet/src/account/mod.rs index b75082a31..444ee9768 100644 --- a/key-wallet/src/account/mod.rs +++ b/key-wallet/src/account/mod.rs @@ -37,7 +37,7 @@ pub use crate::managed_account::managed_account_type::ManagedAccountType; pub use crate::managed_account::transaction_record::{ InputDetail, OutputDetail, OutputRole, TransactionDirection, TransactionRecord, }; -pub use crate::managed_account::ManagedCoreAccount; +pub use crate::managed_account::ManagedCoreFundsAccount; pub use account_collection::AccountCollection; pub use account_trait::AccountTrait; pub use account_type::{AccountType, StandardAccountType}; diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 307b30bd8..f297054de 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -12,9 +12,10 @@ 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::ManagedCoreAccount; +use crate::managed_account::ManagedCoreFundsAccount; use crate::transaction_checking::account_checker::CoreAccountTypeMatch; use crate::{Account, AccountCollection}; use crate::{KeySource, Network}; @@ -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, @@ -113,35 +114,35 @@ macro_rules! get_by_account_type_match_impl { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ManagedAccountCollection { /// Standard BIP44 accounts by index - pub standard_bip44_accounts: BTreeMap, + pub standard_bip44_accounts: BTreeMap, /// Standard BIP32 accounts by index - pub standard_bip32_accounts: BTreeMap, + pub standard_bip32_accounts: BTreeMap, /// CoinJoin accounts by index - pub coinjoin_accounts: BTreeMap, + pub coinjoin_accounts: BTreeMap, /// Identity registration account (optional) - pub identity_registration: Option, + pub identity_registration: Option, /// Identity top-up accounts by registration index - pub identity_topup: BTreeMap, + pub identity_topup: BTreeMap, /// Identity top-up not bound to identity (optional) - pub identity_topup_not_bound: Option, + pub identity_topup_not_bound: Option, /// Identity invitation account (optional) - pub identity_invitation: Option, + pub identity_invitation: Option, /// Asset lock address top-up account (optional) - pub asset_lock_address_topup: Option, + pub asset_lock_address_topup: Option, /// Asset lock shielded address top-up account (optional) - pub asset_lock_shielded_address_topup: Option, + pub asset_lock_shielded_address_topup: Option, /// Provider voting keys (optional) - pub provider_voting_keys: Option, + pub provider_voting_keys: Option, /// Provider owner keys (optional) - pub provider_owner_keys: Option, + pub provider_owner_keys: Option, /// Provider operator keys (optional) - pub provider_operator_keys: Option, + pub provider_operator_keys: Option, /// Provider platform keys (optional) - pub provider_platform_keys: Option, + pub provider_platform_keys: Option, /// DashPay receiving funds accounts keyed by (index, user_id, friend_id) - pub dashpay_receival_accounts: BTreeMap, + pub dashpay_receival_accounts: BTreeMap, /// DashPay external accounts keyed by (index, user_id, friend_id) - pub dashpay_external_accounts: BTreeMap, + pub dashpay_external_accounts: BTreeMap, /// Platform Payment accounts (DIP-17) /// Uses ManagedPlatformAccount for simplified balance tracking without transactions/UTXOs pub platform_payment_accounts: BTreeMap, @@ -266,10 +267,10 @@ impl ManagedAccountCollection { /// /// Returns an error if a PlatformPayment account type is passed, since those /// should use `insert_platform_account()` with `ManagedPlatformAccount` instead. - pub fn insert(&mut self, account: ManagedCoreAccount) -> Result<(), crate::error::Error> { + 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, @@ -369,7 +370,7 @@ impl ManagedAccountCollection { .. } => { // Platform Payment accounts should use insert_platform_account() instead - // as they use ManagedPlatformAccount, not ManagedCoreAccount + // as they use ManagedPlatformAccount, not ManagedCoreFundsAccount return Err(crate::error::Error::InvalidParameter( "Use insert_platform_account() for Platform Payment accounts".into(), )); @@ -507,7 +508,7 @@ impl ManagedAccountCollection { /// Create a ManagedAccount from an Account fn create_managed_account_from_account( account: &Account, - ) -> Result { + ) -> Result { // Use the account's existing public key let key_source = KeySource::Public(account.account_xpub); Self::create_managed_account_from_account_type( @@ -521,8 +522,8 @@ impl ManagedAccountCollection { /// Create a ManagedAccount from a BLS Account #[cfg(feature = "bls")] fn create_managed_account_from_bls_account( - account: &super::BLSAccount, - ) -> Result { + account: &crate::account::BLSAccount, + ) -> Result { let key_source = KeySource::BLSPublic(account.bls_public_key.clone()); Self::create_managed_account_from_account_type( account.account_type, @@ -535,9 +536,9 @@ impl ManagedAccountCollection { /// Create a ManagedAccount from an EdDSA Account #[cfg(feature = "eddsa")] fn create_managed_account_from_eddsa_account( - account: &super::EdDSAAccount, + account: &crate::account::EdDSAAccount, xpriv: Option, - ) -> Result { + ) -> Result { // EdDSA requires hardened derivation, so we need the private key to generate addresses let key_source = match xpriv { Some(priv_key) => KeySource::EdDSAPrivate(priv_key), @@ -557,7 +558,7 @@ impl ManagedAccountCollection { network: Network, is_watch_only: bool, key_source: &KeySource, - ) -> Result { + ) -> Result { // Get the derivation path for this account type let base_path = account_type .derivation_path(network) @@ -785,7 +786,7 @@ impl ManagedAccountCollection { } }; - Ok(ManagedCoreAccount::new(managed_type, network, is_watch_only)) + Ok(ManagedCoreFundsAccount::new(managed_type, network, is_watch_only)) } /// Create a ManagedPlatformAccount from an Account for Platform Payment accounts @@ -819,7 +820,7 @@ impl ManagedAccountCollection { )) } - pub fn get(&self, index: u32) -> Option<&ManagedCoreAccount> { + pub fn get(&self, index: u32) -> Option<&ManagedCoreFundsAccount> { // Try standard BIP44 first if let Some(account) = self.standard_bip44_accounts.get(&index) { return Some(account); @@ -844,7 +845,7 @@ impl ManagedAccountCollection { } /// Get a mutable account by index - pub fn get_mut(&mut self, index: u32) -> Option<&mut ManagedCoreAccount> { + pub fn get_mut(&mut self, index: u32) -> Option<&mut ManagedCoreFundsAccount> { // Try standard BIP44 first if let Some(account) = self.standard_bip44_accounts.get_mut(&index) { return Some(account); @@ -872,7 +873,7 @@ impl ManagedAccountCollection { pub fn get_by_account_type_match( &self, account_type_match: &CoreAccountTypeMatch, - ) -> Option<&ManagedCoreAccount> { + ) -> Option<&ManagedCoreFundsAccount> { get_by_account_type_match_impl!(self, account_type_match, get, as_ref, values) } @@ -880,12 +881,12 @@ impl ManagedAccountCollection { pub fn get_by_account_type_match_mut( &mut self, account_type_match: &CoreAccountTypeMatch, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { get_by_account_type_match_impl!(self, account_type_match, get_mut, as_mut, values_mut) } /// Remove an account from the collection - pub fn remove(&mut self, index: u32) -> Option { + pub fn remove(&mut self, index: u32) -> Option { // Try standard BIP44 first if let Some(account) = self.standard_bip44_accounts.remove(&index) { return Some(account); @@ -935,7 +936,7 @@ impl ManagedAccountCollection { } /// Get all accounts - pub fn all_accounts(&self) -> Vec<&ManagedCoreAccount> { + pub fn all_accounts(&self) -> Vec<&ManagedCoreFundsAccount> { let mut accounts = Vec::new(); // Add standard BIP44 accounts @@ -994,7 +995,7 @@ impl ManagedAccountCollection { } /// Get all accounts mutably - pub fn all_accounts_mut(&mut self) -> Vec<&mut ManagedCoreAccount> { + pub fn all_accounts_mut(&mut self) -> Vec<&mut ManagedCoreFundsAccount> { let mut accounts = Vec::new(); // Add standard BIP44 accounts 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 new file mode 100644 index 000000000..88d3abc15 --- /dev/null +++ b/key-wallet/src/managed_account/managed_core_funds_account.rs @@ -0,0 +1,658 @@ +//! Managed core funds account: keys-account state plus balance, UTXOs, and spent outpoints. +//! +//! 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::TransactionRecord; +use crate::managed_account::address_pool; +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, +}; +use crate::transaction_checking::transaction_router::TransactionType; +use crate::transaction_checking::{AccountMatch, TransactionContext}; +use crate::utxo::Utxo; +use crate::wallet::balance::WalletCoreBalance; +use crate::{ExtendedPubKey, Network}; +use dashcore::blockdata::transaction::OutPoint; +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 including balance and UTXOs. +/// +/// 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 { + /// Shared keys-account state (address pools, transactions, network, + /// is_watch_only, monitor revision). + keys: ManagedCoreKeysAccount, + /// Account balance information + pub balance: WalletCoreBalance, + /// 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, +} + +impl ManagedCoreFundsAccount { + /// Create a new managed funds account + pub fn new( + managed_account_type: ManagedAccountType, + network: Network, + is_watch_only: bool, + ) -> Self { + Self { + keys: ManagedCoreKeysAccount::new(managed_account_type, network, is_watch_only), + balance: WalletCoreBalance::default(), + utxos: BTreeMap::new(), + spent_outpoints: HashSet::new(), + } + } + + /// Create a `ManagedCoreFundsAccount` from an [`Account`](super::super::Account). + pub fn from_account(account: &super::super::Account) -> Self { + Self::wrap(ManagedCoreKeysAccount::from_account(account)) + } + + /// Create a `ManagedCoreFundsAccount` from a [`BLSAccount`]. + #[cfg(feature = "bls")] + pub fn from_bls_account(account: &BLSAccount) -> Self { + Self::wrap(ManagedCoreKeysAccount::from_bls_account(account)) + } + + /// Create a `ManagedCoreFundsAccount` from an [`EdDSAAccount`]. + #[cfg(feature = "eddsa")] + pub fn from_eddsa_account(account: &EdDSAAccount) -> Self { + Self::wrap(ManagedCoreKeysAccount::from_eddsa_account(account)) + } + + fn wrap(keys: ManagedCoreKeysAccount) -> Self { + Self { + keys, + balance: WalletCoreBalance::default(), + utxos: BTreeMap::new(), + spent_outpoints: HashSet::new(), + } + } + + /// Get a reference to the inner keys-account state. + pub fn keys(&self) -> &ManagedCoreKeysAccount { + &self.keys + } + + /// Get a mutable reference to the inner keys-account state. + pub fn keys_mut(&mut self) -> &mut ManagedCoreKeysAccount { + &mut self.keys + } + + /// 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 UTXOs for received outputs, remove spent ones. + fn update_utxos( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + ) { + // Update UTXOs only for spendable account types + match self.keys.managed_account_type() { + ManagedAccountType::Standard { + .. + } + | ManagedAccountType::CoinJoin { + .. + } + | ManagedAccountType::DashpayReceivingFunds { + .. + } + | ManagedAccountType::DashpayExternalAccount { + .. + } => { + let involved_addrs: BTreeSet<_> = account_match + .account_type_match + .all_involved_addresses() + .iter() + .map(|info| info.address.clone()) + .collect(); + let change_addrs: BTreeSet<_> = account_match + .account_type_match + .involved_change_addresses() + .iter() + .map(|info| info.address.clone()) + .collect(); + + // Detect a self-send: this account owns at least one input being + // spent. `account_match.sent` is computed by matching inputs against + // this account's UTXO set, so a non-zero value means we owned at + // least one of the spent outpoints. + let has_owned_input = account_match.sent > 0; + + 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, network) { + if involved_addrs.contains(&addr) { + let outpoint = OutPoint { + txid, + vout: vout as u32, + }; + + // Check if this outpoint was already spent by a transaction we've seen. + // This handles out-of-order block processing during rescan where a + // spending transaction at a higher height may be processed before + // the transaction that created the UTXO. + // TODO: This is mostly needed for wallet rescan from storage with the + // there is a timing issue with event processing which might lead to + // invalid UTXO set / balances. There might be a way around it. + if self.is_outpoint_spent(&outpoint) { + tracing::debug!( + outpoint = %outpoint, + "Skipping UTXO already spent by previously processed transaction" + ); + continue; + } + + // Flag outputs from a "trusted" mempool transaction we created — + // one that spends at least one of our own UTXOs and pays this + // output back to one of our internal (change) addresses. Such + // an output is just our previously-tracked funds returning, so + // `update_balance` credits it to the confirmed bucket even + // before the parent transaction settles. + let is_trusted_output = has_owned_input && change_addrs.contains(&addr); + let txout = dashcore::TxOut { + value: output.value, + script_pubkey: output.script_pubkey.clone(), + }; + let block_height = context.block_info().map_or(0, |info| info.height); + let mut utxo = + Utxo::new(outpoint, txout, addr, block_height, tx.is_coin_base()); + utxo.is_confirmed = context.confirmed(); + utxo.is_instantlocked = + matches!(context, TransactionContext::InstantSend(_)); + utxo.is_trusted = is_trusted_output; + self.utxos.insert(outpoint, utxo); + utxos_changed = true; + } + } + } + + // Remove UTXOs spent by this transaction and track spent outpoints + for input in &tx.input { + self.spent_outpoints.insert(input.previous_output); + + if self.utxos.remove(&input.previous_output).is_some() { + tracing::debug!( + outpoint = %input.previous_output, + txid = %tx.txid(), + "Removed spent UTXO" + ); + utxos_changed = true; + } + } + + if utxos_changed { + self.keys.bump_monitor_revision(); + } + } + _ => {} + } + } + + /// Re-process an existing transaction with updated context (e.g., mempool→block confirmation) + /// and potentially new address matches from gap limit rescans. + pub(crate) fn confirm_transaction( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + transaction_type: TransactionType, + ) -> bool { + 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.keys.transactions_mut().get_mut(&tx.txid()) { + debug_assert_eq!( + tx_record.transaction_type, + transaction_type, + "transaction_type changed between recordings for {}", + tx.txid() + ); + if tx_record.context != context { + let was_confirmed = tx_record.context.confirmed(); + tx_record.update_context(context.clone()); + // Only signal a change when confirmation status actually changes, + // not for upgrades within the confirmed state (e.g. InBlock → InChainLockedBlock). + // TODO: emit a change event for InBlock → InChainLockedBlock once chainlock + // wallet transaction events are properly handled + changed = !was_confirmed; + } + } + self.update_utxos(tx, account_match, context); + changed + } + + /// Record a new transaction and update UTXOs for spendable account types + pub(crate) fn record_transaction( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + transaction_type: TransactionType, + ) -> TransactionRecord { + let net_amount = account_match.received as i64 - account_match.sent as i64; + + let receive_addrs: HashSet<_> = account_match + .account_type_match + .involved_receive_addresses() + .iter() + .map(|info| &info.address) + .collect(); + let change_addrs: HashSet<_> = account_match + .account_type_match + .involved_change_addresses() + .iter() + .map(|info| &info.address) + .collect(); + + // Input details must be built before `update_utxos` removes spent UTXOs + let mut input_details = Vec::new(); + if !tx.is_coin_base() { + for (idx, input) in tx.input.iter().enumerate() { + if let Some(utxo) = self.utxos.get(&input.previous_output) { + input_details.push(InputDetail { + index: idx as u32, + value: utxo.txout.value, + address: utxo.address.clone(), + }); + } + } + } + + // Use both UTXO-based input details and `account_match.sent` as signals + // that we created this transaction. The UTXO set may be incomplete + // (e.g., partial rescan) so `account_match.sent > 0` catches cases where + // 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, network).ok()) + .collect(); + + // Build output details — annotate every output with its role + let mut output_details = Vec::new(); + for (idx, output) in tx.output.iter().enumerate() { + let role = match &resolved_outputs[idx] { + Some(addr) if receive_addrs.contains(addr) => OutputRole::Received, + Some(addr) if change_addrs.contains(addr) => OutputRole::Change, + Some(_) if has_inputs => OutputRole::Sent, + Some(_) => continue, + None => { + if output.script_pubkey.is_provably_unspendable() { + OutputRole::Unspendable + } else if has_inputs { + OutputRole::Sent + } else { + continue; + } + } + }; + output_details.push(OutputDetail { + index: idx as u32, + role, + address: resolved_outputs[idx].clone(), + value: output.value, + }); + } + + // Determine direction + let has_sent = output_details.iter().any(|d| d.role == OutputRole::Sent); + let has_our_outputs = output_details + .iter() + .any(|d| d.role == OutputRole::Received || d.role == OutputRole::Change); + let direction = if transaction_type == TransactionType::CoinJoin { + TransactionDirection::CoinJoin + } else if !has_sent && has_inputs && has_our_outputs { + TransactionDirection::Internal + } else if has_inputs { + TransactionDirection::Outgoing + } else { + TransactionDirection::Incoming + }; + + let tx_record = TransactionRecord::new( + tx.clone(), + self.keys.managed_account_type().to_account_type(), + context.clone(), + transaction_type, + direction, + input_details, + output_details, + net_amount, + ); + + let record = tx_record.clone(); + self.keys.transactions_mut().insert(tx.txid(), tx_record); + + self.update_utxos(tx, account_match, context); + record + } + + /// Mark all UTXOs belonging to a transaction as InstantSend-locked. + /// Returns `true` if any UTXO was newly marked. + pub(crate) fn mark_utxos_instant_send(&mut self, txid: &Txid) -> bool { + let mut any_changed = false; + for utxo in self.utxos.values_mut() { + if utxo.outpoint.txid == *txid && !utxo.is_instantlocked { + utxo.is_instantlocked = true; + any_changed = true; + } + } + any_changed + } + + /// Return the UTXOs of this account for which + /// [`Utxo::is_spendable`] holds at `last_processed_height`. See that method + /// for the exact policy. Call this per-account rather than + /// aggregating across the wallet, since spendability is + /// account-type specific. + pub fn spendable_utxos(&self, last_processed_height: u32) -> BTreeSet<&Utxo> { + self.utxos.values().filter(|utxo| utxo.is_spendable(last_processed_height)).collect() + } + + /// Update the account balance. + /// + /// Mature, non-locked UTXOs land in either the `confirmed` bucket + /// (in a block, InstantSend-locked, or trusted mempool change) or + /// the `unconfirmed` bucket (untrusted mempool only). Trusted + /// mempool change is surfaced as confirmed because it is just our + /// previously-tracked funds returning — see [`Utxo::is_trusted`]. + /// Both buckets are spendable per [`Utxo::is_spendable`]; the split + /// is only for display. + pub fn update_balance(&mut self, last_processed_height: u32) { + let mut confirmed = 0; + let mut unconfirmed = 0; + let mut immature = 0; + let mut locked = 0; + for utxo in self.utxos.values() { + let value = utxo.txout.value; + if utxo.is_locked { + locked += value; + } else if !utxo.is_mature(last_processed_height) { + immature += value; + } else if utxo.is_confirmed || utxo.is_instantlocked || utxo.is_trusted { + confirmed += value; + } else { + unconfirmed += value; + } + } + self.balance = WalletCoreBalance::new(confirmed, unconfirmed, immature, locked); + } + + /// Generate the next receive address using the optionally provided extended public key + /// 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 { + if let ManagedAccountType::Standard { + external_addresses, + .. + } = self.keys.managed_account_type_mut() + { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::Public(*xpub), + None => address_pool::KeySource::NoKeySource, + }; + + let addr = + external_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 receive address", + })?; + 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. + /// Only valid for Standard accounts. + pub fn next_change_address( + &mut self, + account_xpub: Option<&ExtendedPubKey>, + add_to_state: bool, + ) -> Result { + if let ManagedAccountType::Standard { + internal_addresses, + .. + } = self.keys.managed_account_type_mut() + { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::Public(*xpub), + None => address_pool::KeySource::NoKeySource, + }; + + let addr = + internal_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 change address", + })?; + 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. + /// Only valid for Standard accounts. + pub fn next_receive_addresses( + &mut self, + account_xpub: Option<&ExtendedPubKey>, + count: usize, + add_to_state: bool, + ) -> Result, String> { + if let ManagedAccountType::Standard { + external_addresses, + .. + } = self.keys.managed_account_type_mut() + { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::Public(*xpub), + None => address_pool::KeySource::NoKeySource, + }; + + let addresses = + external_addresses.next_unused_multiple(count, &key_source, add_to_state); + if addresses.is_empty() && count > 0 { + Err("Failed to generate any receive addresses".to_string()) + } else if addresses.len() < count + && matches!(key_source, address_pool::KeySource::NoKeySource) + { + Err(format!( + "Could only generate {} out of {} requested addresses (no key source)", + addresses.len(), + count + )) + } else { + Ok(addresses) + } + } else { + Err("Cannot generate receive addresses for non-standard account type".to_string()) + } + } + + /// 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> { + if let ManagedAccountType::Standard { + internal_addresses, + .. + } = self.keys.managed_account_type_mut() + { + let key_source = match account_xpub { + Some(xpub) => address_pool::KeySource::Public(*xpub), + None => address_pool::KeySource::NoKeySource, + }; + + let addresses = + internal_addresses.next_unused_multiple(count, &key_source, add_to_state); + if addresses.is_empty() && count > 0 { + Err("Failed to generate any change addresses".to_string()) + } else if addresses.len() < count + && matches!(key_source, address_pool::KeySource::NoKeySource) + { + Err(format!( + "Could only generate {} out of {} requested addresses (no key source)", + addresses.len(), + count + )) + } else { + Ok(addresses) + } + } else { + Err("Cannot generate change addresses for non-standard account type".to_string()) + } + } + + /// Get the external gap limit for standard accounts + pub fn external_gap_limit(&self) -> Option { + match self.keys.managed_account_type() { + ManagedAccountType::Standard { + external_addresses, + .. + } => Some(external_addresses.gap_limit), + _ => None, + } + } + + /// Get the internal gap limit for standard accounts + pub fn internal_gap_limit(&self) -> Option { + match self.keys.managed_account_type() { + ManagedAccountType::Standard { + internal_addresses, + .. + } => Some(internal_addresses.gap_limit), + _ => None, + } + } +} + +impl ManagedAccountTrait for ManagedCoreFundsAccount { + fn managed_account_type(&self) -> &ManagedAccountType { + self.keys.managed_account_type() + } + + fn managed_account_type_mut(&mut self) -> &mut ManagedAccountType { + self.keys.managed_account_type_mut() + } + + fn network(&self) -> Network { + self.keys.network() + } + + fn is_watch_only(&self) -> bool { + self.keys.is_watch_only() + } + + fn transactions(&self) -> &BTreeMap { + self.keys.transactions() + } + + fn transactions_mut(&mut self) -> &mut BTreeMap { + self.keys.transactions_mut() + } + + fn monitor_revision(&self) -> u64 { + self.keys.monitor_revision() + } + + fn bump_monitor_revision(&mut self) { + self.keys.bump_monitor_revision() + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for ManagedCoreFundsAccount { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Helper { + keys: ManagedCoreKeysAccount, + balance: WalletCoreBalance, + utxos: BTreeMap, + } + + let helper = Helper::deserialize(deserializer)?; + + let spent_outpoints = helper + .keys + .transactions() + .values() + .flat_map(|record| &record.transaction.input) + .map(|input| input.previous_output) + .collect(); + + Ok(ManagedCoreFundsAccount { + keys: helper.keys, + balance: helper.balance, + utxos: helper.utxos, + spent_outpoints, + }) + } +} diff --git a/key-wallet/src/managed_account/managed_core_keys_account.rs b/key-wallet/src/managed_account/managed_core_keys_account.rs new file mode 100644 index 000000000..b9d26b632 --- /dev/null +++ b/key-wallet/src/managed_account/managed_core_keys_account.rs @@ -0,0 +1,157 @@ +//! Managed core keys account: address pools and key derivation without funds tracking +//! +//! This module contains a lightweight mutable account state that omits the funds +//! bookkeeping (balance, UTXOs, spent outpoints) carried by [`crate::managed_account::ManagedCoreFundsAccount`]. +//! It is intended for accounts that exist primarily to derive keys/addresses for +//! special-purpose flows (identity registration, asset locks, masternode provider +//! keys) rather than to hold and spend Dash directly. + +#[cfg(feature = "bls")] +use crate::account::BLSAccount; +#[cfg(feature = "eddsa")] +use crate::account::EdDSAAccount; +use crate::account::TransactionRecord; +use crate::managed_account::address_pool; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; +use crate::managed_account::managed_account_type::ManagedAccountType; +use crate::Network; +use dashcore::Txid; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// 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. +/// +/// 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 + managed_account_type: ManagedAccountType, + /// Network this account belongs to + network: Network, + /// Whether this is a watch-only account + is_watch_only: bool, + /// Transaction history for this account + 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))] + monitor_revision: u64, +} + +impl ManagedCoreKeysAccount { + /// Create a new managed keys account + pub fn new( + managed_account_type: ManagedAccountType, + network: Network, + is_watch_only: bool, + ) -> Self { + Self { + managed_account_type, + network, + is_watch_only, + transactions: BTreeMap::new(), + monitor_revision: 0, + } + } + + /// 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( + account.account_type, + account.network, + &key_source, + ) + .unwrap_or_else(|_| { + 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) + } + + /// 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()); + let managed_type = ManagedAccountType::from_account_type( + account.account_type, + account.network, + &key_source, + ) + .unwrap_or_else(|_| { + 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) + } + + /// 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, + account.network, + &key_source, + ) + .expect("Should succeed with NoKeySource"); + + Self::new(managed_type, account.network, account.is_watch_only) + } +} + +impl ManagedAccountTrait for ManagedCoreKeysAccount { + fn managed_account_type(&self) -> &ManagedAccountType { + &self.managed_account_type + } + + fn managed_account_type_mut(&mut self) -> &mut ManagedAccountType { + &mut self.managed_account_type + } + + fn network(&self) -> Network { + self.network + } + + fn is_watch_only(&self) -> bool { + self.is_watch_only + } + + fn transactions(&self) -> &BTreeMap { + &self.transactions + } + + fn transactions_mut(&mut self) -> &mut BTreeMap { + &mut self.transactions + } + + fn monitor_revision(&self) -> u64 { + self.monitor_revision + } + + fn bump_monitor_revision(&mut self) { + self.monitor_revision += 1; + } +} diff --git a/key-wallet/src/managed_account/managed_platform_account.rs b/key-wallet/src/managed_account/managed_platform_account.rs index 6087d0f8d..2b825f526 100644 --- a/key-wallet/src/managed_account/managed_platform_account.rs +++ b/key-wallet/src/managed_account/managed_platform_account.rs @@ -1,7 +1,7 @@ //! Managed Platform Account for DIP-17 Platform Payment addresses //! //! This module provides the `ManagedPlatformAccount` type which is a simplified -//! account structure for Platform Payment accounts. Unlike `ManagedCoreAccount`, +//! account structure for Platform Payment accounts. Unlike `ManagedCoreFundsAccount`, //! this type: //! - Uses a simple `u64` balance instead of `WalletCoreBalance` //! - Tracks per-address balances directly @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; /// Managed Platform Account for DIP-17 Platform Payment addresses /// /// This is a simplified account structure designed specifically for Platform -/// Payment accounts (DIP-17). It differs from `ManagedCoreAccount` in that: +/// Payment accounts (DIP-17). It differs from `ManagedCoreFundsAccount` in that: /// /// - **Balance**: Simple `u64` credit balance (1000 credits = 1 duff) /// - **Address Balances**: Direct mapping of addresses to their credit balances diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 7d5c0147f..f5801e1c8 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -1,1353 +1,26 @@ -//! Managed account structure with mutable state +//! Managed account state and helpers //! -//! This module contains the mutable account state that changes during wallet operation, -//! kept separate from the immutable Account structure. - -#[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; -#[cfg(any(feature = "bls", feature = "eddsa"))] -use crate::managed_account::address_pool::PublicKeyType; -use crate::managed_account::transaction_record::{ - InputDetail, OutputDetail, OutputRole, TransactionDirection, -}; -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 managed_account_type::ManagedAccountType; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::collections::{BTreeSet, HashSet}; +//! This module groups the mutable account state used during wallet operation, +//! kept separate from the immutable [`Account`](crate::Account) structure. +//! +//! Two managed-account variants are exposed: +//! +//! - [`ManagedCoreFundsAccount`]: full state including balance, UTXO set and +//! spent-outpoint tracking. Used for accounts that hold and spend funds +//! (Standard, CoinJoin, DashPay). +//! - [`ManagedCoreKeysAccount`]: lightweight state without balance/UTXO/spent-outpoint +//! tracking. Intended for accounts that primarily derive keys for special-purpose +//! flows (identity registration, asset locks, masternode provider keys). pub mod address_pool; pub mod managed_account_collection; pub mod managed_account_trait; pub mod managed_account_type; +pub mod managed_core_funds_account; +pub mod managed_core_keys_account; pub mod managed_platform_account; pub mod platform_address; pub mod transaction_record; -/// Managed account with mutable state -/// -/// This struct contains the mutable state of an account including address pools -/// and balance information. It is managed separately from the immutable Account -/// structure. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize))] -pub struct ManagedCoreAccount { - /// 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, - /// 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 ManagedCoreAccount { - /// Create a new managed account - pub fn new( - managed_account_type: ManagedAccountType, - network: Network, - is_watch_only: bool, - ) -> Self { - Self { - 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 - pub fn from_account(account: &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) - } - - /// Create a ManagedAccount from a BLS Account - #[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) - } - - /// Create a ManagedAccount from an EdDSA Account - #[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() - } - - /// 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 - } - } - - /// 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 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)) - } - } - } - - /// 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) - } - - /// Add new ones for received outputs, remove spent ones - fn update_utxos( - &mut self, - tx: &Transaction, - account_match: &AccountMatch, - context: TransactionContext, - ) { - // Update UTXOs only for spendable account types - match &mut self.managed_account_type { - ManagedAccountType::Standard { - .. - } - | ManagedAccountType::CoinJoin { - .. - } - | ManagedAccountType::DashpayReceivingFunds { - .. - } - | ManagedAccountType::DashpayExternalAccount { - .. - } => { - let involved_addrs: BTreeSet<_> = account_match - .account_type_match - .all_involved_addresses() - .iter() - .map(|info| info.address.clone()) - .collect(); - let change_addrs: BTreeSet<_> = account_match - .account_type_match - .involved_change_addresses() - .iter() - .map(|info| info.address.clone()) - .collect(); - - // Detect a self-send: this account owns at least one input being - // spent. `account_match.sent` is computed by matching inputs against - // this account's UTXO set, so a non-zero value means we owned at - // least one of the spent outpoints. - let has_owned_input = account_match.sent > 0; - - let txid = tx.txid(); - let mut utxos_changed = false; - - // 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 involved_addrs.contains(&addr) { - let outpoint = OutPoint { - txid, - vout: vout as u32, - }; - - // Check if this outpoint was already spent by a transaction we've seen. - // This handles out-of-order block processing during rescan where a - // spending transaction at a higher height may be processed before - // the transaction that created the UTXO. - // TODO: This is mostly needed for wallet rescan from storage with the - // there is a timing issue with event processing which might lead to - // invalid UTXO set / balances. There might be a way around it. - if self.is_outpoint_spent(&outpoint) { - tracing::debug!( - outpoint = %outpoint, - "Skipping UTXO already spent by previously processed transaction" - ); - continue; - } - - // Flag outputs from a "trusted" mempool transaction we created — - // one that spends at least one of our own UTXOs and pays this - // output back to one of our internal (change) addresses. Such - // an output is just our previously-tracked funds returning, so - // `update_balance` credits it to the confirmed bucket even - // before the parent transaction settles. - let is_trusted_output = has_owned_input && change_addrs.contains(&addr); - let txout = dashcore::TxOut { - value: output.value, - script_pubkey: output.script_pubkey.clone(), - }; - let block_height = context.block_info().map_or(0, |info| info.height); - let mut utxo = - Utxo::new(outpoint, txout, addr, block_height, tx.is_coin_base()); - utxo.is_confirmed = context.confirmed(); - utxo.is_instantlocked = - matches!(context, TransactionContext::InstantSend(_)); - utxo.is_trusted = is_trusted_output; - self.utxos.insert(outpoint, utxo); - utxos_changed = true; - } - } - } - - // Remove UTXOs spent by this transaction and track spent outpoints - for input in &tx.input { - self.spent_outpoints.insert(input.previous_output); - - if self.utxos.remove(&input.previous_output).is_some() { - tracing::debug!( - outpoint = %input.previous_output, - txid = %tx.txid(), - "Removed spent UTXO" - ); - utxos_changed = true; - } - } - - if utxos_changed { - self.monitor_revision += 1; - } - } - _ => {} - } - } - - /// Re-process an existing transaction with updated context (e.g., mempool→block confirmation) - /// and potentially new address matches from gap limit rescans. - pub(crate) fn confirm_transaction( - &mut self, - tx: &Transaction, - account_match: &AccountMatch, - context: TransactionContext, - transaction_type: TransactionType, - ) -> bool { - if !self.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()) { - debug_assert_eq!( - tx_record.transaction_type, - transaction_type, - "transaction_type changed between recordings for {}", - tx.txid() - ); - if tx_record.context != context { - let was_confirmed = tx_record.context.confirmed(); - tx_record.update_context(context.clone()); - // Only signal a change when confirmation status actually changes, - // not for upgrades within the confirmed state (e.g. InBlock → InChainLockedBlock). - // TODO: emit a change event for InBlock → InChainLockedBlock once chainlock - // wallet transaction events are properly handled - changed = !was_confirmed; - } - } - self.update_utxos(tx, account_match, context); - changed - } - - /// Record a new transaction and update UTXOs for spendable account types - pub(crate) fn record_transaction( - &mut self, - tx: &Transaction, - account_match: &AccountMatch, - context: TransactionContext, - transaction_type: TransactionType, - ) -> TransactionRecord { - let net_amount = account_match.received as i64 - account_match.sent as i64; - - let receive_addrs: HashSet<_> = account_match - .account_type_match - .involved_receive_addresses() - .iter() - .map(|info| &info.address) - .collect(); - let change_addrs: HashSet<_> = account_match - .account_type_match - .involved_change_addresses() - .iter() - .map(|info| &info.address) - .collect(); - - // Input details must be built before `update_utxos` removes spent UTXOs - let mut input_details = Vec::new(); - if !tx.is_coin_base() { - for (idx, input) in tx.input.iter().enumerate() { - if let Some(utxo) = self.utxos.get(&input.previous_output) { - input_details.push(InputDetail { - index: idx as u32, - value: utxo.txout.value, - address: utxo.address.clone(), - }); - } - } - } - - // Use both UTXO-based input details and `account_match.sent` as signals - // that we created this transaction. The UTXO set may be incomplete - // (e.g., partial rescan) so `account_match.sent > 0` catches cases where - // the transaction still spent our funds even without matching UTXOs. - let has_inputs = !input_details.is_empty() || account_match.sent > 0; - - let resolved_outputs: Vec> = tx - .output - .iter() - .map(|output| Address::from_script(&output.script_pubkey, self.network).ok()) - .collect(); - - // Build output details — annotate every output with its role - let mut output_details = Vec::new(); - for (idx, output) in tx.output.iter().enumerate() { - let role = match &resolved_outputs[idx] { - Some(addr) if receive_addrs.contains(addr) => OutputRole::Received, - Some(addr) if change_addrs.contains(addr) => OutputRole::Change, - Some(_) if has_inputs => OutputRole::Sent, - Some(_) => continue, - None => { - if output.script_pubkey.is_provably_unspendable() { - OutputRole::Unspendable - } else if has_inputs { - OutputRole::Sent - } else { - continue; - } - } - }; - output_details.push(OutputDetail { - index: idx as u32, - role, - address: resolved_outputs[idx].clone(), - value: output.value, - }); - } - - // Determine direction - let has_sent = output_details.iter().any(|d| d.role == OutputRole::Sent); - let has_our_outputs = output_details - .iter() - .any(|d| d.role == OutputRole::Received || d.role == OutputRole::Change); - let direction = if transaction_type == TransactionType::CoinJoin { - TransactionDirection::CoinJoin - } else if !has_sent && has_inputs && has_our_outputs { - TransactionDirection::Internal - } else if has_inputs { - TransactionDirection::Outgoing - } else { - TransactionDirection::Incoming - }; - - let tx_record = TransactionRecord::new( - tx.clone(), - self.managed_account_type.to_account_type(), - context.clone(), - transaction_type, - direction, - input_details, - output_details, - net_amount, - ); - - let record = tx_record.clone(); - self.transactions.insert(tx.txid(), tx_record); - - self.update_utxos(tx, account_match, context); - record - } - - /// Mark all UTXOs belonging to a transaction as InstantSend-locked. - /// Returns `true` if any UTXO was newly marked. - pub(crate) fn mark_utxos_instant_send(&mut self, txid: &Txid) -> bool { - let mut any_changed = false; - for utxo in self.utxos.values_mut() { - if utxo.outpoint.txid == *txid && !utxo.is_instantlocked { - utxo.is_instantlocked = true; - any_changed = true; - } - } - any_changed - } - - /// Return the UTXOs of this account for which - /// [`Utxo::is_spendable`] holds at `last_processed_height`. See that method - /// for the exact policy. Call this per-account rather than - /// aggregating across the wallet, since spendability is - /// account-type specific. - pub fn spendable_utxos(&self, last_processed_height: u32) -> BTreeSet<&Utxo> { - self.utxos.values().filter(|utxo| utxo.is_spendable(last_processed_height)).collect() - } - - /// Update the account balance. - /// - /// Mature, non-locked UTXOs land in either the `confirmed` bucket - /// (in a block, InstantSend-locked, or trusted mempool change) or - /// the `unconfirmed` bucket (untrusted mempool only). Trusted - /// mempool change is surfaced as confirmed because it is just our - /// previously-tracked funds returning — see [`Utxo::is_trusted`]. - /// Both buckets are spendable per [`Utxo::is_spendable`]; the split - /// is only for display. - pub fn update_balance(&mut self, last_processed_height: u32) { - let mut confirmed = 0; - let mut unconfirmed = 0; - let mut immature = 0; - let mut locked = 0; - for utxo in self.utxos.values() { - let value = utxo.txout.value; - if utxo.is_locked { - locked += value; - } else if !utxo.is_mature(last_processed_height) { - immature += value; - } else if utxo.is_confirmed || utxo.is_instantlocked || utxo.is_trusted { - confirmed += value; - } else { - unconfirmed += value; - } - } - 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 - 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 - { - // 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, - }; - - let addr = - external_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 receive address", - })?; - self.monitor_revision += 1; - 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 - 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 - { - // 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, - }; - - let addr = - internal_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 change address", - })?; - self.monitor_revision += 1; - 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. - 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 - { - // 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, - }; - - let addresses = - external_addresses.next_unused_multiple(count, &key_source, add_to_state); - if addresses.is_empty() && count > 0 { - Err("Failed to generate any receive addresses".to_string()) - } else if addresses.len() < count - && matches!(key_source, address_pool::KeySource::NoKeySource) - { - Err(format!( - "Could only generate {} out of {} requested addresses (no key source)", - addresses.len(), - count - )) - } else { - Ok(addresses) - } - } else { - Err("Cannot generate receive addresses for non-standard account type".to_string()) - } - } - - /// 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. - 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 - { - // 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, - }; - - let addresses = - internal_addresses.next_unused_multiple(count, &key_source, add_to_state); - if addresses.is_empty() && count > 0 { - Err("Failed to generate any change addresses".to_string()) - } else if addresses.len() < count - && matches!(key_source, address_pool::KeySource::NoKeySource) - { - Err(format!( - "Could only generate {} out of {} requested addresses (no key source)", - addresses.len(), - count - )) - } else { - Ok(addresses) - } - } else { - Err("Cannot generate change addresses for non-standard account type".to_string()) - } - } - - /// 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 { - ManagedAccountType::Standard { - external_addresses, - .. - } => Some(external_addresses.gap_limit), - _ => None, - } - } - - /// Get the internal gap limit for standard accounts - pub fn internal_gap_limit(&self) -> Option { - match &self.managed_account_type { - ManagedAccountType::Standard { - internal_addresses, - .. - } => Some(internal_addresses.gap_limit), - _ => 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 ManagedCoreAccount { - fn managed_account_type(&self) -> &ManagedAccountType { - &self.managed_account_type - } - - fn managed_account_type_mut(&mut self) -> &mut ManagedAccountType { - &mut self.managed_account_type - } - - fn network(&self) -> Network { - self.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 - } - - fn transactions(&self) -> &BTreeMap { - &self.transactions - } - - fn transactions_mut(&mut self) -> &mut BTreeMap { - &mut self.transactions - } - - fn utxos(&self) -> &BTreeMap { - &self.utxos - } - - fn utxos_mut(&mut self) -> &mut BTreeMap { - &mut self.utxos - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for ManagedCoreAccount { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper { - managed_account_type: ManagedAccountType, - network: Network, - is_watch_only: bool, - balance: WalletCoreBalance, - transactions: BTreeMap, - utxos: BTreeMap, - } - - let helper = Helper::deserialize(deserializer)?; - - let spent_outpoints = helper - .transactions - .values() - .flat_map(|record| &record.transaction.input) - .map(|input| input.previous_output) - .collect(); - - Ok(ManagedCoreAccount { - managed_account_type: helper.managed_account_type, - network: helper.network, - is_watch_only: helper.is_watch_only, - balance: helper.balance, - transactions: helper.transactions, - utxos: helper.utxos, - spent_outpoints, - monitor_revision: 0, - }) - } -} +pub use managed_core_funds_account::ManagedCoreFundsAccount; +pub use managed_core_keys_account::ManagedCoreKeysAccount; diff --git a/key-wallet/src/test_utils/account.rs b/key-wallet/src/test_utils/account.rs index a4d8fa5ba..720ab9492 100644 --- a/key-wallet/src/test_utils/account.rs +++ b/key-wallet/src/test_utils/account.rs @@ -1,10 +1,10 @@ use crate::account::StandardAccountType; use crate::managed_account::address_pool::{AddressPool, AddressPoolType, KeySource}; use crate::managed_account::managed_account_type::ManagedAccountType; -use crate::managed_account::ManagedCoreAccount; +use crate::managed_account::ManagedCoreFundsAccount; use crate::{DerivationPath, Network}; -impl ManagedCoreAccount { +impl ManagedCoreFundsAccount { /// Create a test managed account with a standard BIP44 type and empty address pools pub fn dummy_bip44() -> Self { let base_path = DerivationPath::master(); @@ -34,6 +34,6 @@ impl ManagedCoreAccount { internal_addresses: internal_pool, }; - ManagedCoreAccount::new(account_type, Network::Regtest, false) + ManagedCoreFundsAccount::new(account_type, Network::Regtest, false) } } diff --git a/key-wallet/src/test_utils/wallet.rs b/key-wallet/src/test_utils/wallet.rs index 2d342d3bf..2a4756dd9 100644 --- a/key-wallet/src/test_utils/wallet.rs +++ b/key-wallet/src/test_utils/wallet.rs @@ -1,7 +1,8 @@ use dashcore::{Address, Network, Transaction, Txid}; use crate::{ - account::{ManagedCoreAccount, TransactionRecord}, + account::{ManagedCoreFundsAccount, TransactionRecord}, + managed_account::managed_account_trait::ManagedAccountTrait, transaction_checking::{TransactionCheckResult, TransactionContext, WalletTransactionChecker}, wallet::{initialization::WalletAccountCreationOptions, ManagedWalletInfo}, ExtendedPubKey, Utxo, Wallet, @@ -55,13 +56,13 @@ impl TestWalletContext { } /// Returns the first BIP44 managed account (immutable). - pub fn bip44_account(&self) -> &ManagedCoreAccount { + pub fn bip44_account(&self) -> &ManagedCoreFundsAccount { self.managed_wallet.first_bip44_managed_account().expect("Should have BIP44 account") } /// 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 05706879b..ecb1f1481 100644 --- a/key-wallet/src/tests/balance_tests.rs +++ b/key-wallet/src/tests/balance_tests.rs @@ -1,6 +1,6 @@ //! Tests for update_balance() UTXO categorization. -use crate::managed_account::ManagedCoreAccount; +use crate::managed_account::ManagedCoreFundsAccount; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::managed_wallet_info::ManagedWalletInfo; use crate::{Utxo, WalletCoreBalance}; @@ -8,7 +8,7 @@ use crate::{Utxo, WalletCoreBalance}; #[test] fn test_balance_with_mixed_utxo_types() { let mut wallet_info = ManagedWalletInfo::dummy(1); - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); // Regular confirmed UTXO let utxo1 = Utxo::dummy(1, 100_000, 1000, false, true); @@ -21,61 +21,61 @@ 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] fn test_coinbase_maturity_boundary() { let mut wallet_info = ManagedWalletInfo::dummy(2); - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); // Coinbase at height 1000 let utxo = Utxo::dummy(1, 50_000_000, 1000, true, true); 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] fn test_locked_utxos_in_locked_balance() { let mut wallet_info = ManagedWalletInfo::dummy(3); - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); let mut utxo = Utxo::dummy(1, 100_000, 1000, false, true); utxo.is_locked = true; 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] fn test_unconfirmed_utxos_in_unconfirmed_balance() { let mut wallet_info = ManagedWalletInfo::dummy(4); - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); let utxo = Utxo::dummy(1, 100_000, 0, false, false); 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 92f92244c..cf8215f58 100644 --- a/key-wallet/src/tests/spent_outpoints_tests.rs +++ b/key-wallet/src/tests/spent_outpoints_tests.rs @@ -4,8 +4,9 @@ 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::ManagedCoreAccount; +use crate::managed_account::ManagedCoreFundsAccount; use crate::transaction_checking::{TransactionContext, TransactionType}; /// Create a transaction that spends the given outpoints. @@ -54,16 +55,16 @@ fn record_from_tx(tx: &Transaction) -> TransactionRecord { #[test] fn fresh_account_has_empty_spent_outpoints() { - let account = ManagedCoreAccount::dummy_bip44(); - assert!(account.transactions.is_empty()); + let account = ManagedCoreFundsAccount::dummy_bip44(); + 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. // We verify indirectly via serde round-trip (spent_outpoints is private). let json = serde_json::to_string(&account).unwrap(); - let deserialized: ManagedCoreAccount = serde_json::from_str(&json).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 @@ -71,55 +72,55 @@ fn fresh_account_has_empty_spent_outpoints() { #[test] fn serde_round_trip_rebuilds_spent_outpoints() { - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); let outpoint_a = OutPoint::new(Txid::from([0x01; 32]), 0); 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(); assert!(!json.contains("spent_outpoints")); // Deserialize: spent_outpoints should be rebuilt from transactions - let deserialized: ManagedCoreAccount = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.transactions.len(), 1); + let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); + 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: ManagedCoreAccount = serde_json::from_str(&json2).unwrap(); - assert_eq!(deserialized2.transactions.len(), 1); + let deserialized2: ManagedCoreFundsAccount = serde_json::from_str(&json2).unwrap(); + assert_eq!(deserialized2.transactions().len(), 1); } #[test] fn receive_only_account_round_trips_correctly() { - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); // 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: ManagedCoreAccount = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.transactions.len(), 1); + let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.transactions().len(), 1); // A second round-trip should be stable let json2 = serde_json::to_string(&deserialized).unwrap(); - let deserialized2: ManagedCoreAccount = serde_json::from_str(&json2).unwrap(); - assert_eq!(deserialized2.transactions.len(), 1); + let deserialized2: ManagedCoreFundsAccount = serde_json::from_str(&json2).unwrap(); + assert_eq!(deserialized2.transactions().len(), 1); } #[test] fn multiple_transactions_all_inputs_tracked_after_round_trip() { - let mut account = ManagedCoreAccount::dummy_bip44(); + let mut account = ManagedCoreFundsAccount::dummy_bip44(); let outpoint_1 = OutPoint::new(Txid::from([0x10; 32]), 0); let outpoint_2 = OutPoint::new(Txid::from([0x20; 32]), 0); @@ -128,16 +129,16 @@ 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: ManagedCoreAccount = serde_json::from_str(&json).unwrap(); + let deserialized: ManagedCoreFundsAccount = serde_json::from_str(&json).unwrap(); // 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 61da3737b..36586fac5 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -6,8 +6,9 @@ use std::collections::BTreeMap; use super::transaction_router::AccountTypeToCheck; -use crate::account::{ManagedAccountCollection, ManagedCoreAccount}; +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; @@ -501,7 +502,7 @@ impl ManagedAccountCollection { /// Check indexed accounts (BTreeMap of accounts) fn check_indexed_accounts( - accounts: &BTreeMap, + accounts: &BTreeMap, tx: &Transaction, ) -> Vec { let mut matches = Vec::new(); @@ -514,10 +515,10 @@ impl ManagedAccountCollection { } } -impl ManagedCoreAccount { +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 ManagedCoreAccount { // 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 ManagedCoreAccount { 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 ManagedCoreAccount { // 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 ManagedCoreAccount { || 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 ManagedCoreAccount { 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 ManagedCoreAccount { 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 ManagedCoreAccount { // 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 ManagedCoreAccount { // 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 ManagedCoreAccount { // 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 ManagedCoreAccount { // 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 a7865acfd..d5d799a0f 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) => { @@ -436,7 +437,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" ); @@ -451,7 +452,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(); @@ -538,7 +539,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)); @@ -596,7 +597,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" ); @@ -610,7 +611,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(); @@ -630,7 +631,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" ); @@ -639,7 +640,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 @@ -677,7 +678,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, @@ -718,10 +719,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(); // Second processing (simulating rescan) - should be marked as existing let result2 = @@ -738,7 +739,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" ); @@ -814,7 +815,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" ); @@ -894,9 +895,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); // Stage 2: IS lock let is_lock = InstantLock { @@ -906,8 +907,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!(ctx.managed_wallet.instant_send_locks.contains(&txid)); @@ -926,7 +927,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"); @@ -937,23 +938,23 @@ 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); // 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 @@ -973,7 +974,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 @@ -981,7 +982,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); } /// Test that the InstantSend branch backfills a `TransactionRecord` on accounts @@ -1056,9 +1057,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 @@ -1087,7 +1088,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(_))); @@ -1113,9 +1114,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, @@ -1145,7 +1146,7 @@ mod tests { assert!(ctx.first_utxo().is_confirmed); } - /// Test `confirm_transaction` backfill directly on `ManagedCoreAccount` when the + /// Test `confirm_transaction` backfill directly on `ManagedCoreFundsAccount` when the /// account has no prior record of the transaction. #[tokio::test] async fn test_managed_account_confirm_backfills_missing_transaction() { @@ -1163,9 +1164,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 @@ -1177,7 +1178,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); @@ -1203,8 +1204,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( @@ -1227,7 +1228,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); @@ -1628,7 +1629,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 { @@ -1688,7 +1689,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"); @@ -1718,8 +1719,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 @@ -1786,9 +1787,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`: @@ -1809,7 +1810,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 4386ed691..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,7 +11,8 @@ use secp256k1::PublicKey; use std::collections::HashMap; use std::fmt; -use crate::managed_account::ManagedCoreAccount; +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; use crate::wallet::managed_wallet_info::fee::FeeRate; @@ -141,7 +142,7 @@ fn resolve_funding_account( accounts: &mut crate::account::ManagedAccountCollection, funding_type: AssetLockFundingType, identity_index: u32, -) -> Result<&mut ManagedCoreAccount, AssetLockError> { +) -> Result<&mut ManagedCoreFundsAccount, AssetLockError> { match funding_type { AssetLockFundingType::IdentityRegistration => accounts .identity_registration @@ -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/helpers.rs b/key-wallet/src/wallet/managed_wallet_info/helpers.rs index 0271c372f..4def07ec7 100644 --- a/key-wallet/src/wallet/managed_wallet_info/helpers.rs +++ b/key-wallet/src/wallet/managed_wallet_info/helpers.rs @@ -2,24 +2,24 @@ use super::ManagedWalletInfo; use crate::account::account_collection::PlatformPaymentAccountKey; -use crate::account::ManagedCoreAccount; +use crate::account::ManagedCoreFundsAccount; use crate::managed_account::managed_platform_account::ManagedPlatformAccount; impl ManagedWalletInfo { // BIP44 Account Helpers /// Get the first BIP44 managed account - pub fn first_bip44_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn first_bip44_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.bip44_managed_account_at_index(0) } /// Get the first BIP44 managed account (mutable) - pub fn first_bip44_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn first_bip44_managed_account_mut(&mut self) -> Option<&mut ManagedCoreFundsAccount> { self.bip44_managed_account_at_index_mut(0) } /// Get a BIP44 managed account at a specific index - pub fn bip44_managed_account_at_index(&self, index: u32) -> Option<&ManagedCoreAccount> { + pub fn bip44_managed_account_at_index(&self, index: u32) -> Option<&ManagedCoreFundsAccount> { self.accounts.standard_bip44_accounts.get(&index) } @@ -27,24 +27,24 @@ impl ManagedWalletInfo { pub fn bip44_managed_account_at_index_mut( &mut self, index: u32, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.standard_bip44_accounts.get_mut(&index) } // BIP32 Account Helpers /// Get the first BIP32 managed account - pub fn first_bip32_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn first_bip32_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.bip32_managed_account_at_index(0) } /// Get the first BIP32 managed account (mutable) - pub fn first_bip32_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn first_bip32_managed_account_mut(&mut self) -> Option<&mut ManagedCoreFundsAccount> { self.bip32_managed_account_at_index_mut(0) } /// Get a BIP32 managed account at a specific index - pub fn bip32_managed_account_at_index(&self, index: u32) -> Option<&ManagedCoreAccount> { + pub fn bip32_managed_account_at_index(&self, index: u32) -> Option<&ManagedCoreFundsAccount> { self.accounts.standard_bip32_accounts.get(&index) } @@ -52,24 +52,27 @@ impl ManagedWalletInfo { pub fn bip32_managed_account_at_index_mut( &mut self, index: u32, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.standard_bip32_accounts.get_mut(&index) } // CoinJoin Account Helpers /// Get the first CoinJoin managed account - pub fn first_coinjoin_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn first_coinjoin_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.coinjoin_managed_account_at_index(0) } /// Get the first CoinJoin managed account (mutable) - pub fn first_coinjoin_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn first_coinjoin_managed_account_mut(&mut self) -> Option<&mut ManagedCoreFundsAccount> { self.coinjoin_managed_account_at_index_mut(0) } /// Get a CoinJoin managed account at a specific index - pub fn coinjoin_managed_account_at_index(&self, index: u32) -> Option<&ManagedCoreAccount> { + pub fn coinjoin_managed_account_at_index( + &self, + index: u32, + ) -> Option<&ManagedCoreFundsAccount> { self.accounts.coinjoin_accounts.get(&index) } @@ -77,19 +80,19 @@ impl ManagedWalletInfo { pub fn coinjoin_managed_account_at_index_mut( &mut self, index: u32, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.coinjoin_accounts.get_mut(&index) } // TopUp Account Helpers /// Get the first TopUp managed account - pub fn first_topup_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn first_topup_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.identity_topup.values().next() } /// Get the first TopUp managed account (mutable) - pub fn first_topup_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn first_topup_managed_account_mut(&mut self) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.identity_topup.values_mut().next() } @@ -97,7 +100,7 @@ impl ManagedWalletInfo { pub fn topup_managed_account_at_registration_index( &self, registration_index: u32, - ) -> Option<&ManagedCoreAccount> { + ) -> Option<&ManagedCoreFundsAccount> { self.accounts.identity_topup.get(®istration_index) } @@ -105,97 +108,105 @@ impl ManagedWalletInfo { pub fn topup_managed_account_at_registration_index_mut( &mut self, registration_index: u32, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.identity_topup.get_mut(®istration_index) } // Identity Registration Account Helper /// Get the identity registration managed account - pub fn identity_registration_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn identity_registration_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.identity_registration.as_ref() } /// Get the identity registration managed account (mutable) - pub fn identity_registration_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn identity_registration_managed_account_mut( + &mut self, + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.identity_registration.as_mut() } // Identity TopUp Not Bound Account Helper /// Get the identity top-up not bound managed account - pub fn identity_topup_not_bound_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn identity_topup_not_bound_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.identity_topup_not_bound.as_ref() } /// Get the identity top-up not bound managed account (mutable) pub fn identity_topup_not_bound_managed_account_mut( &mut self, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.identity_topup_not_bound.as_mut() } // Identity Invitation Account Helper /// Get the identity invitation managed account - pub fn identity_invitation_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn identity_invitation_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.identity_invitation.as_ref() } /// Get the identity invitation managed account (mutable) - pub fn identity_invitation_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn identity_invitation_managed_account_mut( + &mut self, + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.identity_invitation.as_mut() } // Provider Voting Keys Account Helper /// Get the provider voting keys managed account - pub fn provider_voting_keys_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn provider_voting_keys_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.provider_voting_keys.as_ref() } /// Get the provider voting keys managed account (mutable) - pub fn provider_voting_keys_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn provider_voting_keys_managed_account_mut( + &mut self, + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.provider_voting_keys.as_mut() } // Provider Owner Keys Account Helper /// Get the provider owner keys managed account - pub fn provider_owner_keys_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn provider_owner_keys_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.provider_owner_keys.as_ref() } /// Get the provider owner keys managed account (mutable) - pub fn provider_owner_keys_managed_account_mut(&mut self) -> Option<&mut ManagedCoreAccount> { + pub fn provider_owner_keys_managed_account_mut( + &mut self, + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.provider_owner_keys.as_mut() } // Provider Operator Keys Account Helper /// Get the provider operator keys managed account - pub fn provider_operator_keys_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn provider_operator_keys_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.provider_operator_keys.as_ref() } /// Get the provider operator keys managed account (mutable) pub fn provider_operator_keys_managed_account_mut( &mut self, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.provider_operator_keys.as_mut() } // Provider Platform Keys Account Helper /// Get the provider platform keys managed account - pub fn provider_platform_keys_managed_account(&self) -> Option<&ManagedCoreAccount> { + pub fn provider_platform_keys_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { self.accounts.provider_platform_keys.as_ref() } /// Get the provider platform keys managed account (mutable) pub fn provider_platform_keys_managed_account_mut( &mut self, - ) -> Option<&mut ManagedCoreAccount> { + ) -> Option<&mut ManagedCoreFundsAccount> { self.accounts.provider_platform_keys.as_mut() } @@ -298,7 +309,7 @@ impl ManagedWalletInfo { } /// Get all accounts - pub fn all_managed_accounts(&self) -> Vec<&ManagedCoreAccount> { + pub fn all_managed_accounts(&self) -> Vec<&ManagedCoreFundsAccount> { self.accounts.all_accounts() } } 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 f21713c1b..4a6ab17a9 100644 --- a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs +++ b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs @@ -7,9 +7,10 @@ use super::{managed_account_operations::ManagedAccountOperations, ManagedWalletI use crate::account::BLSAccount; #[cfg(feature = "eddsa")] use crate::account::EdDSAAccount; -use crate::account::{Account, AccountType, ManagedCoreAccount}; +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 { @@ -42,7 +43,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { })?; // Create the ManagedAccount from the Account - let managed_account = ManagedCoreAccount::from_account(account); + let managed_account = ManagedCoreFundsAccount::from_account(account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -117,7 +118,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { let account = Account::new(None, account_type, account_xpub, self.network)?; // Create the ManagedAccount from the Account - let managed_account = ManagedCoreAccount::from_account(&account); + let managed_account = ManagedCoreFundsAccount::from_account(&account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -162,7 +163,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { })?; // Create the ManagedAccount from the BLS Account - let managed_account = ManagedCoreAccount::from_bls_account(bls_account); + let managed_account = ManagedCoreFundsAccount::from_bls_account(bls_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -234,7 +235,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { BLSAccount::from_public_key_bytes(None, account_type, bls_public_key, self.network)?; // Create the ManagedAccount from the BLS Account - let managed_account = ManagedCoreAccount::from_bls_account(&bls_account); + let managed_account = ManagedCoreFundsAccount::from_bls_account(&bls_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -280,7 +281,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { })?; // Create the ManagedAccount from the EdDSA Account - let managed_account = ManagedCoreAccount::from_eddsa_account(eddsa_account); + let managed_account = ManagedCoreFundsAccount::from_eddsa_account(eddsa_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -356,7 +357,7 @@ impl ManagedAccountOperations for ManagedWalletInfo { )?; // Create the ManagedAccount from the EdDSA Account - let managed_account = ManagedCoreAccount::from_eddsa_account(&eddsa_account); + let managed_account = ManagedCoreFundsAccount::from_eddsa_account(&eddsa_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { 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 93a076122..bc53e771a 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 @@ -75,7 +75,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() } @@ -209,7 +209,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; } @@ -217,7 +217,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 } @@ -245,7 +245,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()); } @@ -274,7 +274,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; }