From 0834e95f2ca57924d665e65cd97db4b7295f892a Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Tue, 12 May 2026 14:11:55 -0700 Subject: [PATCH] test(dash-spv): cover wallet reload pool-state loss past gap limit --- dash-spv/tests/dashd_sync/tests_restart.rs | 149 +++++++++++++++++- key-wallet-ffi/src/managed_account.rs | 2 +- key-wallet-ffi/src/utxo_tests.rs | 11 +- .../managed_core_funds_account.rs | 78 ++++++++- key-wallet/src/test_utils/wallet.rs | 2 +- key-wallet/src/tests/balance_tests.rs | 13 +- .../transaction_checking/account_checker.rs | 2 +- .../transaction_checking/wallet_checker.rs | 36 ++--- .../managed_wallet_info/asset_lock_builder.rs | 4 +- .../transaction_builder.rs | 2 +- .../transaction_building.rs | 4 +- .../wallet_info_interface.rs | 4 +- 12 files changed, 259 insertions(+), 48 deletions(-) diff --git a/dash-spv/tests/dashd_sync/tests_restart.rs b/dash-spv/tests/dashd_sync/tests_restart.rs index 4c043fd58..37e4039ac 100644 --- a/dash-spv/tests/dashd_sync/tests_restart.rs +++ b/dash-spv/tests/dashd_sync/tests_restart.rs @@ -12,6 +12,13 @@ use dash_spv::test_utils::SYNC_TIMEOUT; use super::setup::{create_and_start_client, TestContext}; use dash_spv::test_utils::{create_test_wallet, TestChain}; +use dashcore::Amount; +use key_wallet::managed_account::address_pool::KeySource; +use key_wallet::managed_account::managed_account_trait::ManagedAccountTrait; +use key_wallet::managed_account::managed_account_type::ManagedAccountType; +use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; +use key_wallet::wallet::Wallet; + /// Verify sync state is identical after stopping and restarting with same storage. #[tokio::test] async fn test_sync_restart_consistency() { @@ -127,11 +134,11 @@ async fn test_sync_with_multiple_restarts() { Ok(ref event) if is_progress_event(event) => { events_seen += 1; if events_seen % 2 == 0 { - tracing::info!("Restarting on: {}", event.description()); + tracing::info!("Restarting on: {}", event); should_restart = true; break; } - tracing::info!("Skipped: {}", event.description()); + tracing::info!("Skipped: {}", event); } Ok(SyncEvent::SyncComplete { .. }) => break, Ok(_) => continue, @@ -191,3 +198,141 @@ async fn test_sync_with_random_restarts() { ctx.assert_synced(&client_handle.client.progress().await).await; tracing::info!("Sync completed after {} random restarts (seed={})", num_restarts, seed); } + +/// When a wallet is rebuilt from xpubs alone (the platform persister +/// flow) and snapshotted UTXOs are re-inserted via [`insert_utxo`], +/// the address pool must reconcile any UTXO whose address lives past +/// the initial gap limit — otherwise signing fails with "no +/// derivation path for input address …". +#[tokio::test] +async fn test_persisted_utxo_outside_initial_gap_limit_resolves_after_reload() { + let Some(ctx) = TestContext::new(TestChain::Minimal).await else { + return; + }; + if !ctx.dashd.supports_mining { + eprintln!("Skipping test (dashd RPC miner not available)"); + return; + } + + let mut client_handle = ctx.spawn_new_client().await; + wait_for_sync(&mut client_handle.progress_receiver, ctx.dashd.initial_height).await; + + // Pick an address well past the initial gap limit so the + // rebuild's default pool can't coincidentally cover it. + const TARGET_INDEX: u32 = 50; + + let high_index_address: dashcore::Address = { + let mut wallet_write = ctx.wallet.write().await; + let (wallet, info) = + wallet_write.get_wallet_and_info_mut(&ctx.wallet_id).expect("wallet under test exists"); + + let xpub = wallet + .accounts + .standard_bip44_accounts + .get(&0) + .expect("BIP-44 account 0 on Wallet") + .account_xpub; + + let funds_acc = info + .accounts + .standard_bip44_accounts + .get_mut(&0) + .expect("BIP-44 account 0 on ManagedWalletInfo"); + + let ManagedAccountType::Standard { + external_addresses, + .. + } = funds_acc.managed_account_type_mut() + else { + panic!("BIP-44 account 0 is not Standard"); + }; + + let key_source = KeySource::Public(xpub); + let highest = external_addresses.highest_generated.unwrap_or(0); + if TARGET_INDEX > highest { + external_addresses + .generate_addresses(TARGET_INDEX - highest, &key_source, true) + .expect("extend external pool"); + } + external_addresses + .info_at_index(TARGET_INDEX) + .expect("address at TARGET_INDEX after extension") + .address + .clone() + }; + + tracing::info!( + "Faucet target: index={} address={} (initial gap-limit = 30)", + TARGET_INDEX, + high_index_address + ); + + let miner_address = ctx.dashd.node.get_new_address_from_wallet("default"); + let send_amount = Amount::from_sat(100_000_000); + let _txid = ctx.dashd.node.send_to_address(&high_index_address, send_amount); + ctx.dashd.node.generate_blocks(1, &miner_address); + wait_for_sync(&mut client_handle.progress_receiver, ctx.dashd.initial_height + 1).await; + client_handle.stop().await; + + { + let wallet_read = ctx.wallet.read().await; + let info = wallet_read.get_wallet_info(&ctx.wallet_id).expect("wallet info"); + let funds_acc = info.accounts.standard_bip44_accounts.get(&0).expect("BIP-44 account 0"); + assert!( + funds_acc.utxos().values().any(|u| u.address == high_index_address), + "live wallet should have tracked the faucet UTXO" + ); + assert!( + funds_acc.address_derivation_path(&high_index_address).is_some(), + "live pool should resolve the address before any reload" + ); + } + + // Mirror the platform persister: snapshot xpubs + UTXOs, then + // rebuild a wallet from xpubs alone and replay the UTXOs. + let (accounts_snapshot, utxos_snapshot) = { + let wallet_read = ctx.wallet.read().await; + let wallet = wallet_read.get_wallet(&ctx.wallet_id).expect("wallet snapshot"); + let info = wallet_read.get_wallet_info(&ctx.wallet_id).expect("wallet info snapshot"); + let accounts = wallet.accounts.clone(); + let utxos: Vec<_> = info + .accounts + .standard_bip44_accounts + .get(&0) + .expect("BIP-44 account 0") + .utxos() + .values() + .cloned() + .collect(); + (accounts, utxos) + }; + + let rebuilt_wallet = + Wallet::new_external_signable(Network::Regtest, ctx.wallet_id, accounts_snapshot); + let mut rebuilt_info = ManagedWalletInfo::from_wallet(&rebuilt_wallet, 0); + let rebuilt_xpub = rebuilt_wallet + .accounts + .standard_bip44_accounts + .get(&0) + .expect("rebuilt BIP-44 account 0 xpub") + .account_xpub; + let rebuilt_funds = rebuilt_info + .accounts + .standard_bip44_accounts + .get_mut(&0) + .expect("rebuilt BIP-44 account 0"); + for utxo in utxos_snapshot { + rebuilt_funds + .insert_utxo(utxo, &KeySource::Public(rebuilt_xpub)) + .expect("insert_utxo reconciles the pool past the initial gap limit"); + } + + assert!( + rebuilt_funds.utxos().values().any(|u| u.address == high_index_address), + "rebuilt funds account should still track the snapshotted UTXO" + ); + assert!( + rebuilt_funds.address_derivation_path(&high_index_address).is_some(), + "rebuilt pool should resolve the derivation path for index {TARGET_INDEX}" + ); +} diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs index bf8e3e8c0..b6c44b02d 100644 --- a/key-wallet-ffi/src/managed_account.rs +++ b/key-wallet-ffi/src/managed_account.rs @@ -729,7 +729,7 @@ pub unsafe extern "C" fn managed_core_account_get_utxo_count( } let account = &*account; - account.as_funds().map_or(0, |f| f.utxos.len() as c_uint) + account.as_funds().map_or(0, |f| f.utxos().len() as c_uint) } /// FFI-compatible owning-account descriptor for a [`FFITransactionRecord`]. diff --git a/key-wallet-ffi/src/utxo_tests.rs b/key-wallet-ffi/src/utxo_tests.rs index 74c0dddca..e455c9223 100644 --- a/key-wallet-ffi/src/utxo_tests.rs +++ b/key-wallet-ffi/src/utxo_tests.rs @@ -2,6 +2,7 @@ mod utxo_tests { use super::super::*; use crate::error::{FFIError, FFIErrorCode}; + use key_wallet::managed_account::address_pool::KeySource; use key_wallet::managed_account::managed_account_type::ManagedAccountType; use key_wallet::Utxo; use std::ffi::CStr; @@ -226,7 +227,7 @@ mod utxo_tests { let mut utxo = Utxo::new(outpoint, txout, address, 100 + i as u32, false); utxo.is_confirmed = true; - bip44_account.utxos.insert(outpoint, utxo); + bip44_account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); } managed_info.accounts.insert(bip44_account).unwrap(); @@ -311,7 +312,7 @@ mod utxo_tests { let utxos = Utxo::dummy_batch(0..2, 10000, 100, false, false); for utxo in utxos { - bip44_account.utxos.insert(utxo.outpoint, utxo); + bip44_account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); } managed_info.accounts.insert(bip44_account).unwrap(); @@ -336,7 +337,7 @@ mod utxo_tests { let utxos = Utxo::dummy_batch(10..11, 20000, 200, false, false); for utxo in utxos { - bip32_account.utxos.insert(utxo.outpoint, utxo); + bip32_account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); } managed_info.accounts.insert(bip32_account).unwrap(); @@ -354,7 +355,7 @@ mod utxo_tests { let utxos = Utxo::dummy_batch(20..22, 30000, 300, false, false); for utxo in utxos { - coinjoin_account.utxos.insert(utxo.outpoint, utxo); + coinjoin_account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); } managed_info.accounts.insert(coinjoin_account).unwrap(); @@ -412,7 +413,7 @@ mod utxo_tests { let utxos = Utxo::dummy_batch(1..2, 10000, 100, false, false); for utxo in utxos { - testnet_account.utxos.insert(utxo.outpoint, utxo); + testnet_account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); } managed_info.accounts.insert(testnet_account).unwrap(); diff --git a/key-wallet/src/managed_account/managed_core_funds_account.rs b/key-wallet/src/managed_account/managed_core_funds_account.rs index ee1fd1daf..8c39fe4e7 100644 --- a/key-wallet/src/managed_account/managed_core_funds_account.rs +++ b/key-wallet/src/managed_account/managed_core_funds_account.rs @@ -16,6 +16,7 @@ use crate::account::BLSAccount; use crate::account::EdDSAAccount; use crate::account::TransactionRecord; use crate::managed_account::address_pool; +use crate::managed_account::address_pool::KeySource; use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::managed_account::managed_account_type::ManagedAccountType; use crate::managed_account::managed_core_keys_account::ManagedCoreKeysAccount; @@ -41,12 +42,10 @@ use std::collections::{BTreeSet, HashSet}; /// state) and adds the funds-specific bookkeeping used by accounts that hold /// and spend Dash directly (Standard, CoinJoin, DashPay). /// -/// Most read/write surface comes from [`ManagedAccountTrait`] default methods -/// — which delegate to the inner keys account via the primitive accessors — -/// so this struct only carries the funds-specific inherent methods (transaction -/// recording, the Standard-account receive/change paths, etc.). The -/// funds-specific state (`balance`, `utxos`) is reachable as a public field -/// directly. +/// The UTXO set is private; all insertions must go through +/// [`ManagedCoreFundsAccount::insert_utxo`] so the account can reconcile the +/// owning address into its address pool before tracking the UTXO. Reads are +/// available via [`ManagedCoreFundsAccount::utxos`]. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize))] pub struct ManagedCoreFundsAccount { @@ -56,7 +55,7 @@ pub struct ManagedCoreFundsAccount { /// Account balance information pub balance: WalletCoreBalance, /// UTXO set for this account - pub utxos: BTreeMap, + utxos: BTreeMap, /// Outpoints spent by recorded transactions. /// Rebuilt from `transactions` during deserialization. #[cfg_attr(feature = "serde", serde(skip_serializing))] @@ -110,6 +109,71 @@ impl ManagedCoreFundsAccount { &mut self.keys } + /// Read-only view of this account's tracked UTXO set. + pub fn utxos(&self) -> &BTreeMap { + &self.utxos + } + + /// Track a UTXO, ensuring its address is indexed in one of this account's + /// pools first. + /// + /// If the address is already in a pool, the UTXO is recorded directly. If + /// not, the pools are extended (one index at a time, interleaved) using + /// `key_source` until the address is derived. Pass + /// [`KeySource::NoKeySource`] to skip pool reconciliation — used by tests + /// with dummy addresses and by internal flows where the address is known + /// to already be indexed. + pub fn insert_utxo(&mut self, utxo: Utxo, key_source: &KeySource) -> crate::error::Result<()> { + let already_indexed = self + .keys + .managed_account_type() + .address_pools() + .iter() + .any(|pool| pool.contains_address(&utxo.address)); + + if !already_indexed && !matches!(key_source, KeySource::NoKeySource) { + self.ensure_address_indexed(&utxo.address, key_source)?; + } + + self.utxos.insert(utxo.outpoint, utxo); + self.keys.bump_monitor_revision(); + Ok(()) + } + + /// Drop all tracked UTXOs. Test-only helper. + #[cfg(test)] + pub(crate) fn clear_utxos(&mut self) { + if !self.utxos.is_empty() { + self.utxos.clear(); + self.keys.bump_monitor_revision(); + } + } + + /// Walk this account's address pools index by index, deriving new + /// entries until `address` shows up. Bounded so a foreign address + /// doesn't pollute pools indefinitely. + fn ensure_address_indexed( + &mut self, + address: &Address, + key_source: &KeySource, + ) -> crate::error::Result<()> { + const MAX_SCAN: u32 = 10_000; + let pool_count = self.keys.managed_account_type().address_pools().len(); + for index in 0..MAX_SCAN { + for pool_idx in 0..pool_count { + let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx]; + let derived = pool.generate_address_at_index(index, key_source, true)?; + if &derived == address { + return Ok(()); + } + } + } + Err(crate::error::Error::InvalidAddress(format!( + "address {} not derivable within scan window for this account", + address + ))) + } + /// Check if an outpoint was spent by a previously recorded transaction. fn is_outpoint_spent(&self, outpoint: &OutPoint) -> bool { self.spent_outpoints.contains(outpoint) diff --git a/key-wallet/src/test_utils/wallet.rs b/key-wallet/src/test_utils/wallet.rs index 2a4756dd9..50e97d272 100644 --- a/key-wallet/src/test_utils/wallet.rs +++ b/key-wallet/src/test_utils/wallet.rs @@ -67,7 +67,7 @@ impl TestWalletContext { /// Returns the first UTXO from the first BIP44 account. pub fn first_utxo(&self) -> &Utxo { - self.bip44_account().utxos.values().next().expect("Should have UTXO") + self.bip44_account().utxos().values().next().expect("Should have UTXO") } /// Processes a transaction: runs `check_core_transaction` with `update_state = true`. diff --git a/key-wallet/src/tests/balance_tests.rs b/key-wallet/src/tests/balance_tests.rs index ecb1f1481..e9b4ae58c 100644 --- a/key-wallet/src/tests/balance_tests.rs +++ b/key-wallet/src/tests/balance_tests.rs @@ -1,5 +1,6 @@ //! Tests for update_balance() UTXO categorization. +use crate::managed_account::address_pool::KeySource; use crate::managed_account::ManagedCoreFundsAccount; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use crate::wallet::managed_wallet_info::ManagedWalletInfo; @@ -12,13 +13,13 @@ fn test_balance_with_mixed_utxo_types() { // Regular confirmed UTXO let utxo1 = Utxo::dummy(1, 100_000, 1000, false, true); - account.utxos.insert(utxo1.outpoint, utxo1); + account.insert_utxo(utxo1, &KeySource::NoKeySource).unwrap(); // Mature coinbase (100+ confirmations at height 1100) let utxo2 = Utxo::dummy(2, 10_000_000, 1000, true, true); - account.utxos.insert(utxo2.outpoint, utxo2); + account.insert_utxo(utxo2, &KeySource::NoKeySource).unwrap(); // Immature coinbase (<100 confirmations at height 1100) let utxo3 = Utxo::dummy(3, 20_000_000, 1050, true, true); - account.utxos.insert(utxo3.outpoint, utxo3); + account.insert_utxo(utxo3, &KeySource::NoKeySource).unwrap(); wallet_info.accounts.insert(account).unwrap(); assert_eq!(wallet_info.balance, WalletCoreBalance::default()); @@ -34,7 +35,7 @@ fn test_coinbase_maturity_boundary() { // Coinbase at height 1000 let utxo = Utxo::dummy(1, 50_000_000, 1000, true, true); - account.utxos.insert(utxo.outpoint, utxo); + account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); wallet_info.accounts.insert(account).unwrap(); assert_eq!(wallet_info.balance, WalletCoreBalance::default()); @@ -56,7 +57,7 @@ fn test_locked_utxos_in_locked_balance() { let mut utxo = Utxo::dummy(1, 100_000, 1000, false, true); utxo.is_locked = true; - account.utxos.insert(utxo.outpoint, utxo); + account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); wallet_info.accounts.insert(account).unwrap(); assert_eq!(wallet_info.balance, WalletCoreBalance::default()); @@ -71,7 +72,7 @@ fn test_unconfirmed_utxos_in_unconfirmed_balance() { let mut account = ManagedCoreFundsAccount::dummy_bip44(); let utxo = Utxo::dummy(1, 100_000, 0, false, false); - account.utxos.insert(utxo.outpoint, utxo); + account.insert_utxo(utxo, &KeySource::NoKeySource).unwrap(); wallet_info.accounts.insert(account).unwrap(); assert_eq!(wallet_info.balance, WalletCoreBalance::default()); diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index 3ff50840c..8d3c53c5f 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -673,7 +673,7 @@ impl ManagedCoreFundsAccount { // Check inputs (sent) - rely on tracked UTXOs to determine spends if !tx.is_coin_base() { for input in &tx.input { - if let Some(utxo) = self.utxos.get(&input.previous_output) { + if let Some(utxo) = self.utxos().get(&input.previous_output) { sent = sent.saturating_add(utxo.txout.value); if let Some(address_info) = self.get_address_info(&utxo.address) { diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index 734fe1566..b673baeab 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -460,8 +460,8 @@ mod tests { ); // 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!(!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 @@ -554,7 +554,7 @@ mod tests { .get(&0) .expect("Should have managed BIP44 account"); - assert!(account.utxos.is_empty(), "Spent UTXO should be removed"); + assert!(account.utxos().is_empty(), "Spent UTXO should be removed"); let record = account .transactions() @@ -619,8 +619,8 @@ mod tests { "Coinbase should be in regular transactions" ); - assert!(!managed_account.utxos.is_empty(), "UTXO should be created for coinbase"); - let utxo = managed_account.utxos.values().next().expect("Should have UTXO"); + 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); @@ -763,8 +763,8 @@ mod tests { ); // Verify UTXO state is unchanged after rescan - assert_eq!(managed_account.utxos.len(), 1, "Should still have exactly one UTXO"); - let utxo = managed_account.utxos.values().next().expect("Should have UTXO"); + assert_eq!(managed_account.utxos().len(), 1, "Should still have exactly one UTXO"); + let utxo = managed_account.utxos().values().next().expect("Should have UTXO"); assert!(utxo.is_confirmed); assert_eq!(utxo.txout.value, 100_000); } @@ -838,7 +838,7 @@ mod tests { ); // One UTXO should exist (the change output from spend_tx) - assert_eq!(account.utxos.len(), 1, "Should have one UTXO (change output)"); + assert_eq!(account.utxos().len(), 1, "Should have one UTXO (change output)"); // Now process the funding tx (which was spent by spend_tx that we already stored) let fund_context = TransactionContext::InBlock(BlockInfo::new( @@ -861,13 +861,13 @@ mod tests { // Should still only have one UTXO (the change from spend_tx) assert_eq!( - account.utxos.len(), + account.utxos().len(), 1, "Should still have only one UTXO (change), funding UTXO should not be added" ); // The one UTXO should be the change output, not the funding output - let utxo = account.utxos.values().next().expect("Should have UTXO"); + let utxo = account.utxos().values().next().expect("Should have UTXO"); assert_eq!( utxo.outpoint.txid, spend_tx.txid(), @@ -1076,9 +1076,9 @@ mod tests { .bip44_managed_account_at_index_mut(1) .expect("Should have managed account 1"); account1.transactions_mut().remove(&txid); - account1.utxos.clear(); + account1.clear_utxos(); assert!(!account1.transactions().contains_key(&txid)); - assert!(account1.utxos.is_empty()); + assert!(account1.utxos().is_empty()); let is_result = managed_wallet .check_core_transaction( @@ -1111,7 +1111,7 @@ mod tests { .expect("Both accounts should hold the record after IS backfill"); assert!(matches!(record.context, TransactionContext::InstantSend(_))); assert!( - account.utxos.values().any(|u| u.outpoint.txid == txid && u.is_instantlocked), + account.utxos().values().any(|u| u.outpoint.txid == txid && u.is_instantlocked), "Account {account_index} should have an IS-locked UTXO from this tx" ); } @@ -1183,9 +1183,9 @@ mod tests { .first_bip44_managed_account_mut() .expect("Should have BIP44 account"); account.transactions_mut().remove(&txid); - account.utxos.clear(); + account.clear_utxos(); assert!(!account.transactions().contains_key(&txid)); - assert!(account.utxos.is_empty()); + assert!(account.utxos().is_empty()); // Call `confirm_transaction` directly — the backfill path should create the record let block_hash = BlockHash::from_slice(&[9u8; 32]).expect("hash"); @@ -1204,8 +1204,8 @@ mod tests { assert_eq!(record.net_amount, 250_000); // Verify UTXO was also created - assert_eq!(account.utxos.len(), 1); - let utxo = account.utxos.values().next().expect("Should have UTXO"); + assert_eq!(account.utxos().len(), 1); + let utxo = account.utxos().values().next().expect("Should have UTXO"); assert_eq!(utxo.outpoint.txid, txid); assert_eq!(utxo.txout.value, 250_000); assert!(utxo.is_confirmed); @@ -1795,7 +1795,7 @@ mod tests { vout: 1, }; let change_utxo = - ctx.bip44_account().utxos.get(&change_outpoint).expect("change UTXO recorded"); + ctx.bip44_account().utxos().get(&change_outpoint).expect("change UTXO recorded"); // The parent transaction is still in the mempool, so `is_confirmed` // stays false; the trust signal is what shifts the UTXO into the // confirmed balance bucket. diff --git a/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs b/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs index 27e1ecd31..3468f6dc5 100644 --- a/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs @@ -601,8 +601,8 @@ mod tests { .standard_bip44_accounts .get_mut(&0) .unwrap() - .utxos - .insert(utxo.outpoint, utxo); + .insert_utxo(utxo, &crate::managed_account::address_pool::KeySource::NoKeySource) + .unwrap(); info.update_last_processed_height(1100); let signer = InMemorySigner { diff --git a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs index b87a0a913..9d0ea4537 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs @@ -77,7 +77,7 @@ impl TransactionBuilder { } pub fn set_funding(mut self, funds_acc: &mut ManagedCoreFundsAccount, acc: &Account) -> Self { - self.inputs = funds_acc.utxos.values().cloned().collect(); + self.inputs = funds_acc.utxos().values().cloned().collect(); self.change_addr = funds_acc.next_change_address(Some(&acc.account_xpub), true).ok(); self } diff --git a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs index 492b35dda..f4f7ce32a 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs @@ -500,8 +500,8 @@ mod tests { .standard_bip44_accounts .get_mut(&0) .unwrap() - .utxos - .insert(utxo.outpoint, utxo); + .insert_utxo(utxo, &crate::managed_account::address_pool::KeySource::NoKeySource) + .unwrap(); info.update_last_processed_height(1100); let signer = InMemorySigner { 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 4de8ab78e..aa47fb0ec 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 @@ -268,7 +268,7 @@ impl WalletInfoInterface for ManagedWalletInfo { fn utxos(&self) -> BTreeSet<&Utxo> { let mut utxos = BTreeSet::new(); for account in self.accounts.all_funding_accounts() { - utxos.extend(account.utxos.values()); + utxos.extend(account.utxos().values()); } utxos } @@ -314,7 +314,7 @@ impl WalletInfoInterface for ManagedWalletInfo { // Coinbase UTXOs only live on funds-bearing accounts. let mut immature_txids: BTreeSet = BTreeSet::new(); for account in self.accounts.all_funding_accounts() { - for utxo in account.utxos.values() { + for utxo in account.utxos().values() { if utxo.is_coinbase && !utxo.is_mature(self.last_processed_height()) { immature_txids.insert(utxo.outpoint.txid); }