diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index 75535f458..7d4675863 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -42,7 +42,7 @@ Functions: 3 ### Wallet Manager -Functions: 19 +Functions: 18 | Function | Description | Module | |----------|-------------|--------| @@ -63,12 +63,11 @@ Functions: 19 | `wallet_manager_get_wallet_ids` | Get wallet IDs # Safety - `manager` must be a valid pointer to an... | wallet_manager | | `wallet_manager_import_wallet_from_bytes` | No description | wallet_manager | | `wallet_manager_process_transaction` | Process a transaction through all wallets Checks a transaction against all... | wallet_manager | -| `wallet_manager_update_height` | Update block height for a network # Safety - `manager` must be a valid... | wallet_manager | | `wallet_manager_wallet_count` | Get wallet count # Safety - `manager` must be a valid pointer to an... | wallet_manager | ### Wallet Operations -Functions: 62 +Functions: 63 | Function | Description | Module | |----------|-------------|--------| @@ -99,6 +98,7 @@ Functions: 62 | `managed_wallet_info_free` | Free managed wallet info returned by wallet_manager_get_managed_wallet_info ... | managed_wallet | | `managed_wallet_mark_address_used` | Mark an address as used in the pool This updates the pool's tracking of... | address_pool | | `managed_wallet_set_gap_limit` | Set the gap limit for an address pool The gap limit determines how many... | address_pool | +| `managed_wallet_synced_height` | Get current synced height from wallet info # Safety - `managed_wallet`... | managed_wallet | | `wallet_add_account` | Add an account to the wallet without xpub # Safety This function... | wallet | | `wallet_add_account_with_string_xpub` | Add an account to the wallet with xpub as string # Safety This function... | wallet | | `wallet_add_account_with_xpub_bytes` | Add an account to the wallet with xpub as byte array # Safety This... | wallet | @@ -666,22 +666,6 @@ Process a transaction through all wallets Checks a transaction against all wall --- -#### `wallet_manager_update_height` - -```c -wallet_manager_update_height(manager: *mut FFIWalletManager, height: c_uint, error: *mut FFIError,) -> bool -``` - -**Description:** -Update block height for a network # Safety - `manager` must be a valid pointer to an FFIWalletManager - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure all pointers remain valid for the duration of this call - -**Safety:** -- `manager` must be a valid pointer to an FFIWalletManager - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure all pointers remain valid for the duration of this call - -**Module:** `wallet_manager` - ---- - #### `wallet_manager_wallet_count` ```c @@ -1120,6 +1104,22 @@ Set the gap limit for an address pool The gap limit determines how many unused --- +#### `managed_wallet_synced_height` + +```c +managed_wallet_synced_height(managed_wallet: *const FFIManagedWalletInfo, error: *mut FFIError,) -> c_uint +``` + +**Description:** +Get current synced height from wallet info # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure all pointers remain valid for the duration of this call + +**Safety:** +- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `error` must be a valid pointer to an FFIError structure or null - The caller must ensure all pointers remain valid for the duration of this call + +**Module:** `managed_wallet` + +--- + #### `wallet_add_account` ```c diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 6cdf65c6f..7da7c404c 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -3089,6 +3089,20 @@ bool managed_wallet_get_balance(const FFIManagedWalletInfo *managed_wallet, FFIError *error) ; +/* + Get current synced height from wallet info + + # Safety + + - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo + - `error` must be a valid pointer to an FFIError structure or null + - The caller must ensure all pointers remain valid for the duration of this call + */ + +unsigned int managed_wallet_synced_height(const FFIManagedWalletInfo *managed_wallet, + FFIError *error) +; + /* Free managed wallet info @@ -4087,21 +4101,6 @@ bool wallet_manager_process_transaction(FFIWalletManager *manager, FFIError *error) ; -/* - Update block height for a network - - # Safety - - - `manager` must be a valid pointer to an FFIWalletManager - - `error` must be a valid pointer to an FFIError structure or null - - The caller must ensure all pointers remain valid for the duration of this call - */ - -bool wallet_manager_update_height(FFIWalletManager *manager, - unsigned int height, - FFIError *error) -; - /* Get current height for a network diff --git a/key-wallet-ffi/src/managed_wallet.rs b/key-wallet-ffi/src/managed_wallet.rs index 33f1931f8..ec5efc442 100644 --- a/key-wallet-ffi/src/managed_wallet.rs +++ b/key-wallet-ffi/src/managed_wallet.rs @@ -5,12 +5,13 @@ //! use std::ffi::CString; -use std::os::raw::c_char; +use std::os::raw::{c_char, c_uint}; use std::ptr; use crate::error::{FFIError, FFIErrorCode}; use crate::types::FFIWallet; use key_wallet::managed_account::address_pool::KeySource; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use std::ffi::c_void; @@ -585,6 +586,31 @@ pub unsafe extern "C" fn managed_wallet_get_balance( true } +/// Get current synced height from wallet info +/// +/// # Safety +/// +/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo +/// - `error` must be a valid pointer to an FFIError structure or null +/// - The caller must ensure all pointers remain valid for the duration of this call +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_synced_height( + managed_wallet: *const FFIManagedWalletInfo, + error: *mut FFIError, +) -> c_uint { + if managed_wallet.is_null() { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Managed wallet is null".to_string(), + ); + return 0; + } + let managed_wallet = unsafe { &*managed_wallet }; + FFIError::set_success(error); + managed_wallet.inner().synced_height() +} + /// Free managed wallet info /// /// # Safety diff --git a/key-wallet-ffi/src/utxo.rs b/key-wallet-ffi/src/utxo.rs index 9e64dde2f..ca9d7b1f9 100644 --- a/key-wallet-ffi/src/utxo.rs +++ b/key-wallet-ffi/src/utxo.rs @@ -1,12 +1,12 @@ //! UTXO management +use crate::error::{FFIError, FFIErrorCode}; +use crate::managed_wallet::FFIManagedWalletInfo; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use std::ffi::CString; use std::os::raw::c_char; use std::ptr; -use crate::error::{FFIError, FFIErrorCode}; -use crate::managed_wallet::FFIManagedWalletInfo; - /// UTXO structure for FFI #[repr(C)] pub struct FFIUTXO { @@ -99,7 +99,7 @@ pub unsafe extern "C" fn managed_wallet_get_utxos( let managed_info = &*managed_info; // Get UTXOs from the managed wallet info - let utxos = managed_info.inner().get_utxos(); + let utxos = managed_info.inner().utxos(); if utxos.is_empty() { *count_out = 0; @@ -108,10 +108,10 @@ pub unsafe extern "C" fn managed_wallet_get_utxos( // Convert UTXOs to FFI format let mut ffi_utxos = Vec::with_capacity(utxos.len()); - for (outpoint, utxo) in utxos { + for utxo in utxos { // Convert txid to byte array let mut txid_bytes = [0u8; 32]; - txid_bytes.copy_from_slice(&outpoint.txid[..]); + txid_bytes.copy_from_slice(&utxo.outpoint.txid[..]); // Convert address to string let address_str = utxo.address.to_string(); @@ -128,7 +128,7 @@ pub unsafe extern "C" fn managed_wallet_get_utxos( let ffi_utxo = FFIUTXO::new( txid_bytes, - outpoint.vout, + utxo.outpoint.vout, utxo.value(), address_str, script_bytes, diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs index 866d2e9c5..f8cb151c6 100644 --- a/key-wallet-ffi/src/wallet_manager.rs +++ b/key-wallet-ffi/src/wallet_manager.rs @@ -775,35 +775,6 @@ pub unsafe extern "C" fn wallet_manager_process_transaction( !relevant_wallets.is_empty() } -/// Update block height for a network -/// -/// # Safety -/// -/// - `manager` must be a valid pointer to an FFIWalletManager -/// - `error` must be a valid pointer to an FFIError structure or null -/// - The caller must ensure all pointers remain valid for the duration of this call -#[no_mangle] -pub unsafe extern "C" fn wallet_manager_update_height( - manager: *mut FFIWalletManager, - height: c_uint, - error: *mut FFIError, -) -> bool { - if manager.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Manager is null".to_string()); - return false; - } - - let manager_ref = &*manager; - - manager_ref.runtime.block_on(async { - let mut manager_guard = manager_ref.manager.write().await; - manager_guard.update_height(height); - }); - - FFIError::set_success(error); - true -} - /// Get current height for a network /// /// # Safety diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 2ca247146..b5e8d9e36 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -207,33 +207,6 @@ mod tests { } } - #[test] - fn test_height_management() { - let mut error = FFIError::success(); - let error = &mut error as *mut FFIError; - - let manager = wallet_manager::wallet_manager_create(FFINetwork::Testnet, error); - assert!(!manager.is_null()); - - // Get initial height - let height = unsafe { wallet_manager::wallet_manager_current_height(manager, error) }; - assert_eq!(height, 0); - - // Update height - let success = - unsafe { wallet_manager::wallet_manager_update_height(manager, 100000, error) }; - assert!(success); - - // Verify height was updated - let height = unsafe { wallet_manager::wallet_manager_current_height(manager, error) }; - assert_eq!(height, 100000); - - // Clean up - unsafe { - wallet_manager::wallet_manager_free(manager); - } - } - #[test] fn test_error_handling() { let mut error = FFIError::success(); @@ -465,7 +438,7 @@ mod tests { } #[test] - fn test_wallet_manager_height_operations() { + fn test_wallet_manager_current_height() { let mut error = FFIError::success(); let error = &mut error as *mut FFIError; @@ -473,12 +446,17 @@ mod tests { assert!(!manager.is_null()); // Get initial height - let _height = unsafe { wallet_manager::wallet_manager_current_height(manager, error) }; + let height = unsafe { wallet_manager::wallet_manager_current_height(manager, error) }; + assert_eq!(height, 0); // Update height let new_height = 12345; unsafe { - wallet_manager::wallet_manager_update_height(manager, new_height, error); + let manager_ref = &*manager; + manager_ref.runtime.block_on(async { + let mut manager_guard = manager_ref.manager.write().await; + manager_guard.update_height(new_height); + }); } // Get updated height diff --git a/key-wallet-manager/src/wallet_manager/mod.rs b/key-wallet-manager/src/wallet_manager/mod.rs index 5ad673754..0ab05f844 100644 --- a/key-wallet-manager/src/wallet_manager/mod.rs +++ b/key-wallet-manager/src/wallet_manager/mod.rs @@ -930,9 +930,12 @@ impl WalletManager { self.current_height } - /// Update current block height for a specific network + /// Update current block height and propagate to all wallet infos pub fn update_height(&mut self, height: u32) { - self.current_height = height + self.current_height = height; + for info in self.wallet_infos.values_mut() { + info.update_chain_height(height); + } } /// Get monitored addresses for all wallets for a specific network diff --git a/key-wallet-manager/tests/integration_test.rs b/key-wallet-manager/tests/integration_test.rs index 99c5e04e9..981e0b6ec 100644 --- a/key-wallet-manager/tests/integration_test.rs +++ b/key-wallet-manager/tests/integration_test.rs @@ -5,6 +5,7 @@ use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::{mnemonic::Language, Mnemonic, Network}; use key_wallet_manager::wallet_manager::{WalletError, WalletManager}; @@ -155,8 +156,75 @@ fn test_balance_calculation() { fn test_block_height_tracking() { let mut manager = WalletManager::::new(Network::Testnet); + // Initial state assert_eq!(manager.current_height(), 0); + // Set height before adding wallets + manager.update_height(1000); + assert_eq!(manager.current_height(), 1000); + + let mnemonic1 = Mnemonic::generate(12, Language::English).unwrap(); + let wallet_id1 = manager + .create_wallet_from_mnemonic( + &mnemonic1.to_string(), + "", + 0, + WalletAccountCreationOptions::Default, + ) + .unwrap(); + + let mnemonic2 = Mnemonic::generate(12, Language::English).unwrap(); + let wallet_id2 = manager + .create_wallet_from_mnemonic( + &mnemonic2.to_string(), + "", + 0, + WalletAccountCreationOptions::Default, + ) + .unwrap(); + + assert_eq!(manager.wallet_count(), 2); + + // Verify both wallets have synced_height of 0 initially + for wallet_info in manager.get_all_wallet_infos().values() { + assert_eq!(wallet_info.synced_height(), 0); + } + + // Update height - should propagate to all wallets manager.update_height(12345); assert_eq!(manager.current_height(), 12345); + + // Verify all wallets got updated + let wallet_info1 = manager.get_wallet_info(&wallet_id1).unwrap(); + let wallet_info2 = manager.get_wallet_info(&wallet_id2).unwrap(); + assert_eq!(wallet_info1.synced_height(), 12345); + assert_eq!(wallet_info2.synced_height(), 12345); + + // Update again - verify subsequent updates work + manager.update_height(20000); + assert_eq!(manager.current_height(), 20000); + + for wallet_info in manager.get_all_wallet_infos().values() { + assert_eq!(wallet_info.synced_height(), 20000); + } + + // Update wallets individually to different heights + let wallet_info1 = manager.get_wallet_info_mut(&wallet_id1).unwrap(); + wallet_info1.update_chain_height(30000); + + let wallet_info2 = manager.get_wallet_info_mut(&wallet_id2).unwrap(); + wallet_info2.update_chain_height(25000); + + // Verify each wallet has its own synced_height + let wallet_info1 = manager.get_wallet_info(&wallet_id1).unwrap(); + let wallet_info2 = manager.get_wallet_info(&wallet_id2).unwrap(); + assert_eq!(wallet_info1.synced_height(), 30000); + assert_eq!(wallet_info2.synced_height(), 25000); + + // Manager update_height still syncs all wallets + manager.update_height(40000); + let wallet_info1 = manager.get_wallet_info(&wallet_id1).unwrap(); + let wallet_info2 = manager.get_wallet_info(&wallet_id2).unwrap(); + assert_eq!(wallet_info1.synced_height(), 40000); + assert_eq!(wallet_info2.synced_height(), 40000); } diff --git a/key-wallet/src/tests/immature_transaction_tests.rs b/key-wallet/src/tests/immature_transaction_tests.rs deleted file mode 100644 index dc8921de6..000000000 --- a/key-wallet/src/tests/immature_transaction_tests.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! Tests for immature transaction tracking -//! -//! Tests coinbase transaction maturity tracking and management. - -use crate::wallet::immature_transaction::{ - AffectedAccounts, ImmatureTransaction, ImmatureTransactionCollection, -}; -use alloc::vec::Vec; -use dashcore::hashes::Hash; -use dashcore::{BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; - -/// Helper to create a coinbase transaction -fn create_test_coinbase(height: u32, value: u64) -> Transaction { - // Create coinbase input with height in scriptSig - let mut script_sig = Vec::new(); - script_sig.push(0x03); // Push 3 bytes - script_sig.extend_from_slice(&height.to_le_bytes()[0..3]); // Height as little-endian - - Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint::null(), // Coinbase has null outpoint - script_sig: ScriptBuf::from(script_sig), - sequence: 0xffffffff, - witness: dashcore::Witness::default(), - }], - output: vec![TxOut { - value, - script_pubkey: ScriptBuf::new(), // Empty for test - }], - special_transaction_payload: None, - } -} - -#[test] -fn test_immature_transaction_creation() { - let tx = create_test_coinbase(100000, 5000000000); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - let immature_tx = ImmatureTransaction::new( - tx.clone(), - 100000, - block_hash, - 1234567890, - 100, // maturity confirmations - true, // is_coinbase - ); - - assert_eq!(immature_tx.txid, tx.txid()); - assert_eq!(immature_tx.height, 100000); - assert!(immature_tx.is_coinbase); -} - -#[test] -fn test_immature_transaction_collection_add() { - let mut collection = ImmatureTransactionCollection::new(); - - // Add transactions at different maturity heights - let tx1 = create_test_coinbase(100000, 5000000000); - let tx2 = create_test_coinbase(100050, 5000000000); - - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - let immature1 = ImmatureTransaction::new(tx1.clone(), 100000, block_hash, 0, 100, true); - let immature2 = ImmatureTransaction::new(tx2.clone(), 100050, block_hash, 0, 100, true); - - collection.insert(immature1); - collection.insert(immature2); - - assert!(collection.contains(&tx1.txid())); - assert!(collection.contains(&tx2.txid())); -} - -#[test] -fn test_immature_transaction_collection_get_mature() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - // Add transactions at different maturity heights - let tx1 = create_test_coinbase(100000, 5000000000); - let tx2 = create_test_coinbase(100050, 5000000000); - let tx3 = create_test_coinbase(100100, 5000000000); - - collection.insert(ImmatureTransaction::new(tx1.clone(), 100000, block_hash, 0, 100, true)); - collection.insert(ImmatureTransaction::new(tx2.clone(), 100050, block_hash, 0, 100, true)); - collection.insert(ImmatureTransaction::new(tx3.clone(), 100100, block_hash, 0, 100, true)); - - // Get transactions that mature at height 100150 or before - let mature = collection.get_matured(100150); - - assert_eq!(mature.len(), 2); - assert!(mature.iter().any(|t| t.txid == tx1.txid())); - assert!(mature.iter().any(|t| t.txid == tx2.txid())); - - // Verify tx3 is not included (matures at 100200) - assert!(!mature.iter().any(|t| t.txid == tx3.txid())); -} - -#[test] -fn test_immature_transaction_collection_remove_mature() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - // Add transactions - let tx1 = create_test_coinbase(100000, 5000000000); - let tx2 = create_test_coinbase(100050, 5000000000); - let tx3 = create_test_coinbase(100100, 5000000000); - - collection.insert(ImmatureTransaction::new(tx1.clone(), 100000, block_hash, 0, 100, true)); - collection.insert(ImmatureTransaction::new(tx2.clone(), 100050, block_hash, 0, 100, true)); - collection.insert(ImmatureTransaction::new(tx3.clone(), 100100, block_hash, 0, 100, true)); - - // Remove mature transactions at height 100150 - let removed = collection.remove_matured(100150); - - assert_eq!(removed.len(), 2); - - // Only tx3 should remain - assert!(!collection.contains(&tx1.txid())); - assert!(!collection.contains(&tx2.txid())); - assert!(collection.contains(&tx3.txid())); -} - -#[test] -fn test_affected_accounts() { - let mut accounts = AffectedAccounts::new(); - - // Add various account types - accounts.add_bip44(0); - accounts.add_bip44(1); - accounts.add_bip44(2); - accounts.add_bip32(0); - accounts.add_coinjoin(0); - - assert_eq!(accounts.count(), 5); - assert!(!accounts.is_empty()); - - assert_eq!(accounts.bip44_accounts.len(), 3); - assert_eq!(accounts.bip32_accounts.len(), 1); - assert_eq!(accounts.coinjoin_accounts.len(), 1); -} - -#[test] -fn test_immature_transaction_collection_clear() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - // Add multiple transactions - for i in 0..5 { - let tx = create_test_coinbase(100000 + i, 5000000000); - collection.insert(ImmatureTransaction::new(tx, 100000 + i, block_hash, 0, 100, true)); - } - - collection.clear(); - assert!(collection.is_empty()); -} - -#[test] -fn test_immature_transaction_height_tracking() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - let tx = create_test_coinbase(100000, 5000000000); - let immature = ImmatureTransaction::new(tx.clone(), 100000, block_hash, 0, 100, true); - - collection.insert(immature); - - // Get the immature transaction - let retrieved = collection.get(&tx.txid()); - assert!(retrieved.is_some()); - assert_eq!(retrieved.unwrap().height, 100000); -} - -#[test] -fn test_immature_transaction_duplicate_add() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - let tx = create_test_coinbase(100000, 5000000000); - - collection.insert(ImmatureTransaction::new(tx.clone(), 100000, block_hash, 0, 100, true)); - - // Adding the same transaction again should replace it - collection.insert(ImmatureTransaction::new(tx.clone(), 100000, block_hash, 0, 100, true)); - - // Still only one transaction - assert!(collection.contains(&tx.txid())); -} - -#[test] -fn test_immature_transaction_batch_maturity() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - // Add multiple transactions that mature at the same height - for i in 0..5 { - let tx = create_test_coinbase(100000 - i, 5000000000); - // All mature at height 100100 (100000 + 100 confirmations) - collection.insert(ImmatureTransaction::new(tx, 100000, block_hash, 0, 100, true)); - } - - // All should mature at height 100100 - let mature = collection.get_matured(100100); - assert_eq!(mature.len(), 5); -} - -#[test] -fn test_immature_transaction_ordering() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - // Add transactions in random order with different maturity heights - let heights = [100, 0, 200, 50]; - let mut txids = Vec::new(); - - for (i, height) in heights.iter().enumerate() { - let tx = create_test_coinbase(100000 + i as u32, 5000000000); - txids.push(tx.txid()); - - collection.insert(ImmatureTransaction::new(tx, 100000 + height, block_hash, 0, 100, true)); - } - - // Get transactions maturing up to height 100200 - let mature = collection.get_matured(100200); - - // Should get transactions at heights 100100, 100150, 100200 (3 total) - assert_eq!(mature.len(), 3); -} - -#[test] -fn test_coinbase_maturity_constant() { - // Verify the standard coinbase maturity is 100 blocks - const COINBASE_MATURITY: u32 = 100; - - let block_height = 500000; - let maturity_height = block_height + COINBASE_MATURITY; - - assert_eq!(maturity_height, 500100); -} - -#[test] -fn test_immature_transaction_empty_account_indices() { - let accounts = AffectedAccounts::new(); - - assert!(accounts.bip44_accounts.is_empty()); - assert!(accounts.bip32_accounts.is_empty()); - assert!(accounts.coinjoin_accounts.is_empty()); - assert!(accounts.is_empty()); -} - -#[test] -fn test_immature_transaction_remove_specific() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - let tx1 = create_test_coinbase(100000, 5000000000); - let tx2 = create_test_coinbase(100050, 5000000000); - - collection.insert(ImmatureTransaction::new(tx1.clone(), 100000, block_hash, 0, 100, true)); - collection.insert(ImmatureTransaction::new(tx2.clone(), 100050, block_hash, 0, 100, true)); - - // Remove specific transaction - let removed = collection.remove(&tx1.txid()); - assert!(removed.is_some()); - - assert!(!collection.contains(&tx1.txid())); - assert!(collection.contains(&tx2.txid())); -} - -#[test] -fn test_immature_transaction_iterator() { - let mut collection = ImmatureTransactionCollection::new(); - let block_hash = BlockHash::from_slice(&[0u8; 32]).unwrap(); - - // Add transactions - let mut expected_txids = Vec::new(); - for i in 0..3 { - let tx = create_test_coinbase(100000 + i, 5000000000); - expected_txids.push(tx.txid()); - - collection.insert(ImmatureTransaction::new(tx, 100000 + i, block_hash, 0, 100, true)); - } - - // Check all transactions are in collection - for txid in &expected_txids { - assert!(collection.contains(txid)); - } -} diff --git a/key-wallet/src/tests/mod.rs b/key-wallet/src/tests/mod.rs index fce02450e..71861e60c 100644 --- a/key-wallet/src/tests/mod.rs +++ b/key-wallet/src/tests/mod.rs @@ -12,8 +12,6 @@ mod backup_restore_tests; mod edge_case_tests; -mod immature_transaction_tests; - mod integration_tests; mod managed_account_collection_tests; diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 52ef3f148..5e2f15cdb 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -5,7 +5,6 @@ pub(crate) use super::account_checker::TransactionCheckResult; use super::transaction_router::TransactionRouter; -use crate::wallet::immature_transaction::ImmatureTransaction; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::managed_wallet_info::ManagedWalletInfo; use crate::{Utxo, Wallet}; @@ -13,7 +12,6 @@ use async_trait::async_trait; use dashcore::blockdata::transaction::Transaction; use dashcore::BlockHash; use dashcore::{Address as DashAddress, OutPoint}; -use dashcore_hashes::Hash; /// Context for transaction processing #[derive(Debug, Clone, Copy)] @@ -79,15 +77,6 @@ impl WalletTransactionChecker for ManagedWalletInfo { // Update state if requested and transaction is relevant if update_state && result.is_relevant { - // Check if this is an immature coinbase transaction before processing accounts - let is_coinbase = tx.is_coin_base(); - let needs_maturity = is_coinbase - && matches!( - context, - TransactionContext::InBlock { .. } - | TransactionContext::InChainLockedBlock { .. } - ); - for account_match in &result.affected_accounts { // Find and update the specific account use super::account_checker::AccountTypeMatch; @@ -178,16 +167,11 @@ impl WalletTransactionChecker for ManagedWalletInfo { is_ours: net_amount < 0, }; - // For immature transactions, skip adding to regular transactions - // They will be added when they mature via process_matured_transactions - if !needs_maturity { - account.transactions.insert(tx.txid(), tx_record); - } + account.transactions.insert(tx.txid(), tx_record); // Ingest UTXOs for outputs that pay to our addresses and // remove UTXOs that are spent by this transaction's inputs. // Only apply for spendable account types (Standard, CoinJoin). - // Skip UTXO creation for immature coinbase transactions. match &mut account.account_type { crate::managed_account::managed_account_type::ManagedAccountType::Standard { .. } | crate::managed_account::managed_account_type::ManagedAccountType::CoinJoin { .. } @@ -206,27 +190,25 @@ impl WalletTransactionChecker for ManagedWalletInfo { | TransactionContext::InChainLockedBlock { height, .. } => (true, height), }; - // Insert UTXOs for matching outputs (skip for immature coinbase) - if !needs_maturity { - let txid = tx.txid(); - for (vout, output) in tx.output.iter().enumerate() { - if let Ok(addr) = DashAddress::from_script(&output.script_pubkey, network) { - if involved_addrs.contains(&addr) { - let outpoint = OutPoint { txid, vout: vout as u32 }; - let txout = dashcore::TxOut { - value: output.value, - script_pubkey: output.script_pubkey.clone(), - }; - let mut utxo = Utxo::new( - outpoint, - txout, - addr, - utxo_height, - tx.is_coin_base(), - ); - utxo.is_confirmed = is_confirmed; - account.utxos.insert(outpoint, utxo); - } + // Insert UTXOs for matching outputs + let txid = tx.txid(); + for (vout, output) in tx.output.iter().enumerate() { + if let Ok(addr) = DashAddress::from_script(&output.script_pubkey, network) { + if involved_addrs.contains(&addr) { + let outpoint = OutPoint { txid, vout: vout as u32 }; + let txout = dashcore::TxOut { + value: output.value, + script_pubkey: output.script_pubkey.clone(), + }; + let mut utxo = Utxo::new( + outpoint, + txout, + addr, + utxo_height, + tx.is_coin_base(), + ); + utxo.is_confirmed = is_confirmed; + account.utxos.insert(outpoint, utxo); } } } @@ -292,66 +274,6 @@ impl WalletTransactionChecker for ManagedWalletInfo { } } - // Store immature transaction if this is a coinbase in a block - if needs_maturity { - if let TransactionContext::InBlock { - height, - block_hash, - timestamp, - } - | TransactionContext::InChainLockedBlock { - height, - block_hash, - timestamp, - } = context - { - let mut immature_tx = ImmatureTransaction::new( - tx.clone(), - height, - block_hash.unwrap_or_else(BlockHash::all_zeros), - timestamp.unwrap_or(0) as u64, - 100, - true, - ); - - use super::account_checker::AccountTypeMatch; - for account_match in &result.affected_accounts { - match &account_match.account_type_match { - AccountTypeMatch::StandardBIP44 { - account_index, - .. - } => { - immature_tx.affected_accounts.add_bip44(*account_index); - } - AccountTypeMatch::StandardBIP32 { - account_index, - .. - } => { - immature_tx.affected_accounts.add_bip32(*account_index); - } - AccountTypeMatch::CoinJoin { - account_index, - .. - } => { - immature_tx.affected_accounts.add_coinjoin(*account_index); - } - _ => {} - } - } - - immature_tx.total_received = result.total_received; - self.add_immature_transaction(immature_tx); - - tracing::info!( - txid = %tx.txid(), - height = height, - maturity_height = height + 100, - received = result.total_received, - "Coinbase transaction stored as immature" - ); - } - } - // Update wallet metadata self.metadata.total_transactions += 1; @@ -391,6 +313,7 @@ impl WalletTransactionChecker for ManagedWalletInfo { mod tests { use super::*; use crate::wallet::initialization::WalletAccountCreationOptions; + use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::{ManagedWalletInfo, Wallet}; use crate::Network; use dashcore::blockdata::script::ScriptBuf; @@ -398,6 +321,7 @@ mod tests { use dashcore::OutPoint; use dashcore::TxOut; use dashcore::{Address, BlockHash, TxIn, Txid}; + use dashcore_hashes::Hash; /// Create a test transaction that sends to a given address fn create_transaction_to_address(address: &Address, amount: u64) -> Transaction { @@ -608,9 +532,13 @@ mod tests { special_transaction_payload: None, }; - // Test with InBlock context (should trigger immature transaction handling) + let block_height = 100000; + // Set synced_height to the block height where coinbase was received + managed_wallet.update_chain_height(block_height); + + // Test with InBlock context let context = TransactionContext::InBlock { - height: 100000, + height: block_height, block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), timestamp: Some(1234567890), }; @@ -622,21 +550,30 @@ mod tests { assert!(result.is_relevant); assert_eq!(result.total_received, 5_000_000_000); - // The transaction should be stored in immature collection, not regular transactions let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); - - // Should NOT be in regular transactions yet assert!( - !managed_account.transactions.contains_key(&coinbase_tx.txid()), - "Immature coinbase should not be in regular transactions" + managed_account.transactions.contains_key(&coinbase_tx.txid()), + "Coinbase should be in regular transactions" ); - // Should be in immature collection + // UTXO should be created with is_coinbase = true + assert!(!managed_account.utxos.is_empty(), "UTXO should be created for coinbase"); + let utxo = managed_account.utxos.values().next().expect("Should have UTXO"); + assert!(utxo.is_coinbase, "UTXO should be marked as coinbase"); + + // Coinbase should be in immature_transactions() since it hasn't matured let immature_txs = managed_wallet.immature_transactions(); + assert_eq!(immature_txs.len(), 1, "Should have one immature transaction"); + assert_eq!(immature_txs[0].txid(), coinbase_tx.txid()); + + // Immature balance should reflect the coinbase value + assert_eq!(managed_wallet.immature_balance(), 5_000_000_000); + + // Spendable UTXOs should be empty (coinbase not mature) assert!( - immature_txs.contains(&coinbase_tx.txid()), - "Coinbase should be in immature collection" + managed_wallet.get_spendable_utxos().is_empty(), + "Coinbase UTXO should not be spendable until mature" ); } @@ -727,7 +664,7 @@ mod tests { assert_eq!(record.net_amount, -(funding_value as i64)); } - /// Test that immature coinbase transactions are properly stored and processed + /// Test the full coinbase maturity flow - immature to mature transition #[tokio::test] async fn test_wallet_checker_immature_transaction_flow() { use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; @@ -771,6 +708,9 @@ mod tests { }; let block_height = 100000; + // Set synced_height to block where coinbase was received + managed_wallet.update_chain_height(block_height); + let context = TransactionContext::InBlock { height: block_height, block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), @@ -785,68 +725,57 @@ mod tests { assert!(result.is_relevant); assert_eq!(result.total_received, 5_000_000_000); - // Verify transaction is NOT in regular transactions yet let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert!( - !managed_account.transactions.contains_key(&coinbase_tx.txid()), - "Immature coinbase should not be in regular transactions" - ); - - // Verify transaction IS in immature collection - let immature_txs = managed_wallet.immature_transactions(); - assert!( - immature_txs.contains(&coinbase_tx.txid()), - "Coinbase should be in immature collection" + managed_account.transactions.contains_key(&coinbase_tx.txid()), + "Coinbase should be in regular transactions" ); - // Verify the immature transaction has correct data - let immature_tx = immature_txs.get(&coinbase_tx.txid()).expect("Should have immature tx"); - assert_eq!(immature_tx.height, block_height); - assert_eq!(immature_tx.total_received, 5_000_000_000); - assert_eq!(immature_tx.maturity_confirmations, 100); - assert!(immature_tx.is_coinbase); - assert!(immature_tx.affected_accounts.bip44_accounts.contains(&0)); + assert!(!managed_account.utxos.is_empty(), "UTXO should be created for coinbase"); + let utxo = managed_account.utxos.values().next().expect("Should have UTXO"); + assert!(utxo.is_coinbase, "UTXO should be marked as coinbase"); + assert_eq!(utxo.height, block_height); - // Verify no UTXOs were created (since it's immature) - assert!(managed_account.utxos.is_empty(), "No UTXOs should exist for immature coinbase"); - - // Verify balance is still zero - assert_eq!( - managed_wallet.balance().total, - 0, - "Balance should be zero while coinbase is immature" - ); + // Coinbase is in immature_transactions() since it hasn't matured + let immature_txs = managed_wallet.immature_transactions(); + assert_eq!(immature_txs.len(), 1, "Should have one immature transaction"); - // Verify immature balance is tracked + // Immature balance should reflect the coinbase value let immature_balance = managed_wallet.immature_balance(); assert_eq!( immature_balance, 5_000_000_000, "Immature balance should reflect the coinbase value" ); + // Spendable UTXOs should be empty (coinbase not mature yet) + assert!( + managed_wallet.get_spendable_utxos().is_empty(), + "No spendable UTXOs while coinbase is immature" + ); + // Now advance the chain height past maturity (100 blocks) let mature_height = block_height + 100; managed_wallet.update_chain_height(mature_height); - // Verify transaction moved from immature to regular let managed_account = managed_wallet.first_bip44_managed_account().expect("Should have managed account"); assert!( managed_account.transactions.contains_key(&coinbase_tx.txid()), - "Matured coinbase should be in regular transactions" + "Coinbase should still be in regular transactions" ); - // Verify transaction is no longer immature + // Coinbase is no longer in immature_transactions() let immature_txs = managed_wallet.immature_transactions(); - assert!( - !immature_txs.contains(&coinbase_tx.txid()), - "Matured coinbase should not be in immature collection" - ); + assert!(immature_txs.is_empty(), "Matured coinbase should not be in immature transactions"); - // Verify immature balance is now zero + // Immature balance should now be zero let immature_balance = managed_wallet.immature_balance(); assert_eq!(immature_balance, 0, "Immature balance should be zero after maturity"); + + // Spendable UTXOs should now contain the matured coinbase + let spendable = managed_wallet.get_spendable_utxos(); + assert_eq!(spendable.len(), 1, "Should have one spendable UTXO after maturity"); } /// Test mempool context for timestamp/height handling diff --git a/key-wallet/src/wallet/immature_transaction.rs b/key-wallet/src/wallet/immature_transaction.rs deleted file mode 100644 index 11e03dcc9..000000000 --- a/key-wallet/src/wallet/immature_transaction.rs +++ /dev/null @@ -1,331 +0,0 @@ -//! Immature transaction tracking for coinbase and special transactions -//! -//! This module provides structures for tracking immature transactions -//! that require confirmations before their outputs can be spent. - -use alloc::collections::BTreeSet; -use alloc::vec::Vec; -use dashcore::blockdata::transaction::Transaction; -use dashcore::{BlockHash, Txid}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -/// Represents an immature transaction with the accounts it affects -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct ImmatureTransaction { - /// The transaction - pub transaction: Transaction, - /// Transaction ID - pub txid: Txid, - /// Block height where transaction was confirmed - pub height: u32, - /// Block hash where transaction was confirmed - pub block_hash: BlockHash, - /// Timestamp of the block - pub timestamp: u64, - /// Number of confirmations needed to mature (typically 100 for coinbase) - pub maturity_confirmations: u32, - /// Accounts affected by this transaction - pub affected_accounts: AffectedAccounts, - /// Total amount received by our accounts - pub total_received: u64, - /// Whether this is a coinbase transaction - pub is_coinbase: bool, -} - -/// Tracks which accounts are affected by an immature transaction -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct AffectedAccounts { - /// BIP44 account indices that received funds - pub bip44_accounts: BTreeSet, - /// BIP32 account indices that received funds - pub bip32_accounts: BTreeSet, - /// CoinJoin account indices that received funds - pub coinjoin_accounts: BTreeSet, -} - -impl AffectedAccounts { - /// Create a new empty set of affected accounts - pub fn new() -> Self { - Self { - bip44_accounts: BTreeSet::new(), - bip32_accounts: BTreeSet::new(), - coinjoin_accounts: BTreeSet::new(), - } - } - - /// Check if any accounts are affected - pub fn is_empty(&self) -> bool { - self.bip44_accounts.is_empty() - && self.bip32_accounts.is_empty() - && self.coinjoin_accounts.is_empty() - } - - /// Get total number of affected accounts - pub fn count(&self) -> usize { - self.bip44_accounts.len() + self.bip32_accounts.len() + self.coinjoin_accounts.len() - } - - /// Add a BIP44 account - pub fn add_bip44(&mut self, index: u32) { - self.bip44_accounts.insert(index); - } - - /// Add a BIP32 account - pub fn add_bip32(&mut self, index: u32) { - self.bip32_accounts.insert(index); - } - - /// Add a CoinJoin account - pub fn add_coinjoin(&mut self, index: u32) { - self.coinjoin_accounts.insert(index); - } -} - -impl ImmatureTransaction { - /// Create a new immature transaction - pub fn new( - transaction: Transaction, - height: u32, - block_hash: BlockHash, - timestamp: u64, - maturity_confirmations: u32, - is_coinbase: bool, - ) -> Self { - let txid = transaction.txid(); - Self { - transaction, - txid, - height, - block_hash, - timestamp, - maturity_confirmations, - affected_accounts: AffectedAccounts::new(), - total_received: 0, - is_coinbase, - } - } - - /// Check if the transaction has matured based on current chain height - pub fn is_mature(&self, current_height: u32) -> bool { - if current_height < self.height { - return false; - } - let confirmations = (current_height - self.height) + 1; - confirmations >= self.maturity_confirmations - } - - /// Get the number of confirmations - pub fn confirmations(&self, current_height: u32) -> u32 { - if current_height >= self.height { - (current_height - self.height) + 1 - } else { - 0 - } - } - - /// Get remaining confirmations until mature - pub fn remaining_confirmations(&self, current_height: u32) -> u32 { - let confirmations = self.confirmations(current_height); - self.maturity_confirmations.saturating_sub(confirmations) - } -} - -/// Collection of immature transactions indexed by maturity height -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct ImmatureTransactionCollection { - /// Map of maturity height to list of transactions that will mature at that height - transactions_by_maturity_height: alloc::collections::BTreeMap>, - /// Secondary index: txid to maturity height for quick lookups - txid_to_height: alloc::collections::BTreeMap, -} - -impl ImmatureTransactionCollection { - /// Create a new empty collection - pub fn new() -> Self { - Self { - transactions_by_maturity_height: alloc::collections::BTreeMap::new(), - txid_to_height: alloc::collections::BTreeMap::new(), - } - } - - /// Add an immature transaction - pub fn insert(&mut self, tx: ImmatureTransaction) { - let maturity_height = tx.height + tx.maturity_confirmations; - let txid = tx.txid; - - // Add to the maturity height index - self.transactions_by_maturity_height.entry(maturity_height).or_default().push(tx); - - // Add to txid index - self.txid_to_height.insert(txid, maturity_height); - } - - /// Remove an immature transaction by txid - pub fn remove(&mut self, txid: &Txid) -> Option { - // Find the maturity height for this txid - if let Some(maturity_height) = self.txid_to_height.remove(txid) { - // Find and remove from the transactions list at that height - if let Some(transactions) = - self.transactions_by_maturity_height.get_mut(&maturity_height) - { - if let Some(pos) = transactions.iter().position(|tx| tx.txid == *txid) { - let tx = transactions.remove(pos); - - // If this was the last transaction at this height, remove the entry - if transactions.is_empty() { - self.transactions_by_maturity_height.remove(&maturity_height); - } - - return Some(tx); - } - } - } - None - } - - /// Get an immature transaction by txid - pub fn get(&self, txid: &Txid) -> Option<&ImmatureTransaction> { - if let Some(maturity_height) = self.txid_to_height.get(txid) { - if let Some(transactions) = self.transactions_by_maturity_height.get(maturity_height) { - return transactions.iter().find(|tx| tx.txid == *txid); - } - } - None - } - - /// Get a mutable reference to an immature transaction - pub fn get_mut(&mut self, txid: &Txid) -> Option<&mut ImmatureTransaction> { - if let Some(maturity_height) = self.txid_to_height.get(txid) { - if let Some(transactions) = - self.transactions_by_maturity_height.get_mut(maturity_height) - { - return transactions.iter_mut().find(|tx| tx.txid == *txid); - } - } - None - } - - /// Check if a transaction is in the collection - pub fn contains(&self, txid: &Txid) -> bool { - self.txid_to_height.contains_key(txid) - } - - /// Get all transactions that have matured at or before the given height - pub fn get_matured(&self, current_height: u32) -> Vec<&ImmatureTransaction> { - let mut matured = Vec::new(); - - // Iterate through all heights up to and including current_height - for (_, transactions) in self.transactions_by_maturity_height.range(..=current_height) { - matured.extend(transactions.iter()); - } - - matured - } - - /// Remove and return all matured transactions - pub fn remove_matured(&mut self, current_height: u32) -> Vec { - let mut matured = Vec::new(); - - // Collect all maturity heights that have been reached - let matured_heights: Vec = self - .transactions_by_maturity_height - .range(..=current_height) - .map(|(height, _)| *height) - .collect(); - - // Remove all transactions at matured heights - for height in matured_heights { - if let Some(transactions) = self.transactions_by_maturity_height.remove(&height) { - // Remove txids from index - for tx in &transactions { - self.txid_to_height.remove(&tx.txid); - } - matured.extend(transactions); - } - } - - matured - } - - /// Get all immature transactions - pub fn all(&self) -> Vec<&ImmatureTransaction> { - self.transactions_by_maturity_height.values().flat_map(|txs| txs.iter()).collect() - } - - /// Get number of immature transactions - pub fn len(&self) -> usize { - self.txid_to_height.len() - } - - /// Check if empty - pub fn is_empty(&self) -> bool { - self.txid_to_height.is_empty() - } - - /// Clear all transactions - pub fn clear(&mut self) { - self.transactions_by_maturity_height.clear(); - self.txid_to_height.clear(); - } - - /// Get total value of all immature transactions - pub fn total_immature_balance(&self) -> u64 { - self.transactions_by_maturity_height - .values() - .flat_map(|txs| txs.iter()) - .map(|tx| tx.total_received) - .sum() - } - - /// Get immature balance for BIP44 accounts - pub fn bip44_immature_balance(&self, account_index: u32) -> u64 { - self.transactions_by_maturity_height - .values() - .flat_map(|txs| txs.iter()) - .filter(|tx| tx.affected_accounts.bip44_accounts.contains(&account_index)) - .map(|tx| tx.total_received) - .sum() - } - - /// Get immature balance for BIP32 accounts - pub fn bip32_immature_balance(&self, account_index: u32) -> u64 { - self.transactions_by_maturity_height - .values() - .flat_map(|txs| txs.iter()) - .filter(|tx| tx.affected_accounts.bip32_accounts.contains(&account_index)) - .map(|tx| tx.total_received) - .sum() - } - - /// Get immature balance for CoinJoin accounts - pub fn coinjoin_immature_balance(&self, account_index: u32) -> u64 { - self.transactions_by_maturity_height - .values() - .flat_map(|txs| txs.iter()) - .filter(|tx| tx.affected_accounts.coinjoin_accounts.contains(&account_index)) - .map(|tx| tx.total_received) - .sum() - } - - /// Get transactions that will mature at a specific height - pub fn at_height(&self, height: u32) -> Vec<&ImmatureTransaction> { - self.transactions_by_maturity_height - .get(&height) - .map(|txs| txs.iter().collect()) - .unwrap_or_default() - } - - /// Get the next maturity height (the lowest height where transactions will mature) - pub fn next_maturity_height(&self) -> Option { - self.transactions_by_maturity_height.keys().next().copied() - } - - /// Get all maturity heights - pub fn maturity_heights(&self) -> Vec { - self.transactions_by_maturity_height.keys().copied().collect() - } -} diff --git a/key-wallet/src/wallet/managed_wallet_info/mod.rs b/key-wallet/src/wallet/managed_wallet_info/mod.rs index 9cf3be33b..e690d6527 100644 --- a/key-wallet/src/wallet/managed_wallet_info/mod.rs +++ b/key-wallet/src/wallet/managed_wallet_info/mod.rs @@ -10,13 +10,11 @@ pub mod managed_account_operations; pub mod managed_accounts; pub mod transaction_builder; pub mod transaction_building; -pub mod utxo; pub mod wallet_info_interface; pub use managed_account_operations::ManagedAccountOperations; use super::balance::WalletBalance; -use super::immature_transaction::ImmatureTransactionCollection; use super::metadata::WalletMetadata; use crate::account::ManagedAccountCollection; use crate::Network; @@ -45,8 +43,6 @@ pub struct ManagedWalletInfo { pub metadata: WalletMetadata, /// All managed accounts pub accounts: ManagedAccountCollection, - /// Immature transactions - pub immature_transactions: ImmatureTransactionCollection, /// Cached wallet balance - should be updated when accounts change pub balance: WalletBalance, } @@ -61,7 +57,6 @@ impl ManagedWalletInfo { description: None, metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::new(), - immature_transactions: ImmatureTransactionCollection::new(), balance: WalletBalance::default(), } } @@ -75,7 +70,6 @@ impl ManagedWalletInfo { description: None, metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::new(), - immature_transactions: ImmatureTransactionCollection::new(), balance: WalletBalance::default(), } } @@ -89,7 +83,6 @@ impl ManagedWalletInfo { description: None, metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::from_account_collection(&wallet.accounts), - immature_transactions: ImmatureTransactionCollection::new(), balance: WalletBalance::default(), } } diff --git a/key-wallet/src/wallet/managed_wallet_info/utxo.rs b/key-wallet/src/wallet/managed_wallet_info/utxo.rs deleted file mode 100644 index 6716a1256..000000000 --- a/key-wallet/src/wallet/managed_wallet_info/utxo.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! UTXO retrieval functionality for managed wallets -//! -//! This module provides methods to retrieve UTXOs from managed wallet accounts. - -use crate::utxo::Utxo; -use alloc::collections::BTreeMap; -use alloc::vec::Vec; -use dashcore::blockdata::transaction::OutPoint; - -use super::ManagedWalletInfo; - -/// Type alias for UTXOs grouped by account type -type UtxosByAccountType = BTreeMap<&'static str, Vec<(u32, Vec<(OutPoint, Utxo)>)>>; - -impl ManagedWalletInfo { - /// Get all UTXOs for the wallet - /// - /// Returns UTXOs from BIP44, BIP32, and CoinJoin accounts only. - /// Does not include UTXOs from identity or provider accounts. - pub fn get_utxos(&self) -> Vec<(OutPoint, Utxo)> { - let mut all_utxos = Vec::new(); - - // Collect UTXOs from standard BIP44 accounts - for account in self.accounts.standard_bip44_accounts.values() { - for (outpoint, utxo) in &account.utxos { - all_utxos.push((*outpoint, utxo.clone())); - } - } - - // Collect UTXOs from standard BIP32 accounts - for account in self.accounts.standard_bip32_accounts.values() { - for (outpoint, utxo) in &account.utxos { - all_utxos.push((*outpoint, utxo.clone())); - } - } - - // Collect UTXOs from CoinJoin accounts - for account in self.accounts.coinjoin_accounts.values() { - for (outpoint, utxo) in &account.utxos { - all_utxos.push((*outpoint, utxo.clone())); - } - } - - all_utxos - } - - /// Get UTXOs grouped by account type - /// - /// Returns a map where: - /// - Keys are account type strings ("bip44", "bip32", "coinjoin") - /// - Values are vectors of (account_index, Vec<(OutPoint, Utxo)>) tuples - pub fn get_utxos_by_account_type(&self) -> UtxosByAccountType { - let mut utxos_by_type = BTreeMap::new(); - - // Collect BIP44 account UTXOs - let mut bip44_utxos = Vec::new(); - for (index, account) in &self.accounts.standard_bip44_accounts { - let account_utxos: Vec<(OutPoint, Utxo)> = - account.utxos.iter().map(|(outpoint, utxo)| (*outpoint, utxo.clone())).collect(); - if !account_utxos.is_empty() { - bip44_utxos.push((*index, account_utxos)); - } - } - if !bip44_utxos.is_empty() { - utxos_by_type.insert("bip44", bip44_utxos); - } - - // Collect BIP32 account UTXOs - let mut bip32_utxos = Vec::new(); - for (index, account) in &self.accounts.standard_bip32_accounts { - let account_utxos: Vec<(OutPoint, Utxo)> = - account.utxos.iter().map(|(outpoint, utxo)| (*outpoint, utxo.clone())).collect(); - if !account_utxos.is_empty() { - bip32_utxos.push((*index, account_utxos)); - } - } - if !bip32_utxos.is_empty() { - utxos_by_type.insert("bip32", bip32_utxos); - } - - // Collect CoinJoin account UTXOs - let mut coinjoin_utxos = Vec::new(); - for (index, account) in &self.accounts.coinjoin_accounts { - let account_utxos: Vec<(OutPoint, Utxo)> = - account.utxos.iter().map(|(outpoint, utxo)| (*outpoint, utxo.clone())).collect(); - if !account_utxos.is_empty() { - coinjoin_utxos.push((*index, account_utxos)); - } - } - if !coinjoin_utxos.is_empty() { - utxos_by_type.insert("coinjoin", coinjoin_utxos); - } - - utxos_by_type - } - - /// Get spendable UTXOs at a given block height - /// - /// Returns only UTXOs that can be spent at the current height from - /// BIP44, BIP32, and CoinJoin accounts. - pub fn get_spendable_utxos(&self, current_height: u32) -> Vec<(OutPoint, Utxo)> { - self.get_utxos().into_iter().filter(|(_, utxo)| utxo.is_spendable(current_height)).collect() - } - - /// Get total value of all UTXOs - /// - /// Returns the sum of all UTXO values from BIP44, BIP32, and CoinJoin accounts - pub fn get_total_utxo_value(&self) -> u64 { - self.get_utxos().iter().map(|(_, utxo)| utxo.value()).sum() - } - - /// Get UTXO count - /// - /// Returns the total number of UTXOs from BIP44, BIP32, and CoinJoin accounts - pub fn get_utxo_count(&self) -> usize { - let mut count = 0; - - // Count BIP44 account UTXOs - for account in self.accounts.standard_bip44_accounts.values() { - count += account.utxos.len(); - } - - // Count BIP32 account UTXOs - for account in self.accounts.standard_bip32_accounts.values() { - count += account.utxos.len(); - } - - // Count CoinJoin account UTXOs - for account in self.accounts.coinjoin_accounts.values() { - count += account.utxos.len(); - } - - count - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::bip32::DerivationPath; - use crate::managed_account::managed_account_type::ManagedAccountType; - use crate::managed_account::ManagedAccount; - use crate::Network; - use dashcore::{Address, PublicKey, ScriptBuf, TxOut, Txid}; - use dashcore_hashes::Hash; - use std::str::FromStr; - - #[test] - fn test_get_utxos_empty() { - let managed_info = ManagedWalletInfo::new(Network::Testnet, [0u8; 32]); - let utxos = managed_info.get_utxos(); - assert_eq!(utxos.len(), 0); - } - - #[test] - fn test_get_utxos_with_accounts() { - let mut managed_info = ManagedWalletInfo::new(Network::Testnet, [0u8; 32]); - - // Create a BIP44 account with some UTXOs - let base_path = DerivationPath::from_str("m/44'/5'/0'").unwrap(); - let external_path = base_path.child(0.into()); - let internal_path = base_path.child(1.into()); - - let mut bip44_account = ManagedAccount::new( - ManagedAccountType::Standard { - index: 0, - standard_account_type: - crate::account::account_type::StandardAccountType::BIP44Account, - external_addresses: - crate::managed_account::address_pool::AddressPool::new_without_generation( - external_path, - crate::managed_account::address_pool::AddressPoolType::External, - 20, - Network::Testnet, - ), - internal_addresses: - crate::managed_account::address_pool::AddressPool::new_without_generation( - internal_path, - crate::managed_account::address_pool::AddressPoolType::Internal, - 20, - Network::Testnet, - ), - }, - Network::Testnet, - false, - ); - - // Add a test UTXO - let outpoint = OutPoint { - txid: Txid::all_zeros(), - vout: 0, - }; - let txout = TxOut { - value: 100000, - script_pubkey: ScriptBuf::new(), - }; - let address = Address::p2pkh( - &PublicKey::from_slice(&[ - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, - ]) - .unwrap(), - Network::Testnet, - ); - let utxo = Utxo::new(outpoint, txout, address, 0, false); - - bip44_account.utxos.insert(outpoint, utxo); - managed_info.accounts.standard_bip44_accounts.insert(0, bip44_account); - - // Test getting UTXOs - let utxos = managed_info.get_utxos(); - assert_eq!(utxos.len(), 1); - assert_eq!(utxos[0].1.value(), 100000); - } -} diff --git a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs index baee130b2..897af1de8 100644 --- a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs +++ b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs @@ -5,7 +5,6 @@ use super::managed_account_operations::ManagedAccountOperations; use crate::managed_account::managed_account_collection::ManagedAccountCollection; use crate::transaction_checking::WalletTransactionChecker; -use crate::wallet::immature_transaction::{ImmatureTransaction, ImmatureTransactionCollection}; use crate::wallet::managed_wallet_info::fee::FeeLevel; use crate::wallet::managed_wallet_info::transaction_building::{ AccountTypePreference, TransactionError, @@ -16,7 +15,7 @@ use crate::{Network, Utxo, Wallet, WalletBalance}; use alloc::collections::BTreeSet; use alloc::vec::Vec; use dashcore::prelude::CoreBlockHeight; -use dashcore::{Address as DashAddress, Address, Transaction}; +use dashcore::{Address as DashAddress, Address, Transaction, Txid}; /// Trait that wallet info types must implement to work with WalletManager pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccountOperations { @@ -61,6 +60,9 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount /// Update last synced timestamp fn update_last_synced(&mut self, timestamp: u64); + /// Get the synced height + fn synced_height(&self) -> CoreBlockHeight; + /// Get all monitored addresses fn monitored_addresses(&self) -> Vec; @@ -85,14 +87,8 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount /// Get accounts (immutable) fn accounts(&self) -> &ManagedAccountCollection; - /// Process matured transactions for a given chain height - fn process_matured_transactions(&mut self, current_height: u32) -> Vec; - - /// Add an immature transaction - fn add_immature_transaction(&mut self, tx: ImmatureTransaction); - /// Get immature transactions - fn immature_transactions(&self) -> &ImmatureTransactionCollection; + fn immature_transactions(&self) -> Vec; /// Get immature balance fn immature_balance(&self) -> u64; @@ -156,6 +152,10 @@ impl WalletInfoInterface for ManagedWalletInfo { self.metadata.birth_height = height; } + fn synced_height(&self) -> CoreBlockHeight { + self.metadata.synced_height + } + fn first_loaded_at(&self) -> u64 { self.metadata.first_loaded_at } @@ -184,10 +184,7 @@ impl WalletInfoInterface for ManagedWalletInfo { utxos } fn get_spendable_utxos(&self) -> BTreeSet<&Utxo> { - self.utxos() - .into_iter() - .filter(|utxo| !utxo.is_locked && (utxo.is_confirmed || utxo.is_instantlocked)) - .collect() + self.utxos().into_iter().filter(|utxo| utxo.is_spendable(self.synced_height())).collect() } fn balance(&self) -> WalletBalance { @@ -233,73 +230,36 @@ impl WalletInfoInterface for ManagedWalletInfo { &self.accounts } - fn process_matured_transactions(&mut self, current_height: u32) -> Vec { - let matured = self.immature_transactions.remove_matured(current_height); - - // Update accounts with matured transactions - for tx in &matured { - // Process BIP44 accounts - for &index in &tx.affected_accounts.bip44_accounts { - if let Some(account) = self.accounts.standard_bip44_accounts.get_mut(&index) { - let tx_record = TransactionRecord::new_confirmed( - tx.transaction.clone(), - tx.height, - tx.block_hash, - tx.timestamp, - tx.total_received as i64, - false, - ); - account.transactions.insert(tx.txid, tx_record); - } - } + fn immature_transactions(&self) -> Vec { + let mut immature_txids: BTreeSet = BTreeSet::new(); - // Process BIP32 accounts - for &index in &tx.affected_accounts.bip32_accounts { - if let Some(account) = self.accounts.standard_bip32_accounts.get_mut(&index) { - let tx_record = TransactionRecord::new_confirmed( - tx.transaction.clone(), - tx.height, - tx.block_hash, - tx.timestamp, - tx.total_received as i64, - false, - ); - account.transactions.insert(tx.txid, tx_record); + // Find txids of immature coinbase UTXOs + for account in self.accounts.all_accounts() { + for utxo in account.utxos.values() { + if utxo.is_coinbase && !utxo.is_mature(self.synced_height()) { + immature_txids.insert(utxo.outpoint.txid); } } + } - // Process CoinJoin accounts - for &index in &tx.affected_accounts.coinjoin_accounts { - if let Some(account) = self.accounts.coinjoin_accounts.get_mut(&index) { - let tx_record = TransactionRecord::new_confirmed( - tx.transaction.clone(), - tx.height, - tx.block_hash, - tx.timestamp, - tx.total_received as i64, - false, - ); - account.transactions.insert(tx.txid, tx_record); + // Get the actual transactions + let mut transactions = Vec::new(); + for account in self.accounts.all_accounts() { + for (txid, record) in &account.transactions { + if immature_txids.contains(txid) { + transactions.push(record.transaction.clone()); } } } - - // Update balance after processing matured transactions - self.update_balance(); - - matured - } - - fn add_immature_transaction(&mut self, tx: ImmatureTransaction) { - self.immature_transactions.insert(tx); - } - - fn immature_transactions(&self) -> &ImmatureTransactionCollection { - &self.immature_transactions + transactions } fn immature_balance(&self) -> u64 { - self.immature_transactions.total_immature_balance() + self.utxos() + .iter() + .filter(|utxo| utxo.is_coinbase && !utxo.is_mature(self.synced_height())) + .map(|utxo| utxo.value()) + .sum() } fn create_unsigned_payment_transaction( @@ -323,15 +283,6 @@ impl WalletInfoInterface for ManagedWalletInfo { } fn update_chain_height(&mut self, current_height: u32) { - let matured = self.process_matured_transactions(current_height); - - if !matured.is_empty() { - tracing::info!( - network = ?self.network, - current_height = current_height, - matured_count = matured.len(), - "Processed matured coinbase transactions" - ); - } + self.metadata.synced_height = current_height; } } diff --git a/key-wallet/src/wallet/metadata.rs b/key-wallet/src/wallet/metadata.rs index 937f836f8..5ecd34371 100644 --- a/key-wallet/src/wallet/metadata.rs +++ b/key-wallet/src/wallet/metadata.rs @@ -16,6 +16,8 @@ pub struct WalletMetadata { pub first_loaded_at: u64, /// Birth height (when wallet was created/restored) - 0 (genesis) if unknown pub birth_height: CoreBlockHeight, + /// Synced to block height + pub synced_height: CoreBlockHeight, /// Last sync timestamp pub last_synced: Option, /// Total transactions diff --git a/key-wallet/src/wallet/mod.rs b/key-wallet/src/wallet/mod.rs index 3623f0821..a17d1210e 100644 --- a/key-wallet/src/wallet/mod.rs +++ b/key-wallet/src/wallet/mod.rs @@ -9,7 +9,6 @@ pub mod balance; #[cfg(feature = "bip38")] pub mod bip38; pub mod helper; -pub mod immature_transaction; pub mod initialization; pub mod managed_wallet_info; pub mod metadata;