diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 6d8a16b73..40c09f365 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -852,7 +852,7 @@ Get the parent wallet ID of a managed account Note: ManagedAccount doesn't stor #### `managed_wallet_check_transaction` ```c -managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_height: c_uint, block_hash: *const u8, // 32 bytes if not null timestamp: u64, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool +managed_wallet_check_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_info: FFIBlockInfo, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool ``` **Description:** @@ -1300,7 +1300,7 @@ Build and sign a transaction using the wallet's managed info This is the recomm #### `wallet_check_transaction` ```c -wallet_check_transaction(wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_height: u32, block_hash: *const u8, // 32 bytes if not null timestamp: u64, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool +wallet_check_transaction(wallet: *mut FFIWallet, tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, block_info: FFIBlockInfo, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError,) -> bool ``` **Description:** diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index 9d5dd42e3..742b2985b 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -11,7 +11,7 @@ use dashcore::hashes::Hash; use crate::address_pool::{FFIAddressPool, FFIAddressPoolType}; use crate::error::{FFIError, FFIErrorCode}; -use crate::types::FFIAccountType; +use crate::types::{FFIAccountType, FFITransactionContextDetails}; use crate::wallet_manager::FFIWalletManager; use crate::FFINetwork; use key_wallet::account::account_collection::{DashpayAccountKey, PlatformPaymentAccountKey}; @@ -666,12 +666,8 @@ pub struct FFITransactionRecord { pub txid: [u8; 32], /// Net amount for this account (positive = received, negative = sent) pub net_amount: i64, - /// Block height if confirmed, 0 if unconfirmed - pub height: u32, - /// Block hash if confirmed (32 bytes), all zeros if unconfirmed - pub block_hash: [u8; 32], - /// Unix timestamp - pub timestamp: u64, + /// Transaction context (mempool, instant-send, in-block, chain-locked + block info) + pub context: FFITransactionContextDetails, /// Fee if known, 0 if unknown pub fee: u64, /// Whether this is our transaction @@ -729,18 +725,8 @@ pub unsafe extern "C" fn managed_core_account_get_transactions( // Copy net amount ffi_record.net_amount = record.net_amount; - // Copy height (0 if unconfirmed) - ffi_record.height = record.height.unwrap_or(0); - - // Copy block hash (zeros if unconfirmed) - if let Some(block_hash) = record.block_hash { - ffi_record.block_hash = block_hash.to_byte_array(); - } else { - ffi_record.block_hash = [0u8; 32]; - } - - // Copy timestamp - ffi_record.timestamp = record.timestamp; + // Copy transaction context + ffi_record.context = FFITransactionContextDetails::from(record.context); // Copy fee (0 if unknown) ffi_record.fee = record.fee.unwrap_or(0); diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 607cc0a9d..a3a84a379 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -14,7 +14,9 @@ use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoIn use secp256k1::{Message, Secp256k1, SecretKey}; use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{block_info_from_ffi, FFINetwork, FFITransactionContext, FFIWallet}; +use crate::types::{ + transaction_context_from_ffi, FFIBlockInfo, FFINetwork, FFITransactionContext, FFIWallet, +}; use crate::FFIWalletManager; // MARK: - Transaction Types @@ -388,9 +390,7 @@ pub unsafe extern "C" fn wallet_check_transaction( tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, - block_height: u32, - block_hash: *const u8, // 32 bytes if not null - timestamp: u64, + block_info: FFIBlockInfo, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError, @@ -419,18 +419,16 @@ pub unsafe extern "C" fn wallet_check_transaction( }; // Build the transaction context - use key_wallet::transaction_checking::TransactionContext; - let context = match context_type { - FFITransactionContext::Mempool => TransactionContext::Mempool, - FFITransactionContext::InBlock => { - let info = block_info_from_ffi(block_height, block_hash, timestamp); - TransactionContext::InBlock(info) - } - FFITransactionContext::InChainLockedBlock => { - let info = block_info_from_ffi(block_height, block_hash, timestamp); - TransactionContext::InChainLockedBlock(info) + let context = match transaction_context_from_ffi(context_type, &block_info) { + Some(ctx) => ctx, + None => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Block info must not be zeroed for confirmed contexts".to_string(), + ); + return false; } - FFITransactionContext::InstantSend => TransactionContext::InstantSend, }; // Create a ManagedWalletInfo from the wallet diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index 17c727199..b2e9ae36b 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -10,11 +10,11 @@ use std::slice; use crate::error::{FFIError, FFIErrorCode}; use crate::managed_wallet::{managed_wallet_info_free, FFIManagedWalletInfo}; -use crate::types::{block_info_from_ffi, FFITransactionContext, FFIWallet}; +use crate::types::{transaction_context_from_ffi, FFIBlockInfo, FFITransactionContext, FFIWallet}; use dashcore::consensus::Decodable; use dashcore::Transaction; use key_wallet::transaction_checking::{ - account_checker::CoreAccountTypeMatch, TransactionContext, WalletTransactionChecker, + account_checker::CoreAccountTypeMatch, WalletTransactionChecker, }; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; @@ -112,9 +112,7 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( tx_bytes: *const u8, tx_len: usize, context_type: FFITransactionContext, - block_height: c_uint, - block_hash: *const u8, // 32 bytes if not null - timestamp: u64, + block_info: FFIBlockInfo, update_state: bool, result_out: *mut FFITransactionCheckResult, error: *mut FFIError, @@ -141,17 +139,16 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( }; // Build the transaction context - let context = match context_type { - FFITransactionContext::Mempool => TransactionContext::Mempool, - FFITransactionContext::InBlock => { - let info = block_info_from_ffi(block_height, block_hash, timestamp); - TransactionContext::InBlock(info) - } - FFITransactionContext::InChainLockedBlock => { - let info = block_info_from_ffi(block_height, block_hash, timestamp); - TransactionContext::InChainLockedBlock(info) + let context = match transaction_context_from_ffi(context_type, &block_info) { + Some(ctx) => ctx, + None => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Block info must not be zeroed for confirmed contexts".to_string(), + ); + return false; } - FFITransactionContext::InstantSend => TransactionContext::InstantSend, }; // Check the transaction - wallet is now required diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index 37b16348f..3ddb6fd75 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -3,28 +3,72 @@ 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::os::raw::c_char; use std::sync::Arc; -/// Convert FFI block parameters to a `BlockInfo`. -/// -/// # Safety +/// FFI-compatible block metadata (height, hash, timestamp). +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FFIBlockInfo { + /// Block height + pub height: u32, + /// Block hash (32 bytes) + pub block_hash: [u8; 32], + /// Unix timestamp + pub timestamp: u32, +} + +impl FFIBlockInfo { + /// All-zeros placeholder used for unconfirmed contexts. + pub fn empty() -> Self { + Self { + height: 0, + block_hash: [0u8; 32], + timestamp: 0, + } + } + + /// Convert to native `BlockInfo`. + pub fn to_block_info(&self) -> BlockInfo { + let block_hash = dashcore::BlockHash::from_byte_array(self.block_hash); + BlockInfo::new(self.height, block_hash, self.timestamp) + } +} + +impl From for FFIBlockInfo { + fn from(info: BlockInfo) -> Self { + Self { + height: info.height(), + block_hash: info.block_hash().to_byte_array(), + timestamp: info.timestamp(), + } + } +} + +/// Convert an `FFIBlockInfo` and context type to a native `TransactionContext`. /// -/// 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) +/// Returns `None` when block info is all-zeros for confirmed contexts (`InBlock`, +/// `InChainLockedBlock`), indicating invalid input from the FFI caller. +pub(crate) fn transaction_context_from_ffi( + context_type: FFITransactionContext, + block_info: &FFIBlockInfo, +) -> Option { + match context_type { + FFITransactionContext::Mempool => Some(TransactionContext::Mempool), + FFITransactionContext::InstantSend => Some(TransactionContext::InstantSend), + FFITransactionContext::InBlock => { + if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 { + return None; + } + Some(TransactionContext::InBlock(block_info.to_block_info())) + } + FFITransactionContext::InChainLockedBlock => { + if block_info.block_hash == [0u8; 32] && block_info.timestamp == 0 { + return None; + } + Some(TransactionContext::InChainLockedBlock(block_info.to_block_info())) + } + } } /// FFI Network type (single network) @@ -760,62 +804,53 @@ impl From for FFITransactionContext { pub struct FFITransactionContextDetails { /// The context type pub context_type: FFITransactionContext, - /// Block height (0 for mempool) - pub height: c_uint, - /// Block hash (32 bytes, null for mempool or if unknown) - pub block_hash: *const u8, - /// Timestamp (0 if unknown) - pub timestamp: c_uint, + /// Block info (zeroed for mempool/instant-send contexts) + pub block_info: FFIBlockInfo, } impl FFITransactionContextDetails { /// Create a mempool context pub fn mempool() -> Self { - FFITransactionContextDetails { + Self { context_type: FFITransactionContext::Mempool, - height: 0, - block_hash: std::ptr::null(), - timestamp: 0, + block_info: FFIBlockInfo::empty(), } } /// Create an in-block context - pub fn in_block(height: c_uint, block_hash: *const u8, timestamp: c_uint) -> Self { - FFITransactionContextDetails { + pub fn in_block(block_info: FFIBlockInfo) -> Self { + Self { context_type: FFITransactionContext::InBlock, - height, - block_hash, - timestamp, + block_info, } } /// Create a chain-locked block context - pub fn in_chain_locked_block(height: c_uint, block_hash: *const u8, timestamp: c_uint) -> Self { - FFITransactionContextDetails { + pub fn in_chain_locked_block(block_info: FFIBlockInfo) -> Self { + Self { context_type: FFITransactionContext::InChainLockedBlock, - height, - block_hash, - timestamp, + block_info, } } - /// Convert to the native TransactionContext - pub fn to_transaction_context(&self) -> TransactionContext { - match self.context_type { - FFITransactionContext::Mempool => TransactionContext::Mempool, - FFITransactionContext::InBlock => { - let info = unsafe { - block_info_from_ffi(self.height, self.block_hash, self.timestamp as u64) - }; - TransactionContext::InBlock(info) - } - FFITransactionContext::InChainLockedBlock => { - let info = unsafe { - block_info_from_ffi(self.height, self.block_hash, self.timestamp as u64) - }; - TransactionContext::InChainLockedBlock(info) - } - FFITransactionContext::InstantSend => TransactionContext::InstantSend, + /// Convert to the native `TransactionContext`. + /// + /// Returns `None` when block info is all-zeros for confirmed contexts. + pub fn to_transaction_context(&self) -> Option { + transaction_context_from_ffi(self.context_type, &self.block_info) + } +} + +impl From for FFITransactionContextDetails { + fn from(ctx: TransactionContext) -> Self { + let context_type = FFITransactionContext::from(ctx); + let block_info = ctx + .block_info() + .map(|info| FFIBlockInfo::from(*info)) + .unwrap_or_else(FFIBlockInfo::empty); + Self { + context_type, + block_info, } } } diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs index ccb844f51..fa4c4102e 100644 --- a/key-wallet-ffi/src/wallet_manager.rs +++ b/key-wallet-ffi/src/wallet_manager.rs @@ -772,7 +772,17 @@ pub unsafe extern "C" fn wallet_manager_process_transaction( }; // Convert FFI context to native TransactionContext - let context = unsafe { (*context).to_transaction_context() }; + let context = match unsafe { (*context).to_transaction_context() } { + Some(ctx) => ctx, + None => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Block info must not be zeroed for confirmed contexts".to_string(), + ); + return false; + } + }; // Get the manager let manager_ref = unsafe { &mut *manager }; diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 6997c237f..06312105a 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -627,19 +627,14 @@ mod tests { ]; // Create transaction contexts for testing - let mempool_context = crate::types::FFITransactionContextDetails { - context_type: crate::types::FFITransactionContext::Mempool, - height: 0, - block_hash: ptr::null(), - timestamp: 0, - }; + let mempool_context = crate::types::FFITransactionContextDetails::mempool(); - let block_context = crate::types::FFITransactionContextDetails { - context_type: crate::types::FFITransactionContext::InBlock, - height: 100000, - block_hash: ptr::null(), - timestamp: 1234567890, - }; + let block_context = + crate::types::FFITransactionContextDetails::in_block(crate::types::FFIBlockInfo { + height: 100000, + block_hash: [0u8; 32], + timestamp: 1234567890, + }); // Test processing a mempool transaction let processed = unsafe { @@ -672,12 +667,14 @@ mod tests { assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); // Test processing a chain-locked block transaction - let chain_locked_context = crate::types::FFITransactionContextDetails { - context_type: crate::types::FFITransactionContext::InChainLockedBlock, - height: 100000, - block_hash: ptr::null(), - timestamp: 1234567890, - }; + let chain_locked_context = + crate::types::FFITransactionContextDetails::in_chain_locked_block( + crate::types::FFIBlockInfo { + height: 100000, + block_hash: [0u8; 32], + timestamp: 1234567890, + }, + ); let processed = unsafe { wallet_manager::wallet_manager_process_transaction( manager, diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 7f605e280..6fc968334 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -344,13 +344,9 @@ impl ManagedCoreAccount { value: output.value, script_pubkey: output.script_pubkey.clone(), }; - let mut utxo = Utxo::new( - outpoint, - txout, - addr, - context.block_info().map(|i| i.height).unwrap_or(0), - tx.is_coin_base(), - ); + let block_height = context.block_info().map_or(0, |info| info.height); + let mut utxo = + Utxo::new(outpoint, txout, addr, block_height, tx.is_coin_base()); utxo.is_confirmed = context.confirmed(); utxo.is_instantlocked = matches!(context, TransactionContext::InstantSend); @@ -391,12 +387,9 @@ 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(info) = context.block_info() { - tx_record.mark_confirmed(info.height, info.block_hash); - tx_record.timestamp = info.timestamp as u64; - changed = true; - } + if tx_record.context != context { + tx_record.update_context(context); + changed = true; } } self.update_utxos(tx, account_match, context); @@ -411,18 +404,7 @@ 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: 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, - is_ours: net_amount < 0, - }; + let tx_record = TransactionRecord::new(tx.clone(), context, net_amount, net_amount < 0); self.transactions.insert(tx.txid(), tx_record); diff --git a/key-wallet/src/managed_account/transaction_record.rs b/key-wallet/src/managed_account/transaction_record.rs index 3f51b6e01..f87df4d3a 100644 --- a/key-wallet/src/managed_account/transaction_record.rs +++ b/key-wallet/src/managed_account/transaction_record.rs @@ -3,9 +3,10 @@ //! This module contains the transaction record structure used to track //! transactions associated with accounts. +use crate::transaction_checking::{BlockInfo, TransactionContext}; use alloc::string::String; use dashcore::blockdata::transaction::Transaction; -use dashcore::{BlockHash, Txid}; +use dashcore::Txid; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -17,12 +18,8 @@ pub struct TransactionRecord { pub transaction: Transaction, /// Transaction ID pub txid: Txid, - /// Block height (if confirmed) - pub height: Option, - /// Block hash (if confirmed) - pub block_hash: Option, - /// Timestamp - pub timestamp: u64, + /// The context in which this transaction was last seen + pub context: TransactionContext, /// Net amount for this account pub net_amount: i64, /// Fee paid (if we created it) @@ -34,28 +31,10 @@ pub struct TransactionRecord { } impl TransactionRecord { - /// Create a new transaction record - pub fn new(transaction: Transaction, timestamp: u64, net_amount: i64, is_ours: bool) -> Self { - let txid = transaction.txid(); - Self { - transaction, - txid, - height: None, - block_hash: None, - timestamp, - net_amount, - fee: None, - label: None, - is_ours, - } - } - - /// Create a confirmed transaction record - pub fn new_confirmed( + /// Create a new transaction record with the given context + pub fn new( transaction: Transaction, - height: u32, - block_hash: BlockHash, - timestamp: u64, + context: TransactionContext, net_amount: i64, is_ours: bool, ) -> Self { @@ -63,9 +42,7 @@ impl TransactionRecord { Self { transaction, txid, - height: Some(height), - block_hash: Some(block_hash), - timestamp, + context, net_amount, fee: None, label: None, @@ -75,18 +52,15 @@ impl TransactionRecord { /// Calculate the number of confirmations based on current chain height pub fn confirmations(&self, current_height: u32) -> u32 { - match self.height { - Some(tx_height) if current_height >= tx_height => { - // Add 1 because the block itself counts as 1 confirmation - (current_height - tx_height) + 1 - } - _ => 0, // Unconfirmed or invalid height + match self.context.block_info() { + Some(info) if current_height >= info.height => (current_height - info.height) + 1, + _ => 0, } } /// Check if the transaction is confirmed (has at least 1 confirmation) pub fn is_confirmed(&self) -> bool { - self.height.is_some() + self.context.confirmed() } /// Check if the transaction has at least the specified number of confirmations @@ -94,6 +68,16 @@ impl TransactionRecord { self.confirmations(current_height) >= required } + /// Block info if confirmed + pub fn block_info(&self) -> Option<&BlockInfo> { + self.context.block_info() + } + + /// Block height if confirmed + pub fn height(&self) -> Option { + self.context.block_info().map(|info| info.height) + } + /// Set the fee for this transaction pub fn set_fee(&mut self, fee: u64) { self.fee = Some(fee); @@ -104,16 +88,9 @@ impl TransactionRecord { self.label = Some(label); } - /// Mark transaction as confirmed - pub fn mark_confirmed(&mut self, height: u32, block_hash: BlockHash) { - self.height = Some(height); - self.block_hash = Some(block_hash); - } - - /// Mark transaction as unconfirmed (e.g., due to reorg) - pub fn mark_unconfirmed(&mut self) { - self.height = None; - self.block_hash = None; + /// Update the transaction context + pub fn update_context(&mut self, context: TransactionContext) { + self.context = context; } /// Check if this is an incoming transaction (positive net amount) @@ -136,14 +113,18 @@ impl TransactionRecord { mod tests { use super::*; use dashcore::hashes::Hash; + use dashcore::BlockHash; + + fn test_block_context(height: u32) -> TransactionContext { + TransactionContext::InBlock(BlockInfo::new(height, BlockHash::all_zeros(), 1234567890)) + } #[test] fn test_transaction_record_creation() { let tx = Transaction::dummy_empty(); - let record = TransactionRecord::new(tx.clone(), 1234567890, 50000, true); + let record = TransactionRecord::new(tx.clone(), TransactionContext::Mempool, 50000, true); assert_eq!(record.txid, tx.txid()); - assert_eq!(record.timestamp, 1234567890); assert_eq!(record.net_amount, 50000); assert!(record.is_ours); assert!(!record.is_confirmed()); @@ -152,14 +133,14 @@ mod tests { #[test] fn test_confirmations_calculation() { let tx = Transaction::dummy_empty(); - let mut record = TransactionRecord::new(tx, 1234567890, 50000, true); + let mut record = TransactionRecord::new(tx, TransactionContext::Mempool, 50000, true); // Unconfirmed transaction assert_eq!(record.confirmations(100), 0); assert!(!record.is_confirmed()); - // Mark as confirmed at height 95 - record.mark_confirmed(95, BlockHash::all_zeros()); + // Confirm at height 95 + record.update_context(test_block_context(95)); assert!(record.is_confirmed()); // At height 100, should have 6 confirmations (100 - 95 + 1) @@ -178,12 +159,14 @@ mod tests { fn test_incoming_outgoing() { let tx = Transaction::dummy_empty(); - let incoming = TransactionRecord::new(tx.clone(), 1234567890, 50000, false); + let incoming = + TransactionRecord::new(tx.clone(), TransactionContext::Mempool, 50000, false); assert!(incoming.is_incoming()); assert!(!incoming.is_outgoing()); assert_eq!(incoming.amount(), 50000); - let outgoing = TransactionRecord::new(tx.clone(), 1234567890, -50000, true); + let outgoing = + TransactionRecord::new(tx.clone(), TransactionContext::Mempool, -50000, true); assert!(!outgoing.is_incoming()); assert!(outgoing.is_outgoing()); assert_eq!(outgoing.amount(), 50000); @@ -192,35 +175,29 @@ mod tests { #[test] fn test_confirmed_transaction_creation() { let tx = Transaction::dummy_empty(); - let block_hash = BlockHash::all_zeros(); - let record = - TransactionRecord::new_confirmed(tx.clone(), 100, block_hash, 1234567890, 50000, true); + let record = TransactionRecord::new(tx.clone(), test_block_context(100), 50000, true); - assert_eq!(record.height, Some(100)); - assert_eq!(record.block_hash, Some(block_hash)); + assert_eq!(record.height(), Some(100)); assert!(record.is_confirmed()); } #[test] - fn test_mark_unconfirmed() { + fn test_update_context_reorg() { let tx = Transaction::dummy_empty(); - let block_hash = BlockHash::all_zeros(); - let mut record = - TransactionRecord::new_confirmed(tx, 100, block_hash, 1234567890, 50000, true); + let mut record = TransactionRecord::new(tx, test_block_context(100), 50000, true); assert!(record.is_confirmed()); - // Simulate reorg - record.mark_unconfirmed(); + // Simulate reorg — back to mempool + record.update_context(TransactionContext::Mempool); assert!(!record.is_confirmed()); - assert_eq!(record.height, None); - assert_eq!(record.block_hash, None); + assert_eq!(record.block_info(), None); } #[test] fn test_labels_and_fees() { let tx = Transaction::dummy_empty(); - let mut record = TransactionRecord::new(tx, 1234567890, -50000, true); + let mut record = TransactionRecord::new(tx, TransactionContext::Mempool, -50000, true); assert_eq!(record.fee, None); assert_eq!(record.label, None); diff --git a/key-wallet/src/tests/spent_outpoints_tests.rs b/key-wallet/src/tests/spent_outpoints_tests.rs index a9e941b26..efd8409c4 100644 --- a/key-wallet/src/tests/spent_outpoints_tests.rs +++ b/key-wallet/src/tests/spent_outpoints_tests.rs @@ -5,6 +5,7 @@ use dashcore::{TxIn, Txid}; use crate::account::TransactionRecord; use crate::managed_account::ManagedCoreAccount; +use crate::transaction_checking::TransactionContext; /// Create a transaction that spends the given outpoints. fn spending_tx(spent: &[OutPoint]) -> Transaction { @@ -35,7 +36,7 @@ fn receive_only_tx() -> Transaction { } fn record_from_tx(tx: &Transaction) -> TransactionRecord { - TransactionRecord::new(tx.clone(), 0, 0, false) + TransactionRecord::new(tx.clone(), TransactionContext::Mempool, 0, false) } #[test] diff --git a/key-wallet/src/transaction_checking/transaction_context.rs b/key-wallet/src/transaction_checking/transaction_context.rs index f80b106b3..ba673d1ff 100644 --- a/key-wallet/src/transaction_checking/transaction_context.rs +++ b/key-wallet/src/transaction_checking/transaction_context.rs @@ -36,6 +36,7 @@ impl BlockInfo { /// Context for transaction processing #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TransactionContext { /// Transaction is in the mempool (unconfirmed) Mempool, @@ -67,7 +68,7 @@ impl TransactionContext { } /// Returns the block info if confirmed. - pub(crate) fn block_info(&self) -> Option<&BlockInfo> { + pub fn block_info(&self) -> Option<&BlockInfo> { match self { TransactionContext::Mempool | TransactionContext::InstantSend => None, TransactionContext::InBlock(info) | TransactionContext::InChainLockedBlock(info) => { diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/helpers.rs b/key-wallet/src/transaction_checking/transaction_router/tests/helpers.rs index 6cbc4b025..0c97453d4 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/helpers.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/helpers.rs @@ -1,9 +1,16 @@ //! Helper functions for transaction router tests +use crate::transaction_checking::BlockInfo; use dashcore::blockdata::transaction::special_transaction::asset_lock::AssetLockPayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; use dashcore::blockdata::transaction::Transaction; -use dashcore::{Address, Network, TxOut}; +use dashcore::hashes::Hash; +use dashcore::{Address, BlockHash, Network, TxOut}; + +/// Creates a `BlockInfo` with the given height and deterministic defaults. +pub fn test_block_info(height: u32) -> BlockInfo { + BlockInfo::new(height, BlockHash::all_zeros(), 1234567890) +} /// Returns a deterministic test address for creating dummy transactions. pub fn test_addr() -> Address { 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 0ffb269ae..adcbd8717 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::{BlockInfo, TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::{ManagedWalletInfo, Wallet}; use crate::Network; @@ -14,7 +14,7 @@ use dashcore::blockdata::transaction::special_transaction::asset_lock::AssetLock use dashcore::blockdata::transaction::special_transaction::TransactionPayload; use dashcore::blockdata::transaction::Transaction; use dashcore::hashes::Hash; -use dashcore::{BlockHash, OutPoint, TxIn, TxOut, Txid}; +use dashcore::{OutPoint, TxIn, TxOut, Txid}; #[test] fn test_identity_registration() { @@ -114,11 +114,7 @@ async fn test_identity_registration_account_routing() { )), }; - let context = TransactionContext::InBlock(BlockInfo::new( - 100000, - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - 1234567890, - )); + let context = TransactionContext::InBlock(test_block_info(100000)); // First check without updating state let result = @@ -190,11 +186,7 @@ async fn test_normal_payment_to_identity_address_not_detected() { script_pubkey: address.script_pubkey(), }); - let context = TransactionContext::InBlock(BlockInfo::new( - 100000, - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - 1234567890, - )); + let context = TransactionContext::InBlock(test_block_info(100000)); let result = managed_wallet_info .check_core_transaction( 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 3dba41bee..ac9aabda9 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -1,6 +1,6 @@ //! Tests for transaction routing logic -use super::helpers::test_addr; +use super::helpers::{test_addr, test_block_info}; use crate::account::{AccountType, StandardAccountType}; use crate::managed_account::address_pool::KeySource; use crate::managed_account::managed_account_type::ManagedAccountType; @@ -8,13 +8,12 @@ use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; -use crate::transaction_checking::{BlockInfo, TransactionContext, WalletTransactionChecker}; +use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::{ManagedWalletInfo, Wallet}; use crate::Network; use dashcore::blockdata::transaction::Transaction; -use dashcore::hashes::Hash; -use dashcore::{BlockHash, ScriptBuf, TxOut}; +use dashcore::{ScriptBuf, TxOut}; #[test] fn test_standard_transaction_routing() { @@ -45,11 +44,7 @@ async fn test_transaction_routing_to_bip44_account() { }); // Check the transaction using the wallet's managed info - let context = TransactionContext::InBlock(BlockInfo::new( - 100000, - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - 1234567890, - )); + let context = TransactionContext::InBlock(test_block_info(100000)); // Check the transaction using the managed wallet info let result = managed_wallet_info @@ -113,11 +108,7 @@ async fn test_transaction_routing_to_bip32_account() { }); // Check the transaction using the managed wallet info - let context = TransactionContext::InBlock(BlockInfo::new( - 100000, - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - 1234567890, - )); + let context = TransactionContext::InBlock(test_block_info(100000)); // Check with update_state = false let result = @@ -233,11 +224,7 @@ async fn test_transaction_routing_to_coinjoin_account() { script_pubkey: ScriptBuf::new(), }); - let context = TransactionContext::InBlock(BlockInfo::new( - 100000, - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - 1234567890, - )); + let context = TransactionContext::InBlock(test_block_info(100000)); let result = managed_wallet_info.check_core_transaction(&tx, context, &mut wallet, true, true).await; @@ -334,11 +321,7 @@ async fn test_transaction_affects_multiple_accounts() { script_pubkey: address2.script_pubkey(), }); - let context = TransactionContext::InBlock(BlockInfo::new( - 100000, - BlockHash::from_slice(&[0u8; 32]).expect("Failed to create block hash from bytes"), - 1234567890, - )); + let context = TransactionContext::InBlock(test_block_info(100000)); // 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 f02e9b5c4..dc5b6caa2 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -628,9 +628,11 @@ mod tests { let stored_tx = managed_account.transactions.get(&tx.txid()).expect("Should have stored transaction"); - assert_eq!(stored_tx.height, None, "Mempool transaction should have no height"); - assert_eq!(stored_tx.block_hash, None, "Mempool transaction should have no block hash"); - assert_eq!(stored_tx.timestamp, 0, "Mempool transaction should have timestamp 0"); + assert_eq!( + stored_tx.context, + TransactionContext::Mempool, + "Mempool transaction should have mempool context" + ); } /// Test that rescanning a block marks transactions as existing @@ -823,7 +825,7 @@ mod tests { // Verify unconfirmed state assert!(!ctx.transaction(&txid).is_confirmed(), "Mempool tx should be unconfirmed"); - assert_eq!(ctx.transaction(&txid).height, None); + assert_eq!(ctx.transaction(&txid).context, TransactionContext::Mempool); assert!(!ctx.first_utxo().is_confirmed, "Mempool UTXO should be unconfirmed"); let total_tx_before = ctx.managed_wallet.metadata.total_transactions; @@ -840,9 +842,9 @@ mod tests { // Verify confirmed state let record = ctx.transaction(&txid); assert!(record.is_confirmed(), "Tx should now be confirmed"); - assert_eq!(record.height, Some(500)); - assert_eq!(record.block_hash, Some(block_hash)); - assert_eq!(record.timestamp, 1700000000); + assert_eq!(record.height(), Some(500)); + assert_eq!(record.block_info().unwrap().block_hash, block_hash); + assert_eq!(record.block_info().unwrap().timestamp, 1700000000); assert!(ctx.first_utxo().is_confirmed, "UTXO should now be confirmed"); assert_eq!( @@ -886,7 +888,7 @@ mod tests { let result = ctx.check_transaction(&tx, block_context).await; assert!(!result.is_new_transaction); assert!(ctx.transaction(&txid).is_confirmed()); - assert_eq!(ctx.transaction(&txid).height, Some(1000)); + assert_eq!(ctx.transaction(&txid).height(), Some(1000)); assert!(ctx.first_utxo().is_confirmed); assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); @@ -971,9 +973,9 @@ mod tests { let record = ctx.transaction(&txid); assert!(record.is_confirmed()); - assert_eq!(record.height, Some(800)); - assert_eq!(record.block_hash, Some(block_hash)); - assert_eq!(record.timestamp, 1700000000); + assert_eq!(record.height(), Some(800)); + assert_eq!(record.block_info().unwrap().block_hash, block_hash); + assert_eq!(record.block_info().unwrap().timestamp, 1700000000); assert!(ctx.first_utxo().is_confirmed); } @@ -1010,9 +1012,9 @@ mod tests { // Verify the transaction was recorded with block context let record = account.transactions.get(&txid).expect("Should have backfilled record"); assert!(record.is_confirmed()); - assert_eq!(record.height, Some(600)); - assert_eq!(record.block_hash, Some(block_hash)); - assert_eq!(record.timestamp, 1700000000); + assert_eq!(record.height(), Some(600)); + assert_eq!(record.block_info().unwrap().block_hash, block_hash); + assert_eq!(record.block_info().unwrap().timestamp, 1700000000); assert_eq!(record.net_amount, 250_000); // Verify UTXO was also created @@ -1059,7 +1061,7 @@ mod tests { let record = account.transactions.get(&txid).expect("Should have record"); assert!(record.is_confirmed()); - assert_eq!(record.height, Some(700)); - assert_eq!(record.block_hash, Some(block_hash)); + assert_eq!(record.height(), Some(700)); + assert_eq!(record.block_info().unwrap().block_hash, block_hash); } }