From 72b3097884b47f981b682b5714140e17ea8c08ce Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 13 Feb 2026 15:04:06 +0700 Subject: [PATCH] fix mempool wallet tests - only send wallet events when tx has actually changed state - when checking balance in functional tests check in-mempool states as well --- test/functional/wallet_multisig_address.py | 4 +-- test/functional/wallet_submit_tx.py | 2 +- test/functional/wallet_tx_compose.py | 2 +- test/functional/wallet_watch_address.py | 2 +- wallet/src/account/mod.rs | 14 +++++---- wallet/src/account/output_cache/mod.rs | 14 +++++++-- wallet/src/account/output_cache/tests.rs | 10 +++++-- wallet/wallet-rpc-lib/tests/basic.rs | 33 ++++++++++++++++++---- 8 files changed, 59 insertions(+), 22 deletions(-) diff --git a/test/functional/wallet_multisig_address.py b/test/functional/wallet_multisig_address.py index c160049c6d..ad12146a57 100644 --- a/test/functional/wallet_multisig_address.py +++ b/test/functional/wallet_multisig_address.py @@ -103,9 +103,9 @@ async def async_test(self): assert_in("The transaction was submitted successfully", await wallet.submit_transaction(encoded_tx, not store_tx_in_wallet)) if store_tx_in_wallet: - assert_in(f"Coins amount: {coins_to_send}", await wallet.get_balance(utxo_states=['inactive'])) + assert_in(f"Coins amount: {coins_to_send}", await wallet.get_balance(utxo_states=['inactive', 'in-mempool'])) else: - assert_in(f"Coins amount: 0", await wallet.get_balance(utxo_states=['inactive'])) + assert_in(f"Coins amount: 0", await wallet.get_balance(utxo_states=['inactive', 'in-mempool'])) assert node.mempool_contains_tx(receive_coins_tx_id) diff --git a/test/functional/wallet_submit_tx.py b/test/functional/wallet_submit_tx.py index 0217dc0d46..52ea3ef086 100644 --- a/test/functional/wallet_submit_tx.py +++ b/test/functional/wallet_submit_tx.py @@ -103,7 +103,7 @@ async def async_test(self): assert_in("The transaction was submitted successfully", await wallet.submit_transaction(encoded_tx, not store_tx_in_wallet)) if store_tx_in_wallet: - assert_in(f"Coins amount: {coins_to_send}", await wallet.get_balance(utxo_states=['inactive'])) + assert_in(f"Coins amount: {coins_to_send}", await wallet.get_balance(utxo_states=['inactive', 'in-mempool'])) else: assert_in(f"Coins amount: 0", await wallet.get_balance(utxo_states=['inactive'])) diff --git a/test/functional/wallet_tx_compose.py b/test/functional/wallet_tx_compose.py index 5dbf6cb3d5..c20b2b6b08 100644 --- a/test/functional/wallet_tx_compose.py +++ b/test/functional/wallet_tx_compose.py @@ -192,7 +192,7 @@ def make_output(pub_key_bytes): assert_in("The transaction was submitted successfully", await wallet.submit_transaction(signed_tx)) - utxos = await wallet.list_utxos('all', 'unlocked', ['inactive']) + utxos = await wallet.list_utxos('all', 'unlocked', ['inactive', 'in-mempool']) assert_equal(1, len(utxos)) # try to compose and sign a transaction with an inactive utxo that is not in chainstate only in the wallet diff --git a/test/functional/wallet_watch_address.py b/test/functional/wallet_watch_address.py index f6ff1ddad1..5257690381 100644 --- a/test/functional/wallet_watch_address.py +++ b/test/functional/wallet_watch_address.py @@ -104,7 +104,7 @@ async def async_test(self): assert_in("The transaction was submitted successfully", await wallet.submit_transaction(encoded_tx, not store_tx_in_wallet)) if store_tx_in_wallet: - assert_in(f"Coins amount: {coins_to_send}", await wallet.get_balance(utxo_states=['inactive'])) + assert_in(f"Coins amount: {coins_to_send}", await wallet.get_balance(utxo_states=['inactive', 'in-mempool'])) else: assert_in("Coins amount: 0", await wallet.get_balance(utxo_states=['inactive'])) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 55ae68f79b..1f5db6f2f8 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -90,7 +90,7 @@ use wallet_types::{ pub use self::output_cache::{ DelegationData, OrderData, OutputCacheInconsistencyError, OwnFungibleTokenInfo, PoolData, - TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, + TxChanged, TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, }; use self::output_cache::{OutputCache, TokenIssuanceData}; use self::transaction_list::{get_transaction_list, TransactionList}; @@ -2089,14 +2089,16 @@ impl Account { let relevant_outputs = self.mark_outputs_as_seen(db_tx, tx.outputs())?; if relevant_inputs || relevant_outputs { let id = AccountWalletTxId::new(self.get_account_id(), tx.id()); - db_tx.set_transaction(&id, &tx)?; - wallet_events.set_transaction(self.account_index(), &tx); - self.output_cache.add_tx( + let changed = self.output_cache.add_tx( &self.chain_config, self.account_info.best_block_height(), - id.into_item_id(), - tx, + id.clone().into_item_id(), + tx.clone(), )?; + if changed == TxChanged::Yes { + db_tx.set_transaction(&id, &tx)?; + wallet_events.set_transaction(self.account_index(), &tx); + } Ok(true) } else { Ok(false) diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index a835c64d01..651fcc32be 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -71,6 +71,14 @@ impl TxInfo { } } +/// Result when adding a transaction representing if anything was changed or the tx already existed +/// in the same state and nothing changed +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum TxChanged { + No, + Yes, +} + pub struct DelegationData { pub pool_id: PoolId, pub destination: Destination, @@ -970,7 +978,7 @@ impl OutputCache { best_block_height: BlockHeight, tx_id: OutPointSourceId, tx: WalletTx, - ) -> WalletResult<()> { + ) -> WalletResult { let existing_tx = self.txs.get(&tx_id); let existing_tx_already_confirmed_or_same = existing_tx.is_some_and(|existing_tx| { matches!( @@ -984,7 +992,7 @@ impl OutputCache { }); if existing_tx_already_confirmed_or_same { - return Ok(()); + return Ok(TxChanged::No); } let already_present = existing_tx.is_some_and(|tx| match tx.state() { @@ -1016,7 +1024,7 @@ impl OutputCache { )?; self.txs.insert(tx_id, tx); - Ok(()) + Ok(TxChanged::Yes) } /// Update the pool states for a newly confirmed transaction diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index 2cbaffada6..5cfcbfef66 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -850,7 +850,7 @@ fn test_add_tx_state_transitions_logic(#[case] seed: Seed) { let tx_a_source_id: OutPointSourceId = tx_a_id.into(); // Add A as Inactive - output_cache + let result = output_cache .add_tx( &chain_config, best_block_height, @@ -858,6 +858,7 @@ fn test_add_tx_state_transitions_logic(#[case] seed: Seed) { WalletTx::Tx(TxData::new(tx_a.clone(), TxState::Inactive(0))), ) .unwrap(); + assert_eq!(result, TxChanged::Yes); assert!( output_cache.unconfirmed_descendants.contains_key(&tx_a_source_id), @@ -867,7 +868,7 @@ fn test_add_tx_state_transitions_logic(#[case] seed: Seed) { // Add A as Confirmed let confirmed_state = TxState::Confirmed(BlockHeight::new(1), BlockTimestamp::from_int_seconds(0), 0); - output_cache + let result = output_cache .add_tx( &chain_config, best_block_height, @@ -875,6 +876,7 @@ fn test_add_tx_state_transitions_logic(#[case] seed: Seed) { WalletTx::Tx(TxData::new(tx_a.clone(), confirmed_state)), ) .unwrap(); + assert_eq!(result, TxChanged::Yes); assert!( !output_cache.unconfirmed_descendants.contains_key(&tx_a_source_id), @@ -884,7 +886,7 @@ fn test_add_tx_state_transitions_logic(#[case] seed: Seed) { // Add A as InMempool // Because existing state is Confirmed, existing_tx_already_confirmed_or_same // returns true. The state remains Confirmed (and not unconfirmed). - output_cache + let result = output_cache .add_tx( &chain_config, best_block_height, @@ -893,6 +895,8 @@ fn test_add_tx_state_transitions_logic(#[case] seed: Seed) { ) .unwrap(); + assert_eq!(result, TxChanged::No); + assert!( !output_cache.unconfirmed_descendants.contains_key(&tx_a_source_id), "Tx A should still NOT be in unconfirmed_descendants because the update to InMempool was ignored" diff --git a/wallet/wallet-rpc-lib/tests/basic.rs b/wallet/wallet-rpc-lib/tests/basic.rs index e09448677a..3b71f04b7b 100644 --- a/wallet/wallet-rpc-lib/tests/basic.rs +++ b/wallet/wallet-rpc-lib/tests/basic.rs @@ -214,20 +214,43 @@ async fn stake_and_send_coins_to_acct1(#[case] seed: Seed) { // Start staking on account 0 to hopefully create a block that contains our transaction let _: () = wallet_rpc.request("staking_start", [ACCOUNT0_ARG]).await.unwrap(); - tokio::time::sleep(std::time::Duration::from_millis(300)).await; - let evt3 = EventInfo::from_json(wallet_events.next().await.unwrap().unwrap()); - assert_eq!(evt3, EventInfo::RewardAdded {}); + // Expect 2 TxUpdated events, one from each account received from the mempool events, and one + // RewardAdded when the new block comes. + let evt3 = EventInfo::from_json(wallet_events.next().await.unwrap().unwrap()); let evt4 = EventInfo::from_json(wallet_events.next().await.unwrap().unwrap()); + let evt5 = EventInfo::from_json(wallet_events.next().await.unwrap().unwrap()); + let events = [evt3, evt4, evt5]; + + let mempool_events: Vec<_> = events + .iter() + .filter(|evt| { + matches!( + evt, + EventInfo::TxUpdated { + state: TxState::InMempool { .. }, + .. + } + ) && evt.tx_id() == evt1.tx_id() + }) + .collect(); + + let reward_events: Vec<_> = + events.iter().filter(|evt| matches!(evt, EventInfo::RewardAdded {})).collect(); + + assert_eq!(mempool_events.len(), 2); + assert_eq!(reward_events.len(), 1); + + let evt6 = EventInfo::from_json(wallet_events.next().await.unwrap().unwrap()); assert!(matches!( - evt4, + evt6, EventInfo::TxUpdated { state: TxState::Confirmed { .. }, id: _, } )); - assert_eq!(evt4.tx_id(), evt1.tx_id()); + assert_eq!(evt6.tx_id(), evt1.tx_id()); std::mem::drop(wallet_rpc); tf.stop().await;