From b63bb2d244b5649fe1554c54a447470e654757ba Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 30 Apr 2026 16:25:22 +0700 Subject: [PATCH 1/6] Reset InMempool txs to Inactive before connecting to the node --- wallet/src/account/mod.rs | 5 ++++ wallet/src/account/output_cache/mod.rs | 5 ++++ wallet/src/account/output_cache/tests.rs | 22 ++++++++++++++ wallet/src/wallet/mod.rs | 9 ++++++ wallet/types/src/wallet_tx.rs | 11 +++++++ wallet/wallet-controller/src/lib.rs | 29 ++++++++++++++----- .../wallet-controller/src/runtime_wallet.rs | 10 +++++++ 7 files changed, 83 insertions(+), 8 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index c733f41b24..c9c7ab8c80 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -2401,6 +2401,11 @@ impl Account { ) } + /// Reset all transactions that are currently in-mempool to inactive state + pub fn reset_inmempool_txs_to_inactive(&mut self) { + self.output_cache.reset_inmempool_txs_to_inactive(); + } + pub fn update_best_block( &mut self, db_tx: &mut impl WalletStorageWriteLocked, diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index bc4bd44371..d059ba853b 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -1431,6 +1431,11 @@ impl OutputCache { Ok(()) } + /// Reset all transactions that are currently in-mempool to inactive state. + pub fn reset_inmempool_txs_to_inactive(&mut self) { + self.txs.values_mut().for_each(|tx| tx.reset_inmempool_to_inactive()); + } + fn is_consumed(&self, utxo_states: UtxoStates, outpoint: &UtxoOutPoint) -> bool { self.consumed .get(outpoint) diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index 979d67ee54..d50547f194 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -1608,6 +1608,28 @@ fn update_conflicting_txs_order_v1_freeze(#[case] seed: Seed) { assert!(!output_cache.orders[&order_id].is_concluded); } +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn reset_inmempool_txs_to_inactive(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = create_unit_test_config(); + let mut output_cache = OutputCache::empty(); + + // add 10 random txs + for _ in 0..10 { + add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + } + + // Reset + output_cache.reset_inmempool_txs_to_inactive(); + + // Check none of the txs are InMempool state + for tx in output_cache.txs.values() { + assert!(!matches!(tx.state(), TxState::InMempool(_))); + } +} + fn add_random_transfer_tx( output_cache: &mut OutputCache, chain_config: &ChainConfig, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 2414b82534..d1a9fad65a 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -812,6 +812,13 @@ where Ok(()) } + /// Resets all transactions in all accounts from in-mempool state to inactive. + pub fn reset_inmempool_txs_to_inactive(&mut self) { + self.accounts + .values_mut() + .for_each(|account| account.reset_inmempool_txs_to_inactive()); + } + fn reset_wallet_transactions( chain_config: Arc, db_tx: &mut impl WalletStorageWriteLocked, @@ -973,6 +980,8 @@ where .into_iter() .map(|account| (account.account_index(), account)) .collect(); + // reset all inmempool transactions to inactive so we can set them properly from the current node again + accounts.values_mut().for_each(|a| a.reset_inmempool_txs_to_inactive()); let latest_median_time = db_tx.get_median_time()?.unwrap_or(chain_config.genesis_block().timestamp()); diff --git a/wallet/types/src/wallet_tx.rs b/wallet/types/src/wallet_tx.rs index 938ae74480..b9cbe68306 100644 --- a/wallet/types/src/wallet_tx.rs +++ b/wallet/types/src/wallet_tx.rs @@ -164,6 +164,17 @@ impl WalletTx { } } + pub fn reset_inmempool_to_inactive(&mut self) { + match self { + WalletTx::Block(_) => {} // Blocks are never InMempool + WalletTx::Tx(tx) => { + if let TxState::InMempool(counter) = tx.state { + tx.state = TxState::Inactive(counter); + } + } + } + } + pub fn inputs(&self) -> &[TxInput] { match self { WalletTx::Block(block) => block.kernel_inputs(), diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index ddf5362255..94c785c46f 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -54,7 +54,6 @@ use types::{ SeedWithPassPhrase, SignatureStats, TransactionToInspect, ValidatedSignatures, WalletInfo, WalletTypeArgsComputed, }; -use utils::set_flag::SetFlag; use wallet_storage::DefaultBackend; use read::ReadOnlyController; @@ -238,7 +237,7 @@ pub struct Controller { wallet_events: W, mempool_events: MempoolEvents, - finished_initial_sync: SetFlag, + should_resecan_mempool_txs: bool, } impl std::fmt::Debug for Controller { @@ -261,8 +260,19 @@ where wallet: RuntimeWallet, wallet_events: W, ) -> Result> { - let mut controller = - Self::new_unsynced(chain_config, rpc_client, wallet, wallet_events).await?; + let mempool_events = rpc_client + .mempool_subscribe_to_events() + .await + .map_err(ControllerError::NodeCallError)?; + let mut controller = Self { + chain_config, + rpc_client, + wallet, + staking_started: BTreeSet::new(), + wallet_events, + mempool_events, + should_resecan_mempool_txs: true, + }; // In the cold mode, try_sync_once is a no-op, so it doesn't matter whether we call it. // We omit the call to avoid printing the "Syncing the wallet" log line, which looks @@ -299,7 +309,7 @@ where staking_started: BTreeSet::new(), wallet_events, mempool_events, - finished_initial_sync: SetFlag::new(), + should_resecan_mempool_txs: true, }) } @@ -1570,7 +1580,7 @@ where match self.wallet_mode { WalletControllerMode::Hot => { // after the first successful sync to the tip fetch all mempool transactions - if !self.finished_initial_sync.test() { + if self.should_resecan_mempool_txs { let txs = self.rpc_client.mempool_get_transactions().await; match txs { @@ -1580,7 +1590,7 @@ where { log::error!("Error adding mempool transactions: {err}"); } else { - self.finished_initial_sync.set(); + self.should_resecan_mempool_txs = false } } Err(err) => { @@ -1609,8 +1619,11 @@ where Some(e) => e, None => { log::error!("Mempool notifications channel is closed"); - tokio::time::sleep(ERROR_DELAY).await; + // reset in-mempool transactions to inactive so we can rescan them when we connect again + self.wallet.reset_inmempool_txs_to_inactive(); + self.should_resecan_mempool_txs = true; + tokio::time::sleep(ERROR_DELAY).await; match self.rpc_client .mempool_subscribe_to_events() .await { diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index b693edfec7..5b02831af6 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -102,6 +102,16 @@ where } } + pub fn reset_inmempool_txs_to_inactive(&mut self) { + match self { + RuntimeWallet::Software(w) => w.reset_inmempool_txs_to_inactive(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.reset_inmempool_txs_to_inactive(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.reset_inmempool_txs_to_inactive(), + } + } + pub fn find_account_destination(&self, acc_outpoint: &AccountOutPoint) -> Option { match self { RuntimeWallet::Software(w) => w.find_account_destination(acc_outpoint), From 16df436a066f8857eac186b789126703265eb987 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 28 May 2026 17:22:52 +0200 Subject: [PATCH 2/6] fix comments --- wallet/src/account/output_cache/tests.rs | 19 +++++++++++++++++-- wallet/wallet-controller/src/lib.rs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index d50547f194..f7a74328f1 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -1618,7 +1618,12 @@ fn reset_inmempool_txs_to_inactive(#[case] seed: Seed) { // add 10 random txs for _ in 0..10 { - add_random_transfer_tx(&mut output_cache, &chain_config, &mut rng); + add_random_transfer_tx_with_state( + &mut output_cache, + &chain_config, + TxStateTag::InMempool, + &mut rng, + ); } // Reset @@ -1634,6 +1639,16 @@ fn add_random_transfer_tx( output_cache: &mut OutputCache, chain_config: &ChainConfig, mut rng: impl Rng, +) { + let state_tag = TxStateTag::iter().choose(&mut rng).unwrap(); + add_random_transfer_tx_with_state(output_cache, chain_config, state_tag, rng); +} + +fn add_random_transfer_tx_with_state( + output_cache: &mut OutputCache, + chain_config: &ChainConfig, + state_tag: TxStateTag, + mut rng: impl Rng, ) { let random_tx_id = Id::::random_using(&mut rng); let random_block_id = Id::::random_using(&mut rng); @@ -1649,7 +1664,7 @@ fn add_random_transfer_tx( .build(); let tx_id = tx.transaction().get_id(); - let tx_state = match TxStateTag::iter().choose(&mut rng).unwrap() { + let tx_state = match state_tag { TxStateTag::Confirmed => TxState::Confirmed( BlockHeight::new(rng.random_range(0..100)), BlockTimestamp::from_int_seconds(rng.random_range(0..100)), diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 94c785c46f..6ad7ec38e8 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -1579,7 +1579,7 @@ where match self.wallet_mode { WalletControllerMode::Hot => { - // after the first successful sync to the tip fetch all mempool transactions + // fetch all mempool transactions after a broken connection or after the initial sync if self.should_resecan_mempool_txs { let txs = self.rpc_client.mempool_get_transactions().await; From 5a390f748e368de134f3b6f7ad5ecd45576b74c7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 6 Jul 2026 19:29:17 +0300 Subject: [PATCH 3/6] Fix after rebase, minor cleanup --- wallet/src/wallet/mod.rs | 2 +- wallet/wallet-controller/src/lib.rs | 25 +++++++------------------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index d1a9fad65a..52403ee62b 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -980,7 +980,7 @@ where .into_iter() .map(|account| (account.account_index(), account)) .collect(); - // reset all inmempool transactions to inactive so we can set them properly from the current node again + // reset all in-mempool transactions to inactive so we can set them properly from the current node again accounts.values_mut().for_each(|a| a.reset_inmempool_txs_to_inactive()); let latest_median_time = diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 6ad7ec38e8..5faa16e7f6 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -237,7 +237,7 @@ pub struct Controller { wallet_events: W, mempool_events: MempoolEvents, - should_resecan_mempool_txs: bool, + should_rescan_mempool_txs: bool, } impl std::fmt::Debug for Controller { @@ -260,19 +260,8 @@ where wallet: RuntimeWallet, wallet_events: W, ) -> Result> { - let mempool_events = rpc_client - .mempool_subscribe_to_events() - .await - .map_err(ControllerError::NodeCallError)?; - let mut controller = Self { - chain_config, - rpc_client, - wallet, - staking_started: BTreeSet::new(), - wallet_events, - mempool_events, - should_resecan_mempool_txs: true, - }; + let mut controller = + Self::new_unsynced(chain_config, rpc_client, wallet, wallet_events).await?; // In the cold mode, try_sync_once is a no-op, so it doesn't matter whether we call it. // We omit the call to avoid printing the "Syncing the wallet" log line, which looks @@ -309,7 +298,7 @@ where staking_started: BTreeSet::new(), wallet_events, mempool_events, - should_resecan_mempool_txs: true, + should_rescan_mempool_txs: true, }) } @@ -1580,7 +1569,7 @@ where match self.wallet_mode { WalletControllerMode::Hot => { // fetch all mempool transactions after a broken connection or after the initial sync - if self.should_resecan_mempool_txs { + if self.should_rescan_mempool_txs { let txs = self.rpc_client.mempool_get_transactions().await; match txs { @@ -1590,7 +1579,7 @@ where { log::error!("Error adding mempool transactions: {err}"); } else { - self.should_resecan_mempool_txs = false + self.should_rescan_mempool_txs = false } } Err(err) => { @@ -1621,7 +1610,7 @@ where log::error!("Mempool notifications channel is closed"); // reset in-mempool transactions to inactive so we can rescan them when we connect again self.wallet.reset_inmempool_txs_to_inactive(); - self.should_resecan_mempool_txs = true; + self.should_rescan_mempool_txs = true; tokio::time::sleep(ERROR_DELAY).await; match self.rpc_client From 30c6d593f49f6b27c3dc5182b0c20d420bbea569 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 8 Jul 2026 13:42:12 +0300 Subject: [PATCH 4/6] OutputCache::reset_inmempool_txs_to_inactive now also updates the `consumed` collection. Add a note and a todo, update changelog --- CHANGELOG.md | 4 ++++ wallet/src/account/output_cache/mod.rs | 4 ++++ wallet/src/account/output_cache/tests.rs | 16 ++++++++++++++-- wallet/types/src/wallet_tx.rs | 17 ++++++++++++++--- wallet/wallet-controller/src/lib.rs | 20 ++++++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b106189f9..f552a3d600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ - Fixed an issue where the mempool wouldn't track non-UTXO dependencies between transactions, which could prevent e.g. several token management transactions from being included in the same block. + - Wallet: + - Fixed an issue where wallet transactions would remain marked as in-mempool after reopening the wallet when they are + actually no longer in the mempool. + ## [1.3.1] - 2026-06-03 ### Fixed diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index d059ba853b..a1ed4dbbb7 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -600,6 +600,7 @@ pub struct OutputCache { // - no confirmed txs are allowed; // - confirmed tx cannot have unconfirmed parent. unconfirmed_descendants: BTreeMap>, + // Map of consumed utxos; the value is the state of the tx that consumed the corresponding utxo. consumed: BTreeMap, pools: BTreeMap, @@ -1434,6 +1435,9 @@ impl OutputCache { /// Reset all transactions that are currently in-mempool to inactive state. pub fn reset_inmempool_txs_to_inactive(&mut self) { self.txs.values_mut().for_each(|tx| tx.reset_inmempool_to_inactive()); + self.consumed + .values_mut() + .for_each(|tx_state| tx_state.reset_inmempool_to_inactive()); } fn is_consumed(&self, utxo_states: UtxoStates, outpoint: &UtxoOutPoint) -> bool { diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index f7a74328f1..ead8453317 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -1626,12 +1626,24 @@ fn reset_inmempool_txs_to_inactive(#[case] seed: Seed) { ); } + // Sanity check - the `consumed` collection is not empty and all the tx states are InMempool. + assert_eq!(output_cache.consumed.len(), 10); + for tx_state in output_cache.consumed.values() { + assert!(matches!(tx_state, TxState::InMempool(_))); + } + // Reset output_cache.reset_inmempool_txs_to_inactive(); - // Check none of the txs are InMempool state + // Check that all txs are now Inactive for tx in output_cache.txs.values() { - assert!(!matches!(tx.state(), TxState::InMempool(_))); + assert!(matches!(tx.state(), TxState::Inactive(_))); + } + + // Check that inside `consumed` the state of the consuming tx has been updated as well. + assert_eq!(output_cache.consumed.len(), 10); + for tx_state in output_cache.consumed.values() { + assert!(matches!(tx_state, TxState::Inactive(_))); } } diff --git a/wallet/types/src/wallet_tx.rs b/wallet/types/src/wallet_tx.rs index b9cbe68306..2413e14f9f 100644 --- a/wallet/types/src/wallet_tx.rs +++ b/wallet/types/src/wallet_tx.rs @@ -91,6 +91,19 @@ impl TxState { TxState::Abandoned => "Abandoned", } } + + pub fn reset_inmempool_to_inactive(&mut self) { + match self { + TxState::InMempool(unconfirmed_tx_counter) => { + *self = TxState::Inactive(*unconfirmed_tx_counter); + } + + TxState::Confirmed(_, _, _) + | TxState::Conflicted(_) + | TxState::Inactive(_) + | TxState::Abandoned => {} + } + } } impl Display for TxState { @@ -168,9 +181,7 @@ impl WalletTx { match self { WalletTx::Block(_) => {} // Blocks are never InMempool WalletTx::Tx(tx) => { - if let TxState::InMempool(counter) = tx.state { - tx.state = TxState::Inactive(counter); - } + tx.state.reset_inmempool_to_inactive(); } } } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 5faa16e7f6..9240ff88ce 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -1607,7 +1607,12 @@ where let event = match maybe_event { Some(e) => e, None => { + // Note: currently the wallet is unable to automatically reconnect to the node when + // the connection is dropped, so for now this branch mostly handles a hypothetical + // situation when the connection is still up, but the stream itself somehow got closed. + log::error!("Mempool notifications channel is closed"); + // reset in-mempool transactions to inactive so we can rescan them when we connect again self.wallet.reset_inmempool_txs_to_inactive(); self.should_rescan_mempool_txs = true; @@ -1628,6 +1633,21 @@ where }; match event { + // TODO: mempool can evict transactions - there is a size limit for the entire mempool + // (MAX_MEMPOOL_SIZE_BYTES by default, which is 300Mb) and an expiration time for each tx + // (DEFAULT_MEMPOOL_EXPIRY, which is currently 2 weeks). The wallet must be aware of this + // and mark all its in-mempool txs that have been evicted as inactive. + // At this moment eviction happens when a new tx is being added to the mempool, but note + // that a `NewTransaction` event is not guaranteed in this case (e.g. if the newly added + // tx gets evicted too). Additionally, txs may be evicted after the mempool has been explicitly + // trimmed via `set_max_size`. + // So, the simpler (but not 100% reliable) solution would be to check all of the wallet's + // in-mempool txs for whether they're still in mempool (preferably via a dedicated rpc method, + // to avoid overhead) whenever a `NewTransaction` event arrives. + // A better solution is to introduce a separate mempool event, `TransactionsRemoved`, + // that would contain ids of all txs that have been evicted or removed due to other reasons + // (plus maybe the reason for the eviction/removal). + MempoolEvent::NewTransaction { tx_id } => { let transaction = self.rpc_client .mempool_get_transaction(tx_id) From 0a376c82af96aed1a6ea488aa1c7f6126b3bfeb6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 8 Jul 2026 16:17:49 +0300 Subject: [PATCH 5/6] After resetting tx's state to Inactive, also write the tx to the wallet db and optionally emit a wallet event --- wallet/src/account/mod.rs | 19 +++++++++-- wallet/src/account/output_cache/mod.rs | 13 ++++--- wallet/src/account/output_cache/tests.rs | 9 ++++- wallet/src/wallet/mod.rs | 34 +++++++++++++++---- wallet/types/src/wallet_tx.rs | 13 ++++--- wallet/wallet-controller/src/lib.rs | 4 +-- .../wallet-controller/src/runtime_wallet.rs | 11 +++--- 7 files changed, 77 insertions(+), 26 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index c9c7ab8c80..c0283cfcca 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -2402,8 +2402,23 @@ impl Account { } /// Reset all transactions that are currently in-mempool to inactive state - pub fn reset_inmempool_txs_to_inactive(&mut self) { - self.output_cache.reset_inmempool_txs_to_inactive(); + pub fn reset_inmempool_txs_to_inactive( + &mut self, + db_tx: &mut StoreTxRw, + wallet_events: Option<&impl WalletEvents>, + ) -> WalletResult<()> { + let acc_id = self.get_account_id(); + let account_index = self.account_index(); + + for tx in self.output_cache.reset_inmempool_txs_to_inactive() { + db_tx.set_transaction(&AccountWalletTxId::new(acc_id.clone(), tx.id()), &tx)?; + + if let Some(wallet_events) = wallet_events { + wallet_events.set_transaction(account_index, &tx); + } + } + + Ok(()) } pub fn update_best_block( diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index a1ed4dbbb7..f9c7660cc8 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -1433,11 +1433,16 @@ impl OutputCache { } /// Reset all transactions that are currently in-mempool to inactive state. - pub fn reset_inmempool_txs_to_inactive(&mut self) { - self.txs.values_mut().for_each(|tx| tx.reset_inmempool_to_inactive()); - self.consumed + /// + /// Return an iterator of the transactions that were reset. + pub fn reset_inmempool_txs_to_inactive(&mut self) -> impl Iterator { + self.consumed.values_mut().for_each(|tx_state| { + tx_state.reset_inmempool_to_inactive(); + }); + + self.txs .values_mut() - .for_each(|tx_state| tx_state.reset_inmempool_to_inactive()); + .filter_map(|tx| tx.reset_inmempool_to_inactive().then_some(&*tx)) } fn is_consumed(&self, utxo_states: UtxoStates, outpoint: &UtxoOutPoint) -> bool { diff --git a/wallet/src/account/output_cache/tests.rs b/wallet/src/account/output_cache/tests.rs index ead8453317..6fed613211 100644 --- a/wallet/src/account/output_cache/tests.rs +++ b/wallet/src/account/output_cache/tests.rs @@ -1632,8 +1632,15 @@ fn reset_inmempool_txs_to_inactive(#[case] seed: Seed) { assert!(matches!(tx_state, TxState::InMempool(_))); } + let all_tx_ids = output_cache.txs.values().map(|tx| tx.id()).collect::>(); + // Reset - output_cache.reset_inmempool_txs_to_inactive(); + let reset_tx_ids = output_cache + .reset_inmempool_txs_to_inactive() + .map(|tx| tx.id()) + .collect::>(); + + assert_eq!(reset_tx_ids, all_tx_ids); // Check that all txs are now Inactive for tx in output_cache.txs.values() { diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 52403ee62b..163ee8bc74 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -813,10 +813,19 @@ where } /// Resets all transactions in all accounts from in-mempool state to inactive. - pub fn reset_inmempool_txs_to_inactive(&mut self) { - self.accounts - .values_mut() - .for_each(|account| account.reset_inmempool_txs_to_inactive()); + pub fn reset_inmempool_txs_to_inactive( + &mut self, + wallet_events: Option<&impl WalletEvents>, + ) -> WalletResult<()> { + let mut db_tx = self.db.transaction_rw(None)?; + + for account in self.accounts.values_mut() { + account.reset_inmempool_txs_to_inactive(&mut db_tx, wallet_events)?; + } + + db_tx.commit()?; + + Ok(()) } fn reset_wallet_transactions( @@ -980,14 +989,27 @@ where .into_iter() .map(|account| (account.account_index(), account)) .collect(); - // reset all in-mempool transactions to inactive so we can set them properly from the current node again - accounts.values_mut().for_each(|a| a.reset_inmempool_txs_to_inactive()); let latest_median_time = db_tx.get_median_time()?.unwrap_or(chain_config.genesis_block().timestamp()); db_tx.close(); + { + // Reset all in-mempool transactions to inactive so we can set them properly from the current node again. + + let mut db_tx = db.transaction_rw(None)?; + + for account in accounts.values_mut() { + account.reset_inmempool_txs_to_inactive( + &mut db_tx, + Option::<&WalletEventsNoOp>::None, + )?; + } + + db_tx.commit()?; + } + let next_unused_account = accounts.pop_last().ok_or(WalletError::WalletNotInitialized)?; Ok(WalletCreation::Wallet(Wallet { diff --git a/wallet/types/src/wallet_tx.rs b/wallet/types/src/wallet_tx.rs index 2413e14f9f..3a3b3b45d0 100644 --- a/wallet/types/src/wallet_tx.rs +++ b/wallet/types/src/wallet_tx.rs @@ -92,16 +92,17 @@ impl TxState { } } - pub fn reset_inmempool_to_inactive(&mut self) { + pub fn reset_inmempool_to_inactive(&mut self) -> bool { match self { TxState::InMempool(unconfirmed_tx_counter) => { *self = TxState::Inactive(*unconfirmed_tx_counter); + true } TxState::Confirmed(_, _, _) | TxState::Conflicted(_) | TxState::Inactive(_) - | TxState::Abandoned => {} + | TxState::Abandoned => false, } } } @@ -177,12 +178,10 @@ impl WalletTx { } } - pub fn reset_inmempool_to_inactive(&mut self) { + pub fn reset_inmempool_to_inactive(&mut self) -> bool { match self { - WalletTx::Block(_) => {} // Blocks are never InMempool - WalletTx::Tx(tx) => { - tx.state.reset_inmempool_to_inactive(); - } + WalletTx::Block(_) => false, // Blocks are never InMempool + WalletTx::Tx(tx) => tx.state.reset_inmempool_to_inactive(), } } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 9240ff88ce..8005e58936 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -1613,8 +1613,8 @@ where log::error!("Mempool notifications channel is closed"); - // reset in-mempool transactions to inactive so we can rescan them when we connect again - self.wallet.reset_inmempool_txs_to_inactive(); + // Reset in-mempool transactions to inactive so we can rescan them when we connect again. + self.wallet.reset_inmempool_txs_to_inactive(Some(&self.wallet_events))?; self.should_rescan_mempool_txs = true; tokio::time::sleep(ERROR_DELAY).await; diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index 5b02831af6..d30a208a30 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -102,13 +102,16 @@ where } } - pub fn reset_inmempool_txs_to_inactive(&mut self) { + pub fn reset_inmempool_txs_to_inactive( + &mut self, + wallet_events: Option<&impl WalletEvents>, + ) -> WalletResult<()> { match self { - RuntimeWallet::Software(w) => w.reset_inmempool_txs_to_inactive(), + RuntimeWallet::Software(w) => w.reset_inmempool_txs_to_inactive(wallet_events), #[cfg(feature = "trezor")] - RuntimeWallet::Trezor(w) => w.reset_inmempool_txs_to_inactive(), + RuntimeWallet::Trezor(w) => w.reset_inmempool_txs_to_inactive(wallet_events), #[cfg(feature = "ledger")] - RuntimeWallet::Ledger(w) => w.reset_inmempool_txs_to_inactive(), + RuntimeWallet::Ledger(w) => w.reset_inmempool_txs_to_inactive(wallet_events), } } From ed575b894509b2d46ee7bf28da304a28aa328641 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 8 Jul 2026 18:37:03 +0300 Subject: [PATCH 6/6] Appease clippy --- wallet/src/account/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index c0283cfcca..59b3ca04d4 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -2411,10 +2411,10 @@ impl Account { let account_index = self.account_index(); for tx in self.output_cache.reset_inmempool_txs_to_inactive() { - db_tx.set_transaction(&AccountWalletTxId::new(acc_id.clone(), tx.id()), &tx)?; + db_tx.set_transaction(&AccountWalletTxId::new(acc_id.clone(), tx.id()), tx)?; if let Some(wallet_events) = wallet_events { - wallet_events.set_transaction(account_index, &tx); + wallet_events.set_transaction(account_index, tx); } }