From 1419b84f9c9781d1c379984917c1df58626aaaea Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 11:16:12 +0300 Subject: [PATCH 1/8] refactor: gate `manager` module behind feature flag in key-wallet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #503 merged key-wallet-manager into key-wallet but made `tokio` and `rayon` unconditional dependencies. This broke the lightweight dependency story — consumers who only need BIP32/BIP39/address generation were forced to pull in an async runtime. This PR: - Adds a `manager` feature flag (included in `default`) - Makes `tokio` and `rayon` optional, gated behind `manager` - Gates `pub mod manager` in lib.rs with `#[cfg(feature = "manager")]` - Gates manager-dependent test utilities (MockWallet, etc.) Existing consumers are unaffected since `manager` is in `default`. Consumers who want a lightweight key-wallet can now opt out: key-wallet = { default-features = false, features = ["std"] } Co-Authored-By: Claude Opus 4.6 --- key-wallet/Cargo.toml | 7 ++++--- key-wallet/src/lib.rs | 1 + key-wallet/src/test_utils/mod.rs | 2 ++ key-wallet/src/test_utils/wallet.rs | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/key-wallet/Cargo.toml b/key-wallet/Cargo.toml index 6fc6a1a9f..da26e475b 100644 --- a/key-wallet/Cargo.toml +++ b/key-wallet/Cargo.toml @@ -9,8 +9,9 @@ readme = "README.md" license = "CC0-1.0" [features] -default = ["std"] +default = ["std", "manager"] std = ["secp256k1/std", "bip39/std", "getrandom", "rand"] +manager = ["dep:tokio", "dep:rayon"] serde = ["dep:serde", "dep:serde_json", "dashcore_hashes/serde", "secp256k1/serde", "dashcore/serde"] bincode = ["serde", "dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dashcore/bincode"] bip38 = ["scrypt", "aes", "bs58", "rand"] @@ -43,8 +44,8 @@ hex = { version = "0.4"} zeroize = { version = "1.8", features = ["derive"] } tracing = "0.1" async-trait = "0.1" -tokio = { version = "1", features = ["macros", "rt", "sync"] } -rayon = { version = "1.11" } +tokio = { version = "1", features = ["macros", "rt", "sync"], optional = true } +rayon = { version = "1.11", optional = true } [dev-dependencies] dashcore = { path="../dash", features = ["test-utils"] } diff --git a/key-wallet/src/lib.rs b/key-wallet/src/lib.rs index b08372884..26344ea77 100644 --- a/key-wallet/src/lib.rs +++ b/key-wallet/src/lib.rs @@ -43,6 +43,7 @@ pub mod dip9; pub mod error; pub mod gap_limit; pub mod managed_account; +#[cfg(feature = "manager")] pub mod manager; pub mod mnemonic; pub mod psbt; diff --git a/key-wallet/src/test_utils/mod.rs b/key-wallet/src/test_utils/mod.rs index ba8709556..ad58a1097 100644 --- a/key-wallet/src/test_utils/mod.rs +++ b/key-wallet/src/test_utils/mod.rs @@ -2,6 +2,8 @@ mod account; mod utxo; mod wallet; +#[cfg(feature = "manager")] pub use wallet::MockWallet; +#[cfg(feature = "manager")] pub use wallet::NonMatchingMockWallet; pub use wallet::TestWalletContext; diff --git a/key-wallet/src/test_utils/wallet.rs b/key-wallet/src/test_utils/wallet.rs index 5cf6ded55..9c69a481e 100644 --- a/key-wallet/src/test_utils/wallet.rs +++ b/key-wallet/src/test_utils/wallet.rs @@ -13,18 +13,27 @@ impl ManagedWalletInfo { } } +#[cfg(feature = "manager")] use crate::manager::MempoolTransactionResult; +#[cfg(feature = "manager")] use crate::manager::{BlockProcessingResult, WalletEvent, WalletInterface}; +#[cfg(feature = "manager")] use dashcore::address::NetworkUnchecked; +#[cfg(feature = "manager")] use dashcore::prelude::CoreBlockHeight; +#[cfg(feature = "manager")] use dashcore::{Block, OutPoint}; +#[cfg(feature = "manager")] use std::str::FromStr; +#[cfg(feature = "manager")] use std::{collections::BTreeMap, sync::Arc}; +#[cfg(feature = "manager")] use tokio::sync::{broadcast, Mutex}; -// Type alias for transaction effects map +#[cfg(feature = "manager")] type TransactionEffectsMap = Arc)>>>; +#[cfg(feature = "manager")] pub struct MockWallet { processed_blocks: Arc>>, processed_transactions: Arc>>, @@ -46,12 +55,14 @@ pub struct MockWallet { monitor_revision: u64, } +#[cfg(feature = "manager")] impl Default for MockWallet { fn default() -> Self { Self::new() } } +#[cfg(feature = "manager")] impl MockWallet { pub fn new() -> Self { let (event_sender, _) = broadcast::channel(16); @@ -110,6 +121,7 @@ impl MockWallet { } } +#[cfg(feature = "manager")] #[async_trait::async_trait] impl WalletInterface for MockWallet { async fn process_block(&mut self, block: &Block, height: u32) -> BlockProcessingResult { @@ -197,18 +209,21 @@ impl WalletInterface for MockWallet { } } +#[cfg(feature = "manager")] /// Mock wallet that returns false for filter checks pub struct NonMatchingMockWallet { synced_height: CoreBlockHeight, event_sender: broadcast::Sender, } +#[cfg(feature = "manager")] impl Default for NonMatchingMockWallet { fn default() -> Self { Self::new() } } +#[cfg(feature = "manager")] impl NonMatchingMockWallet { pub fn new() -> Self { let (event_sender, _) = broadcast::channel(16); @@ -219,6 +234,7 @@ impl NonMatchingMockWallet { } } +#[cfg(feature = "manager")] #[async_trait::async_trait] impl WalletInterface for NonMatchingMockWallet { async fn process_block(&mut self, _block: &Block, _height: u32) -> BlockProcessingResult { From 3a4cf54655e6888aabe2ef29f2dcd675be4f81d8 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 11:19:56 +0300 Subject: [PATCH 2/8] refactor: make rayon optional with parallel-filters feature Separates rayon from the manager feature. The `parallel-filters` feature enables parallel compact block filter checking via rayon. Without it, filter checking falls back to sequential iteration. This keeps rayon out of the dependency tree for consumers who need the wallet manager but not parallel filter matching. Co-Authored-By: Claude Opus 4.6 --- key-wallet/Cargo.toml | 3 ++- key-wallet/src/manager/matching.rs | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/key-wallet/Cargo.toml b/key-wallet/Cargo.toml index da26e475b..ed2f485bd 100644 --- a/key-wallet/Cargo.toml +++ b/key-wallet/Cargo.toml @@ -11,7 +11,8 @@ license = "CC0-1.0" [features] default = ["std", "manager"] std = ["secp256k1/std", "bip39/std", "getrandom", "rand"] -manager = ["dep:tokio", "dep:rayon"] +manager = ["dep:tokio"] +parallel-filters = ["manager", "dep:rayon"] serde = ["dep:serde", "dep:serde_json", "dashcore_hashes/serde", "secp256k1/serde", "dashcore/serde"] bincode = ["serde", "dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dashcore/bincode"] bip38 = ["scrypt", "aes", "bs58", "rand"] diff --git a/key-wallet/src/manager/matching.rs b/key-wallet/src/manager/matching.rs index 3a3b008c1..92200eac9 100644 --- a/key-wallet/src/manager/matching.rs +++ b/key-wallet/src/manager/matching.rs @@ -2,6 +2,7 @@ use alloc::vec::Vec; use dashcore::bip158::BlockFilter; use dashcore::prelude::CoreBlockHeight; use dashcore::{Address, BlockHash}; +#[cfg(feature = "parallel-filters")] use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use std::collections::{BTreeSet, HashMap}; @@ -34,15 +35,22 @@ pub fn check_compact_filters_for_addresses( let script_pubkey_bytes: Vec> = addresses.iter().map(|address| address.script_pubkey().to_bytes()).collect(); - input - .into_par_iter() - .filter_map(|(key, filter)| { - filter - .match_any(key.hash(), script_pubkey_bytes.iter().map(|v| v.as_slice())) - .unwrap_or(false) - .then_some(key.clone()) - }) - .collect() + let match_filter = |(key, filter): (&FilterMatchKey, &BlockFilter)| { + filter + .match_any(key.hash(), script_pubkey_bytes.iter().map(|v| v.as_slice())) + .unwrap_or(false) + .then_some(key.clone()) + }; + + #[cfg(feature = "parallel-filters")] + { + input.into_par_iter().filter_map(match_filter).collect() + } + + #[cfg(not(feature = "parallel-filters"))] + { + input.iter().filter_map(match_filter).collect() + } } #[cfg(test)] From fd8e4e5e22c70010cda24438692faf9612094734 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 11:21:26 +0300 Subject: [PATCH 3/8] feat: enable parallel-filters for dash-spv dash-spv uses compact block filter matching during sync and benefits from parallel iteration via rayon. Co-Authored-By: Claude Opus 4.6 --- dash-spv/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-spv/Cargo.toml b/dash-spv/Cargo.toml index 08064df27..c05f46999 100644 --- a/dash-spv/Cargo.toml +++ b/dash-spv/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.89" # Core Dash libraries dashcore = { path = "../dash", features = ["serde", "core-block-hash-use-x11", "message_verification", "bls", "quorum_validation"] } dashcore_hashes = { path = "../hashes" } -key-wallet = { path = "../key-wallet" } +key-wallet = { path = "../key-wallet", features = ["parallel-filters"] } # BLS signatures blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900" } From 0362b14e052dbe3465b45e3cfa6ee1cc8cdf640c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 11:48:06 +0300 Subject: [PATCH 4/8] fix: enable manager feature for key-wallet-ffi key-wallet-ffi uses default-features = false and explicitly lists features. Since manager is no longer in the implicit defaults for this consumer, it needs to be listed explicitly. Co-Authored-By: Claude Opus 4.6 --- key-wallet-ffi/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/key-wallet-ffi/Cargo.toml b/key-wallet-ffi/Cargo.toml index f6b293056..cfbe497d7 100644 --- a/key-wallet-ffi/Cargo.toml +++ b/key-wallet-ffi/Cargo.toml @@ -20,7 +20,7 @@ eddsa = ["dashcore/eddsa", "key-wallet/eddsa"] bls = ["dashcore/bls", "key-wallet/bls"] [dependencies] -key-wallet = { path = "../key-wallet", default-features = false, features = ["std"] } +key-wallet = { path = "../key-wallet", default-features = false, features = ["std", "manager"] } dashcore = { path = "../dash" } secp256k1 = { version = "0.30.0", features = ["global-context"] } tokio = { version = "1.32", features = ["rt-multi-thread", "sync"] } @@ -31,6 +31,6 @@ hex = "0.4" cbindgen = "0.29" [dev-dependencies] -key-wallet = { path = "../key-wallet", default-features = false, features = ["std", "test-utils"] } +key-wallet = { path = "../key-wallet", default-features = false, features = ["std", "manager", "test-utils"] } tempfile = "3.0" hex = "0.4" From ac7ee25d74e6d7acdc9df6c772e77fe629666fc4 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 14:36:28 +0300 Subject: [PATCH 5/8] refactor: extract mock wallets into dedicated test_utils/mock_wallet module Instead of gating each item with #[cfg(feature = "manager")] in wallet.rs, move MockWallet and NonMatchingMockWallet into a separate mock_wallet.rs module that is conditionally compiled as a whole. wallet.rs now only contains ManagedWalletInfo::dummy and TestWalletContext, which have no manager dependency. Co-Authored-By: Claude Opus 4.6 --- key-wallet/src/test_utils/mock_wallet.rs | 246 +++++++++++++++++++++ key-wallet/src/test_utils/mod.rs | 6 +- key-wallet/src/test_utils/wallet.rs | 261 ----------------------- 3 files changed, 250 insertions(+), 263 deletions(-) create mode 100644 key-wallet/src/test_utils/mock_wallet.rs diff --git a/key-wallet/src/test_utils/mock_wallet.rs b/key-wallet/src/test_utils/mock_wallet.rs new file mode 100644 index 000000000..856dc31e0 --- /dev/null +++ b/key-wallet/src/test_utils/mock_wallet.rs @@ -0,0 +1,246 @@ +use crate::manager::MempoolTransactionResult; +use crate::manager::{BlockProcessingResult, WalletEvent, WalletInterface}; +use crate::transaction_checking::TransactionContext; +use dashcore::address::NetworkUnchecked; +use dashcore::prelude::CoreBlockHeight; +use dashcore::{Address, Block, OutPoint, Transaction, Txid}; +use std::collections::BTreeMap; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::{broadcast, Mutex}; + +// Type alias for transaction effects map +type TransactionEffectsMap = Arc)>>>; + +pub struct MockWallet { + processed_blocks: Arc>>, + processed_transactions: Arc>>, + // Map txid -> (net_amount, addresses) + effects: TransactionEffectsMap, + synced_height: CoreBlockHeight, + event_sender: broadcast::Sender, + /// When true, process_mempool_transaction returns is_relevant=true. + mempool_relevant: bool, + /// Addresses returned by monitored_addresses. + addresses: Vec
, + /// Outpoints returned by watched_outpoints. + outpoints: Vec, + /// New addresses returned by process_mempool_transaction. + mempool_new_addresses: Vec
, + /// Recorded status change notifications for test assertions. + status_changes: Arc>>, + /// Monitor revision counter for staleness detection. + monitor_revision: u64, +} + +impl Default for MockWallet { + fn default() -> Self { + Self::new() + } +} + +impl MockWallet { + pub fn new() -> Self { + let (event_sender, _) = broadcast::channel(16); + Self { + processed_blocks: Arc::new(Mutex::new(Vec::new())), + processed_transactions: Arc::new(Mutex::new(Vec::new())), + effects: Arc::new(Mutex::new(BTreeMap::new())), + synced_height: 0, + event_sender, + mempool_relevant: false, + addresses: Vec::new(), + outpoints: Vec::new(), + mempool_new_addresses: Vec::new(), + status_changes: Arc::new(Mutex::new(Vec::new())), + monitor_revision: 0, + } + } + + /// Configure whether mempool transactions are reported as relevant. + pub fn set_mempool_relevant(&mut self, relevant: bool) { + self.mempool_relevant = relevant; + } + + /// Set the addresses returned by monitored_addresses. + pub fn set_addresses(&mut self, addresses: Vec
) { + self.addresses = addresses; + self.monitor_revision += 1; + } + + /// Set the outpoints returned by watched_outpoints. + pub fn set_outpoints(&mut self, outpoints: Vec) { + self.outpoints = outpoints; + self.monitor_revision += 1; + } + + /// Set new addresses returned by process_mempool_transaction. + pub fn set_mempool_new_addresses(&mut self, addresses: Vec
) { + self.mempool_new_addresses = addresses; + } + + pub fn status_changes(&self) -> Arc>> { + self.status_changes.clone() + } + + pub async fn set_effect(&self, txid: dashcore::Txid, net: i64, addresses: Vec) { + let mut map = self.effects.lock().await; + map.insert(txid, (net, addresses)); + } + + pub fn processed_blocks(&self) -> Arc>> { + self.processed_blocks.clone() + } + + pub fn processed_transactions(&self) -> Arc>> { + self.processed_transactions.clone() + } +} + +#[async_trait::async_trait] +impl WalletInterface for MockWallet { + async fn process_block(&mut self, block: &Block, height: u32) -> BlockProcessingResult { + let mut processed = self.processed_blocks.lock().await; + processed.push((block.block_hash(), height)); + + BlockProcessingResult { + new_txids: block.txdata.iter().map(|tx| tx.txid()).collect(), + existing_txids: Vec::new(), + new_addresses: Vec::new(), + } + } + + async fn process_mempool_transaction( + &mut self, + tx: &Transaction, + _is_instant_send: bool, + ) -> MempoolTransactionResult { + let mut processed = self.processed_transactions.lock().await; + processed.push(tx.txid()); + + if !self.mempool_relevant { + return MempoolTransactionResult::default(); + } + + let effects = self.effects.lock().await; + let (net_amount, addresses) = if let Some((net, addr_strs)) = effects.get(&tx.txid()) { + let addrs = addr_strs + .iter() + .filter_map(|s| { + Address::::from_str(s).ok().map(|a| a.assume_checked()) + }) + .collect(); + (*net, addrs) + } else { + (0, Vec::new()) + }; + + MempoolTransactionResult { + is_relevant: true, + net_amount, + is_outgoing: net_amount < 0, + addresses, + new_addresses: self.mempool_new_addresses.clone(), + } + } + + async fn describe(&self) -> String { + "MockWallet (test implementation)".to_string() + } + + async fn transaction_effect(&self, tx: &Transaction) -> Option<(i64, Vec)> { + let map = self.effects.lock().await; + map.get(&tx.txid()).cloned() + } + + fn monitored_addresses(&self) -> Vec
{ + self.addresses.clone() + } + + fn watched_outpoints(&self) -> Vec { + self.outpoints.clone() + } + + fn synced_height(&self) -> CoreBlockHeight { + self.synced_height + } + + fn update_synced_height(&mut self, height: CoreBlockHeight) { + self.synced_height = height; + } + + fn monitor_revision(&self) -> u64 { + self.monitor_revision + } + + fn subscribe_events(&self) -> broadcast::Receiver { + self.event_sender.subscribe() + } + + fn process_instant_send_lock(&mut self, txid: Txid) { + let mut changes = + self.status_changes.try_lock().expect("status_changes lock contention in test helper"); + changes.push((txid, TransactionContext::InstantSend)); + } +} + +/// Mock wallet that returns false for filter checks +pub struct NonMatchingMockWallet { + synced_height: CoreBlockHeight, + event_sender: broadcast::Sender, +} + +impl Default for NonMatchingMockWallet { + fn default() -> Self { + Self::new() + } +} + +impl NonMatchingMockWallet { + pub fn new() -> Self { + let (event_sender, _) = broadcast::channel(16); + Self { + synced_height: 0, + event_sender, + } + } +} + +#[async_trait::async_trait] +impl WalletInterface for NonMatchingMockWallet { + async fn process_block(&mut self, _block: &Block, _height: u32) -> BlockProcessingResult { + BlockProcessingResult::default() + } + + async fn process_mempool_transaction( + &mut self, + _tx: &Transaction, + _is_instant_send: bool, + ) -> MempoolTransactionResult { + MempoolTransactionResult::default() + } + + fn monitored_addresses(&self) -> Vec
{ + Vec::new() + } + + fn watched_outpoints(&self) -> Vec { + Vec::new() + } + + fn synced_height(&self) -> CoreBlockHeight { + self.synced_height + } + + fn update_synced_height(&mut self, height: CoreBlockHeight) { + self.synced_height = height; + } + + fn subscribe_events(&self) -> broadcast::Receiver { + self.event_sender.subscribe() + } + + async fn describe(&self) -> String { + "NonMatchingWallet (test implementation)".to_string() + } +} diff --git a/key-wallet/src/test_utils/mod.rs b/key-wallet/src/test_utils/mod.rs index ad58a1097..bb5390835 100644 --- a/key-wallet/src/test_utils/mod.rs +++ b/key-wallet/src/test_utils/mod.rs @@ -1,9 +1,11 @@ mod account; +#[cfg(feature = "manager")] +mod mock_wallet; mod utxo; mod wallet; #[cfg(feature = "manager")] -pub use wallet::MockWallet; +pub use mock_wallet::MockWallet; #[cfg(feature = "manager")] -pub use wallet::NonMatchingMockWallet; +pub use mock_wallet::NonMatchingMockWallet; pub use wallet::TestWalletContext; diff --git a/key-wallet/src/test_utils/wallet.rs b/key-wallet/src/test_utils/wallet.rs index 9c69a481e..139bca640 100644 --- a/key-wallet/src/test_utils/wallet.rs +++ b/key-wallet/src/test_utils/wallet.rs @@ -13,267 +13,6 @@ impl ManagedWalletInfo { } } -#[cfg(feature = "manager")] -use crate::manager::MempoolTransactionResult; -#[cfg(feature = "manager")] -use crate::manager::{BlockProcessingResult, WalletEvent, WalletInterface}; -#[cfg(feature = "manager")] -use dashcore::address::NetworkUnchecked; -#[cfg(feature = "manager")] -use dashcore::prelude::CoreBlockHeight; -#[cfg(feature = "manager")] -use dashcore::{Block, OutPoint}; -#[cfg(feature = "manager")] -use std::str::FromStr; -#[cfg(feature = "manager")] -use std::{collections::BTreeMap, sync::Arc}; -#[cfg(feature = "manager")] -use tokio::sync::{broadcast, Mutex}; - -#[cfg(feature = "manager")] -type TransactionEffectsMap = Arc)>>>; - -#[cfg(feature = "manager")] -pub struct MockWallet { - processed_blocks: Arc>>, - processed_transactions: Arc>>, - // Map txid -> (net_amount, addresses) - effects: TransactionEffectsMap, - synced_height: CoreBlockHeight, - event_sender: broadcast::Sender, - /// When true, process_mempool_transaction returns is_relevant=true. - mempool_relevant: bool, - /// Addresses returned by monitored_addresses. - addresses: Vec
, - /// Outpoints returned by watched_outpoints. - outpoints: Vec, - /// New addresses returned by process_mempool_transaction. - mempool_new_addresses: Vec
, - /// Recorded status change notifications for test assertions. - status_changes: Arc>>, - /// Monitor revision counter for staleness detection. - monitor_revision: u64, -} - -#[cfg(feature = "manager")] -impl Default for MockWallet { - fn default() -> Self { - Self::new() - } -} - -#[cfg(feature = "manager")] -impl MockWallet { - pub fn new() -> Self { - let (event_sender, _) = broadcast::channel(16); - Self { - processed_blocks: Arc::new(Mutex::new(Vec::new())), - processed_transactions: Arc::new(Mutex::new(Vec::new())), - effects: Arc::new(Mutex::new(BTreeMap::new())), - synced_height: 0, - event_sender, - mempool_relevant: false, - addresses: Vec::new(), - outpoints: Vec::new(), - mempool_new_addresses: Vec::new(), - status_changes: Arc::new(Mutex::new(Vec::new())), - monitor_revision: 0, - } - } - - /// Configure whether mempool transactions are reported as relevant. - pub fn set_mempool_relevant(&mut self, relevant: bool) { - self.mempool_relevant = relevant; - } - - /// Set the addresses returned by monitored_addresses. - pub fn set_addresses(&mut self, addresses: Vec
) { - self.addresses = addresses; - self.monitor_revision += 1; - } - - /// Set the outpoints returned by watched_outpoints. - pub fn set_outpoints(&mut self, outpoints: Vec) { - self.outpoints = outpoints; - self.monitor_revision += 1; - } - - /// Set new addresses returned by process_mempool_transaction. - pub fn set_mempool_new_addresses(&mut self, addresses: Vec
) { - self.mempool_new_addresses = addresses; - } - - pub fn status_changes(&self) -> Arc>> { - self.status_changes.clone() - } - - pub async fn set_effect(&self, txid: dashcore::Txid, net: i64, addresses: Vec) { - let mut map = self.effects.lock().await; - map.insert(txid, (net, addresses)); - } - - pub fn processed_blocks(&self) -> Arc>> { - self.processed_blocks.clone() - } - - pub fn processed_transactions(&self) -> Arc>> { - self.processed_transactions.clone() - } -} - -#[cfg(feature = "manager")] -#[async_trait::async_trait] -impl WalletInterface for MockWallet { - async fn process_block(&mut self, block: &Block, height: u32) -> BlockProcessingResult { - let mut processed = self.processed_blocks.lock().await; - processed.push((block.block_hash(), height)); - - BlockProcessingResult { - new_txids: block.txdata.iter().map(|tx| tx.txid()).collect(), - existing_txids: Vec::new(), - new_addresses: Vec::new(), - } - } - - async fn process_mempool_transaction( - &mut self, - tx: &Transaction, - _is_instant_send: bool, - ) -> MempoolTransactionResult { - let mut processed = self.processed_transactions.lock().await; - processed.push(tx.txid()); - - if !self.mempool_relevant { - return MempoolTransactionResult::default(); - } - - let effects = self.effects.lock().await; - let (net_amount, addresses) = if let Some((net, addr_strs)) = effects.get(&tx.txid()) { - let addrs = addr_strs - .iter() - .filter_map(|s| { - Address::::from_str(s).ok().map(|a| a.assume_checked()) - }) - .collect(); - (*net, addrs) - } else { - (0, Vec::new()) - }; - - MempoolTransactionResult { - is_relevant: true, - net_amount, - is_outgoing: net_amount < 0, - addresses, - new_addresses: self.mempool_new_addresses.clone(), - } - } - - async fn describe(&self) -> String { - "MockWallet (test implementation)".to_string() - } - - async fn transaction_effect(&self, tx: &Transaction) -> Option<(i64, Vec)> { - let map = self.effects.lock().await; - map.get(&tx.txid()).cloned() - } - - fn monitored_addresses(&self) -> Vec
{ - self.addresses.clone() - } - - fn watched_outpoints(&self) -> Vec { - self.outpoints.clone() - } - - fn synced_height(&self) -> CoreBlockHeight { - self.synced_height - } - - fn update_synced_height(&mut self, height: CoreBlockHeight) { - self.synced_height = height; - } - - fn monitor_revision(&self) -> u64 { - self.monitor_revision - } - - fn subscribe_events(&self) -> broadcast::Receiver { - self.event_sender.subscribe() - } - - fn process_instant_send_lock(&mut self, txid: Txid) { - let mut changes = - self.status_changes.try_lock().expect("status_changes lock contention in test helper"); - changes.push((txid, TransactionContext::InstantSend)); - } -} - -#[cfg(feature = "manager")] -/// Mock wallet that returns false for filter checks -pub struct NonMatchingMockWallet { - synced_height: CoreBlockHeight, - event_sender: broadcast::Sender, -} - -#[cfg(feature = "manager")] -impl Default for NonMatchingMockWallet { - fn default() -> Self { - Self::new() - } -} - -#[cfg(feature = "manager")] -impl NonMatchingMockWallet { - pub fn new() -> Self { - let (event_sender, _) = broadcast::channel(16); - Self { - synced_height: 0, - event_sender, - } - } -} - -#[cfg(feature = "manager")] -#[async_trait::async_trait] -impl WalletInterface for NonMatchingMockWallet { - async fn process_block(&mut self, _block: &Block, _height: u32) -> BlockProcessingResult { - BlockProcessingResult::default() - } - - async fn process_mempool_transaction( - &mut self, - _tx: &Transaction, - _is_instant_send: bool, - ) -> MempoolTransactionResult { - MempoolTransactionResult::default() - } - - fn monitored_addresses(&self) -> Vec
{ - Vec::new() - } - - fn watched_outpoints(&self) -> Vec { - Vec::new() - } - - fn synced_height(&self) -> CoreBlockHeight { - self.synced_height - } - - fn update_synced_height(&mut self, height: CoreBlockHeight) { - self.synced_height = height; - } - - fn subscribe_events(&self) -> broadcast::Receiver { - self.event_sender.subscribe() - } - - async fn describe(&self) -> String { - "NonMatchingWallet (test implementation)".to_string() - } -} - /// Pre-built wallet context for transaction checking tests. /// /// Provides a testnet wallet with a default BIP44 account, a pre-derived From bd840dbe2939a1a3301cf8fbe407b7c92d251440 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 15:01:15 +0300 Subject: [PATCH 6/8] fix: use std::sync::Mutex for synchronously-accessed status_changes The status_changes field was using tokio::sync::Mutex but only accessed synchronously via try_lock().expect(), which panics on contention. Switched to std::sync::Mutex with lock().unwrap() which only panics on poison (not contention). Co-Authored-By: Claude Opus 4.6 --- key-wallet/src/test_utils/mock_wallet.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/key-wallet/src/test_utils/mock_wallet.rs b/key-wallet/src/test_utils/mock_wallet.rs index 856dc31e0..bf04b5059 100644 --- a/key-wallet/src/test_utils/mock_wallet.rs +++ b/key-wallet/src/test_utils/mock_wallet.rs @@ -6,7 +6,7 @@ use dashcore::prelude::CoreBlockHeight; use dashcore::{Address, Block, OutPoint, Transaction, Txid}; use std::collections::BTreeMap; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Mutex as StdMutex}; use tokio::sync::{broadcast, Mutex}; // Type alias for transaction effects map @@ -28,7 +28,7 @@ pub struct MockWallet { /// New addresses returned by process_mempool_transaction. mempool_new_addresses: Vec
, /// Recorded status change notifications for test assertions. - status_changes: Arc>>, + status_changes: Arc>>, /// Monitor revision counter for staleness detection. monitor_revision: u64, } @@ -52,7 +52,7 @@ impl MockWallet { addresses: Vec::new(), outpoints: Vec::new(), mempool_new_addresses: Vec::new(), - status_changes: Arc::new(Mutex::new(Vec::new())), + status_changes: Arc::new(StdMutex::new(Vec::new())), monitor_revision: 0, } } @@ -79,7 +79,7 @@ impl MockWallet { self.mempool_new_addresses = addresses; } - pub fn status_changes(&self) -> Arc>> { + pub fn status_changes(&self) -> Arc>> { self.status_changes.clone() } @@ -178,8 +178,7 @@ impl WalletInterface for MockWallet { } fn process_instant_send_lock(&mut self, txid: Txid) { - let mut changes = - self.status_changes.try_lock().expect("status_changes lock contention in test helper"); + let mut changes = self.status_changes.lock().unwrap(); changes.push((txid, TransactionContext::InstantSend)); } } From 938d8415e7efc0444b366a15057fe8fde8b1e94b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 15:15:45 +0300 Subject: [PATCH 7/8] fix: update dash-spv tests for std::sync::Mutex on status_changes The MockWallet.status_changes field was changed from tokio::sync::Mutex to std::sync::Mutex, so callers need .lock().unwrap() instead of .lock().await. Co-Authored-By: Claude Opus 4.6 --- dash-spv/src/sync/mempool/manager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-spv/src/sync/mempool/manager.rs b/dash-spv/src/sync/mempool/manager.rs index 4250672af..200a3403d 100644 --- a/dash-spv/src/sync/mempool/manager.rs +++ b/dash-spv/src/sync/mempool/manager.rs @@ -926,7 +926,7 @@ mod tests { let wallet = manager.wallet.read().await; let status_changes = wallet.status_changes(); - let changes = status_changes.lock().await; + let changes = status_changes.lock().unwrap(); assert_eq!(changes.len(), 1); assert_eq!(changes[0].0, txid); assert_eq!(changes[0].1, TransactionContext::InstantSend); @@ -942,7 +942,7 @@ mod tests { // No immediate wallet notification let wallet = manager.wallet.read().await; let status_changes = wallet.status_changes(); - let changes = status_changes.lock().await; + let changes = status_changes.lock().unwrap(); assert!(changes.is_empty()); // But the txid is remembered for when the transaction arrives From ee5ade047595e2fd998207b8d7599ff0a7506447 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Thu, 26 Mar 2026 15:21:43 +0300 Subject: [PATCH 8/8] revert: keep status_changes as tokio::sync::Mutex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverting the std::sync::Mutex change — dash-spv tests .await on the lock, so it must remain tokio::sync::Mutex. The try_lock() in process_instant_send_lock is fine since it's test-only code and the lock is never held across that call. Co-Authored-By: Claude Opus 4.6 --- dash-spv/src/sync/mempool/manager.rs | 4 ++-- key-wallet/src/test_utils/mock_wallet.rs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dash-spv/src/sync/mempool/manager.rs b/dash-spv/src/sync/mempool/manager.rs index 200a3403d..4250672af 100644 --- a/dash-spv/src/sync/mempool/manager.rs +++ b/dash-spv/src/sync/mempool/manager.rs @@ -926,7 +926,7 @@ mod tests { let wallet = manager.wallet.read().await; let status_changes = wallet.status_changes(); - let changes = status_changes.lock().unwrap(); + let changes = status_changes.lock().await; assert_eq!(changes.len(), 1); assert_eq!(changes[0].0, txid); assert_eq!(changes[0].1, TransactionContext::InstantSend); @@ -942,7 +942,7 @@ mod tests { // No immediate wallet notification let wallet = manager.wallet.read().await; let status_changes = wallet.status_changes(); - let changes = status_changes.lock().unwrap(); + let changes = status_changes.lock().await; assert!(changes.is_empty()); // But the txid is remembered for when the transaction arrives diff --git a/key-wallet/src/test_utils/mock_wallet.rs b/key-wallet/src/test_utils/mock_wallet.rs index bf04b5059..856dc31e0 100644 --- a/key-wallet/src/test_utils/mock_wallet.rs +++ b/key-wallet/src/test_utils/mock_wallet.rs @@ -6,7 +6,7 @@ use dashcore::prelude::CoreBlockHeight; use dashcore::{Address, Block, OutPoint, Transaction, Txid}; use std::collections::BTreeMap; use std::str::FromStr; -use std::sync::{Arc, Mutex as StdMutex}; +use std::sync::Arc; use tokio::sync::{broadcast, Mutex}; // Type alias for transaction effects map @@ -28,7 +28,7 @@ pub struct MockWallet { /// New addresses returned by process_mempool_transaction. mempool_new_addresses: Vec
, /// Recorded status change notifications for test assertions. - status_changes: Arc>>, + status_changes: Arc>>, /// Monitor revision counter for staleness detection. monitor_revision: u64, } @@ -52,7 +52,7 @@ impl MockWallet { addresses: Vec::new(), outpoints: Vec::new(), mempool_new_addresses: Vec::new(), - status_changes: Arc::new(StdMutex::new(Vec::new())), + status_changes: Arc::new(Mutex::new(Vec::new())), monitor_revision: 0, } } @@ -79,7 +79,7 @@ impl MockWallet { self.mempool_new_addresses = addresses; } - pub fn status_changes(&self) -> Arc>> { + pub fn status_changes(&self) -> Arc>> { self.status_changes.clone() } @@ -178,7 +178,8 @@ impl WalletInterface for MockWallet { } fn process_instant_send_lock(&mut self, txid: Txid) { - let mut changes = self.status_changes.lock().unwrap(); + let mut changes = + self.status_changes.try_lock().expect("status_changes lock contention in test helper"); changes.push((txid, TransactionContext::InstantSend)); } }