diff --git a/dash-spv/tests/dashd_sync/helpers.rs b/dash-spv/tests/dashd_sync/helpers.rs index f1b67b605..28d9bea30 100644 --- a/dash-spv/tests/dashd_sync/helpers.rs +++ b/dash-spv/tests/dashd_sync/helpers.rs @@ -2,7 +2,6 @@ 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; diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 3b7a7be0a..9fd0cd37e 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -233,14 +233,14 @@ Functions: 108 | `managed_core_account_free_transactions` | Free transactions array returned by managed_core_account_get_transactions ... | managed_account | | `managed_core_account_get_account_type` | Get the account type of a managed account # Safety - `account` must be a... | managed_account | | `managed_core_account_get_address_pool` | Get an address pool from a managed account by type This function returns... | managed_account | -| `managed_core_account_get_balance` | Get the balance of a managed account # Safety - `account` must be a valid... | managed_account | +| `managed_core_account_get_balance` | Get the balance of a managed account | managed_account | | `managed_core_account_get_external_address_pool` | Get the external address pool from a managed account This function returns... | managed_account | | `managed_core_account_get_index` | Get the account index from a managed account Returns the primary account... | managed_account | | `managed_core_account_get_internal_address_pool` | Get the internal address pool from a managed account This function returns... | managed_account | | `managed_core_account_get_network` | Get the network of a managed account # Safety - `account` must be a valid... | managed_account | | `managed_core_account_get_transaction_count` | Get the number of transactions in a managed account Only available with the... | managed_account | | `managed_core_account_get_transactions` | Get all transactions from a managed account Returns an array of... | managed_account | -| `managed_core_account_get_utxo_count` | Get the number of UTXOs in a managed account # Safety - `account` must be... | managed_account | +| `managed_core_account_get_utxo_count` | Get the number of UTXOs in a managed account | managed_account | | `managed_platform_account_free` | Free a managed platform account handle # Safety - `account` must be a... | managed_account | | `managed_platform_account_get_account_index` | Get the account index of a managed platform account # Safety - `account`... | managed_account | | `managed_platform_account_get_address_pool` | Get the address pool from a managed platform account Platform accounts only... | managed_account | @@ -3095,7 +3095,7 @@ managed_core_account_get_balance(account: *const FFIManagedCoreAccount, balance_ ``` **Description:** -Get the balance of a managed account # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `balance_out` must be a valid pointer to an FFIBalance structure +Get the balance of a managed account. Returns `false` (and leaves `balance_out` untouched) when the handle wraps a keys-only account (identity / asset-lock / provider) — those don't track per-account balances. Use [`managed_core_account_get_account_type`] to disambiguate, or only call this for funds-bearing accounts. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `balance_out` must be a valid pointer to an FFIBalance structure **Safety:** - `account` must be a valid pointer to an FFIManagedCoreAccount instance - `balance_out` must be a valid pointer to an FFIBalance structure @@ -3207,7 +3207,7 @@ managed_core_account_get_utxo_count(account: *const FFIManagedCoreAccount,) -> c ``` **Description:** -Get the number of UTXOs in a managed account # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance +Get the number of UTXOs in a managed account. Always returns 0 for keys-only accounts (identity / asset-lock / provider), which do not track per-account UTXOs. # Safety - `account` must be a valid pointer to an FFIManagedCoreAccount instance **Safety:** - `account` must be a valid pointer to an FFIManagedCoreAccount instance diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index 3ed280ad5..780dc4f23 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -16,58 +16,74 @@ use key_wallet::managed_account::address_pool::{ AddressInfo, AddressPool, KeySource, PublicKeyType, }; use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; -use key_wallet::managed_account::ManagedCoreFundsAccount; +use key_wallet::managed_account::{ManagedAccountRef, ManagedAccountRefMut}; use key_wallet::AccountType; -// Helper functions to get managed accounts by type +// Helper functions to get managed accounts by type. Identity / asset-lock / +// provider variants are stored as keys-only accounts; Standard / CoinJoin +// stay funds-bearing. The returned [`ManagedAccountRef`] enum exposes the +// shared trait surface (address pools, managed type, network) used here +// without forcing callers to dispatch on the variant. fn get_managed_account_by_type<'a>( collection: &'a ManagedAccountCollection, account_type: &AccountType, -) -> Option<&'a ManagedCoreFundsAccount> { +) -> Option> { match account_type { AccountType::Standard { index, standard_account_type, } => match standard_account_type { key_wallet::account::StandardAccountType::BIP44Account => { - collection.standard_bip44_accounts.get(index) + collection.standard_bip44_accounts.get(index).map(ManagedAccountRef::Funds) } key_wallet::account::StandardAccountType::BIP32Account => { - collection.standard_bip32_accounts.get(index) + collection.standard_bip32_accounts.get(index).map(ManagedAccountRef::Funds) } }, AccountType::CoinJoin { index, - } => collection.coinjoin_accounts.get(index), - AccountType::IdentityRegistration => collection.identity_registration.as_ref(), + } => collection.coinjoin_accounts.get(index).map(ManagedAccountRef::Funds), + AccountType::IdentityRegistration => { + collection.identity_registration.as_ref().map(ManagedAccountRef::Keys) + } AccountType::IdentityTopUp { registration_index, - } => collection.identity_topup.get(registration_index), + } => collection.identity_topup.get(registration_index).map(ManagedAccountRef::Keys), AccountType::IdentityTopUpNotBoundToIdentity => { - collection.identity_topup_not_bound.as_ref() + collection.identity_topup_not_bound.as_ref().map(ManagedAccountRef::Keys) + } + AccountType::IdentityInvitation => { + collection.identity_invitation.as_ref().map(ManagedAccountRef::Keys) + } + AccountType::AssetLockAddressTopUp => { + collection.asset_lock_address_topup.as_ref().map(ManagedAccountRef::Keys) } - AccountType::IdentityInvitation => collection.identity_invitation.as_ref(), - AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_ref(), AccountType::AssetLockShieldedAddressTopUp => { - collection.asset_lock_shielded_address_topup.as_ref() + collection.asset_lock_shielded_address_topup.as_ref().map(ManagedAccountRef::Keys) + } + AccountType::ProviderVotingKeys => { + collection.provider_voting_keys.as_ref().map(ManagedAccountRef::Keys) + } + AccountType::ProviderOwnerKeys => { + collection.provider_owner_keys.as_ref().map(ManagedAccountRef::Keys) + } + AccountType::ProviderOperatorKeys => { + collection.provider_operator_keys.as_ref().map(ManagedAccountRef::Keys) + } + AccountType::ProviderPlatformKeys => { + collection.provider_platform_keys.as_ref().map(ManagedAccountRef::Keys) } - AccountType::ProviderVotingKeys => collection.provider_voting_keys.as_ref(), - AccountType::ProviderOwnerKeys => collection.provider_owner_keys.as_ref(), - AccountType::ProviderOperatorKeys => collection.provider_operator_keys.as_ref(), - AccountType::ProviderPlatformKeys => collection.provider_platform_keys.as_ref(), AccountType::DashpayReceivingFunds { .. } | AccountType::DashpayExternalAccount { .. - } => { - // DashPay managed accounts are not currently persisted in ManagedAccountCollection - None } - AccountType::PlatformPayment { + | AccountType::PlatformPayment { .. } => { - // Platform Payment accounts are not currently persisted in ManagedAccountCollection + // DashPay and Platform Payment accounts are not reachable through + // this address-pool helper. None } } @@ -76,38 +92,52 @@ 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 ManagedCoreFundsAccount> { +) -> Option> { match account_type { AccountType::Standard { index, standard_account_type, } => match standard_account_type { key_wallet::account::StandardAccountType::BIP44Account => { - collection.standard_bip44_accounts.get_mut(index) + collection.standard_bip44_accounts.get_mut(index).map(ManagedAccountRefMut::Funds) } key_wallet::account::StandardAccountType::BIP32Account => { - collection.standard_bip32_accounts.get_mut(index) + collection.standard_bip32_accounts.get_mut(index).map(ManagedAccountRefMut::Funds) } }, AccountType::CoinJoin { index, - } => collection.coinjoin_accounts.get_mut(index), - AccountType::IdentityRegistration => collection.identity_registration.as_mut(), + } => collection.coinjoin_accounts.get_mut(index).map(ManagedAccountRefMut::Funds), + AccountType::IdentityRegistration => { + collection.identity_registration.as_mut().map(ManagedAccountRefMut::Keys) + } AccountType::IdentityTopUp { registration_index, - } => collection.identity_topup.get_mut(registration_index), + } => collection.identity_topup.get_mut(registration_index).map(ManagedAccountRefMut::Keys), AccountType::IdentityTopUpNotBoundToIdentity => { - collection.identity_topup_not_bound.as_mut() + collection.identity_topup_not_bound.as_mut().map(ManagedAccountRefMut::Keys) + } + AccountType::IdentityInvitation => { + collection.identity_invitation.as_mut().map(ManagedAccountRefMut::Keys) + } + AccountType::AssetLockAddressTopUp => { + collection.asset_lock_address_topup.as_mut().map(ManagedAccountRefMut::Keys) } - AccountType::IdentityInvitation => collection.identity_invitation.as_mut(), - AccountType::AssetLockAddressTopUp => collection.asset_lock_address_topup.as_mut(), AccountType::AssetLockShieldedAddressTopUp => { - collection.asset_lock_shielded_address_topup.as_mut() + collection.asset_lock_shielded_address_topup.as_mut().map(ManagedAccountRefMut::Keys) + } + AccountType::ProviderVotingKeys => { + collection.provider_voting_keys.as_mut().map(ManagedAccountRefMut::Keys) + } + AccountType::ProviderOwnerKeys => { + collection.provider_owner_keys.as_mut().map(ManagedAccountRefMut::Keys) + } + AccountType::ProviderOperatorKeys => { + collection.provider_operator_keys.as_mut().map(ManagedAccountRefMut::Keys) + } + AccountType::ProviderPlatformKeys => { + collection.provider_platform_keys.as_mut().map(ManagedAccountRefMut::Keys) } - AccountType::ProviderVotingKeys => collection.provider_voting_keys.as_mut(), - AccountType::ProviderOwnerKeys => collection.provider_owner_keys.as_mut(), - AccountType::ProviderOperatorKeys => collection.provider_operator_keys.as_mut(), - AccountType::ProviderPlatformKeys => collection.provider_platform_keys.as_mut(), AccountType::DashpayReceivingFunds { .. } @@ -384,7 +414,7 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( let account_type_rust = account_type.to_account_type(account_index); // Get the specific managed account - let managed_account = unwrap_or_return!( + let mut managed_account = unwrap_or_return!( get_managed_account_by_type_mut(&mut managed_wallet.accounts, &account_type_rust), error ); @@ -471,7 +501,7 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( let key_source = KeySource::Public(xpub); // Get the specific managed account - let managed_account = unwrap_or_return!( + let mut managed_account = unwrap_or_return!( get_managed_account_by_type_mut(&mut managed_wallet.accounts, &account_type_rust), error ); diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index 9595526a1..21f069fde 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -24,26 +24,69 @@ use key_wallet::account::TransactionRecord; use key_wallet::managed_account::address_pool::AddressPool; use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; use key_wallet::managed_account::managed_platform_account::ManagedPlatformAccount; -use key_wallet::managed_account::ManagedCoreFundsAccount; +use key_wallet::managed_account::{ManagedCoreFundsAccount, ManagedCoreKeysAccount}; use key_wallet::AccountType; -/// Opaque managed account handle that wraps ManagedAccount +/// Internal handle variant: a funds-bearing account or a keys-only account. +pub(crate) enum FFIManagedCoreAccountInner { + Funds(Arc), + Keys(Arc), +} + +/// Opaque managed account handle. +/// +/// Wraps either a [`ManagedCoreFundsAccount`] (Standard, CoinJoin, DashPay) +/// or a [`ManagedCoreKeysAccount`] (identity, asset-lock, provider). Funds-only +/// accessors (`get_balance`, `get_utxo_count`, …) return zero / null / +/// false on the keys variant; trait-shared accessors (`get_network`, +/// `get_account_type`, address pools, transactions) work on both. pub struct FFIManagedCoreAccount { - /// The underlying managed account - pub(crate) account: Arc, + pub(crate) inner: FFIManagedCoreAccountInner, } impl FFIManagedCoreAccount { - /// Create a new FFI managed account handle + /// Create a new FFI managed account handle wrapping a funds-bearing account. pub fn new(account: &ManagedCoreFundsAccount) -> Self { FFIManagedCoreAccount { - account: Arc::new(account.clone()), + inner: FFIManagedCoreAccountInner::Funds(Arc::new(account.clone())), } } - /// Get a reference to the inner managed account - pub fn inner(&self) -> &ManagedCoreFundsAccount { - self.account.as_ref() + /// Create a new FFI managed account handle wrapping a keys-only account. + pub fn new_keys(account: &ManagedCoreKeysAccount) -> Self { + FFIManagedCoreAccount { + inner: FFIManagedCoreAccountInner::Keys(Arc::new(account.clone())), + } + } + + /// Returns the funds-bearing account if this handle wraps one, `None` + /// otherwise. Use this in funds-only FFI entry points. + pub fn as_funds(&self) -> Option<&ManagedCoreFundsAccount> { + match &self.inner { + FFIManagedCoreAccountInner::Funds(a) => Some(a.as_ref()), + FFIManagedCoreAccountInner::Keys(_) => None, + } + } + + /// Returns the keys-only account if this handle wraps one, `None` + /// otherwise. + pub fn as_keys(&self) -> Option<&ManagedCoreKeysAccount> { + match &self.inner { + FFIManagedCoreAccountInner::Funds(_) => None, + FFIManagedCoreAccountInner::Keys(a) => Some(a.as_ref()), + } + } + + /// Returns the inner [`ManagedCoreKeysAccount`] regardless of variant — + /// for the funds-bearing variant this returns the composed-inner keys + /// account; for the keys-only variant it returns the account itself. + /// Use this when the desired data lives on the shared keys-account state + /// (address pools, transactions, network, monitor revision). + pub fn keys_account(&self) -> &ManagedCoreKeysAccount { + match &self.inner { + FFIManagedCoreAccountInner::Funds(a) => a.keys(), + FFIManagedCoreAccountInner::Keys(a) => a.as_ref(), + } } } @@ -228,53 +271,76 @@ pub unsafe extern "C" fn managed_wallet_get_account( use key_wallet::account::StandardAccountType; let managed_collection = &managed_wallet.inner().accounts; - let managed_account = match account_type_rust { + let ffi_account: Option = match account_type_rust { AccountType::Standard { index, standard_account_type, } => match standard_account_type { - StandardAccountType::BIP44Account => { - managed_collection.standard_bip44_accounts.get(&index) - } - StandardAccountType::BIP32Account => { - managed_collection.standard_bip32_accounts.get(&index) - } + StandardAccountType::BIP44Account => managed_collection + .standard_bip44_accounts + .get(&index) + .map(FFIManagedCoreAccount::new), + StandardAccountType::BIP32Account => managed_collection + .standard_bip32_accounts + .get(&index) + .map(FFIManagedCoreAccount::new), }, AccountType::CoinJoin { index, - } => managed_collection.coinjoin_accounts.get(&index), - AccountType::IdentityRegistration => managed_collection.identity_registration.as_ref(), + } => managed_collection.coinjoin_accounts.get(&index).map(FFIManagedCoreAccount::new), + AccountType::IdentityRegistration => managed_collection + .identity_registration + .as_ref() + .map(FFIManagedCoreAccount::new_keys), AccountType::IdentityTopUp { registration_index, - } => managed_collection.identity_topup.get(®istration_index), - AccountType::IdentityTopUpNotBoundToIdentity => { - managed_collection.identity_topup_not_bound.as_ref() + } => managed_collection + .identity_topup + .get(®istration_index) + .map(FFIManagedCoreAccount::new_keys), + AccountType::IdentityTopUpNotBoundToIdentity => managed_collection + .identity_topup_not_bound + .as_ref() + .map(FFIManagedCoreAccount::new_keys), + AccountType::IdentityInvitation => { + managed_collection.identity_invitation.as_ref().map(FFIManagedCoreAccount::new_keys) } - AccountType::IdentityInvitation => managed_collection.identity_invitation.as_ref(), - AccountType::AssetLockAddressTopUp => { - managed_collection.asset_lock_address_topup.as_ref() - } - AccountType::AssetLockShieldedAddressTopUp => { - managed_collection.asset_lock_shielded_address_topup.as_ref() + AccountType::AssetLockAddressTopUp => managed_collection + .asset_lock_address_topup + .as_ref() + .map(FFIManagedCoreAccount::new_keys), + AccountType::AssetLockShieldedAddressTopUp => managed_collection + .asset_lock_shielded_address_topup + .as_ref() + .map(FFIManagedCoreAccount::new_keys), + AccountType::ProviderVotingKeys => managed_collection + .provider_voting_keys + .as_ref() + .map(FFIManagedCoreAccount::new_keys), + AccountType::ProviderOwnerKeys => { + managed_collection.provider_owner_keys.as_ref().map(FFIManagedCoreAccount::new_keys) } - AccountType::ProviderVotingKeys => managed_collection.provider_voting_keys.as_ref(), - AccountType::ProviderOwnerKeys => managed_collection.provider_owner_keys.as_ref(), - AccountType::ProviderOperatorKeys => managed_collection.provider_operator_keys.as_ref(), - AccountType::ProviderPlatformKeys => managed_collection.provider_platform_keys.as_ref(), + AccountType::ProviderOperatorKeys => managed_collection + .provider_operator_keys + .as_ref() + .map(FFIManagedCoreAccount::new_keys), + AccountType::ProviderPlatformKeys => managed_collection + .provider_platform_keys + .as_ref() + .map(FFIManagedCoreAccount::new_keys), AccountType::DashpayReceivingFunds { .. - } => None, - AccountType::DashpayExternalAccount { + } + | AccountType::DashpayExternalAccount { .. - } => None, - AccountType::PlatformPayment { + } + | AccountType::PlatformPayment { .. } => None, }; - match managed_account { - Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + match ffi_account { + Some(ffi_account) => { FFIManagedCoreAccountResult::success(Box::into_raw(Box::new(ffi_account))) } None => FFIManagedCoreAccountResult::error( @@ -343,7 +409,7 @@ pub unsafe extern "C" fn managed_wallet_get_top_up_account_with_registration_ind let result = match managed_wallet.inner().accounts.identity_topup.get(®istration_index) { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); FFIManagedCoreAccountResult::success(Box::into_raw(Box::new(ffi_account))) } None => FFIManagedCoreAccountResult::error( @@ -499,7 +565,7 @@ pub unsafe extern "C" fn managed_core_account_get_network( } let account = &*account; - account.inner().network().into() + account.keys_account().network().into() } /// Get the parent wallet ID of a managed account @@ -537,7 +603,7 @@ pub unsafe extern "C" fn managed_core_account_get_account_type( } let account = &*account; - let managed_account = account.inner(); + let managed_account = account.keys_account(); let account_type_rust = managed_account.managed_account_type().to_account_type(); // Set the index if output pointer is provided @@ -586,7 +652,12 @@ pub unsafe extern "C" fn managed_core_account_get_account_type( } } -/// Get the balance of a managed account +/// Get the balance of a managed account. +/// +/// Returns `false` (and leaves `balance_out` untouched) when the handle wraps +/// a keys-only account (identity / asset-lock / provider) — those don't track +/// per-account balances. Use [`managed_core_account_get_account_type`] to +/// disambiguate, or only call this for funds-bearing accounts. /// /// # Safety /// @@ -602,7 +673,10 @@ pub unsafe extern "C" fn managed_core_account_get_balance( } let account = &*account; - let balance = account.inner().balance; + let Some(funds) = account.as_funds() else { + return false; + }; + let balance = funds.balance; *balance_out = crate::types::FFIBalance { confirmed: balance.confirmed(), @@ -635,10 +709,13 @@ pub unsafe extern "C" fn managed_core_account_get_transaction_count( } let account = &*account; - account.inner().transactions().len() as c_uint + account.keys_account().transactions().len() as c_uint } -/// Get the number of UTXOs in a managed account +/// Get the number of UTXOs in a managed account. +/// +/// Always returns 0 for keys-only accounts (identity / asset-lock / provider), +/// which do not track per-account UTXOs. /// /// # Safety /// @@ -652,7 +729,7 @@ pub unsafe extern "C" fn managed_core_account_get_utxo_count( } let account = &*account; - account.inner().utxos.len() as c_uint + account.as_funds().map_or(0, |f| f.utxos.len() as c_uint) } /// FFI-compatible owning-account descriptor for a [`FFITransactionRecord`]. @@ -948,7 +1025,7 @@ pub unsafe extern "C" fn managed_core_account_get_transactions( } let account = &*account; - let transactions = account.inner().transactions(); + let transactions = account.keys_account().transactions(); if transactions.is_empty() { *transactions_out = std::ptr::null_mut(); @@ -1052,7 +1129,36 @@ pub unsafe extern "C" fn managed_wallet_get_account_count( + accounts.standard_bip32_accounts.len() + accounts.coinjoin_accounts.len() + accounts.identity_registration.is_some() as usize - + accounts.identity_topup.len(); + + accounts.identity_topup.len() + + accounts.identity_topup_not_bound.is_some() as usize + + accounts.identity_invitation.is_some() as usize + + accounts.asset_lock_address_topup.is_some() as usize + + accounts.asset_lock_shielded_address_topup.is_some() as usize + + accounts.provider_voting_keys.is_some() as usize + + accounts.provider_owner_keys.is_some() as usize + + { + #[cfg(feature = "bls")] + { + accounts.provider_operator_keys.is_some() as usize + } + #[cfg(not(feature = "bls"))] + { + 0 + } + } + + { + #[cfg(feature = "eddsa")] + { + accounts.provider_platform_keys.is_some() as usize + } + #[cfg(not(feature = "eddsa"))] + { + 0 + } + } + + accounts.dashpay_receival_accounts.len() + + accounts.dashpay_external_accounts.len() + + accounts.platform_payment_accounts.len(); // Clean up the wallet pointer crate::wallet::wallet_free_const(wallet_ptr); @@ -1080,7 +1186,7 @@ pub unsafe extern "C" fn managed_core_account_get_index( } let account = &*account; - account.inner().managed_account_type().index_or_default() + account.keys_account().managed_account_type().index_or_default() } /// Get the external address pool from a managed account @@ -1101,7 +1207,7 @@ pub unsafe extern "C" fn managed_core_account_get_external_address_pool( } let account = &*account; - let managed_account = account.inner(); + let managed_account = account.keys_account(); // Get external address pool if this is a standard account match managed_account.managed_account_type() { @@ -1137,7 +1243,7 @@ pub unsafe extern "C" fn managed_core_account_get_internal_address_pool( } let account = &*account; - let managed_account = account.inner(); + let managed_account = account.keys_account(); // Get internal address pool if this is a standard account match managed_account.managed_account_type() { @@ -1177,7 +1283,7 @@ pub unsafe extern "C" fn managed_core_account_get_address_pool( } let account = &*account; - let managed_account = account.inner(); + let managed_account = account.keys_account(); use key_wallet::managed_account::managed_account_type::ManagedAccountType; diff --git a/key-wallet-ffi/src/managed_account_collection.rs b/key-wallet-ffi/src/managed_account_collection.rs index 19d6c6760..5912abd1e 100644 --- a/key-wallet-ffi/src/managed_account_collection.rs +++ b/key-wallet-ffi/src/managed_account_collection.rs @@ -354,7 +354,7 @@ pub unsafe extern "C" fn managed_account_collection_get_identity_registration( let collection = &*collection; match &collection.collection.identity_registration { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) } None => ptr::null_mut(), @@ -396,7 +396,7 @@ pub unsafe extern "C" fn managed_account_collection_get_identity_topup( let collection = &*collection; match collection.collection.identity_topup.get(®istration_index) { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) } None => ptr::null_mut(), @@ -460,7 +460,7 @@ pub unsafe extern "C" fn managed_account_collection_get_identity_topup_not_bound let collection = &*collection; match &collection.collection.identity_topup_not_bound { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) } None => ptr::null_mut(), @@ -501,7 +501,7 @@ pub unsafe extern "C" fn managed_account_collection_get_identity_invitation( let collection = &*collection; match &collection.collection.identity_invitation { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) } None => ptr::null_mut(), @@ -544,7 +544,7 @@ pub unsafe extern "C" fn managed_account_collection_get_provider_voting_keys( let collection = &*collection; match &collection.collection.provider_voting_keys { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) } None => ptr::null_mut(), @@ -585,7 +585,7 @@ pub unsafe extern "C" fn managed_account_collection_get_provider_owner_keys( let collection = &*collection; match &collection.collection.provider_owner_keys { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) } None => ptr::null_mut(), @@ -629,7 +629,7 @@ pub unsafe extern "C" fn managed_account_collection_get_provider_operator_keys( let collection = &*collection; match &collection.collection.provider_operator_keys { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) as *mut std::os::raw::c_void } None => ptr::null_mut(), @@ -689,7 +689,7 @@ pub unsafe extern "C" fn managed_account_collection_get_provider_platform_keys( let collection = &*collection; match &collection.collection.provider_platform_keys { Some(account) => { - let ffi_account = FFIManagedCoreAccount::new(account); + let ffi_account = FFIManagedCoreAccount::new_keys(account); Box::into_raw(Box::new(ffi_account)) as *mut std::os::raw::c_void } None => ptr::null_mut(), diff --git a/key-wallet/src/managed_account/managed_account_collection.rs b/key-wallet/src/managed_account/managed_account_collection.rs index 49a154cce..0739b6fb1 100644 --- a/key-wallet/src/managed_account/managed_account_collection.rs +++ b/key-wallet/src/managed_account/managed_account_collection.rs @@ -12,104 +12,32 @@ 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_ref::{ + ManagedAccountRef, ManagedAccountRefMut, OwnedManagedCoreAccount, +}; use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; use crate::managed_account::managed_platform_account::ManagedPlatformAccount; -use crate::managed_account::ManagedCoreFundsAccount; +use crate::managed_account::{ManagedCoreFundsAccount, ManagedCoreKeysAccount}; use crate::transaction_checking::account_checker::CoreAccountTypeMatch; use crate::{Account, AccountCollection}; use crate::{KeySource, Network}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -/// Macro to look up an account by CoreAccountTypeMatch, parameterized by accessor methods -macro_rules! get_by_account_type_match_impl { - ($self:expr, $match:expr, $get:ident, $as_opt:ident, $values:ident) => { - match $match { - CoreAccountTypeMatch::StandardBIP44 { - account_index, - .. - } => $self.standard_bip44_accounts.$get(account_index), - CoreAccountTypeMatch::StandardBIP32 { - account_index, - .. - } => $self.standard_bip32_accounts.$get(account_index), - CoreAccountTypeMatch::CoinJoin { - account_index, - .. - } => $self.coinjoin_accounts.$get(account_index), - CoreAccountTypeMatch::IdentityRegistration { - .. - } => $self.identity_registration.$as_opt(), - CoreAccountTypeMatch::IdentityTopUp { - account_index, - .. - } => $self.identity_topup.$get(account_index), - CoreAccountTypeMatch::IdentityTopUpNotBound { - .. - } => $self.identity_topup_not_bound.$as_opt(), - CoreAccountTypeMatch::IdentityInvitation { - .. - } => $self.identity_invitation.$as_opt(), - CoreAccountTypeMatch::AssetLockAddressTopUp { - .. - } => $self.asset_lock_address_topup.$as_opt(), - CoreAccountTypeMatch::AssetLockShieldedAddressTopUp { - .. - } => $self.asset_lock_shielded_address_topup.$as_opt(), - CoreAccountTypeMatch::ProviderVotingKeys { - .. - } => $self.provider_voting_keys.$as_opt(), - CoreAccountTypeMatch::ProviderOwnerKeys { - .. - } => $self.provider_owner_keys.$as_opt(), - CoreAccountTypeMatch::ProviderOperatorKeys { - .. - } => $self.provider_operator_keys.$as_opt(), - CoreAccountTypeMatch::ProviderPlatformKeys { - .. - } => $self.provider_platform_keys.$as_opt(), - CoreAccountTypeMatch::DashpayReceivingFunds { - account_index, - involved_addresses, - } => $self.dashpay_receival_accounts.$values().find(|account| { - match account.managed_account_type() { - ManagedAccountType::DashpayReceivingFunds { - index, - addresses, - .. - } => { - *index == *account_index - && involved_addresses - .iter() - .any(|addr| addresses.contains_address(&addr.address)) - } - _ => false, - } - }), - CoreAccountTypeMatch::DashpayExternalAccount { - account_index, - involved_addresses, - } => $self.dashpay_external_accounts.$values().find(|account| { - match account.managed_account_type() { - ManagedAccountType::DashpayExternalAccount { - index, - addresses, - .. - } => { - *index == *account_index - && involved_addresses - .iter() - .any(|addr| addresses.contains_address(&addr.address)) - } - _ => false, - } - }), - } - }; -} - -/// Collection of managed accounts organized by type +// Note: `get_by_account_type_match` and `get_by_account_type_match_mut` are +// defined inline below (rather than via a shared macro) because their match +// arms now wrap results in different concrete variants of [`ManagedAccountRef`] +// / [`ManagedAccountRefMut`] depending on whether the account field holds a +// funds-bearing or keys-only account. + +/// Collection of managed accounts organized by type. +/// +/// Account types that hold and spend funds (Standard, CoinJoin, DashPay) use +/// the funds-bearing [`ManagedCoreFundsAccount`]. Account types that derive +/// special-purpose keys but do not track per-account UTXOs (identity, +/// asset-lock, provider) use the lightweight [`ManagedCoreKeysAccount`] — +/// avoiding the memory cost of always-empty balance / UTXO state. #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct ManagedAccountCollection { @@ -120,25 +48,25 @@ pub struct ManagedAccountCollection { /// CoinJoin accounts by index 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, /// DashPay external accounts keyed by (index, user_id, friend_id) @@ -263,11 +191,35 @@ impl ManagedAccountCollection { } } - /// Insert a managed account into the collection + /// Insert a managed account into the collection. /// - /// 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: ManagedCoreFundsAccount) -> Result<(), crate::error::Error> { + /// Accepts either a [`ManagedCoreFundsAccount`] or a + /// [`ManagedCoreKeysAccount`] (via [`From`]) so the caller can pick the + /// appropriate variant for the account type. Returns an error if: + /// - a [`ManagedAccountType::PlatformPayment`] account is passed (use + /// [`Self::insert_platform_account`] instead), or + /// - the variant doesn't match the account type (e.g. a funds account + /// for an identity-registration type). + pub fn insert( + &mut self, + account: impl Into, + ) -> Result<(), crate::error::Error> { + match account.into() { + OwnedManagedCoreAccount::Funds(a) => self.insert_funds_bearing_account(a), + OwnedManagedCoreAccount::Keys(a) => self.insert_keys_bearing_account(a), + } + } + + /// Insert a funds-bearing account. + /// + /// Errors if the account's [`ManagedAccountType`] does not correspond to + /// a funds-bearing variant (Standard / CoinJoin / DashPay) — identity, + /// asset-lock, and provider variants must be inserted as + /// [`ManagedCoreKeysAccount`] via [`Self::insert_keys_bearing_account`]. + pub fn insert_funds_bearing_account( + &mut self, + account: ManagedCoreFundsAccount, + ) -> Result<(), crate::error::Error> { use crate::account::StandardAccountType; match account.managed_account_type() { @@ -289,6 +241,60 @@ impl ManagedAccountCollection { } => { self.coinjoin_accounts.insert(*index, account); } + ManagedAccountType::DashpayReceivingFunds { + index, + user_identity_id, + friend_identity_id, + .. + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_receival_accounts.insert(key, account); + } + ManagedAccountType::DashpayExternalAccount { + index, + user_identity_id, + friend_identity_id, + .. + } => { + let key = DashpayAccountKey { + index: *index, + user_identity_id: *user_identity_id, + friend_identity_id: *friend_identity_id, + }; + self.dashpay_external_accounts.insert(key, account); + } + ManagedAccountType::PlatformPayment { + .. + } => { + return Err(crate::error::Error::InvalidParameter( + "Use insert_platform_account() for Platform Payment accounts".into(), + )); + } + other => { + return Err(crate::error::Error::InvalidParameter(format!( + "Account type {:?} cannot be stored as ManagedCoreFundsAccount; use insert_keys_bearing_account instead", + other.to_account_type(), + ))); + } + } + Ok(()) + } + + /// Insert a keys-only account. + /// + /// Errors if the account's [`ManagedAccountType`] does not correspond to + /// a keys-only variant (identity / asset-lock / provider) — Standard, + /// CoinJoin, and DashPay variants must be inserted as + /// [`ManagedCoreFundsAccount`] via [`Self::insert_funds_bearing_account`]. + pub fn insert_keys_bearing_account( + &mut self, + account: ManagedCoreKeysAccount, + ) -> Result<(), crate::error::Error> { + match account.managed_account_type() { ManagedAccountType::IdentityRegistration { .. } => { @@ -340,40 +346,11 @@ impl ManagedAccountCollection { } => { self.provider_platform_keys = Some(account); } - ManagedAccountType::DashpayReceivingFunds { - index, - user_identity_id, - friend_identity_id, - .. - } => { - let key = DashpayAccountKey { - index: *index, - user_identity_id: *user_identity_id, - friend_identity_id: *friend_identity_id, - }; - self.dashpay_receival_accounts.insert(key, account); - } - ManagedAccountType::DashpayExternalAccount { - index, - user_identity_id, - friend_identity_id, - .. - } => { - let key = DashpayAccountKey { - index: *index, - user_identity_id: *user_identity_id, - friend_identity_id: *friend_identity_id, - }; - self.dashpay_external_accounts.insert(key, account); - } - ManagedAccountType::PlatformPayment { - .. - } => { - // Platform Payment accounts should use insert_platform_account() instead - // as they use ManagedPlatformAccount, not ManagedCoreFundsAccount - return Err(crate::error::Error::InvalidParameter( - "Use insert_platform_account() for Platform Payment accounts".into(), - )); + other => { + return Err(crate::error::Error::InvalidParameter(format!( + "Account type {:?} cannot be stored as ManagedCoreKeysAccount; use insert_funds_bearing_account instead", + other.to_account_type(), + ))); } } Ok(()) @@ -395,77 +372,79 @@ impl ManagedAccountCollection { // Convert standard BIP44 accounts for (index, account) in &account_collection.standard_bip44_accounts { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_funds_account_from_account(account) { managed_collection.standard_bip44_accounts.insert(*index, managed_account); } } // Convert standard BIP32 accounts for (index, account) in &account_collection.standard_bip32_accounts { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_funds_account_from_account(account) { managed_collection.standard_bip32_accounts.insert(*index, managed_account); } } // Convert CoinJoin accounts for (index, account) in &account_collection.coinjoin_accounts { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_funds_account_from_account(account) { managed_collection.coinjoin_accounts.insert(*index, managed_account); } } - // Convert special purpose accounts + // Convert special purpose accounts (identity / asset-lock / provider) — + // keys-only variants (no balance / UTXO state). if let Some(account) = &account_collection.identity_registration { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.identity_registration = Some(managed_account); } } for (index, account) in &account_collection.identity_topup { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.identity_topup.insert(*index, managed_account); } } if let Some(account) = &account_collection.identity_topup_not_bound { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.identity_topup_not_bound = Some(managed_account); } } if let Some(account) = &account_collection.identity_invitation { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.identity_invitation = Some(managed_account); } } if let Some(account) = &account_collection.asset_lock_address_topup { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.asset_lock_address_topup = Some(managed_account); } } if let Some(account) = &account_collection.asset_lock_shielded_address_topup { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.asset_lock_shielded_address_topup = Some(managed_account); } } if let Some(account) = &account_collection.provider_voting_keys { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.provider_voting_keys = Some(managed_account); } } if let Some(account) = &account_collection.provider_owner_keys { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_account(account) { managed_collection.provider_owner_keys = Some(managed_account); } } #[cfg(feature = "bls")] if let Some(account) = &account_collection.provider_operator_keys { - if let Ok(managed_account) = Self::create_managed_account_from_bls_account(account) { + if let Ok(managed_account) = Self::create_managed_keys_account_from_bls_account(account) + { managed_collection.provider_operator_keys = Some(managed_account); } } @@ -473,7 +452,7 @@ impl ManagedAccountCollection { #[cfg(feature = "eddsa")] if let Some(account) = &account_collection.provider_platform_keys { if let Ok(managed_account) = - Self::create_managed_account_from_eddsa_account(account, None) + Self::create_managed_keys_account_from_eddsa_account(account, None) { managed_collection.provider_platform_keys = Some(managed_account); } @@ -481,14 +460,14 @@ impl ManagedAccountCollection { // Convert DashPay receiving accounts for (key, account) in &account_collection.dashpay_receival_accounts { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_funds_account_from_account(account) { managed_collection.dashpay_receival_accounts.insert(*key, managed_account); } } // Convert DashPay external accounts for (key, account) in &account_collection.dashpay_external_accounts { - if let Ok(managed_account) = Self::create_managed_account_from_account(account) { + if let Ok(managed_account) = Self::create_managed_funds_account_from_account(account) { managed_collection.dashpay_external_accounts.insert(*key, managed_account); } } @@ -505,56 +484,63 @@ impl ManagedAccountCollection { managed_collection } - /// Create a ManagedAccount from an Account - fn create_managed_account_from_account( + /// Create a funds-bearing managed account from an [`Account`]. + fn create_managed_funds_account_from_account( account: &Account, ) -> Result { - // Use the account's existing public key let key_source = KeySource::Public(account.account_xpub); - Self::create_managed_account_from_account_type( - account.account_type, - account.network, - &key_source, - ) + let managed_type = + Self::build_managed_account_type(account.account_type, account.network, &key_source)?; + Ok(ManagedCoreFundsAccount::new(managed_type, account.network)) + } + + /// Create a keys-only managed account from an [`Account`]. + fn create_managed_keys_account_from_account( + account: &Account, + ) -> Result { + let key_source = KeySource::Public(account.account_xpub); + let managed_type = + Self::build_managed_account_type(account.account_type, account.network, &key_source)?; + Ok(ManagedCoreKeysAccount::new(managed_type, account.network)) } - /// Create a ManagedAccount from a BLS Account + /// Create a keys-only managed account from a BLS provider-operator-keys + /// account. ProviderOperatorKeys is always keys-only. #[cfg(feature = "bls")] - fn create_managed_account_from_bls_account( + fn create_managed_keys_account_from_bls_account( account: &crate::account::BLSAccount, - ) -> Result { + ) -> Result { let key_source = KeySource::BLSPublic(account.bls_public_key.clone()); - Self::create_managed_account_from_account_type( - account.account_type, - account.network, - &key_source, - ) + let managed_type = + Self::build_managed_account_type(account.account_type, account.network, &key_source)?; + Ok(ManagedCoreKeysAccount::new(managed_type, account.network)) } - /// Create a ManagedAccount from an EdDSA Account + /// Create a keys-only managed account from an EdDSA provider-platform-keys + /// account. ProviderPlatformKeys is always keys-only. #[cfg(feature = "eddsa")] - fn create_managed_account_from_eddsa_account( + fn create_managed_keys_account_from_eddsa_account( account: &crate::account::EdDSAAccount, xpriv: Option, - ) -> Result { - // EdDSA requires hardened derivation, so we need the private key to generate addresses + ) -> Result { let key_source = match xpriv { Some(priv_key) => KeySource::EdDSAPrivate(priv_key), None => KeySource::NoKeySource, }; - Self::create_managed_account_from_account_type( - account.account_type, - account.network, - &key_source, - ) + let managed_type = + Self::build_managed_account_type(account.account_type, account.network, &key_source)?; + Ok(ManagedCoreKeysAccount::new(managed_type, account.network)) } - /// Create a ManagedAccount from an Account type with network - fn create_managed_account_from_account_type( + /// Build the [`ManagedAccountType`] (address pools + variant data) for an + /// account type and network. Shared between the funds-bearing and + /// keys-only construction helpers above — the wrap into one variant or + /// the other happens in the caller. + fn build_managed_account_type( account_type: AccountType, network: Network, key_source: &KeySource, - ) -> Result { + ) -> Result { // Get the derivation path for this account type let base_path = account_type .derivation_path(network) @@ -782,7 +768,7 @@ impl ManagedAccountCollection { } }; - Ok(ManagedCoreFundsAccount::new(managed_type, network)) + Ok(managed_type) } /// Create a ManagedPlatformAccount from an Account for Platform Payment accounts @@ -816,236 +802,371 @@ impl ManagedAccountCollection { )) } + /// Get a funds-bearing account by primary index across Standard BIP44, + /// Standard BIP32, and CoinJoin accounts. + /// + /// Returns only [`ManagedCoreFundsAccount`] entries — keys-only accounts + /// (identity / asset-lock / provider) are not reachable via this index + /// lookup. Use [`Self::get_by_account_type_match`] or direct field access + /// for those. 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); } - - // Try standard BIP32 if let Some(account) = self.standard_bip32_accounts.get(&index) { return Some(account); } - - // Try CoinJoin if let Some(account) = self.coinjoin_accounts.get(&index) { return Some(account); } - - // For identity top-up with registration index - if let Some(account) = self.identity_topup.get(&index) { - return Some(account); - } - None } - /// Get a mutable account by index + /// Get a mutable funds-bearing account by primary index. See [`Self::get`] + /// for which account types are reachable. 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); } - - // Try standard BIP32 if let Some(account) = self.standard_bip32_accounts.get_mut(&index) { return Some(account); } - - // Try CoinJoin if let Some(account) = self.coinjoin_accounts.get_mut(&index) { return Some(account); } - - // For identity top-up with registration index - if let Some(account) = self.identity_topup.get_mut(&index) { - return Some(account); - } - None } - /// Get an account reference by CoreAccountTypeMatch + /// Get an account reference by [`CoreAccountTypeMatch`]. Returns either + /// the funds-bearing or keys-only variant wrapped in + /// [`ManagedAccountRef`]. pub fn get_by_account_type_match( &self, account_type_match: &CoreAccountTypeMatch, - ) -> Option<&ManagedCoreFundsAccount> { - get_by_account_type_match_impl!(self, account_type_match, get, as_ref, values) + ) -> Option> { + match account_type_match { + CoreAccountTypeMatch::StandardBIP44 { + account_index, + .. + } => self.standard_bip44_accounts.get(account_index).map(ManagedAccountRef::Funds), + CoreAccountTypeMatch::StandardBIP32 { + account_index, + .. + } => self.standard_bip32_accounts.get(account_index).map(ManagedAccountRef::Funds), + CoreAccountTypeMatch::CoinJoin { + account_index, + .. + } => self.coinjoin_accounts.get(account_index).map(ManagedAccountRef::Funds), + CoreAccountTypeMatch::IdentityRegistration { + .. + } => self.identity_registration.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::IdentityTopUp { + account_index, + .. + } => self.identity_topup.get(account_index).map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::IdentityTopUpNotBound { + .. + } => self.identity_topup_not_bound.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::IdentityInvitation { + .. + } => self.identity_invitation.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::AssetLockAddressTopUp { + .. + } => self.asset_lock_address_topup.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::AssetLockShieldedAddressTopUp { + .. + } => self.asset_lock_shielded_address_topup.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::ProviderVotingKeys { + .. + } => self.provider_voting_keys.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::ProviderOwnerKeys { + .. + } => self.provider_owner_keys.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::ProviderOperatorKeys { + .. + } => self.provider_operator_keys.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::ProviderPlatformKeys { + .. + } => self.provider_platform_keys.as_ref().map(ManagedAccountRef::Keys), + CoreAccountTypeMatch::DashpayReceivingFunds { + account_index, + involved_addresses, + } => self + .dashpay_receival_accounts + .values() + .find(|account| match account.managed_account_type() { + ManagedAccountType::DashpayReceivingFunds { + index, + addresses, + .. + } => { + *index == *account_index + && involved_addresses + .iter() + .any(|addr| addresses.contains_address(&addr.address)) + } + _ => false, + }) + .map(ManagedAccountRef::Funds), + CoreAccountTypeMatch::DashpayExternalAccount { + account_index, + involved_addresses, + } => self + .dashpay_external_accounts + .values() + .find(|account| match account.managed_account_type() { + ManagedAccountType::DashpayExternalAccount { + index, + addresses, + .. + } => { + *index == *account_index + && involved_addresses + .iter() + .any(|addr| addresses.contains_address(&addr.address)) + } + _ => false, + }) + .map(ManagedAccountRef::Funds), + } } - /// Get a mutable account reference by AccountTypeMatch + /// Get a mutable account reference by [`CoreAccountTypeMatch`]. Returns + /// either the funds-bearing or keys-only variant wrapped in + /// [`ManagedAccountRefMut`]. pub fn get_by_account_type_match_mut( &mut self, account_type_match: &CoreAccountTypeMatch, - ) -> Option<&mut ManagedCoreFundsAccount> { - get_by_account_type_match_impl!(self, account_type_match, get_mut, as_mut, values_mut) + ) -> Option> { + match account_type_match { + CoreAccountTypeMatch::StandardBIP44 { + account_index, + .. + } => { + self.standard_bip44_accounts.get_mut(account_index).map(ManagedAccountRefMut::Funds) + } + CoreAccountTypeMatch::StandardBIP32 { + account_index, + .. + } => { + self.standard_bip32_accounts.get_mut(account_index).map(ManagedAccountRefMut::Funds) + } + CoreAccountTypeMatch::CoinJoin { + account_index, + .. + } => self.coinjoin_accounts.get_mut(account_index).map(ManagedAccountRefMut::Funds), + CoreAccountTypeMatch::IdentityRegistration { + .. + } => self.identity_registration.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::IdentityTopUp { + account_index, + .. + } => self.identity_topup.get_mut(account_index).map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::IdentityTopUpNotBound { + .. + } => self.identity_topup_not_bound.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::IdentityInvitation { + .. + } => self.identity_invitation.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::AssetLockAddressTopUp { + .. + } => self.asset_lock_address_topup.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::AssetLockShieldedAddressTopUp { + .. + } => self.asset_lock_shielded_address_topup.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::ProviderVotingKeys { + .. + } => self.provider_voting_keys.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::ProviderOwnerKeys { + .. + } => self.provider_owner_keys.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::ProviderOperatorKeys { + .. + } => self.provider_operator_keys.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::ProviderPlatformKeys { + .. + } => self.provider_platform_keys.as_mut().map(ManagedAccountRefMut::Keys), + CoreAccountTypeMatch::DashpayReceivingFunds { + account_index, + involved_addresses, + } => self + .dashpay_receival_accounts + .values_mut() + .find(|account| match account.managed_account_type() { + ManagedAccountType::DashpayReceivingFunds { + index, + addresses, + .. + } => { + *index == *account_index + && involved_addresses + .iter() + .any(|addr| addresses.contains_address(&addr.address)) + } + _ => false, + }) + .map(ManagedAccountRefMut::Funds), + CoreAccountTypeMatch::DashpayExternalAccount { + account_index, + involved_addresses, + } => self + .dashpay_external_accounts + .values_mut() + .find(|account| match account.managed_account_type() { + ManagedAccountType::DashpayExternalAccount { + index, + addresses, + .. + } => { + *index == *account_index + && involved_addresses + .iter() + .any(|addr| addresses.contains_address(&addr.address)) + } + _ => false, + }) + .map(ManagedAccountRefMut::Funds), + } } - /// Remove an account from the collection + /// Remove a funds-bearing account by primary index. Mirrors [`Self::get`] + /// in scope: only Standard BIP44, Standard BIP32, and CoinJoin accounts + /// are removable through this method. 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); } - - // Try standard BIP32 if let Some(account) = self.standard_bip32_accounts.remove(&index) { return Some(account); } - - // Try CoinJoin if let Some(account) = self.coinjoin_accounts.remove(&index) { return Some(account); } - - // For identity top-up with registration index - if let Some(account) = self.identity_topup.remove(&index) { - return Some(account); - } - None } - /// Check if an account exists + /// Whether a funds-bearing account exists at this primary index. Mirrors + /// [`Self::get`] in scope. pub fn contains_key(&self, index: u32) -> bool { - // Check standard BIP44 - if self.standard_bip44_accounts.contains_key(&index) { - return true; - } - - // Check standard BIP32 - if self.standard_bip32_accounts.contains_key(&index) { - return true; - } - - // Check CoinJoin - if self.coinjoin_accounts.contains_key(&index) { - return true; - } - - // Check identity top-up with registration index - if self.identity_topup.contains_key(&index) { - return true; - } - - false + self.standard_bip44_accounts.contains_key(&index) + || self.standard_bip32_accounts.contains_key(&index) + || self.coinjoin_accounts.contains_key(&index) } - /// Get all accounts - pub fn all_accounts(&self) -> Vec<&ManagedCoreFundsAccount> { + /// Get all accounts in the collection as [`ManagedAccountRef`] values. + pub fn all_accounts(&self) -> Vec> { let mut accounts = Vec::new(); - // Add standard BIP44 accounts - accounts.extend(self.standard_bip44_accounts.values()); + accounts.extend(self.standard_bip44_accounts.values().map(ManagedAccountRef::Funds)); + accounts.extend(self.standard_bip32_accounts.values().map(ManagedAccountRef::Funds)); + accounts.extend(self.coinjoin_accounts.values().map(ManagedAccountRef::Funds)); - // Add standard BIP32 accounts - accounts.extend(self.standard_bip32_accounts.values()); - - // Add CoinJoin accounts - accounts.extend(self.coinjoin_accounts.values()); - - // Add special purpose accounts if let Some(account) = &self.identity_registration { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - - accounts.extend(self.identity_topup.values()); - + accounts.extend(self.identity_topup.values().map(ManagedAccountRef::Keys)); if let Some(account) = &self.identity_topup_not_bound { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.identity_invitation { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.asset_lock_address_topup { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.asset_lock_shielded_address_topup { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.provider_voting_keys { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.provider_owner_keys { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.provider_operator_keys { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - if let Some(account) = &self.provider_platform_keys { - accounts.push(account); + accounts.push(ManagedAccountRef::Keys(account)); } - // Add DashPay accounts - accounts.extend(self.dashpay_receival_accounts.values()); - accounts.extend(self.dashpay_external_accounts.values()); + accounts.extend(self.dashpay_receival_accounts.values().map(ManagedAccountRef::Funds)); + accounts.extend(self.dashpay_external_accounts.values().map(ManagedAccountRef::Funds)); accounts } - /// Get all accounts mutably - pub fn all_accounts_mut(&mut self) -> Vec<&mut ManagedCoreFundsAccount> { + /// Get all accounts in the collection as mutable + /// [`ManagedAccountRefMut`] values. + pub fn all_accounts_mut(&mut self) -> Vec> { let mut accounts = Vec::new(); - // Add standard BIP44 accounts - accounts.extend(self.standard_bip44_accounts.values_mut()); - - // Add standard BIP32 accounts - accounts.extend(self.standard_bip32_accounts.values_mut()); - - // Add CoinJoin accounts - accounts.extend(self.coinjoin_accounts.values_mut()); + accounts.extend(self.standard_bip44_accounts.values_mut().map(ManagedAccountRefMut::Funds)); + accounts.extend(self.standard_bip32_accounts.values_mut().map(ManagedAccountRefMut::Funds)); + accounts.extend(self.coinjoin_accounts.values_mut().map(ManagedAccountRefMut::Funds)); - // Add special purpose accounts if let Some(account) = &mut self.identity_registration { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - - accounts.extend(self.identity_topup.values_mut()); - + accounts.extend(self.identity_topup.values_mut().map(ManagedAccountRefMut::Keys)); if let Some(account) = &mut self.identity_topup_not_bound { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.identity_invitation { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.asset_lock_address_topup { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.asset_lock_shielded_address_topup { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.provider_voting_keys { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.provider_owner_keys { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.provider_operator_keys { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - if let Some(account) = &mut self.provider_platform_keys { - accounts.push(account); + accounts.push(ManagedAccountRefMut::Keys(account)); } - // Add DashPay accounts + accounts + .extend(self.dashpay_receival_accounts.values_mut().map(ManagedAccountRefMut::Funds)); + accounts + .extend(self.dashpay_external_accounts.values_mut().map(ManagedAccountRefMut::Funds)); + + accounts + } + + /// Get all funds-bearing accounts (Standard BIP44/32, CoinJoin, DashPay). + /// + /// Use this from callsites that operate on balance / UTXO state — keys-only + /// accounts (identity, asset-lock, provider) don't track those, so iterating + /// [`Self::all_accounts`] and filtering via [`ManagedAccountRef::as_funds`] + /// in those callsites is just noise. + pub fn all_funding_accounts(&self) -> Vec<&ManagedCoreFundsAccount> { + let mut accounts = Vec::new(); + accounts.extend(self.standard_bip44_accounts.values()); + accounts.extend(self.standard_bip32_accounts.values()); + accounts.extend(self.coinjoin_accounts.values()); + accounts.extend(self.dashpay_receival_accounts.values()); + accounts.extend(self.dashpay_external_accounts.values()); + accounts + } + + /// Get all funds-bearing accounts mutably. See [`Self::all_funding_accounts`] + /// for which account types are visited. + pub fn all_funding_accounts_mut(&mut self) -> Vec<&mut ManagedCoreFundsAccount> { + let mut accounts = Vec::new(); + accounts.extend(self.standard_bip44_accounts.values_mut()); + accounts.extend(self.standard_bip32_accounts.values_mut()); + accounts.extend(self.coinjoin_accounts.values_mut()); accounts.extend(self.dashpay_receival_accounts.values_mut()); accounts.extend(self.dashpay_external_accounts.values_mut()); - accounts } diff --git a/key-wallet/src/managed_account/managed_account_ref.rs b/key-wallet/src/managed_account/managed_account_ref.rs new file mode 100644 index 000000000..22c495990 --- /dev/null +++ b/key-wallet/src/managed_account/managed_account_ref.rs @@ -0,0 +1,377 @@ +//! Borrowed enum spanning [`ManagedCoreFundsAccount`] and [`ManagedCoreKeysAccount`]. +//! +//! Several collection-level accessors (`all_accounts`, `get_by_account_type_match`, +//! …) need to return references to either funds-bearing or keys-only managed +//! accounts. [`ManagedAccountRef`] (and its mutable counterpart +//! [`ManagedAccountRefMut`]) provides the shared API surface for those callers +//! without requiring them to dispatch on the concrete account type. +//! +//! Operations that only make sense on funds accounts (balance, UTXOs) are NOT +//! exposed here — callers that need them must use [`ManagedAccountRef::as_funds`] +//! / [`ManagedAccountRefMut::as_funds_mut`] to access the funds variant +//! directly. + +use crate::account::TransactionRecord; +use crate::managed_account::address_pool::AddressInfo; +use crate::managed_account::managed_account_trait::ManagedAccountTrait; +use crate::managed_account::managed_account_type::ManagedAccountType; +use crate::managed_account::{ManagedCoreFundsAccount, ManagedCoreKeysAccount}; +use crate::transaction_checking::account_checker::AccountMatch; +use crate::transaction_checking::transaction_router::TransactionType; +use crate::transaction_checking::TransactionContext; +use crate::Network; +use dashcore::{Address, ScriptBuf, Transaction, Txid}; +use std::collections::BTreeMap; + +/// Immutable reference to a managed core account, either funds-bearing or +/// keys-only. +/// +/// See the [module-level docs](self) for context. +#[derive(Debug, Clone, Copy)] +pub enum ManagedAccountRef<'a> { + /// Funds-bearing variant (Standard, CoinJoin, DashPay). + Funds(&'a ManagedCoreFundsAccount), + /// Keys-only variant (identity, asset-lock, provider). + Keys(&'a ManagedCoreKeysAccount), +} + +/// Mutable reference to a managed core account, either funds-bearing or +/// keys-only. +/// +/// See the [module-level docs](self) for context. +#[derive(Debug)] +pub enum ManagedAccountRefMut<'a> { + /// Funds-bearing variant (Standard, CoinJoin, DashPay). + Funds(&'a mut ManagedCoreFundsAccount), + /// Keys-only variant (identity, asset-lock, provider). + Keys(&'a mut ManagedCoreKeysAccount), +} + +impl<'a> ManagedAccountRef<'a> { + /// Returns the funds account if this is the [`Funds`](Self::Funds) variant. + pub fn as_funds(self) -> Option<&'a ManagedCoreFundsAccount> { + match self { + ManagedAccountRef::Funds(a) => Some(a), + ManagedAccountRef::Keys(_) => None, + } + } + + /// Returns the keys account if this is the [`Keys`](Self::Keys) variant. + pub fn as_keys(self) -> Option<&'a ManagedCoreKeysAccount> { + match self { + ManagedAccountRef::Funds(_) => None, + ManagedAccountRef::Keys(a) => Some(a), + } + } + + /// Returns a reference to the underlying [`ManagedCoreKeysAccount`], + /// regardless of variant. For [`Funds`](Self::Funds) this returns the + /// inner keys account composed inside the funds account; for + /// [`Keys`](Self::Keys) it returns the account itself. + pub fn keys_account(self) -> &'a ManagedCoreKeysAccount { + match self { + ManagedAccountRef::Funds(a) => a.keys(), + ManagedAccountRef::Keys(a) => a, + } + } + + /// Get the managed account type. + pub fn managed_account_type(self) -> &'a ManagedAccountType { + match self { + ManagedAccountRef::Funds(a) => a.managed_account_type(), + ManagedAccountRef::Keys(a) => a.managed_account_type(), + } + } + + /// Get the network this account belongs to. + pub fn network(self) -> Network { + match self { + ManagedAccountRef::Funds(a) => a.network(), + ManagedAccountRef::Keys(a) => a.network(), + } + } + + /// Get the transaction history map. + pub fn transactions(self) -> &'a BTreeMap { + match self { + ManagedAccountRef::Funds(a) => a.transactions(), + ManagedAccountRef::Keys(a) => a.transactions(), + } + } + + /// Whether this account has already processed `txid`. + pub fn has_transaction(self, txid: &Txid) -> bool { + match self { + ManagedAccountRef::Funds(a) => a.has_transaction(txid), + ManagedAccountRef::Keys(a) => a.has_transaction(txid), + } + } + + /// Whether `txid` has been finalized (chainlocked). + pub fn transaction_is_finalized(self, txid: &Txid) -> bool { + match self { + ManagedAccountRef::Funds(a) => a.transaction_is_finalized(txid), + ManagedAccountRef::Keys(a) => a.transaction_is_finalized(txid), + } + } + + /// Return the current monitor revision. + pub fn monitor_revision(self) -> u64 { + match self { + ManagedAccountRef::Funds(a) => a.monitor_revision(), + ManagedAccountRef::Keys(a) => a.monitor_revision(), + } + } + + /// Whether `address` belongs to this account. + pub fn contains_address(self, address: &Address) -> bool { + match self { + ManagedAccountRef::Funds(a) => a.contains_address(address), + ManagedAccountRef::Keys(a) => a.contains_address(address), + } + } + + /// Whether `script_pub_key` belongs to this account. + pub fn contains_script_pub_key(self, script_pub_key: &ScriptBuf) -> bool { + match self { + ManagedAccountRef::Funds(a) => a.contains_script_pub_key(script_pub_key), + ManagedAccountRef::Keys(a) => a.contains_script_pub_key(script_pub_key), + } + } + + /// Get [`AddressInfo`] for `address`, if owned by this account. + pub fn get_address_info(self, address: &Address) -> Option { + match self { + ManagedAccountRef::Funds(a) => a.get_address_info(address), + ManagedAccountRef::Keys(a) => a.get_address_info(address), + } + } + + /// Return all addresses tracked by this account (across all pools). + pub fn all_addresses(self) -> Vec
{ + match self { + ManagedAccountRef::Funds(a) => a.all_addresses(), + ManagedAccountRef::Keys(a) => a.all_addresses(), + } + } +} + +impl<'a> ManagedAccountRefMut<'a> { + /// Borrow this mutable reference as an immutable [`ManagedAccountRef`]. + pub fn as_ref(&self) -> ManagedAccountRef<'_> { + match self { + ManagedAccountRefMut::Funds(a) => ManagedAccountRef::Funds(a), + ManagedAccountRefMut::Keys(a) => ManagedAccountRef::Keys(a), + } + } + + /// Returns the funds account if this is the [`Funds`](Self::Funds) variant. + pub fn as_funds(&self) -> Option<&ManagedCoreFundsAccount> { + match self { + ManagedAccountRefMut::Funds(a) => Some(a), + ManagedAccountRefMut::Keys(_) => None, + } + } + + /// Returns the keys account if this is the [`Keys`](Self::Keys) variant. + pub fn as_keys(&self) -> Option<&ManagedCoreKeysAccount> { + match self { + ManagedAccountRefMut::Funds(_) => None, + ManagedAccountRefMut::Keys(a) => Some(a), + } + } + + /// Returns the funds account if this is the [`Funds`](Self::Funds) variant. + pub fn as_funds_mut(&mut self) -> Option<&mut ManagedCoreFundsAccount> { + match self { + ManagedAccountRefMut::Funds(a) => Some(a), + ManagedAccountRefMut::Keys(_) => None, + } + } + + /// Returns the keys account if this is the [`Keys`](Self::Keys) variant. + pub fn as_keys_mut(&mut self) -> Option<&mut ManagedCoreKeysAccount> { + match self { + ManagedAccountRefMut::Funds(_) => None, + ManagedAccountRefMut::Keys(a) => Some(a), + } + } + + /// Get the managed account type. + pub fn managed_account_type(&self) -> &ManagedAccountType { + match self { + ManagedAccountRefMut::Funds(a) => a.managed_account_type(), + ManagedAccountRefMut::Keys(a) => a.managed_account_type(), + } + } + + /// Get a mutable reference to the managed account type. + pub fn managed_account_type_mut(&mut self) -> &mut ManagedAccountType { + match self { + ManagedAccountRefMut::Funds(a) => a.managed_account_type_mut(), + ManagedAccountRefMut::Keys(a) => a.managed_account_type_mut(), + } + } + + /// Get the network this account belongs to. + pub fn network(&self) -> Network { + match self { + ManagedAccountRefMut::Funds(a) => a.network(), + ManagedAccountRefMut::Keys(a) => a.network(), + } + } + + /// Get the transaction history map. + pub fn transactions(&self) -> &BTreeMap { + match self { + ManagedAccountRefMut::Funds(a) => a.transactions(), + ManagedAccountRefMut::Keys(a) => a.transactions(), + } + } + + /// Get a mutable reference to the transaction history map. + pub fn transactions_mut(&mut self) -> &mut BTreeMap { + match self { + ManagedAccountRefMut::Funds(a) => a.transactions_mut(), + ManagedAccountRefMut::Keys(a) => a.transactions_mut(), + } + } + + /// Whether this account has already processed `txid`. + pub fn has_transaction(&self, txid: &Txid) -> bool { + match self { + ManagedAccountRefMut::Funds(a) => a.has_transaction(txid), + ManagedAccountRefMut::Keys(a) => a.has_transaction(txid), + } + } + + /// Whether `txid` has been finalized (chainlocked). + pub fn transaction_is_finalized(&self, txid: &Txid) -> bool { + match self { + ManagedAccountRefMut::Funds(a) => a.transaction_is_finalized(txid), + ManagedAccountRefMut::Keys(a) => a.transaction_is_finalized(txid), + } + } + + /// Mark the address as used in whichever pool owns it. Returns `true` if + /// the address was found and updated. + pub fn mark_address_used(&mut self, address: &Address) -> bool { + match self { + ManagedAccountRefMut::Funds(a) => a.mark_address_used(address), + ManagedAccountRefMut::Keys(a) => a.mark_address_used(address), + } + } + + /// Bump the monitor revision counter — call this when the monitored + /// address set changes (e.g. new addresses generated). + pub fn bump_monitor_revision(&mut self) { + match self { + ManagedAccountRefMut::Funds(a) => a.bump_monitor_revision(), + ManagedAccountRefMut::Keys(a) => a.bump_monitor_revision(), + } + } + + /// Return the current monitor revision. + pub fn monitor_revision(&self) -> u64 { + match self { + ManagedAccountRefMut::Funds(a) => a.monitor_revision(), + ManagedAccountRefMut::Keys(a) => a.monitor_revision(), + } + } + + /// Record a new transaction for this account. + /// + /// Funds variants update UTXO state and balance; keys variants update + /// only the transaction history. Both are subject to the + /// `keep-finalized-transactions` Cargo feature for chainlocked records. + pub fn record_transaction( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + transaction_type: TransactionType, + ) -> TransactionRecord { + match self { + ManagedAccountRefMut::Funds(a) => { + a.record_transaction(tx, account_match, context, transaction_type) + } + ManagedAccountRefMut::Keys(a) => { + a.record_transaction(tx, account_match, context, transaction_type) + } + } + } + + /// Re-process an existing transaction with updated context. + /// + /// Funds variants additionally refresh UTXO state. Returns the updated + /// record only when confirmation status actually changes. + pub fn confirm_transaction( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + transaction_type: TransactionType, + ) -> Option { + match self { + ManagedAccountRefMut::Funds(a) => { + a.confirm_transaction(tx, account_match, context, transaction_type) + } + ManagedAccountRefMut::Keys(a) => { + a.confirm_transaction(tx, account_match, context, transaction_type) + } + } + } + + /// Mark all UTXOs belonging to `txid` as InstantSend-locked. + /// + /// Returns `true` if any UTXO was newly marked. Always returns `false` + /// for the [`Keys`](Self::Keys) variant (no UTXOs to mark). + pub fn mark_utxos_instant_send(&mut self, txid: &Txid) -> bool { + match self { + ManagedAccountRefMut::Funds(a) => a.mark_utxos_instant_send(txid), + ManagedAccountRefMut::Keys(_) => false, + } + } +} + +/// Owned managed core account, either funds-bearing or keys-only. +/// +/// Used by [`ManagedAccountCollection::insert`] so the collection can accept +/// either variant in a single entry point. Use [`OwnedManagedCoreAccount::Funds`] +/// or [`OwnedManagedCoreAccount::Keys`] explicitly when constructing one. +/// +/// [`ManagedAccountCollection::insert`]: crate::managed_account::managed_account_collection::ManagedAccountCollection::insert +#[derive(Debug, Clone)] +pub enum OwnedManagedCoreAccount { + /// Funds-bearing variant (Standard, CoinJoin, DashPay). + Funds(ManagedCoreFundsAccount), + /// Keys-only variant (identity, asset-lock, provider). + Keys(ManagedCoreKeysAccount), +} + +impl OwnedManagedCoreAccount { + /// Borrow this owned account as a [`ManagedAccountRef`]. + pub fn as_ref(&self) -> ManagedAccountRef<'_> { + match self { + OwnedManagedCoreAccount::Funds(a) => ManagedAccountRef::Funds(a), + OwnedManagedCoreAccount::Keys(a) => ManagedAccountRef::Keys(a), + } + } + + /// Get the managed account type. + pub fn managed_account_type(&self) -> &ManagedAccountType { + self.as_ref().managed_account_type() + } +} + +impl From for OwnedManagedCoreAccount { + fn from(value: ManagedCoreFundsAccount) -> Self { + OwnedManagedCoreAccount::Funds(value) + } +} + +impl From for OwnedManagedCoreAccount { + fn from(value: ManagedCoreKeysAccount) -> Self { + OwnedManagedCoreAccount::Keys(value) + } +} diff --git a/key-wallet/src/managed_account/managed_core_keys_account.rs b/key-wallet/src/managed_account/managed_core_keys_account.rs index 8a520cc22..114b1c56c 100644 --- a/key-wallet/src/managed_account/managed_core_keys_account.rs +++ b/key-wallet/src/managed_account/managed_core_keys_account.rs @@ -14,8 +14,12 @@ 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::transaction_record::TransactionDirection; +use crate::transaction_checking::account_checker::AccountMatch; +use crate::transaction_checking::transaction_router::TransactionType; +use crate::transaction_checking::TransactionContext; use crate::Network; -use dashcore::Txid; +use dashcore::{Transaction, Txid}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -159,6 +163,120 @@ impl ManagedCoreKeysAccount { Self::new(managed_type, account.network) } + + /// Record a new transaction for this keys account. + /// + /// The keys-account record is intentionally a thin marker: it captures + /// "this tx involved this keys account" plus the `net_amount` flowing + /// to our addresses, and no more. The wallet-level details + /// (per-input UTXO origins, per-output roles) live on the **funding + /// account's** record — keys-account flows (identity registration, + /// asset lock, provider-key registration / update) are typically + /// funded from a Standard or CoinJoin account in the same wallet, + /// and that account's `record_transaction` already populates + /// `input_details` (from its UTXO set) and `output_details` + /// (classified into receive / change / sent). Duplicating that work + /// on the keys-account side would double-count and de-sync if the + /// classification ever changes. + /// + /// Direction is [`TransactionDirection::Internal`]: from the wallet's + /// perspective these txs move funds from one of its accounts to + /// another, even when only the keys account is matched here. + 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 tx_record = TransactionRecord::new( + tx.clone(), + self.managed_account_type.to_account_type(), + context.clone(), + transaction_type, + TransactionDirection::Internal, + Vec::new(), + Vec::new(), + net_amount, + ); + + let record = tx_record.clone(); + let txid = tx.txid(); + self.transactions.insert(txid, tx_record); + + // If this first sighting is already chainlocked (e.g. a wallet + // rescan from storage), drop the full record now and keep only the + // txid in `finalized_txids`. No-op when the feature is on (we want + // to keep the full record). + #[cfg(not(feature = "keep-finalized-transactions"))] + if context.is_chain_locked() { + self.drop_finalized_transaction(&txid); + } + + record + } + + /// Re-process a transaction with updated context for this keys account. + /// + /// Mirrors [`ManagedCoreFundsAccount::confirm_transaction`](crate::managed_account::ManagedCoreFundsAccount::confirm_transaction) + /// but without UTXO updates. Returns the updated record only when the + /// confirmation status actually changes (e.g. mempool → in-block). + pub(crate) fn confirm_transaction( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + transaction_type: TransactionType, + ) -> Option { + let txid = tx.txid(); + + // Already finalized via a chainlock: the tx is immutable — + // no record update, no event needed. + if self.transaction_is_finalized(&txid) { + return None; + } + + if !self.has_transaction(&txid) { + // Genuinely new sighting — delegate to record_transaction + // (which handles finalize-on-record itself). + let record = self.record_transaction(tx, account_match, context, transaction_type); + return Some(record); + } + + let mut changed = false; + if let Some(tx_record) = self.transactions.get_mut(&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()); + changed = !was_confirmed; + } + } + + let record_after = if changed { + self.transactions.get(&txid).cloned() + } else { + None + }; + + // Drop the full record on chainlock when the feature is off; the + // surrounding block-confirmation event has already updated context. + #[cfg(not(feature = "keep-finalized-transactions"))] + if context.is_chain_locked() { + self.drop_finalized_transaction(&txid); + } + + let _ = account_match; + + record_after + } } impl ManagedAccountTrait for ManagedCoreKeysAccount { diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index f5801e1c8..25b483624 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -14,6 +14,7 @@ pub mod address_pool; pub mod managed_account_collection; +pub mod managed_account_ref; pub mod managed_account_trait; pub mod managed_account_type; pub mod managed_core_funds_account; @@ -22,5 +23,6 @@ pub mod managed_platform_account; pub mod platform_address; pub mod transaction_record; +pub use managed_account_ref::{ManagedAccountRef, ManagedAccountRefMut, OwnedManagedCoreAccount}; pub use managed_core_funds_account::ManagedCoreFundsAccount; pub use managed_core_keys_account::ManagedCoreKeysAccount; diff --git a/key-wallet/src/tests/mod.rs b/key-wallet/src/tests/mod.rs index 4e5a01b72..8db87a128 100644 --- a/key-wallet/src/tests/mod.rs +++ b/key-wallet/src/tests/mod.rs @@ -22,6 +22,8 @@ mod managed_account_collection_tests; mod performance_tests; +mod special_transaction_matching_tests; + mod special_transaction_tests; mod transaction_tests; diff --git a/key-wallet/src/tests/special_transaction_matching_tests.rs b/key-wallet/src/tests/special_transaction_matching_tests.rs new file mode 100644 index 000000000..5f62ebf38 --- /dev/null +++ b/key-wallet/src/tests/special_transaction_matching_tests.rs @@ -0,0 +1,637 @@ +//! End-to-end matching tests for the special-transaction → keys-account +//! paths exercised by this PR. +//! +//! For each special-transaction type that drives the keys-account +//! `check_*_for_match` methods on [`ManagedCoreKeysAccount`], construct a +//! transaction targeting the relevant account's address / key and assert +//! [`ManagedWalletInfo::check_core_transaction`] flags the right +//! [`AccountTypeToCheck`]. +//! +//! Skipped on purpose: +//! +//! - `AssetUnlockPayloadType` and `CoinbasePayloadType` — match Standard +//! funds-bearing accounts, not the keys-only variants this PR touches. +//! - `QuorumCommitmentPayloadType` — no key/address fields the wallet looks +//! for, no match path. +//! - `ProviderUpdateRevocationPayloadType` — the payload has no key-hash or +//! pubkey field for the wallet to match against. + +use crate::managed_account::managed_account_trait::ManagedAccountTrait; +use crate::transaction_checking::transaction_router::AccountTypeToCheck; +use crate::transaction_checking::wallet_checker::WalletTransactionChecker; +use crate::transaction_checking::{BlockInfo, TransactionContext}; +use crate::wallet::initialization::WalletAccountCreationOptions; +use crate::wallet::managed_wallet_info::managed_account_operations::ManagedAccountOperations; +use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; +use crate::wallet::managed_wallet_info::ManagedWalletInfo; +use crate::wallet::Wallet; +use crate::Network; + +use dashcore::blockdata::script::ScriptBuf; +use dashcore::blockdata::transaction::outpoint::OutPoint; +use dashcore::blockdata::transaction::special_transaction::asset_lock::AssetLockPayload; +use dashcore::blockdata::transaction::special_transaction::provider_registration::{ + ProviderMasternodeType, ProviderRegistrationPayload, +}; +use dashcore::blockdata::transaction::special_transaction::provider_update_registrar::ProviderUpdateRegistrarPayload; +use dashcore::blockdata::transaction::special_transaction::TransactionPayload; +use dashcore::blockdata::transaction::txin::TxIn; +use dashcore::blockdata::transaction::txout::TxOut; +use dashcore::blockdata::transaction::Transaction; +use dashcore::blockdata::witness::Witness; +use dashcore::hashes::Hash; +use dashcore::{BlockHash, Txid}; + +const TEST_NETWORK: Network = Network::Testnet; +const ASSET_LOCK_VALUE: u64 = 100_000_000; // 1 DASH +const TEST_HEIGHT: u32 = 100_000; + +fn test_block_context() -> TransactionContext { + TransactionContext::InBlock(BlockInfo::new( + TEST_HEIGHT, + BlockHash::from_slice(&[0u8; 32]).expect("32-byte block hash"), + 1_700_000_000, + )) +} + +fn make_wallet() -> (Wallet, ManagedWalletInfo) { + let wallet = Wallet::new_random(TEST_NETWORK, WalletAccountCreationOptions::Default) + .expect("create wallet with default account types"); + let info = ManagedWalletInfo::from_wallet_with_name(&wallet, "matching-tests".to_string(), 0); + (wallet, info) +} + +/// Build an AssetLock transaction whose `credit_outputs` pay `value` to +/// `script`. Inputs / regular outputs are placeholders the wallet won't +/// match against. +fn asset_lock_to(script: ScriptBuf, value: u64) -> Transaction { + Transaction { + version: 3, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([1u8; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: Witness::default(), + }], + output: Vec::new(), + special_transaction_payload: Some(TransactionPayload::AssetLockPayloadType( + AssetLockPayload { + version: 1, + credit_outputs: vec![TxOut { + value, + script_pubkey: script, + }], + }, + )), + } +} + +/// Assert the result is_relevant and contains the expected +/// `AccountTypeToCheck` among the affected accounts. +fn assert_matched_account_type( + result: &crate::transaction_checking::wallet_checker::TransactionCheckResult, + expected: AccountTypeToCheck, +) { + assert!(result.is_relevant, "transaction should be relevant"); + let affected: Vec<_> = result + .affected_accounts + .iter() + .map(|acc| acc.account_type_match.to_account_type_to_check()) + .collect(); + assert!( + affected.contains(&expected), + "expected {expected:?} in affected accounts, got {affected:?}", + ); +} + +// --------------------------------------------------------------------------- +// AssetLock → keys-account variants +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn asset_lock_credit_output_to_identity_registration_address_matches() { + let (mut wallet, mut info) = make_wallet(); + let xpub = + wallet.accounts.identity_registration.as_ref().expect("default options").account_xpub; + let address = info + .identity_registration_managed_account_mut() + .expect("identity registration managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let tx = asset_lock_to(address.script_pubkey(), ASSET_LOCK_VALUE); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::IdentityRegistration); + assert_eq!( + result.total_received_for_credit_conversion, ASSET_LOCK_VALUE, + "asset-lock credit-output value flows into credit conversion, not spendable balance", + ); +} + +#[tokio::test] +async fn asset_lock_regular_output_to_identity_topup_address_matches() { + // Note: unlike the other identity / asset-lock variants, the IdentityTopUp + // dispatch in `account_checker::check_account_type` calls + // `check_transaction_for_match` (regular tx outputs) rather than + // `check_asset_lock_transaction_for_match` (credit outputs). That's a + // pre-existing dispatch quirk independent of this PR. To exercise the + // path the dispatch actually takes, we put the topup address on a + // **regular output** of an AssetLock-classified transaction. + let (mut wallet, mut info) = make_wallet(); + let registration_index = 0u32; + // IdentityTopUp accounts are per-registration-index — not auto-created + // by the `Default` options. + wallet + .add_account( + crate::AccountType::IdentityTopUp { + registration_index, + }, + None, + ) + .expect("add IdentityTopUp account"); + info.add_managed_account( + &wallet, + crate::AccountType::IdentityTopUp { + registration_index, + }, + ) + .expect("attach managed IdentityTopUp"); + let xpub = wallet + .accounts + .identity_topup + .get(®istration_index) + .expect("identity_topup[0]") + .account_xpub; + let address = info + .topup_managed_account_at_registration_index_mut(registration_index) + .expect("identity_topup[0] managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let mut tx = asset_lock_to(ScriptBuf::new(), ASSET_LOCK_VALUE); + tx.output.push(TxOut { + value: ASSET_LOCK_VALUE, + script_pubkey: address.script_pubkey(), + }); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::IdentityTopUp); +} + +#[tokio::test] +async fn asset_lock_credit_output_to_identity_topup_not_bound_address_matches() { + let (mut wallet, mut info) = make_wallet(); + let xpub = + wallet.accounts.identity_topup_not_bound.as_ref().expect("default options").account_xpub; + let address = info + .identity_topup_not_bound_managed_account_mut() + .expect("identity_topup_not_bound managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let tx = asset_lock_to(address.script_pubkey(), ASSET_LOCK_VALUE); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::IdentityTopUpNotBound); +} + +#[tokio::test] +async fn asset_lock_credit_output_to_identity_invitation_address_matches() { + let (mut wallet, mut info) = make_wallet(); + let xpub = wallet.accounts.identity_invitation.as_ref().expect("default options").account_xpub; + let address = info + .identity_invitation_managed_account_mut() + .expect("identity_invitation managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let tx = asset_lock_to(address.script_pubkey(), ASSET_LOCK_VALUE); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::IdentityInvitation); +} + +#[tokio::test] +async fn asset_lock_credit_output_to_asset_lock_address_topup_address_matches() { + let (mut wallet, mut info) = make_wallet(); + let xpub = + wallet.accounts.asset_lock_address_topup.as_ref().expect("default options").account_xpub; + let address = info + .accounts_mut() + .asset_lock_address_topup + .as_mut() + .expect("asset_lock_address_topup managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let tx = asset_lock_to(address.script_pubkey(), ASSET_LOCK_VALUE); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::AssetLockAddressTopUp); +} + +#[tokio::test] +async fn asset_lock_credit_output_to_asset_lock_shielded_address_topup_address_matches() { + let (mut wallet, mut info) = make_wallet(); + let xpub = wallet + .accounts + .asset_lock_shielded_address_topup + .as_ref() + .expect("default options") + .account_xpub; + let address = info + .accounts_mut() + .asset_lock_shielded_address_topup + .as_mut() + .expect("asset_lock_shielded_address_topup managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let tx = asset_lock_to(address.script_pubkey(), ASSET_LOCK_VALUE); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::AssetLockShieldedAddressTopUp); +} + +// --------------------------------------------------------------------------- +// ProviderRegistration → provider-key variants +// --------------------------------------------------------------------------- + +/// Build a regular-masternode `ProRegTx` populated with the given key +/// hashes / public key. Fields not relevant to matching get placeholder +/// bytes. +#[allow(clippy::too_many_arguments)] +fn prov_reg_tx( + masternode_type: ProviderMasternodeType, + owner_key_hash: dashcore::PubkeyHash, + voting_key_hash: dashcore::PubkeyHash, + operator_public_key: dashcore::bls_sig_utils::BLSPublicKey, + script_payout: ScriptBuf, + platform_node_id: Option, +) -> Transaction { + Transaction { + version: 3, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([1u8; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: Witness::default(), + }], + output: vec![TxOut { + value: 1_000_000, + script_pubkey: ScriptBuf::new(), + }], + special_transaction_payload: Some(TransactionPayload::ProviderRegistrationPayloadType( + ProviderRegistrationPayload { + version: 1, + masternode_type, + masternode_mode: 0, + collateral_outpoint: OutPoint { + txid: Txid::from_byte_array([1u8; 32]), + vout: 0, + }, + service_address: "127.0.0.1:19999".parse().expect("service address"), + owner_key_hash, + operator_public_key, + voting_key_hash, + operator_reward: 0, + script_payout, + inputs_hash: dashcore::hash_types::InputsHash::from_slice(&[6u8; 32]) + .expect("32-byte inputs hash"), + signature: vec![7u8; 65], + platform_node_id, + platform_p2p_port: platform_node_id.map(|_| 26656), + platform_http_port: platform_node_id.map(|_| 8080), + }, + )), + } +} + +fn derive_pubkey_hash(addr: &dashcore::Address) -> dashcore::PubkeyHash { + *addr.payload().as_pubkey_hash().expect("provider account uses P2PKH") +} + +#[tokio::test] +async fn provider_registration_with_owner_key_hash_matches_provider_owner_keys() { + let (mut wallet, mut info) = make_wallet(); + let (mut _other_wallet, mut other_info) = make_wallet(); + + let owner_addr = info + .provider_owner_keys_managed_account_mut() + .expect("provider_owner_keys managed") + .next_address(None, true) + .expect("derive owner"); + let voting_addr = other_info + .provider_voting_keys_managed_account_mut() + .expect("other voting") + .next_address(None, true) + .expect("derive voting"); + let operator_pk = other_info + .provider_operator_keys_managed_account_mut() + .expect("other operator") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let tx = prov_reg_tx( + ProviderMasternodeType::Regular, + derive_pubkey_hash(&owner_addr), + derive_pubkey_hash(&voting_addr), + operator_pk.0.to_compressed().into(), + ScriptBuf::new(), + None, + ); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderOwnerKeys); +} + +#[tokio::test] +async fn provider_registration_with_voting_key_hash_matches_provider_voting_keys() { + let (mut wallet, mut info) = make_wallet(); + let (_other_wallet, mut other_info) = make_wallet(); + + let owner_addr = other_info + .provider_owner_keys_managed_account_mut() + .expect("other owner") + .next_address(None, true) + .expect("derive owner"); + let voting_addr = info + .provider_voting_keys_managed_account_mut() + .expect("provider_voting_keys managed") + .next_address(None, true) + .expect("derive voting"); + let operator_pk = other_info + .provider_operator_keys_managed_account_mut() + .expect("other operator") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let tx = prov_reg_tx( + ProviderMasternodeType::Regular, + derive_pubkey_hash(&owner_addr), + derive_pubkey_hash(&voting_addr), + operator_pk.0.to_compressed().into(), + ScriptBuf::new(), + None, + ); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderVotingKeys); +} + +#[cfg(feature = "bls")] +#[tokio::test] +async fn provider_registration_with_operator_public_key_matches_provider_operator_keys() { + let (mut wallet, mut info) = make_wallet(); + let (_other_wallet, mut other_info) = make_wallet(); + + let owner_addr = other_info + .provider_owner_keys_managed_account_mut() + .expect("other owner") + .next_address(None, true) + .expect("derive owner"); + let voting_addr = other_info + .provider_voting_keys_managed_account_mut() + .expect("other voting") + .next_address(None, true) + .expect("derive voting"); + let operator_pk = info + .provider_operator_keys_managed_account_mut() + .expect("provider_operator_keys managed") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let tx = prov_reg_tx( + ProviderMasternodeType::Regular, + derive_pubkey_hash(&owner_addr), + derive_pubkey_hash(&voting_addr), + operator_pk.0.to_compressed().into(), + ScriptBuf::new(), + None, + ); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderOperatorKeys); +} + +#[cfg(feature = "eddsa")] +#[tokio::test] +async fn provider_registration_with_platform_node_id_matches_provider_platform_keys() { + let (mut wallet, mut info) = make_wallet(); + let (_other_wallet, mut other_info) = make_wallet(); + + let owner_addr = other_info + .provider_owner_keys_managed_account_mut() + .expect("other owner") + .next_address(None, true) + .expect("derive owner"); + let voting_addr = other_info + .provider_voting_keys_managed_account_mut() + .expect("other voting") + .next_address(None, true) + .expect("derive voting"); + let operator_pk = other_info + .provider_operator_keys_managed_account_mut() + .expect("other operator") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let root = wallet.root_extended_priv_key().expect("root extended priv key"); + let eddsa = root.to_eddsa_extended_priv_key(TEST_NETWORK).expect("eddsa extended priv"); + let (_platform_pk, platform_info) = info + .provider_platform_keys_managed_account_mut() + .expect("provider_platform_keys managed") + .next_eddsa_platform_key(eddsa, true) + .expect("derive platform"); + let platform_node_id = derive_pubkey_hash(&platform_info.address); + + let tx = prov_reg_tx( + ProviderMasternodeType::HighPerformance, + derive_pubkey_hash(&owner_addr), + derive_pubkey_hash(&voting_addr), + operator_pk.0.to_compressed().into(), + ScriptBuf::new(), + Some(platform_node_id), + ); + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderPlatformKeys); +} + +// --------------------------------------------------------------------------- +// ProviderUpdateRegistrar → voting / operator key changes +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn provider_update_registrar_with_voting_key_change_matches_provider_voting_keys() { + let (mut wallet, mut info) = make_wallet(); + + let voting_addr = info + .provider_voting_keys_managed_account_mut() + .expect("provider_voting_keys managed") + .next_address(None, true) + .expect("derive voting"); + // Operator key sourced from a fresh wallet so it doesn't accidentally + // match this wallet's operator account. + let (_other_wallet, mut other_info) = make_wallet(); + let operator_pk = other_info + .provider_operator_keys_managed_account_mut() + .expect("other operator") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let tx = Transaction { + version: 3, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([1u8; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: Witness::default(), + }], + output: vec![TxOut { + value: 1_000_000, + script_pubkey: ScriptBuf::new(), + }], + special_transaction_payload: Some(TransactionPayload::ProviderUpdateRegistrarPayloadType( + ProviderUpdateRegistrarPayload { + version: 1, + pro_tx_hash: Txid::from_byte_array([1u8; 32]), + provider_mode: 0, + operator_public_key: operator_pk.0.to_compressed().into(), + voting_key_hash: derive_pubkey_hash(&voting_addr), + script_payout: ScriptBuf::new(), + inputs_hash: [3u8; 32].into(), + payload_sig: vec![4u8; 65], + }, + )), + }; + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderVotingKeys); +} + +#[cfg(feature = "bls")] +#[tokio::test] +async fn provider_update_registrar_with_operator_key_change_matches_provider_operator_keys() { + let (mut wallet, mut info) = make_wallet(); + + // Voting addr from a fresh wallet so it doesn't double-match. + let (_other_wallet, mut other_info) = make_wallet(); + let voting_addr = other_info + .provider_voting_keys_managed_account_mut() + .expect("other voting") + .next_address(None, true) + .expect("derive voting"); + let operator_pk = info + .provider_operator_keys_managed_account_mut() + .expect("provider_operator_keys managed") + .next_bls_operator_key(None, true) + .expect("derive operator"); + + let tx = Transaction { + version: 3, + lock_time: 0, + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array([1u8; 32]), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: 0xffffffff, + witness: Witness::default(), + }], + output: vec![TxOut { + value: 1_000_000, + script_pubkey: ScriptBuf::new(), + }], + special_transaction_payload: Some(TransactionPayload::ProviderUpdateRegistrarPayloadType( + ProviderUpdateRegistrarPayload { + version: 1, + pro_tx_hash: Txid::from_byte_array([1u8; 32]), + provider_mode: 0, + operator_public_key: operator_pk.0.to_compressed().into(), + voting_key_hash: derive_pubkey_hash(&voting_addr), + script_payout: ScriptBuf::new(), + inputs_hash: [3u8; 32].into(), + payload_sig: vec![4u8; 65], + }, + )), + }; + let result = + info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + assert_matched_account_type(&result, AccountTypeToCheck::ProviderOperatorKeys); +} + +// --------------------------------------------------------------------------- +// Recorded shape of a keys-account `TransactionRecord` +// --------------------------------------------------------------------------- + +/// Locks in the post-PR contract: a keys-account record is a thin marker. +/// `direction = Internal`, `input_details` and `output_details` are empty +/// (the funding-side Standard / CoinJoin account's record carries those). +#[tokio::test] +async fn keys_account_record_is_thin_marker_internal_with_no_details() { + use crate::managed_account::transaction_record::TransactionDirection; + + let (mut wallet, mut info) = make_wallet(); + let xpub = + wallet.accounts.identity_registration.as_ref().expect("default options").account_xpub; + let address = info + .identity_registration_managed_account_mut() + .expect("identity_registration managed") + .next_address(Some(&xpub), true) + .expect("derive address"); + + let tx = asset_lock_to(address.script_pubkey(), ASSET_LOCK_VALUE); + let txid = tx.txid(); + let _ = info.check_core_transaction(&tx, test_block_context(), &mut wallet, true, true).await; + + let stored = info + .accounts() + .identity_registration + .as_ref() + .expect("identity_registration present") + .transactions() + .get(&txid) + .expect("record inserted") + .clone(); + + assert_eq!( + stored.direction, + TransactionDirection::Internal, + "keys-account flows are internal: funded from a Standard/CoinJoin account in the same wallet", + ); + assert!( + stored.input_details.is_empty(), + "keys-account record should not carry per-input details — those live on the funding account's record", + ); + assert!( + stored.output_details.is_empty(), + "keys-account record should not carry per-output classification — those live on the funding account's record", + ); +} diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index a015f683c..3ff50840c 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -444,7 +444,7 @@ impl ManagedAccountCollection { .into_iter() .collect(), AccountTypeToCheck::IdentityTopUp => { - Self::check_indexed_accounts(&self.identity_topup, tx) + Self::check_indexed_keys_accounts(&self.identity_topup, tx) } AccountTypeToCheck::IdentityTopUpNotBound => self .identity_topup_not_bound @@ -519,7 +519,7 @@ impl ManagedAccountCollection { } } - /// Check indexed accounts (BTreeMap of accounts) + /// Check indexed funds-bearing accounts. fn check_indexed_accounts( accounts: &BTreeMap, tx: &Transaction, @@ -532,6 +532,20 @@ impl ManagedAccountCollection { } matches } + + /// Check indexed keys-only accounts. + fn check_indexed_keys_accounts( + accounts: &BTreeMap, + tx: &Transaction, + ) -> Vec { + let mut matches = Vec::new(); + for (index, account) in accounts { + if let Some(match_info) = account.check_transaction_for_match(tx, Some(*index)) { + matches.push(match_info); + } + } + matches + } } impl ManagedCoreFundsAccount { @@ -1158,6 +1172,376 @@ impl ManagedCoreFundsAccount { } } +impl crate::managed_account::ManagedCoreKeysAccount { + /// Check if a script pubkey is a provider payout that belongs to this account. + fn check_provider_payout(&self, script_pubkey: &ScriptBuf) -> Option { + if self.contains_script_pub_key(script_pubkey) { + if let Ok(address) = Address::from_script(script_pubkey, self.network()) { + return self.get_address_info(&address); + } + } + None + } + + /// Check a single keys account for transaction involvement. + /// + /// Mirrors [`ManagedCoreFundsAccount::check_transaction_for_match`] but + /// skips UTXO-based input matching (`sent` is always 0 for keys accounts). + pub fn check_transaction_for_match( + &self, + tx: &Transaction, + index: Option, + ) -> Option { + let mut involved_other_addresses = Vec::new(); + let mut received = 0u64; + let mut provider_payout_involved = false; + + // Provider payouts in special transactions + if let Some(payload) = &tx.special_transaction_payload { + let script_payout = match payload { + TransactionPayload::ProviderRegistrationPayloadType(reg) => { + Some(®.script_payout) + } + TransactionPayload::ProviderUpdateRegistrarPayloadType(update) => { + Some(&update.script_payout) + } + TransactionPayload::ProviderUpdateServicePayloadType(update) => { + Some(&update.script_payout) + } + _ => None, + }; + + if let Some(payout_script) = script_payout { + if let Some(payout_info) = self.check_provider_payout(payout_script) { + provider_payout_involved = true; + if Address::from_script(payout_script, self.network()).is_ok() { + involved_other_addresses.push(payout_info); + } + } + } + } + + // Outputs (received) — keys accounts never own change addresses, + // so every match goes into `involved_other_addresses`. + 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 Some(address_info) = self.get_address_info(&address) { + involved_other_addresses.push(address_info); + } + } + received += output.value; + } + } + + let has_addresses = !involved_other_addresses.is_empty() || provider_payout_involved; + if !has_addresses { + return None; + } + + let account_type_match = match self.managed_account_type() { + ManagedAccountType::IdentityRegistration { + .. + } => CoreAccountTypeMatch::IdentityRegistration { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::IdentityTopUp { + .. + } => CoreAccountTypeMatch::IdentityTopUp { + account_index: index.unwrap_or(0), + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::IdentityTopUpNotBoundToIdentity { + .. + } => CoreAccountTypeMatch::IdentityTopUpNotBound { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::IdentityInvitation { + .. + } => CoreAccountTypeMatch::IdentityInvitation { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::AssetLockAddressTopUp { + .. + } => CoreAccountTypeMatch::AssetLockAddressTopUp { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::AssetLockShieldedAddressTopUp { + .. + } => CoreAccountTypeMatch::AssetLockShieldedAddressTopUp { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::ProviderVotingKeys { + .. + } => CoreAccountTypeMatch::ProviderVotingKeys { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::ProviderOwnerKeys { + .. + } => CoreAccountTypeMatch::ProviderOwnerKeys { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::ProviderOperatorKeys { + .. + } => CoreAccountTypeMatch::ProviderOperatorKeys { + involved_addresses: involved_other_addresses, + }, + ManagedAccountType::ProviderPlatformKeys { + .. + } => CoreAccountTypeMatch::ProviderPlatformKeys { + involved_addresses: involved_other_addresses, + }, + // Funds-bearing variants are not expected on keys accounts. + _ => return None, + }; + + Some(AccountMatch { + account_type_match, + received, + sent: 0, + received_for_credit_conversion: 0, + }) + } + + /// Check AssetLock transaction credit_outputs for involvement of this + /// keys account. Mirrors the funds-account version verbatim — neither + /// implementation touches UTXO state. + pub fn check_asset_lock_transaction_for_match( + &self, + tx: &Transaction, + index: Option, + ) -> Option { + if let Some(TransactionPayload::AssetLockPayloadType(ref payload)) = + tx.special_transaction_payload + { + let mut involved_addresses = Vec::new(); + let mut received = 0u64; + + 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()) + { + if let Some(address_info) = self.get_address_info(&address) { + involved_addresses.push(address_info.clone()); + } + } + received += credit_output.value; + } + } + + if !involved_addresses.is_empty() { + let account_type_match = match self.managed_account_type() { + ManagedAccountType::IdentityRegistration { + .. + } => CoreAccountTypeMatch::IdentityRegistration { + involved_addresses, + }, + ManagedAccountType::IdentityTopUp { + .. + } => CoreAccountTypeMatch::IdentityTopUp { + account_index: index.unwrap_or(0), + involved_addresses, + }, + ManagedAccountType::IdentityTopUpNotBoundToIdentity { + .. + } => CoreAccountTypeMatch::IdentityTopUpNotBound { + involved_addresses, + }, + ManagedAccountType::IdentityInvitation { + .. + } => CoreAccountTypeMatch::IdentityInvitation { + involved_addresses, + }, + ManagedAccountType::AssetLockAddressTopUp { + .. + } => CoreAccountTypeMatch::AssetLockAddressTopUp { + involved_addresses, + }, + ManagedAccountType::AssetLockShieldedAddressTopUp { + .. + } => CoreAccountTypeMatch::AssetLockShieldedAddressTopUp { + involved_addresses, + }, + _ => return None, + }; + + return Some(AccountMatch { + account_type_match, + received: 0, + sent: 0, + received_for_credit_conversion: received, + }); + } + } + + None + } + + /// Check if the transaction contains a provider voting key from this account. + pub fn check_provider_voting_key_in_transaction_for_match( + &self, + tx: &Transaction, + ) -> Option { + if let ManagedAccountType::ProviderVotingKeys { + addresses, + } = self.managed_account_type() + { + if let Some(payload) = &tx.special_transaction_payload { + let voting_key_hash = match payload { + TransactionPayload::ProviderRegistrationPayloadType(reg) => { + ®.voting_key_hash + } + TransactionPayload::ProviderUpdateRegistrarPayloadType(update) => { + &update.voting_key_hash + } + _ => return None, + }; + + for (address, &addr_index) in &addresses.address_index { + if let Payload::PubkeyHash(addr_hash) = address.payload() { + if addr_hash == voting_key_hash { + if let Some(address_info) = addresses.addresses.get(&addr_index) { + return Some(AccountMatch { + account_type_match: CoreAccountTypeMatch::ProviderVotingKeys { + involved_addresses: vec![address_info.clone()], + }, + received: 0, + sent: 0, + received_for_credit_conversion: 0, + }); + } + } + } + } + } + } + None + } + + /// Check if the transaction contains a provider owner key from this account. + pub fn check_provider_owner_key_in_transaction_for_match( + &self, + tx: &Transaction, + ) -> Option { + if let ManagedAccountType::ProviderOwnerKeys { + addresses, + } = self.managed_account_type() + { + if let Some(payload) = &tx.special_transaction_payload { + let owner_key_hash = match payload { + TransactionPayload::ProviderRegistrationPayloadType(reg) => ®.owner_key_hash, + _ => return None, + }; + + for (address, &addr_index) in &addresses.address_index { + if let Payload::PubkeyHash(addr_hash) = address.payload() { + if addr_hash == owner_key_hash { + if let Some(address_info) = addresses.addresses.get(&addr_index) { + return Some(AccountMatch { + account_type_match: CoreAccountTypeMatch::ProviderOwnerKeys { + involved_addresses: vec![address_info.clone()], + }, + received: 0, + sent: 0, + received_for_credit_conversion: 0, + }); + } + } + } + } + } + } + None + } + + /// Check if the transaction contains a provider operator key from this account. + pub fn check_provider_operator_key_in_transaction_for_match( + &self, + tx: &Transaction, + ) -> Option { + if let ManagedAccountType::ProviderOperatorKeys { + addresses, + } = self.managed_account_type() + { + #[cfg(feature = "bls")] + if let Some(payload) = &tx.special_transaction_payload { + let operator_public_key = match payload { + TransactionPayload::ProviderRegistrationPayloadType(reg) => { + ®.operator_public_key + } + TransactionPayload::ProviderUpdateRegistrarPayloadType(reg) => { + ®.operator_public_key + } + _ => return None, + }; + + for address_info in addresses.addresses.values() { + if let Some(PublicKeyType::BLS(bls_key)) = &address_info.public_key { + let operator_key_bytes: &[u8; 48] = operator_public_key.as_ref(); + if bls_key.len() == 48 && bls_key.as_slice() == operator_key_bytes { + return Some(AccountMatch { + account_type_match: CoreAccountTypeMatch::ProviderOperatorKeys { + involved_addresses: vec![address_info.clone()], + }, + received: 0, + sent: 0, + received_for_credit_conversion: 0, + }); + } + } + } + } + #[cfg(not(feature = "bls"))] + let _ = (tx, addresses); + } + None + } + + /// Check if the transaction contains a provider platform key from this account. + pub fn check_provider_platform_key_in_transaction_for_match( + &self, + tx: &Transaction, + ) -> Option { + if let ManagedAccountType::ProviderPlatformKeys { + addresses, + } = self.managed_account_type() + { + if let Some(payload) = &tx.special_transaction_payload { + let platform_node_id = match payload { + TransactionPayload::ProviderRegistrationPayloadType(reg) => { + if let Some(platform_node_id) = ®.platform_node_id { + platform_node_id + } else { + return None; + } + } + _ => return None, + }; + + for (address, &addr_index) in &addresses.address_index { + if let Payload::PubkeyHash(addr_hash) = address.payload() { + if addr_hash == platform_node_id { + if let Some(address_info) = addresses.addresses.get(&addr_index) { + return Some(AccountMatch { + account_type_match: + CoreAccountTypeMatch::ProviderPlatformKeys { + involved_addresses: vec![address_info.clone()], + }, + received: 0, + sent: 0, + received_for_credit_conversion: 0, + }); + } + } + } + } + } + } + None + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index d29844a9b..734fe1566 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; +#[cfg(test)] 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; @@ -111,7 +112,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { // before marking UTXOs so the freshly registered UTXOs get the // IS-lock flag too. for account_match in result.affected_accounts.clone() { - let Some(account) = self + let Some(mut account) = self .accounts .get_by_account_type_match_mut(&account_match.account_type_match) else { @@ -148,7 +149,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { // Process each affected account for account_match in result.affected_accounts.clone() { - let Some(account) = + let Some(mut account) = self.accounts.get_by_account_type_match_mut(&account_match.account_type_match) else { continue; 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 18e121188..52acc008b 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 @@ -12,7 +12,6 @@ use std::collections::HashMap; use std::fmt; use crate::managed_account::managed_account_trait::ManagedAccountTrait; -use crate::managed_account::ManagedCoreFundsAccount; use crate::signer::{Signer, SignerMethod}; use crate::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use crate::wallet::managed_wallet_info::fee::FeeRate; @@ -138,11 +137,15 @@ impl From for AssetLockError { } /// Resolve a funding key account from the managed account collection. +/// +/// Funding-key accounts (identity / asset-lock) are stored as +/// [`ManagedCoreKeysAccount`] in the collection — they derive keys for asset +/// locks but don't track per-account funds. fn resolve_funding_account( accounts: &mut crate::account::ManagedAccountCollection, funding_type: AssetLockFundingType, identity_index: u32, -) -> Result<&mut ManagedCoreFundsAccount, AssetLockError> { +) -> Result<&mut crate::managed_account::ManagedCoreKeysAccount, AssetLockError> { match funding_type { AssetLockFundingType::IdentityRegistration => accounts .identity_registration diff --git a/key-wallet/src/wallet/managed_wallet_info/helpers.rs b/key-wallet/src/wallet/managed_wallet_info/helpers.rs index 4def07ec7..5237f1c5f 100644 --- a/key-wallet/src/wallet/managed_wallet_info/helpers.rs +++ b/key-wallet/src/wallet/managed_wallet_info/helpers.rs @@ -4,6 +4,7 @@ use super::ManagedWalletInfo; use crate::account::account_collection::PlatformPaymentAccountKey; use crate::account::ManagedCoreFundsAccount; use crate::managed_account::managed_platform_account::ManagedPlatformAccount; +use crate::managed_account::ManagedCoreKeysAccount; impl ManagedWalletInfo { // BIP44 Account Helpers @@ -87,12 +88,12 @@ impl ManagedWalletInfo { // TopUp Account Helpers /// Get the first TopUp managed account - pub fn first_topup_managed_account(&self) -> Option<&ManagedCoreFundsAccount> { + pub fn first_topup_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { self.accounts.identity_topup.values().next() } /// Get the first TopUp managed account (mutable) - pub fn first_topup_managed_account_mut(&mut self) -> Option<&mut ManagedCoreFundsAccount> { + pub fn first_topup_managed_account_mut(&mut self) -> Option<&mut ManagedCoreKeysAccount> { self.accounts.identity_topup.values_mut().next() } @@ -100,7 +101,7 @@ impl ManagedWalletInfo { pub fn topup_managed_account_at_registration_index( &self, registration_index: u32, - ) -> Option<&ManagedCoreFundsAccount> { + ) -> Option<&ManagedCoreKeysAccount> { self.accounts.identity_topup.get(®istration_index) } @@ -108,105 +109,105 @@ impl ManagedWalletInfo { pub fn topup_managed_account_at_registration_index_mut( &mut self, registration_index: u32, - ) -> Option<&mut ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn identity_registration_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { self.accounts.identity_registration.as_ref() } /// Get the identity registration managed account (mutable) pub fn identity_registration_managed_account_mut( &mut self, - ) -> Option<&mut ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn identity_topup_not_bound_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { 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 ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn identity_invitation_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { self.accounts.identity_invitation.as_ref() } /// Get the identity invitation managed account (mutable) pub fn identity_invitation_managed_account_mut( &mut self, - ) -> Option<&mut ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn provider_voting_keys_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { 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 ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn provider_owner_keys_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { 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 ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn provider_operator_keys_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { 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 ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { 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<&ManagedCoreFundsAccount> { + pub fn provider_platform_keys_managed_account(&self) -> Option<&ManagedCoreKeysAccount> { 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 ManagedCoreFundsAccount> { + ) -> Option<&mut ManagedCoreKeysAccount> { self.accounts.provider_platform_keys.as_mut() } @@ -308,8 +309,8 @@ impl ManagedWalletInfo { self.accounts.all_accounts().len() } - /// Get all accounts - pub fn all_managed_accounts(&self) -> Vec<&ManagedCoreFundsAccount> { + /// Get all accounts (mixed funds and keys variants). + pub fn all_managed_accounts(&self) -> Vec> { 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 4a6ab17a9..c4b097ef7 100644 --- a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs +++ b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs @@ -11,8 +11,30 @@ 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::managed_account::{ManagedCoreKeysAccount, OwnedManagedCoreAccount}; use crate::wallet::{Wallet, WalletType}; +/// Wrap an [`Account`] as the right [`OwnedManagedCoreAccount`] variant for +/// its [`AccountType`] — funds-bearing for Standard / CoinJoin / DashPay, +/// keys-only for identity / asset-lock / provider variants. +fn owned_from_account(account: &Account) -> OwnedManagedCoreAccount { + match account.account_type { + AccountType::Standard { + .. + } + | AccountType::CoinJoin { + .. + } + | AccountType::DashpayReceivingFunds { + .. + } + | AccountType::DashpayExternalAccount { + .. + } => ManagedCoreFundsAccount::from_account(account).into(), + _ => ManagedCoreKeysAccount::from_account(account).into(), + } +} + impl ManagedAccountOperations for ManagedWalletInfo { /// Add a new managed account from an existing wallet account /// @@ -42,11 +64,11 @@ impl ManagedAccountOperations for ManagedWalletInfo { )) })?; - // Create the ManagedAccount from the Account - let managed_account = ManagedCoreFundsAccount::from_account(account); + // Wrap as the right managed-account variant for the account type. + let managed_account = owned_from_account(account); // Check if managed account already exists - if self.accounts.contains_managed_account_type(managed_account.managed_type()) { + if self.accounts.contains_managed_account_type(managed_account.managed_account_type()) { return Err(Error::InvalidParameter(format!( "Managed account type {:?} already exists for network {:?}", account_type, self.network @@ -117,11 +139,11 @@ impl ManagedAccountOperations for ManagedWalletInfo { // Create an Account with no wallet ID (standalone managed account) let account = Account::new(None, account_type, account_xpub, self.network)?; - // Create the ManagedAccount from the Account - let managed_account = ManagedCoreFundsAccount::from_account(&account); + // Wrap as the right managed-account variant for the account type. + let managed_account = owned_from_account(&account); // Check if managed account already exists - if self.accounts.contains_managed_account_type(managed_account.managed_type()) { + if self.accounts.contains_managed_account_type(managed_account.managed_account_type()) { return Err(Error::InvalidParameter(format!( "Managed account type {:?} already exists for network {:?}", account_type, self.network @@ -162,8 +184,8 @@ impl ManagedAccountOperations for ManagedWalletInfo { )) })?; - // Create the ManagedAccount from the BLS Account - let managed_account = ManagedCoreFundsAccount::from_bls_account(bls_account); + // ProviderOperatorKeys is always a keys-only variant. + let managed_account = ManagedCoreKeysAccount::from_bls_account(bls_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -234,8 +256,8 @@ impl ManagedAccountOperations for ManagedWalletInfo { let bls_account = BLSAccount::from_public_key_bytes(None, account_type, bls_public_key, self.network)?; - // Create the ManagedAccount from the BLS Account - let managed_account = ManagedCoreFundsAccount::from_bls_account(&bls_account); + // ProviderOperatorKeys is always a keys-only variant. + let managed_account = ManagedCoreKeysAccount::from_bls_account(&bls_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -280,8 +302,8 @@ impl ManagedAccountOperations for ManagedWalletInfo { )) })?; - // Create the ManagedAccount from the EdDSA Account - let managed_account = ManagedCoreFundsAccount::from_eddsa_account(eddsa_account); + // ProviderPlatformKeys is always a keys-only variant. + let managed_account = ManagedCoreKeysAccount::from_eddsa_account(eddsa_account); // Check if managed account already exists if self.accounts.contains_managed_account_type(managed_account.managed_type()) { @@ -357,7 +379,8 @@ impl ManagedAccountOperations for ManagedWalletInfo { )?; // Create the ManagedAccount from the EdDSA Account - let managed_account = ManagedCoreFundsAccount::from_eddsa_account(&eddsa_account); + // ProviderPlatformKeys is always a keys-only variant. + let managed_account = ManagedCoreKeysAccount::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 bc53e771a..b964edf28 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 @@ -71,11 +71,16 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount fn update_balance(&mut self); /// Per-account balances keyed by `AccountType`. + /// + /// Only funds-bearing accounts (Standard, CoinJoin, DashPay) carry a + /// balance — keys-only accounts (identity, asset-lock, provider) are + /// excluded from the result entirely rather than reported with a zero + /// balance. fn account_balances(&self) -> BTreeMap { self.accounts() - .all_accounts() + .all_funding_accounts() .iter() - .map(|acc| (acc.managed_account_type().to_account_type(), acc.balance)) + .map(|funds| (funds.managed_account_type().to_account_type(), funds.balance)) .collect() } @@ -188,7 +193,7 @@ impl WalletInfoInterface for ManagedWalletInfo { fn utxos(&self) -> BTreeSet<&Utxo> { let mut utxos = BTreeSet::new(); - for account in self.accounts.all_accounts() { + for account in self.accounts.all_funding_accounts() { utxos.extend(account.utxos.values()); } utxos @@ -205,11 +210,12 @@ impl WalletInfoInterface for ManagedWalletInfo { } fn update_balance(&mut self) { + // Only funds-bearing accounts contribute to the wallet balance. let mut balance = WalletCoreBalance::default(); 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; + for funds in self.accounts.all_funding_accounts_mut() { + funds.update_balance(last_processed_height); + balance += funds.balance; } self.balance = balance; } @@ -231,10 +237,9 @@ impl WalletInfoInterface for ManagedWalletInfo { } fn immature_transactions(&self) -> Vec { + // Coinbase UTXOs only live on funds-bearing accounts. let mut immature_txids: BTreeSet = BTreeSet::new(); - - // Find txids of immature coinbase UTXOs - for account in self.accounts.all_accounts() { + for account in self.accounts.all_funding_accounts() { for utxo in account.utxos.values() { if utxo.is_coinbase && !utxo.is_mature(self.last_processed_height()) { immature_txids.insert(utxo.outpoint.txid); @@ -242,9 +247,9 @@ impl WalletInfoInterface for ManagedWalletInfo { } } - // Get the actual transactions + // Look up the matching transaction records on the same funds accounts. let mut transactions = Vec::new(); - for account in self.accounts.all_accounts() { + for account in self.accounts.all_funding_accounts() { for (txid, record) in account.transactions() { if immature_txids.contains(txid) { transactions.push(record.transaction.clone()); @@ -272,8 +277,9 @@ impl WalletInfoInterface for ManagedWalletInfo { if new_height <= old_height { return Vec::new(); } + // Coinbase records only land on funds-bearing accounts. let mut matured = Vec::new(); - for account in self.accounts.all_accounts() { + for account in self.accounts.all_funding_accounts() { for record in account.transactions().values() { if !record.transaction.is_coin_base() { continue; @@ -295,7 +301,7 @@ impl WalletInfoInterface for ManagedWalletInfo { return false; } let mut any_changed = false; - for account in self.accounts.all_accounts_mut() { + for mut account in self.accounts.all_accounts_mut() { if account.mark_utxos_instant_send(txid) { any_changed = true; }