From 53c4c5656e97f650f22780d2139c7d88d2b0e563 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Fri, 12 Dec 2025 03:15:57 +1100 Subject: [PATCH 1/3] feat: Capture new addresses from `maintain_gap_limit` --- dash-spv/src/client/block_processor.rs | 10 +++---- dash-spv/src/client/block_processor_test.rs | 16 +++++++++--- dash-spv/src/sync/message_handlers.rs | 8 +++--- key-wallet-ffi/src/wallet_manager.rs | 4 +-- key-wallet-manager/src/lib.rs | 1 + key-wallet-manager/src/wallet_interface.rs | 23 +++++++++++++--- key-wallet-manager/src/wallet_manager/mod.rs | 26 ++++++++++++++----- .../src/wallet_manager/process_block.rs | 26 ++++++++++--------- .../transaction_checking/account_checker.rs | 3 +++ .../transaction_checking/wallet_checker.rs | 14 +++++++--- 10 files changed, 89 insertions(+), 42 deletions(-) diff --git a/dash-spv/src/client/block_processor.rs b/dash-spv/src/client/block_processor.rs index 0fa11c715..1b5f78942 100644 --- a/dash-spv/src/client/block_processor.rs +++ b/dash-spv/src/client/block_processor.rs @@ -220,11 +220,11 @@ impl BlockProcessor { // Process block with wallet let mut wallet = self.wallet.write().await; - let txids = wallet.process_block(&block, height).await; - if !txids.is_empty() { + let result = wallet.process_block(&block, height).await; + if !result.relevant_txids.is_empty() { tracing::info!( "🎯 Wallet found {} relevant transactions in block {} at height {}", - txids.len(), + result.relevant_txids.len(), block_hash, height ); @@ -236,7 +236,7 @@ impl BlockProcessor { } // Emit TransactionDetected events for each relevant transaction - for txid in &txids { + for txid in &result.relevant_txids { if let Some(tx) = block.txdata.iter().find(|t| &t.txid() == txid) { // Ask the wallet for the precise effect of this transaction let effect = wallet.transaction_effect(tx).await; @@ -269,7 +269,7 @@ impl BlockProcessor { height, hash: block_hash.to_string(), transactions_count: block.txdata.len(), - relevant_transactions: txids.len(), + relevant_transactions: result.relevant_txids.len(), }); // Update chain state if needed diff --git a/dash-spv/src/client/block_processor_test.rs b/dash-spv/src/client/block_processor_test.rs index a8330a2d2..f34bc630d 100644 --- a/dash-spv/src/client/block_processor_test.rs +++ b/dash-spv/src/client/block_processor_test.rs @@ -8,6 +8,7 @@ mod tests { use crate::types::{SpvEvent, SpvStats}; use dashcore::{blockdata::constants::genesis_block, Block, Network, Transaction}; + use key_wallet_manager::BlockProcessingResult; use std::sync::Arc; use tokio::sync::{mpsc, oneshot, Mutex, RwLock}; @@ -40,12 +41,15 @@ mod tests { #[async_trait::async_trait] impl key_wallet_manager::wallet_interface::WalletInterface for MockWallet { - async fn process_block(&mut self, block: &Block, height: u32) -> Vec { + 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)); // Return txids of all transactions in block as "relevant" - block.txdata.iter().map(|tx| tx.txid()).collect() + BlockProcessingResult { + relevant_txids: block.txdata.iter().map(|tx| tx.txid()).collect(), + new_addresses: Vec::new(), + } } async fn process_mempool_transaction(&mut self, tx: &Transaction) { @@ -248,8 +252,12 @@ mod tests { #[async_trait::async_trait] impl key_wallet_manager::wallet_interface::WalletInterface for NonMatchingWallet { - async fn process_block(&mut self, _block: &Block, _height: u32) -> Vec { - Vec::new() + async fn process_block( + &mut self, + _block: &Block, + _height: u32, + ) -> BlockProcessingResult { + BlockProcessingResult::default() } async fn process_mempool_transaction(&mut self, _tx: &Transaction) {} diff --git a/dash-spv/src/sync/message_handlers.rs b/dash-spv/src/sync/message_handlers.rs index 6a8bbbfd6..794279512 100644 --- a/dash-spv/src/sync/message_handlers.rs +++ b/dash-spv/src/sync/message_handlers.rs @@ -745,18 +745,18 @@ impl SyncManager, + /// New addresses generated during gap limit maintenance + pub new_addresses: Vec
, +} /// Trait for wallet implementations to receive SPV events #[async_trait] pub trait WalletInterface: Send + Sync + 'static { - /// Called when a new block is received that may contain relevant transactions - /// Returns transaction IDs that were relevant to the wallet - async fn process_block(&mut self, block: &Block, height: CoreBlockHeight) -> Vec; + /// Called when a new block is received that may contain relevant transactions. + /// Returns processing result including relevant transactions and any new addresses + /// generated during gap limit maintenance. + async fn process_block( + &mut self, + block: &Block, + height: CoreBlockHeight, + ) -> BlockProcessingResult; /// Called when a transaction is seen in the mempool async fn process_mempool_transaction(&mut self, tx: &Transaction); diff --git a/key-wallet-manager/src/wallet_manager/mod.rs b/key-wallet-manager/src/wallet_manager/mod.rs index d52b8c368..a5971ec99 100644 --- a/key-wallet-manager/src/wallet_manager/mod.rs +++ b/key-wallet-manager/src/wallet_manager/mod.rs @@ -49,6 +49,15 @@ pub struct AddressGenerationResult { pub account_type_used: Option, } +/// Result of checking a transaction against all wallets +#[derive(Debug, Clone, Default)] +pub struct CheckTransactionsResult { + /// Wallets that found the transaction relevant + pub affected_wallets: Vec, + /// New addresses generated during gap limit maintenance + pub new_addresses: Vec
, +} + /// High-level wallet manager that manages multiple wallets /// /// Each wallet can contain multiple accounts following BIP44 standard. @@ -450,14 +459,15 @@ impl WalletManager { Ok(wallet_id) } - /// Check a transaction against all wallets and update their states if relevant + /// Check a transaction against all wallets and update their states if relevant. + /// Returns affected wallets and any new addresses generated during gap limit maintenance. pub async fn check_transaction_in_all_wallets( &mut self, tx: &Transaction, context: TransactionContext, update_state_if_found: bool, - ) -> Vec { - let mut relevant_wallets = Vec::new(); + ) -> CheckTransactionsResult { + let mut result = CheckTransactionsResult::default(); // We need to iterate carefully since we're mutating let wallet_ids: Vec = self.wallets.keys().cloned().collect(); @@ -469,18 +479,20 @@ impl WalletManager { let wallet_info_opt = self.wallet_infos.get_mut(&wallet_id); if let (Some(wallet), Some(wallet_info)) = (wallet_opt, wallet_info_opt) { - let result = + let check_result = wallet_info.check_transaction(tx, context, wallet, update_state_if_found).await; // If the transaction is relevant - if result.is_relevant { - relevant_wallets.push(wallet_id); + if check_result.is_relevant { + result.affected_wallets.push(wallet_id); // Note: balance update is already handled in check_transaction } + + result.new_addresses.extend(check_result.new_addresses); } } - relevant_wallets + result } /// Create an account in a specific wallet diff --git a/key-wallet-manager/src/wallet_manager/process_block.rs b/key-wallet-manager/src/wallet_manager/process_block.rs index fae442d58..3d5f7cbb2 100644 --- a/key-wallet-manager/src/wallet_manager/process_block.rs +++ b/key-wallet-manager/src/wallet_manager/process_block.rs @@ -1,4 +1,4 @@ -use crate::wallet_interface::WalletInterface; +use crate::wallet_interface::{BlockProcessingResult, WalletInterface}; use crate::WalletManager; use alloc::string::String; use alloc::vec::Vec; @@ -6,15 +6,19 @@ use async_trait::async_trait; use core::fmt::Write as _; use dashcore::bip158::BlockFilter; use dashcore::prelude::CoreBlockHeight; -use dashcore::{Block, BlockHash, Transaction, Txid}; +use dashcore::{Block, BlockHash, Transaction}; use key_wallet::transaction_checking::transaction_router::TransactionRouter; use key_wallet::transaction_checking::TransactionContext; use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; #[async_trait] impl WalletInterface for WalletManager { - async fn process_block(&mut self, block: &Block, height: CoreBlockHeight) -> Vec { - let mut relevant_txids = Vec::new(); + async fn process_block( + &mut self, + block: &Block, + height: CoreBlockHeight, + ) -> BlockProcessingResult { + let mut result = BlockProcessingResult::default(); let block_hash = Some(block.block_hash()); let timestamp = block.header.time; @@ -26,20 +30,18 @@ impl WalletInterface for WalletM timestamp: Some(timestamp), }; - let affected_wallets = self - .check_transaction_in_all_wallets( - tx, context, true, // update state - ) - .await; + let check_result = self.check_transaction_in_all_wallets(tx, context, true).await; - if !affected_wallets.is_empty() { - relevant_txids.push(tx.txid()); + if !check_result.affected_wallets.is_empty() { + result.relevant_txids.push(tx.txid()); } + + result.new_addresses.extend(check_result.new_addresses); } self.current_height = height; - relevant_txids + result } async fn process_mempool_transaction(&mut self, tx: &Transaction) { diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 3ea246d95..a2040f197 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -38,6 +38,8 @@ pub struct TransactionCheckResult { pub total_sent: u64, /// Total value received for Platform credit conversion pub total_received_for_credit_conversion: u64, + /// New addresses generated during gap limit maintenance + pub new_addresses: Vec
, } /// Enum representing the type of account that matched with embedded data @@ -284,6 +286,7 @@ impl ManagedAccountCollection { total_received: 0, total_sent: 0, total_received_for_credit_conversion: 0, + new_addresses: Vec::new(), }; for account_type in account_types { diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 8ce9d6b23..88af947d4 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -75,7 +75,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { let relevant_types = TransactionRouter::get_relevant_account_types(&tx_type); // Check only relevant account types - let result = self.accounts.check_transaction(tx, &relevant_types); + let mut result = self.accounts.check_transaction(tx, &relevant_types); // Update state if requested and transaction is relevant if update_state && result.is_relevant { @@ -281,11 +281,17 @@ impl WalletTransactionChecker for ManagedWalletInfo { internal_addresses, .. } = &mut account.account_type { - let _ = external_addresses.maintain_gap_limit(&key_source); - let _ = internal_addresses.maintain_gap_limit(&key_source); + if let Ok(new_addrs) = external_addresses.maintain_gap_limit(&key_source) { + result.new_addresses.extend(new_addrs); + } + if let Ok(new_addrs) = internal_addresses.maintain_gap_limit(&key_source) { + result.new_addresses.extend(new_addrs); + } } else { for pool in account.account_type.address_pools_mut() { - let _ = pool.maintain_gap_limit(&key_source); + if let Ok(new_addrs) = pool.maintain_gap_limit(&key_source) { + result.new_addresses.extend(new_addrs); + } } } } From 09a4e883350a8b2ab8acb191a53651d8d60fb708 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Fri, 12 Dec 2025 03:16:06 +1100 Subject: [PATCH 2/3] Add tests --- key-wallet-manager/Cargo.toml | 1 + .../tests/spv_integration_tests.rs | 65 +++++++++++++++---- test-utils/src/builders.rs | 7 ++ 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/key-wallet-manager/Cargo.toml b/key-wallet-manager/Cargo.toml index 6c47aacb2..e709554ed 100644 --- a/key-wallet-manager/Cargo.toml +++ b/key-wallet-manager/Cargo.toml @@ -26,6 +26,7 @@ bincode = { version = "=2.0.0-rc.3", optional = true } zeroize = { version = "1.8", features = ["derive"] } [dev-dependencies] +dashcore-test-utils = { path = "../test-utils" } hex = "0.4" serde_json = "1.0" tokio = { version = "1.32", features = ["full"] } diff --git a/key-wallet-manager/tests/spv_integration_tests.rs b/key-wallet-manager/tests/spv_integration_tests.rs index ea855b2b9..de85bccac 100644 --- a/key-wallet-manager/tests/spv_integration_tests.rs +++ b/key-wallet-manager/tests/spv_integration_tests.rs @@ -1,14 +1,13 @@ //! Integration tests for SPV wallet functionality +use dashcore::bip158::{BlockFilter, BlockFilterWriter}; use dashcore::blockdata::block::{Block, Header, Version}; use dashcore::blockdata::script::ScriptBuf; -use dashcore::blockdata::transaction::{OutPoint, Transaction}; +use dashcore::blockdata::transaction::Transaction; use dashcore::pow::CompactTarget; -use dashcore::{BlockHash, Txid}; -use dashcore::{TxIn, TxOut}; +use dashcore::{BlockHash, OutPoint, TxIn, TxOut, Txid}; use dashcore_hashes::Hash; - -use dashcore::bip158::{BlockFilter, BlockFilterWriter}; +use dashcore_test_utils::create_transaction_to_address; use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::Network; @@ -98,23 +97,61 @@ async fn test_filter_checking() { #[tokio::test] async fn test_block_processing() { let mut manager = WalletManager::::new(Network::Testnet); - - // Create a test wallet let _wallet_id = manager .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default) .expect("Failed to create wallet"); - // Create a transaction - let tx = create_test_transaction(100000); + let addresses = manager.monitored_addresses(); + assert!(!addresses.is_empty()); + let external = dashcore::Address::p2pkh( + &dashcore::PublicKey::from_slice(&[0x02; 33]).expect("valid pubkey"), + Network::Testnet, + ); + + let addresses_before = manager.monitored_addresses(); + assert!(!addresses_before.is_empty()); + + let tx1 = create_transaction_to_address(&addresses[0], 100_000); + let tx2 = create_transaction_to_address(&addresses[1], 200_000); + let tx3 = create_transaction_to_address(&external, 300_000); + + let block = create_test_block(100, vec![tx1.clone(), tx2.clone(), tx3.clone()]); + let result = manager.process_block(&block, 100).await; + + assert_eq!(result.relevant_txids.len(), 2); + assert!(result.relevant_txids.contains(&tx1.txid())); + assert!(result.relevant_txids.contains(&tx2.txid())); + assert!(!result.relevant_txids.contains(&tx3.txid())); + assert_eq!(result.new_addresses.len(), 2); + + let addresses_after = manager.monitored_addresses(); + let actual_increase = addresses_after.len() - addresses_before.len(); + assert_eq!(result.new_addresses.len(), actual_increase); + + for new_addr in &result.new_addresses { + assert!(addresses_after.contains(new_addr)); + } +} + +#[tokio::test] +async fn test_block_processing_result_empty() { + let mut manager = WalletManager::::new(Network::Testnet); + let _wallet_id = manager + .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default) + .expect("Failed to create wallet"); - // Create a block with this transaction - let block = create_test_block(100, vec![tx.clone()]); + let external = dashcore::Address::p2pkh( + &dashcore::PublicKey::from_slice(&[0x02; 33]).expect("valid pubkey"), + Network::Testnet, + ); + let tx1 = create_transaction_to_address(&external, 100_000); + let tx2 = create_transaction_to_address(&external, 200_000); - // Process the block + let block = create_test_block(100, vec![tx1, tx2]); let result = manager.process_block(&block, 100).await; - // Since we're not watching specific addresses, no transactions should be relevant - assert_eq!(result.len(), 0); + assert!(result.relevant_txids.is_empty()); + assert!(result.new_addresses.is_empty()); } #[tokio::test] diff --git a/test-utils/src/builders.rs b/test-utils/src/builders.rs index 22b626ef4..9fbcc6411 100644 --- a/test-utils/src/builders.rs +++ b/test-utils/src/builders.rs @@ -161,6 +161,13 @@ impl TestTransactionBuilder { } } +pub fn create_transaction_to_address(address: &dashcore::Address, value: u64) -> Transaction { + TestTransactionBuilder::new() + .add_input(random_txid(), 0) + .add_output(value, address.script_pubkey()) + .build() +} + /// Create a chain of test headers pub fn create_header_chain(count: usize, start_height: u32) -> Vec
{ let mut headers = Vec::with_capacity(count); From 2b96afc7431e5a3b7adf4a888e69c185193e9a44 Mon Sep 17 00:00:00 2001 From: xdustinface Date: Sat, 20 Dec 2025 09:49:34 +0100 Subject: [PATCH 3/3] Log failures. --- .../transaction_checking/wallet_checker.rs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 88af947d4..d29a01181 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -281,16 +281,40 @@ impl WalletTransactionChecker for ManagedWalletInfo { internal_addresses, .. } = &mut account.account_type { - if let Ok(new_addrs) = external_addresses.maintain_gap_limit(&key_source) { - result.new_addresses.extend(new_addrs); + match external_addresses.maintain_gap_limit(&key_source) { + Ok(new_addrs) => result.new_addresses.extend(new_addrs), + Err(e) => { + tracing::error!( + account_index = ?account_match.account_type_match.account_index(), + pool_type = "external", + error = %e, + "Failed to maintain gap limit for address pool" + ); + } } - if let Ok(new_addrs) = internal_addresses.maintain_gap_limit(&key_source) { - result.new_addresses.extend(new_addrs); + match internal_addresses.maintain_gap_limit(&key_source) { + Ok(new_addrs) => result.new_addresses.extend(new_addrs), + Err(e) => { + tracing::error!( + account_index = ?account_match.account_type_match.account_index(), + pool_type = "internal", + error = %e, + "Failed to maintain gap limit for address pool" + ); + } } } else { for pool in account.account_type.address_pools_mut() { - if let Ok(new_addrs) = pool.maintain_gap_limit(&key_source) { - result.new_addresses.extend(new_addrs); + match pool.maintain_gap_limit(&key_source) { + Ok(new_addrs) => result.new_addresses.extend(new_addrs), + Err(e) => { + tracing::error!( + account_index = ?account_match.account_type_match.account_index(), + pool_type = ?pool.pool_type, + error = %e, + "Failed to maintain gap limit for address pool" + ); + } } } }