diff --git a/test/functional/wallet_multisig_address.py b/test/functional/wallet_multisig_address.py index c160049c6d..6190ea195f 100644 --- a/test/functional/wallet_multisig_address.py +++ b/test/functional/wallet_multisig_address.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_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..137f0973ea 100644 --- a/wallet/wallet-rpc-lib/tests/basic.rs +++ b/wallet/wallet-rpc-lib/tests/basic.rs @@ -15,13 +15,11 @@ mod utils; -use logging::log; -use rstest::*; - use common::{ chain::{Block, Transaction, UtxoOutPoint}, primitives::{Amount, BlockHeight, Id}, }; +use logging::log; use utils::{ make_seedable_rng, ClientT, JsonValue, Seed, Subscription, SubscriptionClientT, ACCOUNT0_ARG, ACCOUNT1_ARG, @@ -34,6 +32,9 @@ use wallet_rpc_lib::{ TxState, }; +use rstest::*; +use tokio::time::{timeout, Duration}; + #[rstest] #[trace] #[case(test_utils::random::Seed::from_entropy())] @@ -60,7 +61,8 @@ async fn startup_shutdown(#[case] seed: Seed) { enum EventInfo { TxUpdated { id: Id, state: TxState }, TxDropped { id: Id }, - RewardAdded {}, + RewardAdded, + NewBlock, } impl EventInfo { @@ -79,7 +81,8 @@ impl EventInfo { let id = serde_json::from_value(obj["tx_id"].clone()).unwrap(); EventInfo::TxDropped { id } } - "RewardAdded" => Self::RewardAdded {}, + "RewardAdded" => Self::RewardAdded, + "NewBlock" => Self::NewBlock, _ => panic!("Unrecognized event"), } } @@ -88,7 +91,7 @@ impl EventInfo { match self { EventInfo::TxUpdated { id, state: _ } => *id, EventInfo::TxDropped { id } => *id, - EventInfo::RewardAdded {} => panic!("Not a transaction event"), + EventInfo::RewardAdded | EventInfo::NewBlock => panic!("Not a transaction event"), } } } @@ -214,20 +217,24 @@ 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 {}); - let evt4 = EventInfo::from_json(wallet_events.next().await.unwrap().unwrap()); - assert!(matches!( - evt4, - EventInfo::TxUpdated { - state: TxState::Confirmed { .. }, - id: _, - } - )); - assert_eq!(evt4.tx_id(), evt1.tx_id()); + let events = collect_until_confirmed(&mut wallet_events, evt1.tx_id(), Duration::from_secs(10)) + .await + .unwrap(); + + // Must have reward added + assert!(events.iter().any(|e| matches!(e, EventInfo::RewardAdded))); + + // Tx must be confirmed + assert!(events.iter().any(|e| { + matches!( + e, + EventInfo::TxUpdated { id, state } + if *id == evt1.tx_id() && + matches!(state, TxState::Confirmed { .. }) + ) + })); std::mem::drop(wallet_rpc); tf.stop().await; @@ -253,3 +260,37 @@ async fn no_hexified_destination(#[case] seed: Seed) { tf.stop().await; } + +async fn collect_until_confirmed( + wallet_events: &mut Subscription, + expected_tx_id: Id, + max_wait: Duration, +) -> anyhow::Result> { + let mut events = Vec::new(); + + timeout(max_wait, async { + while let Some(raw_evt) = wallet_events.next().await { + let raw_evt = raw_evt?; + let evt = EventInfo::from_json(raw_evt); + + // store it + events.push(evt.clone()); + + // stop condition + if matches!( + evt, + EventInfo::TxUpdated { id, state } + if id == expected_tx_id && + matches!(state, TxState::Confirmed { .. }) + ) { + break; + } + } + + Ok::<_, anyhow::Error>(()) + }) + .await + .map_err(|_| anyhow::anyhow!("Timed out waiting for confirmed tx"))??; + + Ok(events) +}