From 9649b93347a183a55145765297701304daedd76b Mon Sep 17 00:00:00 2001 From: Borja Castellano Date: Wed, 24 Jun 2026 07:41:38 -0700 Subject: [PATCH] feat(key-wallet): support for more account types + strategy to consume all utxo available --- .../tests/dashd_sync/tests_transaction.rs | 107 ++++++++++++++ key-wallet-ffi/src/transaction.rs | 6 +- key-wallet-manager/src/lib.rs | 7 +- .../managed_wallet_info/coin_selection.rs | 61 ++++++++ .../src/wallet/managed_wallet_info/mod.rs | 6 + .../transaction_builder.rs | 47 +++++- .../transaction_building.rs | 136 ++++++++++++++---- .../wallet_info_interface.rs | 25 ++++ 8 files changed, 361 insertions(+), 34 deletions(-) diff --git a/dash-spv/tests/dashd_sync/tests_transaction.rs b/dash-spv/tests/dashd_sync/tests_transaction.rs index ec3b66957..b7761738e 100644 --- a/dash-spv/tests/dashd_sync/tests_transaction.rs +++ b/dash-spv/tests/dashd_sync/tests_transaction.rs @@ -11,13 +11,18 @@ use super::setup::{create_and_start_client, TestContext}; use dash_spv::test_utils::{create_test_wallet, TestChain}; use dashcore::address::NetworkUnchecked; use key_wallet::account::ManagedAccountTrait; +use key_wallet::wallet::initialization::WalletAccountCreationOptions; +use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; +use key_wallet::wallet::managed_wallet_info::fee::FeeRate; use key_wallet::wallet::managed_wallet_info::transaction_builder::{ BuilderError, TransactionBuilder, }; +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::ManagedWalletInfo; use key_wallet::ManagedAccountType; use key_wallet_manager::{WalletId, WalletManager}; +use std::collections::BTreeSet; /// Verify incremental sync works by generating blocks after initial sync. /// @@ -385,3 +390,105 @@ async fn test_spend_incoming_balance() { client_handle.stop().await; } + +/// Drain every UTXO of one account into another account of the same wallet. +/// +/// Funds a BIP32 account with 3 UTXOs, then drains it into a BIP44 receive address with +/// `SelectionStrategy::All` (spend every UTXO; single output = total - fee, no change) +#[tokio::test] +async fn test_drain_account_into_another() { + let Some(ctx) = TestContext::new(TestChain::Minimal).await else { + return; + }; + if !ctx.dashd.supports_mining { + eprintln!("Skipping test (dashd RPC miner not available)"); + return; + } + + const FUNDING: u64 = 50_000_000; + const FEE: u64 = 488; + + // Fresh wallet with a BIP32 account 0 (drain source) and a BIP44 account 0 (destination). + // The default test options only create the BIP44 account. + let (wallet, wallet_id) = { + let mut manager = WalletManager::::new(Network::Regtest); + let id = manager + .create_wallet_from_mnemonic( + EMPTY_MNEMONIC, + 0, + WalletAccountCreationOptions::SpecificAccounts( + BTreeSet::from([0]), // BIP44 account 0 + BTreeSet::from([0]), // BIP32 account 0 + BTreeSet::new(), + BTreeSet::new(), + BTreeSet::new(), + None, + ), + ) + .expect("create wallet with bip32 + bip44 accounts"); + (Arc::new(RwLock::new(manager)), id) + }; + + let mut client_handle = create_and_start_client(&ctx.client_config, Arc::clone(&wallet)).await; + wait_for_sync(&mut client_handle.progress_receiver, ctx.dashd.initial_height).await; + + let (fund_addresses, dest) = { + let mut lock = wallet.write().await; + let (w, info) = lock.get_wallet_and_info_mut(&wallet_id).expect("wallet"); + let fund: Vec
= (0..3) + .map(|_| info.next_receive_address(w, 0, AccountTypePreference::BIP32, true).unwrap()) + .collect(); + let dest = info.next_receive_address(w, 0, AccountTypePreference::BIP44, true).unwrap(); + (fund, dest) + }; + + let miner = ctx.dashd.node.get_new_address_from_wallet("default"); + for address in &fund_addresses { + ctx.dashd.node.send_to_address(address, Amount::from_sat(FUNDING)); + } + ctx.dashd.node.generate_blocks(1, &miner); + + let funded_height = ctx.dashd.initial_height + 1; + wait_for_sync(&mut client_handle.progress_receiver, funded_height).await; + wait_for_wallet_synced(&wallet, &wallet_id, funded_height).await; + + // Drain the BIP32 account into the BIP44 destination (amount ignored under `All`). + let (tx, _) = { + let mut lock = wallet.write().await; + + let (w, info) = lock.get_wallet_and_info_mut(&wallet_id).expect("wallet"); + info.build_and_sign_transaction( + w, + AccountTypePreference::BIP32, + 0, + vec![(dest.as_unchecked().clone(), 0)], + FeeRate::normal(), + SelectionStrategy::All, + ) + .await + .expect("drain") + }; + + client_handle.client.broadcast_transaction(&tx).await.expect("broadcast"); + wait_for_mempool_tx(&mut client_handle.wallet_event_receiver, MEMPOOL_TIMEOUT) + .await + .expect("mempool"); + ctx.dashd.node.generate_blocks(1, &miner); + + let drained_height = funded_height + 1; + wait_for_sync(&mut client_handle.progress_receiver, drained_height).await; + wait_for_wallet_synced(&wallet, &wallet_id, drained_height).await; + + // Final state: BIP32 emptied, BIP44 holds the single drained UTXO. + let reader = wallet.read().await; + let info = reader.get_wallet_info(&wallet_id).expect("wallet"); + let balance = |pref| info.account_balance(pref, 0).unwrap().total(); + let utxos = |pref| info.funds_account(pref, 0).unwrap().utxos.len(); + + assert_eq!(balance(AccountTypePreference::BIP32), 0); + assert_eq!(utxos(AccountTypePreference::BIP32), 0); + assert_eq!(balance(AccountTypePreference::BIP44), FUNDING * 3 - FEE); + assert_eq!(utxos(AccountTypePreference::BIP44), 1); + + client_handle.stop().await; +} diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 03b0f3bc4..7878506bd 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -14,7 +14,9 @@ use dashcore::{ use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ AssetLockFundingType, CreditOutputFunding, }; +use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy::BranchAndBound; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; +use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; use secp256k1::{Message, Secp256k1, SecretKey}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; @@ -135,9 +137,11 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( manager .build_and_sign_transaction( &wallet_id, + AccountTypePreference::BIP44, account_index, outputs, - FeeRate::new(fee_per_kb) + FeeRate::new(fee_per_kb), + BranchAndBound ) .await, error diff --git a/key-wallet-manager/src/lib.rs b/key-wallet-manager/src/lib.rs index 3d0df8aad..874d39020 100644 --- a/key-wallet-manager/src/lib.rs +++ b/key-wallet-manager/src/lib.rs @@ -29,6 +29,7 @@ use dashcore::prelude::CoreBlockHeight; use key_wallet::account::AccountCollection; use key_wallet::managed_account::transaction_record::TransactionRecord; use key_wallet::transaction_checking::{DerivedAddressInfo, TransactionContext}; +use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; 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; @@ -578,9 +579,11 @@ impl WalletManager { pub async fn build_and_sign_transaction( &mut self, wallet_id: &WalletId, - account_index: u32, + source: AccountTypePreference, + source_index: u32, outputs: Vec<(Address, u64)>, fee_rate: FeeRate, + strategy: SelectionStrategy, ) -> Result<(Transaction, u64), WalletError> { // Get the managed account for UTXOs and signing data let (wallet, managed_wallet) = self @@ -588,7 +591,7 @@ impl WalletManager { .ok_or(WalletError::WalletNotFound(*wallet_id))?; managed_wallet - .build_and_sign_transaction(wallet, account_index, outputs, fee_rate) + .build_and_sign_transaction(wallet, source, source_index, outputs, fee_rate, strategy) .await .map_err(|e| WalletError::TransactionBuild(e.to_string())) } diff --git a/key-wallet/src/wallet/managed_wallet_info/coin_selection.rs b/key-wallet/src/wallet/managed_wallet_info/coin_selection.rs index 523d1eb49..5bbd7d53a 100644 --- a/key-wallet/src/wallet/managed_wallet_info/coin_selection.rs +++ b/key-wallet/src/wallet/managed_wallet_info/coin_selection.rs @@ -36,6 +36,9 @@ pub enum SelectionStrategy { OptimalConsolidation, /// Random selection for privacy Random, + /// Select EVERY spendable UTXO (drain / sweep an account empty). There is no change: the + /// single deliverable output is worth the total minus the fee to spend them all + All, } /// Result of UTXO selection @@ -249,6 +252,38 @@ impl CoinSelector { input_size, ) } + SelectionStrategy::All => { + let selected: Vec = + utxos.into_iter().filter(|u| u.is_spendable(current_height)).cloned().collect(); + + if selected.is_empty() { + return Err(SelectionError::NoUtxosAvailable); + } + + let total_value: u64 = selected.iter().map(|u| u.value()).sum(); + let estimated_size = base_size + selected.len() * input_size; + let estimated_fee = fee_rate.calculate_fee(estimated_size); + + // The caller's `target_amount` is ignored: a drain spends everything, there is no + // target to satisfy + let deliverable = total_value + .checked_sub(estimated_fee) + .filter(|d| *d > self.dust_threshold) + .ok_or(SelectionError::InsufficientFunds { + available: total_value, + required: estimated_fee + self.dust_threshold + 1, + })?; + + Ok(SelectionResult { + selected, + total_value, + target_amount: deliverable, + change_amount: 0, + estimated_size, + estimated_fee, + exact_match: true, + }) + } } } @@ -660,6 +695,32 @@ impl std::error::Error for SelectionError {} mod tests { use super::*; + #[test] + fn test_select_all_drains_everything() { + let utxos = vec![ + Utxo::dummy(0, 10000, 100, false, true), + Utxo::dummy(0, 20000, 100, false, true), + Utxo::dummy(0, 30000, 100, false, true), + ]; + let selector = CoinSelector::new(SelectionStrategy::All); + // Pass a non-zero target to prove it is IGNORED: a drain takes everything regardless. + let result = selector.select_coins(&utxos, 12_345, FeeRate::new(1000), 200).unwrap(); + + assert_eq!(result.selected.len(), 3, "All selects every spendable UTXO"); + assert_eq!(result.total_value, 60000); + assert!(result.estimated_fee > 0); + assert_eq!(result.target_amount, 60000 - result.estimated_fee, "deliverable = total - fee"); + assert_eq!(result.change_amount, 0, "a drain leaves no change"); + assert!(result.exact_match, "no change output for a drain"); + } + + #[test] + fn test_select_all_empty_is_error() { + let selector = CoinSelector::new(SelectionStrategy::All); + let result = selector.select_coins(&[], 0, FeeRate::new(1000), 200); + assert!(matches!(result, Err(SelectionError::NoUtxosAvailable))); + } + #[test] fn test_smallest_first_selection() { let utxos = vec![ diff --git a/key-wallet/src/wallet/managed_wallet_info/mod.rs b/key-wallet/src/wallet/managed_wallet_info/mod.rs index d062160b7..247d82df1 100644 --- a/key-wallet/src/wallet/managed_wallet_info/mod.rs +++ b/key-wallet/src/wallet/managed_wallet_info/mod.rs @@ -174,6 +174,9 @@ impl ManagedWalletInfo { address } + AccountTypePreference::CoinJoin => { + unimplemented!("CoinJoin accounts are spend-only in our current use cases") + } }; address @@ -217,6 +220,9 @@ impl ManagedWalletInfo { address } + AccountTypePreference::CoinJoin => { + unimplemented!("CoinJoin accounts are spend-only in our current use cases") + } }; address 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..d86ea7649 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs @@ -20,6 +20,10 @@ use secp256k1::ecdsa::Signature; use secp256k1::{Message, PublicKey, Secp256k1}; use std::cmp::Ordering; +/// A transaction with more inputs would exceed the relay standard-size cap (~100 KB at ~148 +/// bytes/signed input) and be rejected by the network +const MAX_STANDARD_TX_INPUTS: usize = 500; + /// Calculate varint size for a given number fn varint_size(n: usize) -> usize { match n { @@ -240,7 +244,7 @@ impl TransactionBuilder { size } - fn assemble_unsigned(self) -> Result<(Transaction, Vec), BuilderError> { + fn assemble_unsigned(mut self) -> Result<(Transaction, Vec), BuilderError> { if let Some(TransactionPayload::AssetLockPayloadType(p)) = &self.special_payload { if p.credit_outputs.is_empty() { return Err(BuilderError::NoOutputs); @@ -249,6 +253,12 @@ impl TransactionBuilder { return Err(BuilderError::NoOutputs); } + // A drain (`All`) never emits change; drop the change address before sizing so the fee + // estimate doesn't include a phantom (~34-byte) change output. + if self.selection_strategy == SelectionStrategy::All { + self.change_addr = None; + } + // For AssetLock the on-chain spend equals the OP_RETURN burn, which // mirrors the sum of the credit_outputs carried in the payload. For // every other tx type, it's just the sum of user-provided outputs. @@ -271,6 +281,14 @@ impl TransactionBuilder { .map_err(BuilderError::CoinSelection)?; let mut selected_inputs = selection.selected; + + if selected_inputs.len() > MAX_STANDARD_TX_INPUTS { + return Err(BuilderError::TooManyInputs { + count: selected_inputs.len(), + max: MAX_STANDARD_TX_INPUTS, + }); + } + let total_input: u64 = selected_inputs.iter().map(|u| u.value()).sum(); if total_input < total_output + selection.estimated_fee { @@ -290,8 +308,17 @@ impl TransactionBuilder { _ => self.outputs, }; - // Add change output if above dust threshold - if change_amount > 546 { + if self.selection_strategy == SelectionStrategy::All { + // Drain: the single output takes the whole balance minus fee (the caller's amount is + // ignored); no change. + let [out] = tx_outputs.as_mut_slice() else { + return Err(BuilderError::InvalidData( + "SelectionStrategy::All requires exactly one output (the destination)".into(), + )); + }; + out.value = total_input.saturating_sub(selection.estimated_fee); + } else if change_amount > 546 { + // Add change output if above dust threshold let Some(change_addr) = self.change_addr else { return Err(BuilderError::NoChangeAddress); }; @@ -506,6 +533,8 @@ pub enum BuilderError { NoOutputs, /// No change address provided NoChangeAddress, + /// The requested funding account does not exist + AccountNotFound(String), /// Insufficient funds InsufficientFunds { available: u64, @@ -521,6 +550,11 @@ pub enum BuilderError { CoinSelection(crate::wallet::managed_wallet_info::coin_selection::SelectionError), /// Signing was attempted with a watch-only wallet WatchOnlyWallet, + /// More inputs than fit in a single standard transaction + TooManyInputs { + count: usize, + max: usize, + }, } impl fmt::Display for BuilderError { @@ -529,6 +563,7 @@ impl fmt::Display for BuilderError { Self::NoInputs => write!(f, "No inputs provided"), Self::NoOutputs => write!(f, "No outputs provided"), Self::NoChangeAddress => write!(f, "No change address provided"), + Self::AccountNotFound(msg) => write!(f, "Account not found: {msg}"), Self::InsufficientFunds { available, required, @@ -540,6 +575,12 @@ impl fmt::Display for BuilderError { Self::SigningFailed(msg) => write!(f, "Signing failed: {}", msg), Self::CoinSelection(err) => write!(f, "Coin selection error: {}", err), Self::WatchOnlyWallet => write!(f, "Cannot sign with a watch-only wallet"), + Self::TooManyInputs { + count, + max, + } => { + write!(f, "Too many inputs for a standard transaction: {count} (max {max})") + } } } } 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 c553c59dc..1b1833b05 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs @@ -2,6 +2,7 @@ use crate::managed_account::managed_account_trait::ManagedAccountTrait; use crate::signer::Signer; +use crate::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use crate::wallet::managed_wallet_info::fee::FeeRate; use crate::wallet::managed_wallet_info::transaction_builder::{BuilderError, TransactionBuilder}; use crate::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; @@ -15,32 +16,47 @@ use dashcore::{Address, Transaction}; pub enum AccountTypePreference { BIP44, BIP32, + CoinJoin, } impl ManagedWalletInfo { pub async fn build_and_sign_transaction( &mut self, wallet: &Wallet, - account_index: u32, + source: AccountTypePreference, + source_index: u32, outputs: Vec<(Address, u64)>, fee_rate: FeeRate, + strategy: SelectionStrategy, ) -> Result<(Transaction, u64), BuilderError> { let height = self.last_processed_height(); - let managed_account = self - .accounts - .standard_bip44_accounts - .get_mut(&account_index) - .ok_or(BuilderError::NoChangeAddress)?; - - let account = wallet - .accounts - .standard_bip44_accounts - .get(&account_index) - .ok_or(BuilderError::NoChangeAddress)?; + let managed_account = match source { + AccountTypePreference::BIP44 => { + self.accounts.standard_bip44_accounts.get_mut(&source_index) + } + AccountTypePreference::BIP32 => { + self.accounts.standard_bip32_accounts.get_mut(&source_index) + } + AccountTypePreference::CoinJoin => { + self.accounts.coinjoin_accounts.get_mut(&source_index) + } + } + .ok_or_else(|| { + BuilderError::AccountNotFound(format!("managed account {source:?} #{source_index}")) + })?; + let account = match source { + AccountTypePreference::BIP44 => wallet.get_bip44_account(source_index), + AccountTypePreference::BIP32 => wallet.get_bip32_account(source_index), + AccountTypePreference::CoinJoin => wallet.get_coinjoin_account(source_index), + } + .ok_or_else(|| { + BuilderError::AccountNotFound(format!("wallet account {source:?} #{source_index}")) + })?; let mut tx_builder = TransactionBuilder::new() .set_fee_rate(fee_rate) + .set_selection_strategy(strategy) .set_current_height(height) .set_funding(managed_account, account); @@ -54,30 +70,45 @@ impl ManagedWalletInfo { tx_builder.build_signed(wallet, |addr| managed_account.address_derivation_path(&addr)).await } + #[allow(clippy::too_many_arguments)] pub async fn build_and_sign_transaction_with_signer( &mut self, wallet: &Wallet, - account_index: u32, + source: AccountTypePreference, + source_index: u32, outputs: Vec<(Address, u64)>, fee_rate: FeeRate, + strategy: SelectionStrategy, signer: &S, ) -> Result<(Transaction, u64), BuilderError> { let height = self.last_processed_height(); - let managed_account = self - .accounts - .standard_bip44_accounts - .get_mut(&account_index) - .ok_or(BuilderError::NoChangeAddress)?; - - let account = wallet - .accounts - .standard_bip44_accounts - .get(&account_index) - .ok_or(BuilderError::NoChangeAddress)?; + let managed_account = match source { + AccountTypePreference::BIP44 => { + self.accounts.standard_bip44_accounts.get_mut(&source_index) + } + AccountTypePreference::BIP32 => { + self.accounts.standard_bip32_accounts.get_mut(&source_index) + } + AccountTypePreference::CoinJoin => { + self.accounts.coinjoin_accounts.get_mut(&source_index) + } + } + .ok_or_else(|| { + BuilderError::AccountNotFound(format!("managed account {source:?} #{source_index}")) + })?; + let account = match source { + AccountTypePreference::BIP44 => wallet.get_bip44_account(source_index), + AccountTypePreference::BIP32 => wallet.get_bip32_account(source_index), + AccountTypePreference::CoinJoin => wallet.get_coinjoin_account(source_index), + } + .ok_or_else(|| { + BuilderError::AccountNotFound(format!("wallet account {source:?} #{source_index}")) + })?; let mut tx_builder = TransactionBuilder::new() .set_fee_rate(fee_rate) + .set_selection_strategy(strategy) .set_current_height(height) .set_funding(managed_account, account); @@ -93,6 +124,7 @@ impl ManagedWalletInfo { } #[cfg(test)] mod tests { + use super::AccountTypePreference; use crate::wallet::managed_wallet_info::coin_selection::SelectionStrategy; use crate::wallet::managed_wallet_info::fee::FeeRate; use crate::wallet::managed_wallet_info::transaction_builder::TransactionBuilder; @@ -143,6 +175,37 @@ mod tests { assert!(change_output.is_some(), "Should have change output"); } + #[test] + fn test_sweep_builder_drains_to_single_output() { + let utxos = vec![ + Utxo::dummy(0, 100000, 100, false, true), + Utxo::dummy(0, 200000, 100, false, true), + Utxo::dummy(0, 300000, 100, false, true), + ]; + let dest = Address::from_str("yTb47qEBpNmgXvYYsHEN4nh8yJwa5iC4Cs") + .unwrap() + .require_network(Network::Testnet) + .unwrap(); + + // `All` selects every input and pays total - fee to `dest` as one output, no change + let total = 600_000u64; + let fee = FeeRate::normal().calculate_fee(8 + 1 + 1 + 34 + 3 * 148); + let deliverable = total - fee; + let (tx, _fee) = TransactionBuilder::new() + .set_fee_rate(FeeRate::normal()) + .set_current_height(200) + .set_selection_strategy(SelectionStrategy::All) + .add_inputs(utxos) + .add_output(&dest, deliverable) + .build_unsigned() + .unwrap(); + + assert_eq!(tx.input.len(), 3, "sweep spends every input"); + assert_eq!(tx.output.len(), 1, "sweep has one real output and no change"); + assert_eq!(tx.output[0].value, deliverable); + assert_eq!(tx.output[0].script_pubkey, dest.script_pubkey()); + } + #[test] fn test_asset_lock_transaction() { // Test based on DSTransactionTests.m testAssetLockTx1 @@ -437,8 +500,8 @@ mod tests { #[tokio::test] async fn test_signer_invalid_account_index() { - // No BIP44 account 99 exists, so next_change_address returns None - // and we surface NoChangeAddress before any signing happens. + // No BIP44 account 99 exists, so account resolution fails with AccountNotFound + // before any funding or signing happens. let (wallet, mut info) = test_wallet_and_info(); let signer = InMemorySigner { root: root_from(&wallet), @@ -447,13 +510,16 @@ mod tests { let result = info .build_and_sign_transaction_with_signer( &wallet, + AccountTypePreference::BIP44, 99, dest_outputs(100_000), FeeRate::normal(), + SelectionStrategy::BranchAndBound, &signer, ) .await; - assert!(matches!(result, Err(BuilderError::NoChangeAddress))); + + assert!(matches!(result, Err(BuilderError::AccountNotFound(_)))); } #[tokio::test] @@ -489,9 +555,11 @@ mod tests { let result = info .build_and_sign_transaction_with_signer( &wallet, + AccountTypePreference::BIP44, 0, dest_outputs(100_000), FeeRate::normal(), + SelectionStrategy::BranchAndBound, &NoDigestSigner, ) .await; @@ -553,9 +621,11 @@ mod tests { let (tx, fee) = info .build_and_sign_transaction_with_signer( &wallet, + AccountTypePreference::BIP44, 0, dest_outputs(send_amount), FeeRate::normal(), + SelectionStrategy::BranchAndBound, &signer, ) .await @@ -592,9 +662,11 @@ mod tests { let result = info .build_and_sign_transaction_with_signer( &wallet, + AccountTypePreference::BIP44, 0, dest_outputs(500_000), FeeRate::normal(), + SelectionStrategy::BranchAndBound, &signer, ) .await; @@ -639,7 +711,15 @@ mod tests { let outputs = vec![(mainnet_dest, 100_000u64)]; let result = info - .build_and_sign_transaction_with_signer(&wallet, 0, outputs, FeeRate::normal(), &signer) + .build_and_sign_transaction_with_signer( + &wallet, + AccountTypePreference::BIP44, + 0, + outputs, + FeeRate::normal(), + SelectionStrategy::BranchAndBound, + &signer, + ) .await; assert!( matches!(result, Err(BuilderError::InvalidData(_))), 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 da54ea329..2b3ea3fbe 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 @@ -8,8 +8,10 @@ use super::managed_account_operations::ManagedAccountOperations; use crate::account::{AccountType, ManagedAccountTrait}; use crate::managed_account::managed_account_collection::ManagedAccountCollection; use crate::managed_account::managed_account_ref::ManagedAccountRefMut; +use crate::managed_account::ManagedCoreFundsAccount; use crate::transaction_checking::TransactionContext; use crate::transaction_checking::WalletTransactionChecker; +use crate::wallet::managed_wallet_info::transaction_building::AccountTypePreference; use crate::wallet::managed_wallet_info::TransactionRecord; use crate::wallet::ManagedWalletInfo; use crate::{Network, Utxo, Wallet, WalletCoreBalance}; @@ -112,6 +114,29 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount .collect() } + /// The funds-bearing account selected by `(preference, index)`, if it exists. + fn funds_account( + &self, + preference: AccountTypePreference, + index: u32, + ) -> Option<&ManagedCoreFundsAccount> { + let accounts = self.accounts(); + match preference { + AccountTypePreference::BIP44 => accounts.standard_bip44_accounts.get(&index), + AccountTypePreference::BIP32 => accounts.standard_bip32_accounts.get(&index), + AccountTypePreference::CoinJoin => accounts.coinjoin_accounts.get(&index), + } + } + + /// Balance of the funds-bearing account selected by `(preference, index)`, if it exists. + fn account_balance( + &self, + preference: AccountTypePreference, + index: u32, + ) -> Option { + self.funds_account(preference, index).map(|account| account.balance) + } + /// Get transaction history fn transaction_history(&self) -> Vec<&TransactionRecord>;