From bc116ee631798b6a0d6225373e460b5c46f251ea Mon Sep 17 00:00:00 2001 From: xdustinface Date: Wed, 24 Dec 2025 09:59:48 +0100 Subject: [PATCH] refactor: simplify immature transactions handling --- key-wallet/src/managed_account/mod.rs | 4 +- .../src/tests/immature_transaction_tests.rs | 289 --------------- key-wallet/src/tests/mod.rs | 2 - .../transaction_checking/wallet_checker.rs | 217 ++++-------- key-wallet/src/wallet/immature_transaction.rs | 331 ------------------ .../src/wallet/managed_wallet_info/mod.rs | 6 - .../wallet_info_interface.rs | 107 ++---- key-wallet/src/wallet/mod.rs | 1 - 8 files changed, 100 insertions(+), 857 deletions(-) delete mode 100644 key-wallet/src/tests/immature_transaction_tests.rs delete mode 100644 key-wallet/src/wallet/immature_transaction.rs diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index c19e4c9cb..5548ec650 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -265,7 +265,7 @@ impl ManagedAccount { } /// Update the account balance - pub fn update_balance(&mut self) { + pub fn update_balance(&mut self, synced_height: u32) { let mut spendable = 0; let mut unconfirmed = 0; let mut locked = 0; @@ -273,7 +273,7 @@ impl ManagedAccount { let value = utxo.txout.value; if utxo.is_locked { locked += value; - } else if utxo.is_confirmed { + } else if utxo.is_spendable(synced_height) { spendable += value; } else { unconfirmed += value; 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 3c1c2039c..1e59f2e3e 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 036faadce..c4ae95ed1 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); } } } @@ -274,66 +256,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; @@ -373,6 +295,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; @@ -380,6 +303,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 { @@ -590,9 +514,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_synced_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), }; @@ -604,21 +532,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" ); } @@ -709,7 +646,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; @@ -753,6 +690,9 @@ mod tests { }; let block_height = 100000; + // Set synced_height to block where coinbase was received + managed_wallet.update_synced_height(block_height); + let context = TransactionContext::InBlock { height: block_height, block_hash: Some(BlockHash::from_slice(&[1u8; 32]).expect("Should create block hash")), @@ -767,68 +707,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_synced_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 0dcf482e5..363b3d7e4 100644 --- a/key-wallet/src/wallet/managed_wallet_info/mod.rs +++ b/key-wallet/src/wallet/managed_wallet_info/mod.rs @@ -15,7 +15,6 @@ 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; @@ -44,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, } @@ -60,7 +57,6 @@ impl ManagedWalletInfo { description: None, metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::new(), - immature_transactions: ImmatureTransactionCollection::new(), balance: WalletBalance::default(), } } @@ -74,7 +70,6 @@ impl ManagedWalletInfo { description: None, metadata: WalletMetadata::default(), accounts: ManagedAccountCollection::new(), - immature_transactions: ImmatureTransactionCollection::new(), balance: WalletBalance::default(), } } @@ -88,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/wallet_info_interface.rs b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs index 223f575f9..54c66f768 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 @@ -6,7 +6,6 @@ use super::managed_account_operations::ManagedAccountOperations; use crate::account::ManagedAccountTrait; 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, @@ -17,7 +16,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 { @@ -89,14 +88,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; @@ -192,10 +185,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 { @@ -204,8 +194,9 @@ impl WalletInfoInterface for ManagedWalletInfo { fn update_balance(&mut self) { let mut balance = WalletBalance::default(); + let synced_height = self.synced_height(); for account in self.accounts.all_accounts_mut() { - account.update_balance(); + account.update_balance(synced_height); balance += *account.balance(); } self.balance = balance; @@ -227,73 +218,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( @@ -318,16 +272,5 @@ impl WalletInfoInterface for ManagedWalletInfo { fn update_synced_height(&mut self, current_height: u32) { self.metadata.synced_height = current_height; - - 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" - ); - } } } diff --git a/key-wallet/src/wallet/mod.rs b/key-wallet/src/wallet/mod.rs index 858635553..91226c98e 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;