diff --git a/dash/src/test_utils/transaction.rs b/dash/src/test_utils/transaction.rs index 9bb292e1e..31d9e9544 100644 --- a/dash/src/test_utils/transaction.rs +++ b/dash/src/test_utils/transaction.rs @@ -3,6 +3,17 @@ use std::ops::Range; use crate::{Address, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness}; impl Transaction { + /// Creates a transaction with no inputs or outputs. + pub fn empty() -> Transaction { + Transaction { + version: 1, + lock_time: 0, + input: Vec::new(), + output: Vec::new(), + special_transaction_payload: None, + } + } + pub fn dummy( address: &Address, inputs_ids_range: Range, diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index c45d2a740..ccedce946 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -177,14 +177,18 @@ typedef enum { Transaction is in the mempool (unconfirmed) */ MEMPOOL = 0, + /* + Transaction is in the mempool with an InstantSend lock + */ + INSTANT_SEND = 1, /* Transaction is in a block at the given height */ - IN_BLOCK = 1, + IN_BLOCK = 2, /* Transaction is in a chain-locked block at the given height */ - IN_CHAIN_LOCKED_BLOCK = 2, + IN_CHAIN_LOCKED_BLOCK = 3, } FFITransactionContext; /* diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 531d4ced8..3ca64f024 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -462,6 +462,7 @@ pub unsafe extern "C" fn wallet_check_transaction( }, } } + 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 4c7ccc1c8..0ad360c28 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -183,6 +183,7 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( }, } } + FFITransactionContext::InstantSend => TransactionContext::InstantSend, }; // Check the transaction - wallet is now required @@ -615,7 +616,8 @@ mod tests { fn test_transaction_context_conversion() { // Test that FFI transaction context values match expectations assert_eq!(FFITransactionContext::Mempool as u32, 0); - assert_eq!(FFITransactionContext::InBlock as u32, 1); - assert_eq!(FFITransactionContext::InChainLockedBlock as u32, 2); + assert_eq!(FFITransactionContext::InstantSend as u32, 1); + assert_eq!(FFITransactionContext::InBlock as u32, 2); + assert_eq!(FFITransactionContext::InChainLockedBlock as u32, 3); } } diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index cc626cf05..cdaff1730 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -1,5 +1,6 @@ //! Common types for FFI interface +use key_wallet::transaction_checking::TransactionContext; use key_wallet::{Network, Wallet}; use std::os::raw::{c_char, c_uint}; use std::sync::Arc; @@ -712,10 +713,27 @@ impl FFIWalletAccountCreationOptions { pub enum FFITransactionContext { /// Transaction is in the mempool (unconfirmed) Mempool = 0, + /// Transaction is in the mempool with an InstantSend lock + InstantSend = 1, /// Transaction is in a block at the given height - InBlock = 1, + InBlock = 2, /// Transaction is in a chain-locked block at the given height - InChainLockedBlock = 2, + InChainLockedBlock = 3, +} + +impl From for FFITransactionContext { + fn from(ctx: TransactionContext) -> Self { + match ctx { + TransactionContext::Mempool => FFITransactionContext::Mempool, + TransactionContext::InstantSend => FFITransactionContext::InstantSend, + TransactionContext::InBlock { + .. + } => FFITransactionContext::InBlock, + TransactionContext::InChainLockedBlock { + .. + } => FFITransactionContext::InChainLockedBlock, + } + } } /// FFI-compatible transaction context details @@ -764,9 +782,7 @@ impl FFITransactionContextDetails { } /// Convert to the native TransactionContext - pub fn to_transaction_context(&self) -> key_wallet::transaction_checking::TransactionContext { - use key_wallet::transaction_checking::TransactionContext; - + pub fn to_transaction_context(&self) -> TransactionContext { match self.context_type { FFITransactionContext::Mempool => TransactionContext::Mempool, FFITransactionContext::InBlock => { @@ -815,6 +831,7 @@ impl FFITransactionContextDetails { }, } } + FFITransactionContext::InstantSend => TransactionContext::InstantSend, } } } diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index dba47ec19..00583224e 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -352,6 +352,8 @@ impl ManagedCoreAccount { tx.is_coin_base(), ); utxo.is_confirmed = context.confirmed(); + utxo.is_instantlocked = + matches!(context, TransactionContext::InstantSend); self.utxos.insert(outpoint, utxo); } } @@ -374,6 +376,30 @@ impl ManagedCoreAccount { } } + /// Re-process an existing transaction with updated context (e.g., mempool→block confirmation) + /// and potentially new address matches from gap limit rescans. + pub(crate) fn confirm_transaction( + &mut self, + tx: &Transaction, + account_match: &AccountMatch, + context: TransactionContext, + ) -> bool { + 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); + changed = true; + } + if let Some(ts) = context.timestamp() { + tx_record.timestamp = ts as u64; + } + } + } + self.update_utxos(tx, account_match, context); + changed + } + /// Record a new transaction and update UTXOs for spendable account types pub(crate) fn record_transaction( &mut self, @@ -399,6 +425,19 @@ impl ManagedCoreAccount { self.update_utxos(tx, account_match, context); } + /// Mark all UTXOs belonging to a transaction as InstantSend-locked. + /// Returns `true` if any UTXO was newly marked. + pub(crate) fn mark_utxos_instant_send(&mut self, txid: &Txid) -> bool { + let mut any_changed = false; + for utxo in self.utxos.values_mut() { + if utxo.outpoint.txid == *txid && !utxo.is_instantlocked { + utxo.is_instantlocked = true; + any_changed = true; + } + } + any_changed + } + /// Update the account balance pub fn update_balance(&mut self, synced_height: u32) { let mut spendable = 0; diff --git a/key-wallet/src/managed_account/transaction_record.rs b/key-wallet/src/managed_account/transaction_record.rs index 8044cab95..4376965db 100644 --- a/key-wallet/src/managed_account/transaction_record.rs +++ b/key-wallet/src/managed_account/transaction_record.rs @@ -137,20 +137,9 @@ mod tests { use super::*; use dashcore::hashes::Hash; - fn create_test_transaction() -> Transaction { - // Create a minimal test transaction - Transaction { - version: 1, - lock_time: 0, - input: Vec::new(), - output: Vec::new(), - special_transaction_payload: None, - } - } - #[test] fn test_transaction_record_creation() { - let tx = create_test_transaction(); + let tx = Transaction::empty(); let record = TransactionRecord::new(tx.clone(), 1234567890, 50000, true); assert_eq!(record.txid, tx.txid()); @@ -162,7 +151,7 @@ mod tests { #[test] fn test_confirmations_calculation() { - let tx = create_test_transaction(); + let tx = Transaction::empty(); let mut record = TransactionRecord::new(tx, 1234567890, 50000, true); // Unconfirmed transaction @@ -187,7 +176,7 @@ mod tests { #[test] fn test_incoming_outgoing() { - let tx = create_test_transaction(); + let tx = Transaction::empty(); let incoming = TransactionRecord::new(tx.clone(), 1234567890, 50000, false); assert!(incoming.is_incoming()); @@ -202,7 +191,7 @@ mod tests { #[test] fn test_confirmed_transaction_creation() { - let tx = create_test_transaction(); + let tx = Transaction::empty(); let block_hash = BlockHash::all_zeros(); let record = TransactionRecord::new_confirmed(tx.clone(), 100, block_hash, 1234567890, 50000, true); @@ -214,7 +203,7 @@ mod tests { #[test] fn test_mark_unconfirmed() { - let tx = create_test_transaction(); + let tx = Transaction::empty(); let block_hash = BlockHash::all_zeros(); let mut record = TransactionRecord::new_confirmed(tx, 100, block_hash, 1234567890, 50000, true); @@ -230,7 +219,7 @@ mod tests { #[test] fn test_labels_and_fees() { - let tx = create_test_transaction(); + let tx = Transaction::empty(); let mut record = TransactionRecord::new(tx, 1234567890, -50000, true); assert_eq!(record.fee, None); diff --git a/key-wallet/src/test_utils/mod.rs b/key-wallet/src/test_utils/mod.rs index 5f69fe1d6..d9de8de38 100644 --- a/key-wallet/src/test_utils/mod.rs +++ b/key-wallet/src/test_utils/mod.rs @@ -1,3 +1,5 @@ mod account; mod utxo; mod wallet; + +pub use wallet::TestWalletContext; diff --git a/key-wallet/src/test_utils/wallet.rs b/key-wallet/src/test_utils/wallet.rs index 653e17bb6..ebc6885cb 100644 --- a/key-wallet/src/test_utils/wallet.rs +++ b/key-wallet/src/test_utils/wallet.rs @@ -1,9 +1,96 @@ -use dashcore::Network; +use dashcore::blockdata::transaction::Transaction; +use dashcore::{Address, Network, Txid}; -use crate::wallet::ManagedWalletInfo; +use crate::managed_account::transaction_record::TransactionRecord; +use crate::managed_account::ManagedCoreAccount; +use crate::transaction_checking::{ + TransactionCheckResult, TransactionContext, WalletTransactionChecker, +}; +use crate::utxo::Utxo; +use crate::wallet::initialization::WalletAccountCreationOptions; +use crate::wallet::{ManagedWalletInfo, Wallet}; +use crate::ExtendedPubKey; impl ManagedWalletInfo { pub fn dummy(id: u8) -> Self { ManagedWalletInfo::new(Network::Regtest, [id; 32]) } } + +/// Pre-built wallet context for transaction checking tests. +/// +/// Provides a testnet wallet with a default BIP44 account, a pre-derived +/// receive address, and the corresponding extended public key. +pub struct TestWalletContext { + pub managed_wallet: ManagedWalletInfo, + pub wallet: Wallet, + pub receive_address: Address, + pub xpub: ExtendedPubKey, +} + +impl TestWalletContext { + /// Creates a new random testnet wallet with a BIP44 account and one + /// pre-derived receive address. + pub fn new_random() -> Self { + let wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) + .expect("Should create wallet"); + let mut managed_wallet = + ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); + + let xpub = wallet + .accounts + .standard_bip44_accounts + .get(&0) + .expect("Should have BIP44 account") + .account_xpub; + + let receive_address = managed_wallet + .first_bip44_managed_account_mut() + .expect("Should have managed account") + .next_receive_address(Some(&xpub), true) + .expect("Should get address"); + + Self { + managed_wallet, + wallet, + receive_address, + xpub, + } + } + + /// Returns the first BIP44 managed account (immutable). + pub fn bip44_account(&self) -> &ManagedCoreAccount { + self.managed_wallet.first_bip44_managed_account().expect("Should have BIP44 account") + } + + /// Returns a transaction record by txid from the first BIP44 account. + pub fn transaction(&self, txid: &Txid) -> &TransactionRecord { + self.bip44_account().transactions.get(txid).expect("Should have transaction") + } + + /// Returns the first UTXO from the first BIP44 account. + pub fn first_utxo(&self) -> &Utxo { + self.bip44_account().utxos.values().next().expect("Should have UTXO") + } + + /// Runs `check_core_transaction` with `update_state = true`. + pub async fn check_transaction( + &mut self, + tx: &Transaction, + context: TransactionContext, + ) -> TransactionCheckResult { + self.managed_wallet.check_core_transaction(tx, context, &mut self.wallet, true).await + } + + /// Funds the wallet's receive address via a mempool transaction and + /// asserts it was accepted. Returns the context and the funding transaction. + pub async fn with_mempool_funding(mut self, amount: u64) -> (Self, Transaction) { + let tx = Transaction::dummy(&self.receive_address, 0..1, &[amount]); + + let result = self.check_transaction(&tx, TransactionContext::Mempool).await; + assert!(result.is_relevant); + assert!(result.is_new_transaction); + + (self, tx) + } +} diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index fe96d0813..15089cef9 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -289,7 +289,7 @@ impl ManagedAccountCollection { ) -> TransactionCheckResult { let mut result = TransactionCheckResult { is_relevant: false, - is_new_transaction: true, + is_new_transaction: false, affected_accounts: Vec::new(), total_received: 0, total_sent: 0, 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 2f2ccf1fb..bf4c0f285 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 @@ -1,20 +1,19 @@ //! Tests for asset unlock transaction handling -use super::helpers::create_test_transaction; +use super::helpers::test_addr; +use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; -use crate::wallet::initialization::WalletAccountCreationOptions; -use crate::wallet::{ManagedWalletInfo, Wallet}; -use crate::Network; 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; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; +use dashcore::blockdata::transaction::Transaction; use dashcore::bls_sig_utils::BLSSignature; use dashcore::hashes::Hash; -use dashcore::{BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; +use dashcore::{BlockHash, OutPoint, ScriptBuf, TxIn, TxOut, Txid}; #[test] fn test_asset_unlock_routing() { @@ -37,7 +36,8 @@ fn test_asset_unlock_routing() { #[test] fn test_asset_unlock_classification() { // Test that AssetUnlock transactions are properly classified - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create an asset unlock payload let base = AssetUnlockBasePayload { @@ -69,31 +69,15 @@ fn test_asset_unlock_classification() { #[tokio::test] async fn test_asset_unlock_transaction_routing() { - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet with default options"); - - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get the BIP44 account - let account = wallet - .accounts - .standard_bip44_accounts - .get(&0) - .expect("Expected BIP44 account at index 0 to exist"); - let xpub = account.account_xpub; - - let managed_account = managed_wallet_info - .first_bip44_managed_account_mut() - .expect("Failed to get first BIP44 managed account"); - - // Get an address from standard account (where unlocked funds go) - let address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Failed to generate receive address"); + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); // Create an asset unlock transaction - let tx = Transaction { + let tx = dashcore::Transaction { version: 3, // Version 3 for special transactions lock_time: 0, input: vec![TxIn { @@ -156,31 +140,16 @@ async fn test_asset_unlock_transaction_routing() { #[tokio::test] async fn test_asset_unlock_routing_to_bip32_account() { - // Test AssetUnlock routing to BIP32 accounts - - // Create wallet with default options (includes both BIP44 and BIP32) - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet"); - - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get address from BIP44 account (we'll use BIP44 to test the routing) - let managed_account = managed_wallet_info - .first_bip44_managed_account_mut() - .expect("Failed to get first BIP44 managed account"); - - // Get the account's xpub from wallet - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Expected BIP44 account at index 0"); - let xpub = account.account_xpub; - - let address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Failed to generate receive address"); + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); // Create an asset unlock transaction to our address - let mut tx = create_test_transaction(0, vec![]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..0, &[]); tx.output.push(TxOut { value: 200_000_000, // 2 DASH unlocked script_pubkey: address.script_pubkey(), diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/classification.rs b/key-wallet/src/transaction_checking/transaction_router/tests/classification.rs index cc67050da..52f40ca84 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/classification.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/classification.rs @@ -11,6 +11,7 @@ use dashcore::blockdata::transaction::special_transaction::provider_update_regis use dashcore::blockdata::transaction::special_transaction::provider_update_revocation::ProviderUpdateRevocationPayload; use dashcore::blockdata::transaction::special_transaction::provider_update_service::ProviderUpdateServicePayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; +use dashcore::blockdata::transaction::Transaction; use dashcore::bls_sig_utils::{BLSPublicKey, BLSSignature}; use dashcore::hash_types::{MerkleRootMasternodeList, MerkleRootQuorums}; use dashcore::hashes::Hash; @@ -19,16 +20,19 @@ use dashcore::Txid; #[test] fn test_classify_standard_transaction() { // Standard payment with 1 input, 2 outputs - let tx = create_test_transaction(1, vec![50_000_000, 49_000_000]); + let addr = test_addr(); + let tx = Transaction::dummy(&addr, 0..1, &[50_000_000, 49_000_000]); assert_eq!(TransactionRouter::classify_transaction(&tx), TransactionType::Standard); } #[test] fn test_classify_coinjoin_transaction() { // CoinJoin with multiple inputs and denomination outputs - let tx = create_test_transaction( - 5, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..5, + &[ 100_000_000, // 1 DASH denomination 100_000_000, // 1 DASH denomination 10_000_000, // 0.1 DASH denomination @@ -48,16 +52,19 @@ fn test_classify_asset_lock_transaction() { #[test] fn test_not_coinjoin_few_inputs() { // Not enough inputs to be CoinJoin - let tx = create_test_transaction(2, vec![100_000_000, 100_000_000]); + let addr = test_addr(); + let tx = Transaction::dummy(&addr, 0..2, &[100_000_000, 100_000_000]); assert_eq!(TransactionRouter::classify_transaction(&tx), TransactionType::Standard); } #[test] fn test_not_coinjoin_no_denominations() { // Many inputs/outputs but no standard denominations - let tx = create_test_transaction( - 4, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..4, + &[ 123_456_789, // Non-standard amount 987_654_321, // Non-standard amount 555_555_555, // Non-standard amount @@ -69,7 +76,8 @@ fn test_not_coinjoin_no_denominations() { #[test] fn test_classify_provider_update_registrar_transaction() { - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create a provider update registrar payload let payload = ProviderUpdateRegistrarPayload { version: 1, @@ -92,7 +100,8 @@ fn test_classify_provider_update_registrar_transaction() { #[test] fn test_classify_provider_update_service_transaction() { - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create a provider update service payload let payload = ProviderUpdateServicePayload { version: 1, @@ -118,7 +127,8 @@ fn test_classify_provider_update_service_transaction() { #[test] fn test_classify_provider_update_revocation_transaction() { - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create a provider update revocation payload let payload = ProviderUpdateRevocationPayload { version: 1, @@ -138,7 +148,8 @@ fn test_classify_provider_update_revocation_transaction() { #[test] fn test_classify_asset_unlock_transaction() { - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create an asset unlock payload let base = AssetUnlockBasePayload { version: 1, @@ -161,7 +172,8 @@ fn test_classify_asset_unlock_transaction() { #[test] fn test_classify_coinbase_transaction() { - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create a coinbase payload let payload = CoinbasePayload { version: 3, 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 acd2c81c1..91a33138a 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs @@ -1,18 +1,17 @@ //! Tests for coinbase transaction handling +use crate::test_utils::TestWalletContext; +use crate::transaction_checking::transaction_router::tests::helpers::test_addr; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; use crate::transaction_checking::{TransactionContext, WalletTransactionChecker}; -use crate::wallet::initialization::WalletAccountCreationOptions; -use crate::wallet::{ManagedWalletInfo, Wallet}; -use crate::Network; use dashcore::blockdata::transaction::special_transaction::coinbase::CoinbasePayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; use dashcore::bls_sig_utils::BLSSignature; use dashcore::hash_types::{MerkleRootMasternodeList, MerkleRootQuorums}; use dashcore::hashes::Hash; -use dashcore::{BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; +use dashcore::{BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; /// Helper to create a coinbase transaction fn create_coinbase_transaction() -> Transaction { @@ -38,53 +37,14 @@ fn create_coinbase_transaction() -> Transaction { } } -/// Helper to create a basic transaction -fn create_basic_transaction() -> Transaction { - Transaction { - version: 2, - 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: dashcore::Witness::default(), - }], - output: vec![TxOut { - value: 100000, - script_pubkey: ScriptBuf::new(), - }], - special_transaction_payload: None, - } -} - #[tokio::test] async fn test_coinbase_transaction_routing_to_bip44_receive_address() { - // Create a wallet with a BIP44 account - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet with BIP44 account for coinbase test"); - - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get the account's xpub for address derivation from the wallet's first BIP44 account - let account = wallet - .accounts - .standard_bip44_accounts - .get(&0) - .expect("Failed to get BIP44 account at index 0"); - let xpub = account.account_xpub; - - let managed_account = managed_wallet_info - .first_bip44_managed_account_mut() - .expect("Failed to get first BIP44 managed account"); - - // Get a receive address from the BIP44 account - let receive_address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Failed to generate receive address from BIP44 account"); + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + receive_address, + .. + } = TestWalletContext::new_random(); // Create a coinbase transaction that pays to our receive address let mut coinbase_tx = create_coinbase_transaction(); @@ -136,27 +96,17 @@ async fn test_coinbase_transaction_routing_to_bip44_receive_address() { #[tokio::test] async fn test_coinbase_transaction_routing_to_bip44_change_address() { - // Create a wallet with a BIP44 account - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet with BIP44 account for coinbase change test"); - - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get the account's xpub for address derivation - let account = wallet - .accounts - .standard_bip44_accounts - .get(&0) - .expect("Failed to get BIP44 account at index 0"); - let xpub = account.account_xpub; - - let managed_account = managed_wallet_info - .first_bip44_managed_account_mut() - .expect("Failed to get first BIP44 managed account"); + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + xpub, + .. + } = TestWalletContext::new_random(); // Get a change address from the BIP44 account - let change_address = managed_account + let change_address = managed_wallet_info + .first_bip44_managed_account_mut() + .expect("Failed to get first BIP44 managed account") .next_change_address(Some(&xpub), true) .expect("Failed to generate change address from BIP44 account"); @@ -210,33 +160,24 @@ async fn test_coinbase_transaction_routing_to_bip44_change_address() { #[tokio::test] async fn test_update_state_flag_behavior() { - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet with default options"); - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - let account = wallet - .accounts - .standard_bip44_accounts - .get(&0) - .expect("Expected BIP44 account at index 0 to exist"); - let xpub = account.account_xpub; - - // Get an address and initial state - let (address, initial_balance, initial_tx_count) = { + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); + + // Capture initial state + let (initial_balance, initial_tx_count) = { let managed_account = managed_wallet_info .first_bip44_managed_account_mut() .expect("Failed to get first BIP44 managed account"); - let address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Failed to generate receive address"); - let balance = managed_account.balance.spendable(); - let tx_count = managed_account.transactions.len(); - (address, balance, tx_count) + (managed_account.balance.spendable(), managed_account.transactions.len()) }; // Create a test transaction - let mut tx = create_basic_transaction(); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000]); tx.output.push(TxOut { value: 75000, script_pubkey: address.script_pubkey(), @@ -307,7 +248,8 @@ async fn test_update_state_flag_behavior() { #[test] fn test_coinbase_classification() { // Test that coinbase transactions are properly classified - let mut tx = create_basic_transaction(); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000]); // Create a coinbase payload let payload = CoinbasePayload { @@ -345,25 +287,12 @@ fn test_coinbase_routing() { #[tokio::test] async fn test_coinbase_transaction_with_payload_routing() { - // Test coinbase with special payload routing to BIP44 account - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet"); - - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get address from BIP44 account - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Expected BIP44 account at index 0"); - let xpub = account.account_xpub; - - let managed_account = managed_wallet_info - .first_bip44_managed_account_mut() - .expect("Failed to get first BIP44 managed account"); - - let address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Failed to generate receive address"); + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); // Create coinbase transaction with special payload let mut coinbase_tx = create_coinbase_transaction(); diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/coinjoin.rs b/key-wallet/src/transaction_checking/transaction_router/tests/coinjoin.rs index a066c3eda..f031a710b 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/coinjoin.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/coinjoin.rs @@ -4,13 +4,16 @@ use super::helpers::*; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; +use dashcore::blockdata::transaction::Transaction; #[test] fn test_coinjoin_mixing_round() { // Standard CoinJoin mixing round - let tx = create_test_transaction( - 6, // Multiple participants - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..6, // Multiple participants + &[ 10_000_000, // 0.1 DASH denomination 10_000_000, // 0.1 DASH denomination 10_000_000, // 0.1 DASH denomination @@ -31,9 +34,11 @@ fn test_coinjoin_mixing_round() { #[test] fn test_coinjoin_with_multiple_denominations() { // CoinJoin with mixed denominations - let tx = create_test_transaction( - 8, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..8, + &[ 100_000_000, // 1 DASH 100_000_000, // 1 DASH 10_000_000, // 0.1 DASH @@ -55,9 +60,11 @@ fn test_coinjoin_with_multiple_denominations() { #[test] fn test_coinjoin_threshold_exactly_half_denominations() { // Edge case: exactly half outputs are denominations - let tx = create_test_transaction( - 4, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..4, + &[ 100_000_000, // Denomination 100_000_000, // Denomination 50_000_000, // Non-denomination @@ -73,9 +80,11 @@ fn test_coinjoin_threshold_exactly_half_denominations() { #[test] fn test_not_coinjoin_just_under_threshold() { // Just under 50% denominations - let tx = create_test_transaction( - 3, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..3, + &[ 100_000_000, // Denomination 50_000_000, // Non-denomination 75_000_000, // Non-denomination 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 40dd02bb3..6cbc4b025 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/helpers.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/helpers.rs @@ -1,50 +1,22 @@ //! Helper functions for transaction router tests -use dashcore::blockdata::script::ScriptBuf; use dashcore::blockdata::transaction::special_transaction::asset_lock::AssetLockPayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; -use dashcore::blockdata::transaction::{OutPoint, Transaction}; -use dashcore::hashes::Hash; -use dashcore::{TxIn, TxOut, Txid, Witness}; +use dashcore::blockdata::transaction::Transaction; +use dashcore::{Address, Network, TxOut}; -/// Helper function to create a test transaction with specified inputs and outputs -pub fn create_test_transaction(num_inputs: usize, outputs: Vec) -> Transaction { - let inputs = (0..num_inputs) - .map(|i| TxIn { - previous_output: OutPoint { - txid: Txid::from_slice(&[i as u8; 32]).unwrap(), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: 0xffffffff, - witness: Witness::default(), - }) - .collect(); - - let outputs = outputs - .into_iter() - .map(|value| TxOut { - value, - script_pubkey: ScriptBuf::new(), - }) - .collect(); - - Transaction { - version: 2, - lock_time: 0, - input: inputs, - output: outputs, - special_transaction_payload: None, - } +/// Returns a deterministic test address for creating dummy transactions. +pub fn test_addr() -> Address { + Address::dummy(Network::Regtest, 0) } /// Helper to create an asset lock transaction (used for identity operations) pub fn create_asset_lock_transaction(inputs: usize, output_value: u64) -> Transaction { - let mut tx = create_test_transaction(inputs, vec![output_value]); - // Create a simple asset lock payload with one credit output + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..inputs as u8, &[output_value]); let credit_output = TxOut { value: output_value, - script_pubkey: ScriptBuf::new(), + script_pubkey: addr.script_pubkey(), }; let payload = AssetLockPayload { version: 1, 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 51fd92e5b..6d7d05cf2 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 @@ -12,30 +12,9 @@ use crate::Network; use dashcore::blockdata::script::ScriptBuf; use dashcore::blockdata::transaction::special_transaction::asset_lock::AssetLockPayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; +use dashcore::blockdata::transaction::Transaction; use dashcore::hashes::Hash; -use dashcore::{BlockHash, OutPoint, Transaction, TxIn, TxOut, Txid}; - -/// Helper to create a basic transaction -fn create_basic_transaction() -> Transaction { - Transaction { - version: 2, - 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: dashcore::Witness::default(), - }], - output: vec![TxOut { - value: 100000, - script_pubkey: ScriptBuf::new(), - }], - special_transaction_payload: None, - } -} +use dashcore::{BlockHash, OutPoint, TxIn, TxOut, Txid}; #[test] fn test_identity_registration() { @@ -86,7 +65,7 @@ async fn test_identity_registration_account_routing() { use dashcore::opcodes; use dashcore::script::Builder; - let tx = Transaction { + let tx = dashcore::Transaction { version: 3, // Version 3 for special transactions lock_time: 0, input: vec![TxIn { @@ -204,8 +183,9 @@ async fn test_normal_payment_to_identity_address_not_detected() { ) }); - // Create a NORMAL transaction (not a special transaction) to the identity address - let mut normal_tx = create_basic_transaction(); + // Create a normal transaction (not a special transaction) to the identity address + let addr = test_addr(); + let mut normal_tx = Transaction::dummy(&addr, 0..1, &[100_000]); normal_tx.output.push(TxOut { value: 50000, script_pubkey: address.script_pubkey(), @@ -271,9 +251,11 @@ fn test_identity_topup() { #[test] fn test_multiple_topups_single_transaction() { // Asset lock with multiple outputs for bulk top-ups - let mut tx = create_test_transaction( - 2, - vec![ + let addr = test_addr(); + let mut tx = Transaction::dummy( + &addr, + 0..2, + &[ 25_000_000, // Top-up 1 25_000_000, // Top-up 2 25_000_000, // Top-up 3 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 b8a53ad49..8aa1a06e3 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs @@ -15,14 +15,16 @@ use dashcore::blockdata::transaction::special_transaction::provider_update_regis use dashcore::blockdata::transaction::special_transaction::provider_update_revocation::ProviderUpdateRevocationPayload; use dashcore::blockdata::transaction::special_transaction::provider_update_service::ProviderUpdateServicePayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; +use dashcore::blockdata::transaction::Transaction; use dashcore::bls_sig_utils::BLSSignature; use dashcore::hashes::Hash; -use dashcore::{BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; +use dashcore::{BlockHash, OutPoint, ScriptBuf, TxIn, TxOut, Txid}; #[test] fn test_provider_update_registrar_classification() { // Test ProviderUpdateRegistrar classification - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); let payload = ProviderUpdateRegistrarPayload { version: 1, @@ -50,7 +52,8 @@ fn test_provider_update_registrar_classification() { #[test] fn test_provider_update_service_classification() { // Test ProviderUpdateService classification - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); let payload = ProviderUpdateServicePayload { version: 1, @@ -81,7 +84,8 @@ fn test_provider_update_service_classification() { #[test] fn test_provider_update_revocation_classification() { // Test ProviderUpdateRevocation classification - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); let payload = ProviderUpdateRevocationPayload { version: 1, @@ -169,7 +173,7 @@ async fn test_provider_registration_transaction_routing_check_owner_only() { }); // Create a ProRegTx transaction - let tx = Transaction { + let tx = dashcore::Transaction { version: 3, // Version 3 for special transactions lock_time: 0, input: vec![TxIn { @@ -305,7 +309,7 @@ async fn test_provider_registration_transaction_routing_check_voting_only() { }); // Create a ProRegTx transaction - let tx = Transaction { + let tx = dashcore::Transaction { version: 3, // Version 3 for special transactions lock_time: 0, input: vec![TxIn { @@ -442,7 +446,7 @@ async fn test_provider_registration_transaction_routing_check_operator_only() { }); // Create a ProRegTx transaction - let tx = Transaction { + let tx = dashcore::Transaction { version: 3, // Version 3 for special transactions lock_time: 0, input: vec![TxIn { @@ -641,7 +645,7 @@ async fn test_provider_registration_transaction_routing_check_platform_only() { }); // Create a ProRegTx transaction with platform fields (HighPerformance/EvoNode) - let tx = Transaction { + let tx = dashcore::Transaction { version: 3, // Version 3 for special transactions lock_time: 0, input: vec![TxIn { @@ -735,7 +739,8 @@ async fn test_provider_registration_transaction_routing_check_platform_only() { #[test] fn test_provider_update_service_with_operator_key() { - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create provider update service payload use dashcore::blockdata::transaction::special_transaction::provider_update_service::ProviderUpdateServicePayload; @@ -800,7 +805,8 @@ async fn test_provider_update_registrar_with_voting_and_operator() { .next_bls_operator_key(None, true) .expect("expected operator key"); - let mut tx = create_test_transaction(1, vec![100_000_000]); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000_000]); // Create provider update registrar payload use dashcore::blockdata::transaction::special_transaction::provider_update_registrar::ProviderUpdateRegistrarPayload; @@ -873,7 +879,8 @@ async fn test_provider_revocation_classification_and_routing() { .next_receive_address(Some(&xpub), true) .expect("Failed to generate receive address"); - let mut tx = create_test_transaction(1, vec![1_000_000_000]); // 10 DASH returned collateral + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[1_000_000_000]); // 10 DASH returned collateral // Add output for returned collateral tx.output.push(TxOut { 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 b583f7b5d..0e041b8ab 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -1,8 +1,10 @@ //! Tests for transaction routing logic +use super::helpers::test_addr; use crate::account::{AccountType, StandardAccountType}; use crate::managed_account::address_pool::KeySource; use crate::managed_account::managed_account_type::ManagedAccountType; +use crate::test_utils::TestWalletContext; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; @@ -10,30 +12,9 @@ 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, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; - -/// Helper to create a basic transaction -fn create_basic_transaction() -> Transaction { - Transaction { - version: 2, - 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: dashcore::Witness::default(), - }], - output: vec![TxOut { - value: 100000, - script_pubkey: ScriptBuf::new(), - }], - special_transaction_payload: None, - } -} +use dashcore::{BlockHash, ScriptBuf, TxOut}; #[test] fn test_standard_transaction_routing() { @@ -46,32 +27,16 @@ fn test_standard_transaction_routing() { #[tokio::test] async fn test_transaction_routing_to_bip44_account() { - // Create a wallet with a BIP44 account - let mut wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default) - .expect("Failed to create wallet with default options"); - - let mut managed_wallet_info = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get the account's xpub for address derivation from the wallet's first BIP44 account - let account = wallet - .accounts - .standard_bip44_accounts - .get(&0) - .expect("Expected BIP44 account at index 0 to exist"); - let xpub = account.account_xpub; - - let managed_account = managed_wallet_info - .first_bip44_managed_account_mut() - .expect("Failed to get first BIP44 managed account"); - - // Get an address from the BIP44 account - let address = managed_account - .next_receive_address(Some(&xpub), true) - .expect("Failed to generate receive address"); + let TestWalletContext { + managed_wallet: mut managed_wallet_info, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); // Create a transaction that sends to this address - let mut tx = create_basic_transaction(); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000]); // Add an output to our address tx.output.push(TxOut { @@ -139,7 +104,8 @@ async fn test_transaction_routing_to_bip32_account() { }; // Create a transaction that sends to this address - let mut tx = create_basic_transaction(); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000]); // Add an output to our address tx.output.push(TxOut { @@ -251,7 +217,8 @@ async fn test_transaction_routing_to_coinjoin_account() { }; // Create a CoinJoin-like transaction (multiple inputs/outputs with same denominations) - let mut tx = create_basic_transaction(); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000]); // Add multiple outputs with CoinJoin denominations tx.output.push(TxOut { @@ -352,7 +319,8 @@ async fn test_transaction_affects_multiple_accounts() { .expect("Failed to generate receive address for BIP32 account"); // Create a transaction that sends to multiple accounts - let mut tx = create_basic_transaction(); + let addr = test_addr(); + let mut tx = Transaction::dummy(&addr, 0..1, &[100_000]); // Add outputs to different accounts tx.output.push(TxOut { diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs index 11de12b8f..182c839a7 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/standard_transactions.rs @@ -4,13 +4,16 @@ use super::helpers::*; use crate::transaction_checking::transaction_router::{ AccountTypeToCheck, TransactionRouter, TransactionType, }; +use dashcore::blockdata::transaction::Transaction; #[test] fn test_single_input_two_outputs_payment() { // Typical payment: 1 input -> payment + change - let tx = create_test_transaction( - 1, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..1, + &[ 25_000_000, // Payment amount 74_900_000, // Change (minus fee) ], @@ -27,9 +30,11 @@ fn test_single_input_two_outputs_payment() { #[test] fn test_multiple_inputs_single_output_consolidation() { // Consolidation: multiple inputs -> single output - let tx = create_test_transaction( - 5, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..5, + &[ 499_900_000, // Consolidated amount minus fee ], ); @@ -47,9 +52,11 @@ fn test_multiple_inputs_single_output_consolidation() { fn test_many_inputs_same_account() { // Spending many small UTXOs from same account // 10 small inputs -> payment + change - let tx = create_test_transaction( - 10, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..10, + &[ 75_000_000, // Payment 24_950_000, // Change ], @@ -67,9 +74,11 @@ fn test_many_inputs_same_account() { #[test] fn test_payment_to_multiple_recipients() { // Batch payment: 1 input -> multiple recipients + change - let tx = create_test_transaction( - 1, - vec![ + let addr = test_addr(); + let tx = Transaction::dummy( + &addr, + 0..1, + &[ 10_000_000, // Recipient 1 15_000_000, // Recipient 2 20_000_000, // Recipient 3 diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 8af75a217..68c7f75a3 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -5,6 +5,7 @@ pub(crate) use super::account_checker::TransactionCheckResult; 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; @@ -13,10 +14,12 @@ use dashcore::prelude::CoreBlockHeight; use dashcore::BlockHash; /// Context for transaction processing -#[derive(Debug, Clone, Copy)] +#[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, @@ -35,6 +38,7 @@ 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, .. @@ -60,7 +64,7 @@ impl TransactionContext { /// Returns the block height if confirmed. pub(crate) fn block_height(&self) -> Option { match self { - TransactionContext::Mempool => None, + TransactionContext::Mempool | TransactionContext::InstantSend => None, TransactionContext::InBlock { height, .. @@ -74,7 +78,7 @@ impl TransactionContext { /// Returns the block hash if confirmed. pub(crate) fn block_hash(&self) -> Option { match self { - TransactionContext::Mempool => None, + TransactionContext::Mempool | TransactionContext::InstantSend => None, TransactionContext::InBlock { block_hash, .. @@ -88,7 +92,7 @@ impl TransactionContext { /// Returns the block time if confirmed. pub(crate) fn timestamp(&self) -> Option { match self { - TransactionContext::Mempool => None, + TransactionContext::Mempool | TransactionContext::InstantSend => None, TransactionContext::InBlock { timestamp, .. @@ -148,14 +152,55 @@ impl WalletTransactionChecker for ManagedWalletInfo { // Check if this transaction already exists in any affected account let txid = tx.txid(); + let mut is_new = true; for account_match in &result.affected_accounts { if let Some(account) = self.accounts.get_by_account_type_match(&account_match.account_type_match) { if account.transactions.contains_key(&txid) { - result.is_new_transaction = false; + is_new = false; + break; + } + } + } + result.is_new_transaction = is_new; + + if !is_new { + // IS lock on a transaction that is already confirmed is stale — ignore + if context == TransactionContext::InstantSend { + if !self.instant_send_locks.insert(txid) { return result; } + // Only accept IS transitions for unconfirmed transactions + let already_confirmed = result.affected_accounts.iter().any(|am| { + self.accounts + .get_by_account_type_match(&am.account_type_match) + .and_then(|a| a.transactions.get(&txid)) + .map_or(false, |r| r.is_confirmed()) + }); + if already_confirmed { + return result; + } + // Mark UTXOs as IS-locked in affected accounts + let mut any_changed = false; + for account_match in &result.affected_accounts { + if let Some(account) = self + .accounts + .get_by_account_type_match_mut(&account_match.account_type_match) + { + if account.mark_utxos_instant_send(&txid) { + any_changed = true; + } + } + } + if any_changed { + self.update_balance(); + } + return result; + } + // Only proceed if the new context is a block confirmation + if !context.confirmed() { + return result; } } @@ -167,7 +212,11 @@ impl WalletTransactionChecker for ManagedWalletInfo { continue; }; - account.record_transaction(tx, &account_match, context); + if is_new { + account.record_transaction(tx, &account_match, context); + } else { + account.confirm_transaction(tx, &account_match, context); + } for address_info in account_match.account_type_match.all_involved_addresses() { account.mark_address_used(&address_info.address); @@ -196,17 +245,26 @@ impl WalletTransactionChecker for ManagedWalletInfo { } } - self.increment_transactions(); + if is_new { + // Populate dedup sets when a tx arrives with an initial IS status + if context == TransactionContext::InstantSend { + self.instant_send_locks.insert(txid); + } + self.increment_transactions(); + + let wallet_net = result.total_received as i64 - result.total_sent as i64; + tracing::info!( + txid = %tx.txid(), + context = %context, + net_change = wallet_net, + received = result.total_received, + sent = result.total_sent, + "New wallet transaction detected" + ); + } - let wallet_net = result.total_received as i64 - result.total_sent as i64; - tracing::info!( - txid = %tx.txid(), - context = %context, - net_change = wallet_net, - received = result.total_received, - sent = result.total_sent, - "New wallet transaction detected" - ); + // Keep cached balance in sync after any UTXO changes + self.update_balance(); result } @@ -215,6 +273,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::TestWalletContext; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::{ManagedWalletInfo, Wallet}; @@ -226,28 +285,6 @@ mod tests { use dashcore::{Address, BlockHash, TxIn, Txid}; use dashcore_hashes::Hash; - /// Create a test transaction that sends to a given address - fn create_transaction_to_address(address: &Address, amount: u64) -> Transaction { - Transaction { - version: 2, - 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: dashcore::Witness::new(), - }], - output: vec![TxOut { - value: amount, - script_pubkey: address.script_pubkey(), - }], - special_transaction_payload: None, - } - } - /// Test wallet checker with unrelated transaction #[tokio::test] async fn test_wallet_checker_unrelated_transaction() { @@ -265,7 +302,7 @@ mod tests { &dashcore::PublicKey::from_slice(&[0x02; 33]).expect("Should create pubkey"), network, ); - let tx = create_transaction_to_address(&dummy_address, 100_000); + let tx = Transaction::dummy(&dummy_address, 0..1, &[100_000]); let context = TransactionContext::Mempool; @@ -340,7 +377,7 @@ mod tests { }; if let (Some(_xpub), Some(address)) = (bip32_xpub, bip32_address) { - let tx = create_transaction_to_address(&address, 50_000); + let tx = Transaction::dummy(&address, 0..1, &[50_000]); let context = TransactionContext::InBlock { height: 100000, @@ -377,7 +414,7 @@ mod tests { }; if let (Some(_xpub), Some(address)) = (coinjoin_xpub, coinjoin_address) { - let tx = create_transaction_to_address(&address, 75_000); + let tx = Transaction::dummy(&address, 0..1, &[75_000]); let context = TransactionContext::InChainLockedBlock { height: 100001, @@ -400,23 +437,12 @@ mod tests { /// Test coinbase transaction handling for immature transaction logic #[tokio::test] async fn test_wallet_checker_coinbase_immature_handling() { - let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) - .expect("Should create wallet"); - - let mut managed_wallet = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get a wallet address - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); - let xpub = account.account_xpub; - - let address = managed_wallet - .first_bip44_managed_account_mut() - .expect("Should have managed account") - .next_receive_address(Some(&xpub), true) - .expect("Should get address"); + let TestWalletContext { + mut managed_wallet, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); // Create a coinbase transaction let coinbase_tx = Transaction { @@ -486,26 +512,16 @@ mod tests { /// Test that spending a wallet-owned UTXO without creating change is detected #[tokio::test] async fn test_wallet_checker_detects_spend_only_transaction() { - let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) - .expect("Should create wallet"); - - let mut managed_wallet = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Prepare a managed BIP44 account and derive a receive address - let wallet_account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); - - let receive_address = managed_wallet - .first_bip44_managed_account_mut() - .expect("Should have managed account") - .next_receive_address(Some(&wallet_account.account_xpub), true) - .expect("Should derive receive address"); + let TestWalletContext { + mut managed_wallet, + mut wallet, + receive_address, + .. + } = TestWalletContext::new_random(); // Fund the wallet with a transaction paying to the receive address let funding_value = 50_000_000u64; - let funding_tx = create_transaction_to_address(&receive_address, funding_value); + 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")), @@ -521,7 +537,7 @@ mod tests { // Build a spend transaction that sends funds to an external address only let external_address = Address::p2pkh( &dashcore::PublicKey::from_slice(&[0x02; 33]).expect("Should create pubkey"), - network, + Network::Testnet, ); let spend_tx = Transaction { version: 2, @@ -575,25 +591,12 @@ mod tests { /// Test the full coinbase maturity flow - immature to mature transition #[tokio::test] async fn test_wallet_checker_immature_transaction_flow() { - use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; - - let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) - .expect("Should create wallet"); - - let mut managed_wallet = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get a wallet address - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); - let xpub = account.account_xpub; - - let address = managed_wallet - .first_bip44_managed_account_mut() - .expect("Should have managed account") - .next_receive_address(Some(&xpub), true) - .expect("Should get address"); + let TestWalletContext { + mut managed_wallet, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); // Create a coinbase transaction let coinbase_tx = Transaction { @@ -691,25 +694,13 @@ mod tests { /// Test mempool context for timestamp/height handling #[tokio::test] async fn test_wallet_checker_mempool_context() { - let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) - .expect("Should create wallet"); - - let mut managed_wallet = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get a wallet address - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); - let xpub = account.account_xpub; - - let address = managed_wallet - .first_bip44_managed_account_mut() - .expect("Should have managed account") - .next_receive_address(Some(&xpub), true) - .expect("Should get address"); - - let tx = create_transaction_to_address(&address, 100_000); + let TestWalletContext { + mut managed_wallet, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); + let tx = Transaction::dummy(&address, 0..1, &[100_000]); // Test with Mempool context let context = TransactionContext::Mempool; @@ -734,25 +725,13 @@ mod tests { /// Test that rescanning a block marks transactions as existing #[tokio::test] async fn test_transaction_rescan_marks_as_existing() { - let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) - .expect("Should create wallet"); - - let mut managed_wallet = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get a wallet address - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); - let xpub = account.account_xpub; - - let address = managed_wallet - .first_bip44_managed_account_mut() - .expect("Should have managed account") - .next_receive_address(Some(&xpub), true) - .expect("Should get address"); - - let tx = create_transaction_to_address(&address, 100_000); + let TestWalletContext { + mut managed_wallet, + mut wallet, + receive_address: address, + .. + } = TestWalletContext::new_random(); + let tx = Transaction::dummy(&address, 0..1, &[100_000]); let context = TransactionContext::InBlock { height: 100, @@ -808,28 +787,23 @@ mod tests { managed_wallet.metadata.total_transactions, total_tx_count_before, "total_transactions should not increase on rescan" ); + + // Verify UTXO state is unchanged after rescan + assert_eq!(managed_account.utxos.len(), 1, "Should still have exactly one UTXO"); + let utxo = managed_account.utxos.values().next().expect("Should have UTXO"); + assert!(utxo.is_confirmed); + assert_eq!(utxo.txout.value, 100_000); } /// Test that UTXO is not created when a spending tx has already been stored #[tokio::test] async fn test_utxo_not_created_when_already_spent() { - let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) - .expect("Should create wallet"); - - let mut managed_wallet = - ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); - - // Get wallet addresses (we need two - one for receive, one for change) - let account = - wallet.accounts.standard_bip44_accounts.get(&0).expect("Should have BIP44 account"); - let xpub = account.account_xpub; - - let receive_address = managed_wallet - .first_bip44_managed_account_mut() - .expect("Should have managed account") - .next_receive_address(Some(&xpub), true) - .expect("Should get address"); + let TestWalletContext { + mut managed_wallet, + mut wallet, + receive_address, + xpub, + } = TestWalletContext::new_random(); let change_address = managed_wallet .first_bip44_managed_account_mut() @@ -838,7 +812,7 @@ mod tests { .expect("Should get change address"); // Create the funding transaction - let funding_tx = create_transaction_to_address(&receive_address, 100_000); + let funding_tx = Transaction::dummy(&receive_address, 0..1, &[100_000]); // Create a spending transaction that: // 1. Spends the funding tx's output @@ -927,4 +901,173 @@ mod tests { ); assert_eq!(utxo.txout.value, 50_000, "UTXO value should be 50k (change amount)"); } + + /// Test that a mempool transaction gets confirmed when later seen in a block + #[tokio::test] + async fn test_mempool_transaction_confirmed_by_block() { + let (mut ctx, tx) = TestWalletContext::new_random().with_mempool_funding(200_000).await; + let txid = tx.txid(); + + // Verify unconfirmed state + assert!(!ctx.transaction(&txid).is_confirmed(), "Mempool tx should be unconfirmed"); + assert_eq!(ctx.transaction(&txid).height, None); + assert!(!ctx.first_utxo().is_confirmed, "Mempool UTXO should be unconfirmed"); + + let total_tx_before = ctx.managed_wallet.metadata.total_transactions; + + // 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 result = ctx.check_transaction(&tx, block_context).await; + assert!(result.is_relevant); + assert!(!result.is_new_transaction, "Re-processing should mark as existing"); + + // 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!(ctx.first_utxo().is_confirmed, "UTXO should now be confirmed"); + + assert_eq!( + ctx.managed_wallet.metadata.total_transactions, total_tx_before, + "total_transactions should not increase for confirmation of existing tx" + ); + } + + /// Test that an IS lock on a mempool transaction marks UTXOs and updates balance + #[tokio::test] + async fn test_instantsend_lock_updates_balance() { + let (mut ctx, tx) = TestWalletContext::new_random().with_mempool_funding(200_000).await; + let txid = tx.txid(); + + // Mempool UTXO should be unconfirmed and not IS-locked + assert!(!ctx.first_utxo().is_confirmed); + assert!(!ctx.first_utxo().is_instantlocked); + assert_eq!(ctx.managed_wallet.balance().spendable(), 0); + assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 200_000); + + let total_tx_before = ctx.managed_wallet.metadata.total_transactions; + + // Apply IS lock + let result = ctx.check_transaction(&tx, TransactionContext::InstantSend).await; + assert!(result.is_relevant); + assert!(!result.is_new_transaction); + + // UTXO should now be IS-locked + assert!(ctx.first_utxo().is_instantlocked, "UTXO should be IS-locked"); + assert!(!ctx.first_utxo().is_confirmed, "UTXO should still be unconfirmed"); + + // Balance should move from unconfirmed to spendable + assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 0); + assert_eq!(ctx.managed_wallet.metadata.total_transactions, total_tx_before); + assert!(ctx.managed_wallet.instant_send_locks.contains(&txid)); + + // Duplicate IS lock should be a no-op + let result_dup = ctx.check_transaction(&tx, TransactionContext::InstantSend).await; + assert!(result_dup.is_relevant); + assert!(!result_dup.is_new_transaction); + assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 0); + } + + /// Test that an IS lock on an already-confirmed transaction is ignored + #[tokio::test] + async fn test_instantsend_lock_on_confirmed_tx_ignored() { + let (mut ctx, tx) = TestWalletContext::new_random().with_mempool_funding(200_000).await; + let txid = tx.txid(); + + // Confirm in a block first + let block_context = TransactionContext::InBlock { + height: 500, + block_hash: Some(BlockHash::from_slice(&[5u8; 32]).expect("hash")), + timestamp: Some(1700000000), + }; + ctx.check_transaction(&tx, block_context).await; + assert!(ctx.transaction(&txid).is_confirmed()); + + let balance_before = ctx.managed_wallet.balance(); + + // Late IS lock should be ignored + let result = ctx.check_transaction(&tx, TransactionContext::InstantSend).await; + assert!(result.is_relevant); + assert!(!result.is_new_transaction); + assert_eq!(ctx.managed_wallet.balance().spendable(), balance_before.spendable()); + } + + /// Test the full lifecycle: mempool -> IS -> block -> chain-locked block + #[tokio::test] + async fn test_full_confirmation_lifecycle() { + let (mut ctx, tx) = TestWalletContext::new_random().with_mempool_funding(200_000).await; + let txid = tx.txid(); + + // Stage 1: mempool (already done in setup) + assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 200_000); + assert_eq!(ctx.managed_wallet.balance().spendable(), 0); + assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); + + // Stage 2: IS lock + ctx.check_transaction(&tx, TransactionContext::InstantSend).await; + assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.balance().unconfirmed(), 0); + assert!(ctx.first_utxo().is_instantlocked); + assert!(!ctx.first_utxo().is_confirmed); + + // 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 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!(ctx.first_utxo().is_confirmed); + 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 result = ctx.check_transaction(&tx, cl_context).await; + assert!(!result.is_new_transaction); + assert_eq!(ctx.managed_wallet.balance().spendable(), 200_000); + assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); + } + + /// Test that a new transaction arriving directly with IS context populates the dedup set + #[tokio::test] + async fn test_new_transaction_with_instantsend_context() { + let mut ctx = TestWalletContext::new_random(); + let tx = Transaction::dummy(&ctx.receive_address, 0..1, &[150_000]); + let txid = tx.txid(); + + // Arrive directly as IS (skipping plain mempool) + let result = ctx.check_transaction(&tx, TransactionContext::InstantSend).await; + assert!(result.is_relevant); + assert!(result.is_new_transaction); + assert_eq!(result.total_received, 150_000); + + // Should be IS-locked and spendable immediately + assert!(ctx.first_utxo().is_instantlocked); + assert_eq!(ctx.managed_wallet.balance().spendable(), 150_000); + assert!(ctx.managed_wallet.instant_send_locks.contains(&txid)); + + // A follow-up IS lock should be a no-op + let result2 = ctx.check_transaction(&tx, TransactionContext::InstantSend).await; + assert!(!result2.is_new_transaction); + assert_eq!(ctx.managed_wallet.balance().spendable(), 150_000); + assert_eq!(ctx.managed_wallet.metadata.total_transactions, 1); + } } diff --git a/key-wallet/src/wallet/managed_wallet_info/mod.rs b/key-wallet/src/wallet/managed_wallet_info/mod.rs index 7c49fb021..26ad6db0d 100644 --- a/key-wallet/src/wallet/managed_wallet_info/mod.rs +++ b/key-wallet/src/wallet/managed_wallet_info/mod.rs @@ -20,8 +20,10 @@ use crate::account::ManagedAccountCollection; use crate::Network; use alloc::string::String; use dashcore::prelude::CoreBlockHeight; +use dashcore::Txid; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::collections::HashSet; /// Information about a managed wallet /// @@ -45,6 +47,9 @@ pub struct ManagedWalletInfo { pub accounts: ManagedAccountCollection, /// Cached wallet core balance - should be updated when accounts change pub balance: WalletCoreBalance, + /// Transactions that have received an InstantSend lock. + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) instant_send_locks: HashSet, } impl ManagedWalletInfo { @@ -58,6 +63,7 @@ impl ManagedWalletInfo { metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::new(), balance: WalletCoreBalance::default(), + instant_send_locks: HashSet::new(), } } @@ -71,6 +77,7 @@ impl ManagedWalletInfo { metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::new(), balance: WalletCoreBalance::default(), + instant_send_locks: HashSet::new(), } } @@ -84,6 +91,7 @@ impl ManagedWalletInfo { metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::from_account_collection(&wallet.accounts), balance: WalletCoreBalance::default(), + instant_send_locks: HashSet::new(), } } 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 a55a1f8f7..5dccd9bbc 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 @@ -90,6 +90,10 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount /// Update chain state and process any matured transactions /// This should be called when the chain tip advances to a new height fn update_synced_height(&mut self, current_height: u32); + + /// Mark UTXOs for a transaction as InstantSend-locked across all accounts. + /// Returns `true` if any UTXO was newly marked. + fn mark_instant_send_utxos(&mut self, txid: &Txid) -> bool; } /// Default implementation for ManagedWalletInfo @@ -228,4 +232,14 @@ impl WalletInfoInterface for ManagedWalletInfo { // Update cached balance self.update_balance(); } + + fn mark_instant_send_utxos(&mut self, txid: &Txid) -> bool { + let mut any_changed = false; + for account in self.accounts.all_accounts_mut() { + if account.mark_utxos_instant_send(txid) { + any_changed = true; + } + } + any_changed + } }