diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index ef48fcc06..607cc0a9d 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -14,7 +14,7 @@ use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoIn use secp256k1::{Message, Secp256k1, SecretKey}; use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{FFINetwork, FFITransactionContext, FFIWallet}; +use crate::types::{block_info_from_ffi, FFINetwork, FFITransactionContext, FFIWallet}; use crate::FFIWalletManager; // MARK: - Transaction Types @@ -423,44 +423,12 @@ pub unsafe extern "C" fn wallet_check_transaction( let context = match context_type { FFITransactionContext::Mempool => TransactionContext::Mempool, FFITransactionContext::InBlock => { - let block_hash = if !block_hash.is_null() { - use dashcore::hashes::Hash; - let hash_bytes = slice::from_raw_parts(block_hash, 32); - let mut hash_array = [0u8; 32]; - hash_array.copy_from_slice(hash_bytes); - Some(dashcore::BlockHash::from_byte_array(hash_array)) - } else { - None - }; - TransactionContext::InBlock { - height: block_height, - block_hash, - timestamp: if timestamp > 0 { - Some(timestamp as u32) - } else { - None - }, - } + let info = block_info_from_ffi(block_height, block_hash, timestamp); + TransactionContext::InBlock(info) } FFITransactionContext::InChainLockedBlock => { - let block_hash = if !block_hash.is_null() { - use dashcore::hashes::Hash; - let hash_bytes = slice::from_raw_parts(block_hash, 32); - let mut hash_array = [0u8; 32]; - hash_array.copy_from_slice(hash_bytes); - Some(dashcore::BlockHash::from_byte_array(hash_array)) - } else { - None - }; - TransactionContext::InChainLockedBlock { - height: block_height, - block_hash, - timestamp: if timestamp > 0 { - Some(timestamp as u32) - } else { - None - }, - } + let info = block_info_from_ffi(block_height, block_hash, timestamp); + TransactionContext::InChainLockedBlock(info) } FFITransactionContext::InstantSend => TransactionContext::InstantSend, }; diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index c25890a8a..17c727199 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -10,7 +10,7 @@ use std::slice; use crate::error::{FFIError, FFIErrorCode}; use crate::managed_wallet::{managed_wallet_info_free, FFIManagedWalletInfo}; -use crate::types::{FFITransactionContext, FFIWallet}; +use crate::types::{block_info_from_ffi, FFITransactionContext, FFIWallet}; use dashcore::consensus::Decodable; use dashcore::Transaction; use key_wallet::transaction_checking::{ @@ -144,44 +144,12 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( let context = match context_type { FFITransactionContext::Mempool => TransactionContext::Mempool, FFITransactionContext::InBlock => { - let block_hash = if !block_hash.is_null() { - use dashcore::hashes::Hash; - let hash_bytes = slice::from_raw_parts(block_hash, 32); - let mut hash_array = [0u8; 32]; - hash_array.copy_from_slice(hash_bytes); - Some(dashcore::BlockHash::from_byte_array(hash_array)) - } else { - None - }; - TransactionContext::InBlock { - height: block_height, - block_hash, - timestamp: if timestamp > 0 { - Some(timestamp as u32) - } else { - None - }, - } + let info = block_info_from_ffi(block_height, block_hash, timestamp); + TransactionContext::InBlock(info) } FFITransactionContext::InChainLockedBlock => { - let block_hash = if !block_hash.is_null() { - use dashcore::hashes::Hash; - let hash_bytes = slice::from_raw_parts(block_hash, 32); - let mut hash_array = [0u8; 32]; - hash_array.copy_from_slice(hash_bytes); - Some(dashcore::BlockHash::from_byte_array(hash_array)) - } else { - None - }; - TransactionContext::InChainLockedBlock { - height: block_height, - block_hash, - timestamp: if timestamp > 0 { - Some(timestamp as u32) - } else { - None - }, - } + let info = block_info_from_ffi(block_height, block_hash, timestamp); + TransactionContext::InChainLockedBlock(info) } FFITransactionContext::InstantSend => TransactionContext::InstantSend, }; diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index cdaff1730..37b16348f 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -1,10 +1,32 @@ //! Common types for FFI interface -use key_wallet::transaction_checking::TransactionContext; +use dashcore::hashes::Hash; +use key_wallet::transaction_checking::{BlockInfo, TransactionContext}; use key_wallet::{Network, Wallet}; use std::os::raw::{c_char, c_uint}; use std::sync::Arc; +/// Convert FFI block parameters to a `BlockInfo`. +/// +/// # Safety +/// +/// If `block_hash` is non-null it must point to 32 readable bytes. +pub(crate) unsafe fn block_info_from_ffi( + height: u32, + block_hash: *const u8, + timestamp: u64, +) -> BlockInfo { + let block_hash = if !block_hash.is_null() { + let hash_bytes = std::slice::from_raw_parts(block_hash, 32); + let mut arr = [0u8; 32]; + arr.copy_from_slice(hash_bytes); + dashcore::BlockHash::from_byte_array(arr) + } else { + dashcore::BlockHash::all_zeros() + }; + BlockInfo::new(height, block_hash, timestamp as u32) +} + /// FFI Network type (single network) #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -726,12 +748,8 @@ impl From for FFITransactionContext { match ctx { TransactionContext::Mempool => FFITransactionContext::Mempool, TransactionContext::InstantSend => FFITransactionContext::InstantSend, - TransactionContext::InBlock { - .. - } => FFITransactionContext::InBlock, - TransactionContext::InChainLockedBlock { - .. - } => FFITransactionContext::InChainLockedBlock, + TransactionContext::InBlock(_) => FFITransactionContext::InBlock, + TransactionContext::InChainLockedBlock(_) => FFITransactionContext::InChainLockedBlock, } } } @@ -786,50 +804,16 @@ impl FFITransactionContextDetails { match self.context_type { FFITransactionContext::Mempool => TransactionContext::Mempool, FFITransactionContext::InBlock => { - let block_hash = if self.block_hash.is_null() { - None - } else { - // Convert the 32-byte hash to BlockHash - let mut hash_bytes = [0u8; 32]; - unsafe { - std::ptr::copy_nonoverlapping(self.block_hash, hash_bytes.as_mut_ptr(), 32); - } - use dashcore::hashes::Hash; - Some(dashcore::BlockHash::from_byte_array(hash_bytes)) + let info = unsafe { + block_info_from_ffi(self.height, self.block_hash, self.timestamp as u64) }; - - TransactionContext::InBlock { - height: self.height, - block_hash, - timestamp: if self.timestamp == 0 { - None - } else { - Some(self.timestamp) - }, - } + TransactionContext::InBlock(info) } FFITransactionContext::InChainLockedBlock => { - let block_hash = if self.block_hash.is_null() { - None - } else { - // Convert the 32-byte hash to BlockHash - let mut hash_bytes = [0u8; 32]; - unsafe { - std::ptr::copy_nonoverlapping(self.block_hash, hash_bytes.as_mut_ptr(), 32); - } - use dashcore::hashes::Hash; - Some(dashcore::BlockHash::from_byte_array(hash_bytes)) + let info = unsafe { + block_info_from_ffi(self.height, self.block_hash, self.timestamp as u64) }; - - TransactionContext::InChainLockedBlock { - height: self.height, - block_hash, - timestamp: if self.timestamp == 0 { - None - } else { - Some(self.timestamp) - }, - } + TransactionContext::InChainLockedBlock(info) } FFITransactionContext::InstantSend => TransactionContext::InstantSend, } diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 82ba2b803..7f605e280 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -348,7 +348,7 @@ impl ManagedCoreAccount { outpoint, txout, addr, - context.block_height().unwrap_or(0), + context.block_info().map(|i| i.height).unwrap_or(0), tx.is_coin_base(), ); utxo.is_confirmed = context.confirmed(); @@ -392,13 +392,11 @@ impl ManagedCoreAccount { let mut changed = false; if let Some(tx_record) = self.transactions.get_mut(&tx.txid()) { if !tx_record.is_confirmed() { - if let (Some(height), Some(hash)) = (context.block_height(), context.block_hash()) { - tx_record.mark_confirmed(height, hash); + if let Some(info) = context.block_info() { + tx_record.mark_confirmed(info.height, info.block_hash); + tx_record.timestamp = info.timestamp as u64; changed = true; } - if let Some(ts) = context.timestamp() { - tx_record.timestamp = ts as u64; - } } } self.update_utxos(tx, account_match, context); @@ -413,12 +411,13 @@ impl ManagedCoreAccount { context: TransactionContext, ) { let net_amount = account_match.received as i64 - account_match.sent as i64; + let block_info = context.block_info(); let tx_record = TransactionRecord { transaction: tx.clone(), txid: tx.txid(), - height: context.block_height(), - block_hash: context.block_hash(), - timestamp: context.timestamp().unwrap_or(0) as u64, + height: block_info.map(|i| i.height), + block_hash: block_info.map(|i| i.block_hash), + timestamp: block_info.map(|i| i.timestamp as u64).unwrap_or(0), net_amount, fee: None, label: None, diff --git a/key-wallet/src/manager/process_block.rs b/key-wallet/src/manager/process_block.rs index 0fb66f9f9..3bc94b09b 100644 --- a/key-wallet/src/manager/process_block.rs +++ b/key-wallet/src/manager/process_block.rs @@ -1,7 +1,7 @@ use crate::manager::wallet_interface::{BlockProcessingResult, WalletInterface}; use crate::manager::{WalletEvent, WalletManager}; use crate::transaction_checking::transaction_router::TransactionRouter; -use crate::transaction_checking::TransactionContext; +use crate::transaction_checking::{BlockInfo, TransactionContext}; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use alloc::string::String; use alloc::vec::Vec; @@ -19,16 +19,11 @@ impl WalletInterface for WalletM height: CoreBlockHeight, ) -> BlockProcessingResult { let mut result = BlockProcessingResult::default(); - let block_hash = Some(block.block_hash()); - let timestamp = block.header.time; + let info = BlockInfo::new(height, block.block_hash(), block.header.time); // Process each transaction using the base manager for tx in &block.txdata { - let context = TransactionContext::InBlock { - height, - block_hash, - timestamp: Some(timestamp), - }; + let context = TransactionContext::InBlock(info); let check_result = self.check_transaction_in_all_wallets(tx, context, true, false).await; diff --git a/key-wallet/src/transaction_checking/mod.rs b/key-wallet/src/transaction_checking/mod.rs index 802c24b31..964fc3656 100644 --- a/key-wallet/src/transaction_checking/mod.rs +++ b/key-wallet/src/transaction_checking/mod.rs @@ -6,10 +6,12 @@ pub mod account_checker; pub mod platform_checker; +pub mod transaction_context; pub mod transaction_router; pub mod wallet_checker; pub use account_checker::{AccountMatch, AddressClassification, TransactionCheckResult}; pub use platform_checker::WalletPlatformChecker; +pub use transaction_context::{BlockInfo, TransactionContext}; pub use transaction_router::{PlatformAccountConversionError, TransactionRouter, TransactionType}; -pub use wallet_checker::{TransactionContext, WalletTransactionChecker}; +pub use wallet_checker::WalletTransactionChecker; diff --git a/key-wallet/src/transaction_checking/transaction_context.rs b/key-wallet/src/transaction_checking/transaction_context.rs new file mode 100644 index 000000000..f80b106b3 --- /dev/null +++ b/key-wallet/src/transaction_checking/transaction_context.rs @@ -0,0 +1,78 @@ +//! Block metadata and transaction context types. + +use dashcore::prelude::CoreBlockHeight; +use dashcore::BlockHash; + +/// Block metadata attached to confirmed transactions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BlockInfo { + pub(crate) height: CoreBlockHeight, + pub(crate) block_hash: BlockHash, + pub(crate) timestamp: u32, +} + +impl BlockInfo { + pub fn new(height: CoreBlockHeight, block_hash: BlockHash, timestamp: u32) -> Self { + Self { + height, + block_hash, + timestamp, + } + } + + pub fn height(&self) -> CoreBlockHeight { + self.height + } + + pub fn block_hash(&self) -> BlockHash { + self.block_hash + } + + pub fn timestamp(&self) -> u32 { + self.timestamp + } +} + +/// Context for transaction processing +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransactionContext { + /// Transaction is in the mempool (unconfirmed) + Mempool, + /// Transaction is in the mempool with an InstantSend lock + InstantSend, + /// Transaction is in a block at the given height + InBlock(BlockInfo), + /// Transaction is in a chain-locked block at the given height + InChainLockedBlock(BlockInfo), +} + +impl std::fmt::Display for TransactionContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransactionContext::Mempool => write!(f, "mempool"), + TransactionContext::InstantSend => write!(f, "instant send"), + TransactionContext::InBlock(info) => write!(f, "block {}", info.height), + TransactionContext::InChainLockedBlock(info) => { + write!(f, "chainlocked block {}", info.height) + } + } + } +} + +impl TransactionContext { + /// Returns the confirmation state. + pub(crate) fn confirmed(&self) -> bool { + matches!(self, TransactionContext::InChainLockedBlock(_) | TransactionContext::InBlock(_)) + } + + /// Returns the block info if confirmed. + pub(crate) fn block_info(&self) -> Option<&BlockInfo> { + match self { + TransactionContext::Mempool | TransactionContext::InstantSend => None, + TransactionContext::InBlock(info) | TransactionContext::InChainLockedBlock(info) => { + Some(info) + } + } + } +} diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs b/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs index 155b6b285..6371710b1 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs @@ -5,7 +5,7 @@ use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; -use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{BlockInfo, TransactionContext, WalletTransactionChecker}; use dashcore::blockdata::transaction::special_transaction::asset_unlock::qualified_asset_unlock::AssetUnlockPayload; use dashcore::blockdata::transaction::special_transaction::asset_unlock::request_info::AssetUnlockRequestInfo; use dashcore::blockdata::transaction::special_transaction::asset_unlock::unqualified_asset_unlock::AssetUnlockBasePayload; @@ -109,13 +109,11 @@ async fn test_asset_unlock_transaction_routing() { )), }; - let context = TransactionContext::InBlock { - height: 500100, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 500100, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -173,11 +171,11 @@ async fn test_asset_unlock_routing_to_bip32_account() { }; tx.special_transaction_payload = Some(TransactionPayload::AssetUnlockPayloadType(payload)); - let context = TransactionContext::InBlock { - height: 600100, - block_hash: Some(BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 600100, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs index 61c709015..57a69fcd4 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs @@ -5,7 +5,7 @@ use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; -use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{BlockInfo, TransactionContext, WalletTransactionChecker}; use dashcore::blockdata::transaction::special_transaction::coinbase::CoinbasePayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; use dashcore::bls_sig_utils::BLSSignature; @@ -56,14 +56,12 @@ async fn test_coinbase_transaction_routing_to_bip44_receive_address() { }; // Check the transaction using the wallet's managed info - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]) - .expect("Failed to create block hash for transaction context"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]) + .expect("Failed to create block hash for transaction context"), + 1234567890, + )); // Check the coinbase transaction let result = managed_wallet_info @@ -121,14 +119,12 @@ async fn test_coinbase_transaction_routing_to_bip44_change_address() { }; // Check the transaction using the wallet's managed info - let context = TransactionContext::InBlock { - height: 100001, - block_hash: Some( - BlockHash::from_slice(&[1u8; 32]) - .expect("Failed to create block hash for transaction context"), - ), - timestamp: Some(1234567900), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100001, + BlockHash::from_slice(&[1u8; 32]) + .expect("Failed to create block hash for transaction context"), + 1234567900, + )); // Check the coinbase transaction let result = managed_wallet_info @@ -185,13 +181,11 @@ async fn test_update_state_flag_behavior() { script_pubkey: address.script_pubkey(), }); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); // First check with update_state = false let result1 = @@ -321,11 +315,11 @@ async fn test_coinbase_transaction_with_payload_routing() { let tx_type = TransactionRouter::classify_transaction(&coinbase_tx); assert_eq!(tx_type, TransactionType::Coinbase); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some(BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash"), + 1234567890, + )); let result = managed_wallet_info .check_core_transaction(&coinbase_tx, context, &mut wallet, true, true) diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs index fb273ef6e..0ffb269ae 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs @@ -5,7 +5,7 @@ use crate::account::AccountType; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; -use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{BlockInfo, TransactionContext, WalletTransactionChecker}; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::{ManagedWalletInfo, Wallet}; use crate::Network; @@ -114,13 +114,11 @@ async fn test_identity_registration_account_routing() { )), }; - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); // First check without updating state let result = @@ -192,13 +190,11 @@ async fn test_normal_payment_to_identity_address_not_detected() { script_pubkey: address.script_pubkey(), }); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info .check_core_transaction( diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs index 54b431967..e257b8538 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs @@ -4,7 +4,7 @@ use super::helpers::*; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; -use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{BlockInfo, TransactionContext, WalletTransactionChecker}; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::{ManagedWalletInfo, Wallet}; use crate::Network; @@ -225,13 +225,11 @@ async fn test_provider_registration_transaction_routing_check_owner_only() { )), }; - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -362,13 +360,11 @@ async fn test_provider_registration_transaction_routing_check_voting_only() { )), }; - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -500,13 +496,11 @@ async fn test_provider_registration_transaction_routing_check_operator_only() { )), }; - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -705,13 +699,11 @@ async fn test_provider_registration_transaction_routing_check_platform_only() { )), }; - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -831,11 +823,11 @@ async fn test_provider_update_registrar_with_voting_and_operator() { tx.special_transaction_payload = Some(TransactionPayload::ProviderUpdateRegistrarPayloadType(payload)); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some(BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -926,11 +918,11 @@ async fn test_provider_revocation_classification_and_routing() { "Should NOT route to provider owner keys" ); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some(BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs index f27578df6..3dba41bee 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -8,7 +8,7 @@ use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; -use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{BlockInfo, TransactionContext, WalletTransactionChecker}; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::{ManagedWalletInfo, Wallet}; use crate::Network; @@ -45,13 +45,11 @@ async fn test_transaction_routing_to_bip44_account() { }); // Check the transaction using the wallet's managed info - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); // Check the transaction using the managed wallet info let result = managed_wallet_info @@ -115,13 +113,11 @@ async fn test_transaction_routing_to_bip32_account() { }); // Check the transaction using the managed wallet info - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); // Check with update_state = false let result = @@ -237,13 +233,11 @@ async fn test_transaction_routing_to_coinjoin_account() { script_pubkey: ScriptBuf::new(), }); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -340,13 +334,11 @@ async fn test_transaction_affects_multiple_accounts() { script_pubkey: address2.script_pubkey(), }); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), + 1234567890, + )); // Check the transaction let result = managed_wallet_info diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index b3124ef75..f02e9b5c4 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -4,106 +4,13 @@ //! if transactions belong to the wallet. pub(crate) use super::account_checker::TransactionCheckResult; +use super::transaction_context::TransactionContext; use super::transaction_router::TransactionRouter; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::managed_wallet_info::ManagedWalletInfo; use crate::{KeySource, Wallet}; use async_trait::async_trait; use dashcore::blockdata::transaction::Transaction; -use dashcore::prelude::CoreBlockHeight; -use dashcore::BlockHash; - -/// Context for transaction processing -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TransactionContext { - /// Transaction is in the mempool (unconfirmed) - Mempool, - /// Transaction is in the mempool with an InstantSend lock - InstantSend, - /// Transaction is in a block at the given height - InBlock { - height: u32, - block_hash: Option, - timestamp: Option, - }, - /// Transaction is in a chain-locked block at the given height - InChainLockedBlock { - height: u32, - block_hash: Option, - timestamp: Option, - }, -} - -impl std::fmt::Display for TransactionContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TransactionContext::Mempool => write!(f, "mempool"), - TransactionContext::InstantSend => write!(f, "instant send"), - TransactionContext::InBlock { - height, - .. - } => write!(f, "block {}", height), - TransactionContext::InChainLockedBlock { - height, - .. - } => { - write!(f, "chainlocked block {}", height) - } - } - } -} - -impl TransactionContext { - /// Returns the confirmation state. - pub(crate) fn confirmed(&self) -> bool { - matches!( - self, - TransactionContext::InChainLockedBlock { .. } | TransactionContext::InBlock { .. } - ) - } - /// Returns the block height if confirmed. - pub(crate) fn block_height(&self) -> Option { - match self { - TransactionContext::Mempool | TransactionContext::InstantSend => None, - TransactionContext::InBlock { - height, - .. - } - | TransactionContext::InChainLockedBlock { - height, - .. - } => Some(*height), - } - } - /// Returns the block hash if confirmed. - pub(crate) fn block_hash(&self) -> Option { - match self { - TransactionContext::Mempool | TransactionContext::InstantSend => None, - TransactionContext::InBlock { - block_hash, - .. - } - | TransactionContext::InChainLockedBlock { - block_hash, - .. - } => *block_hash, - } - } - /// Returns the block time if confirmed. - pub(crate) fn timestamp(&self) -> Option { - match self { - TransactionContext::Mempool | TransactionContext::InstantSend => None, - TransactionContext::InBlock { - timestamp, - .. - } - | TransactionContext::InChainLockedBlock { - timestamp, - .. - } => *timestamp, - } - } -} /// Extension trait for ManagedWalletInfo to add transaction checking capabilities #[async_trait] @@ -278,6 +185,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { mod tests { use super::*; use crate::test_utils::TestWalletContext; + use crate::transaction_checking::BlockInfo; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::{ManagedWalletInfo, Wallet}; @@ -383,13 +291,11 @@ mod tests { if let (Some(_xpub), Some(address)) = (bip32_xpub, bip32_address) { let tx = Transaction::dummy(&address, 0..1, &[50_000]); - let context = TransactionContext::InBlock { - height: 100000, - block_hash: Some( - BlockHash::from_slice(&[0u8; 32]).expect("Should create block hash"), - ), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100000, + BlockHash::from_slice(&[0u8; 32]).expect("Should create block hash"), + 1234567890, + )); // This should exercise BIP32 account branch in the update logic let result = @@ -420,13 +326,11 @@ mod tests { if let (Some(_xpub), Some(address)) = (coinjoin_xpub, coinjoin_address) { let tx = Transaction::dummy(&address, 0..1, &[75_000]); - let context = TransactionContext::InChainLockedBlock { - height: 100001, - block_hash: Some( - BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), - ), - timestamp: Some(1234567891), - }; + let context = TransactionContext::InChainLockedBlock(BlockInfo::new( + 100001, + BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), + 1234567891, + )); // This should exercise CoinJoin account branch in the update logic let result = @@ -471,11 +375,11 @@ mod tests { let block_height = 100000; // Test with InBlock context - let context = TransactionContext::InBlock { - height: block_height, - block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + block_height, + BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), + 1234567890, + )); let result = managed_wallet .check_core_transaction(&coinbase_tx, context, &mut wallet, true, true) @@ -527,11 +431,11 @@ mod tests { // Fund the wallet with a transaction paying to the receive address let funding_value = 50_000_000u64; let funding_tx = Transaction::dummy(&receive_address, 0..1, &[funding_value]); - let funding_context = TransactionContext::InBlock { - height: 1, - block_hash: Some(BlockHash::from_slice(&[2u8; 32]).expect("Should create block hash")), - timestamp: Some(1_650_000_000), - }; + let funding_context = TransactionContext::InBlock(BlockInfo::new( + 1, + BlockHash::from_slice(&[2u8; 32]).expect("Should create block hash"), + 1_650_000_000, + )); let funding_result = managed_wallet .check_core_transaction(&funding_tx, funding_context, &mut wallet, true, true) @@ -563,11 +467,11 @@ mod tests { special_transaction_payload: None, }; - let spend_context = TransactionContext::InBlock { - height: 2, - block_hash: Some(BlockHash::from_slice(&[3u8; 32]).expect("Should create block hash")), - timestamp: Some(1_650_000_100), - }; + let spend_context = TransactionContext::InBlock(BlockInfo::new( + 2, + BlockHash::from_slice(&[3u8; 32]).expect("Should create block hash"), + 1_650_000_100, + )); let spend_result = managed_wallet .check_core_transaction(&spend_tx, spend_context, &mut wallet, true, true) @@ -625,11 +529,11 @@ mod tests { let block_height = 100000; - let context = TransactionContext::InBlock { - height: block_height, - block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + block_height, + BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), + 1234567890, + )); // Process the coinbase transaction let result = managed_wallet @@ -740,11 +644,11 @@ mod tests { } = TestWalletContext::new_random(); let tx = Transaction::dummy(&address, 0..1, &[100_000]); - let context = TransactionContext::InBlock { - height: 100, - block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), - timestamp: Some(1234567890), - }; + let context = TransactionContext::InBlock(BlockInfo::new( + 100, + BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), + 1234567890, + )); // First processing - should be marked as new let result1 = @@ -847,11 +751,11 @@ mod tests { // Process spending tx FIRST (out of order) // This time it HAS an output to our wallet, so it should be stored - let spend_context = TransactionContext::InBlock { - height: 100, - block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), - timestamp: Some(1234567890), - }; + let spend_context = TransactionContext::InBlock(BlockInfo::new( + 100, + BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash"), + 1234567890, + )); let spend_result = managed_wallet .check_core_transaction(&spend_tx, spend_context, &mut wallet, true, true) @@ -876,11 +780,11 @@ mod tests { assert_eq!(account.utxos.len(), 1, "Should have one UTXO (change output)"); // Now process the funding tx (which was spent by spend_tx that we already stored) - let fund_context = TransactionContext::InBlock { - height: 99, - block_hash: Some(BlockHash::from_slice(&[2u8; 32]).expect("Should create block hash")), - timestamp: Some(1234567880), - }; + let fund_context = TransactionContext::InBlock(BlockInfo::new( + 99, + BlockHash::from_slice(&[2u8; 32]).expect("Should create block hash"), + 1234567880, + )); let fund_result = managed_wallet .check_core_transaction(&funding_tx, fund_context, &mut wallet, true, true) @@ -926,11 +830,8 @@ mod tests { // Same transaction now seen in a block let block_hash = BlockHash::from_slice(&[5u8; 32]).expect("Should create block hash"); - let block_context = TransactionContext::InBlock { - height: 500, - block_hash: Some(block_hash), - timestamp: Some(1700000000), - }; + let block_context = + TransactionContext::InBlock(BlockInfo::new(500, block_hash, 1700000000)); let result = ctx.check_transaction(&tx, block_context).await; assert!(result.is_relevant); @@ -980,11 +881,8 @@ mod tests { // Stage 3: block confirmation let block_hash = BlockHash::from_slice(&[10u8; 32]).expect("hash"); - let block_context = TransactionContext::InBlock { - height: 1000, - block_hash: Some(block_hash), - timestamp: Some(1700000000), - }; + let block_context = + TransactionContext::InBlock(BlockInfo::new(1000, block_hash, 1700000000)); let result = ctx.check_transaction(&tx, block_context).await; assert!(!result.is_new_transaction); assert!(ctx.transaction(&txid).is_confirmed()); @@ -993,11 +891,8 @@ mod tests { assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); // Stage 4: chain-locked block (rescan with stronger context) - let cl_context = TransactionContext::InChainLockedBlock { - height: 1000, - block_hash: Some(block_hash), - timestamp: Some(1700000000), - }; + let cl_context = + TransactionContext::InChainLockedBlock(BlockInfo::new(1000, block_hash, 1700000000)); let result = ctx.check_transaction(&tx, cl_context).await; assert!(!result.is_new_transaction); assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); @@ -1064,11 +959,8 @@ mod tests { // // Cleaner approach: test `confirm_transaction` directly on the account. let block_hash = BlockHash::from_slice(&[7u8; 32]).expect("hash"); - let block_context = TransactionContext::InBlock { - height: 800, - block_hash: Some(block_hash), - timestamp: Some(1700000000), - }; + let block_context = + TransactionContext::InBlock(BlockInfo::new(800, block_hash, 1700000000)); // Re-check the transaction: check_core_transaction will see no record in any // account, so it will treat it as new and call `record_transaction`. This still @@ -1110,11 +1002,8 @@ mod tests { // Call `confirm_transaction` directly — the backfill path should create the record let block_hash = BlockHash::from_slice(&[9u8; 32]).expect("hash"); - let block_context = TransactionContext::InBlock { - height: 600, - block_hash: Some(block_hash), - timestamp: Some(1700000000), - }; + let block_context = + TransactionContext::InBlock(BlockInfo::new(600, block_hash, 1700000000)); let changed = account.confirm_transaction(&tx, &account_match, block_context); assert!(changed, "Should return true when backfilling a missing record"); @@ -1158,11 +1047,8 @@ mod tests { let account_match = result.affected_accounts[0].clone(); let block_hash = BlockHash::from_slice(&[11u8; 32]).expect("hash"); - let block_context = TransactionContext::InBlock { - height: 700, - block_hash: Some(block_hash), - timestamp: Some(1700000000), - }; + let block_context = + TransactionContext::InBlock(BlockInfo::new(700, block_hash, 1700000000)); let account = ctx .managed_wallet