diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index be3773d3..70d0243f 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1511,7 +1511,7 @@ impl Wallet { let (required_utxos, optional_utxos) = { // NOTE: manual selection overrides unspendable - let mut required: Vec = params.utxos.values().cloned().collect(); + let mut required: Vec = params.utxos.order_weighted_utxos(); let optional = self.filter_utxos(¶ms, current_height.to_consensus_u32()); // if drain_wallet is true, all UTxOs are required @@ -1785,7 +1785,7 @@ impl Wallet { } }) }) - .collect::, BuildFeeBumpError>>()?; + .collect::>()?; if tx.output.len() > 1 { let mut change_index = None; @@ -2574,6 +2574,68 @@ impl Wallet { } } +#[derive(Debug, Default, Clone)] +pub(crate) struct OrderUtxos { + utxos: Vec, + utxos_map: HashMap, +} + +impl Deref for OrderUtxos { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.utxos_map + } +} + +impl OrderUtxos { + fn insert(&mut self, outpoint: OutPoint, weighted_utxo: WeightedUtxo) -> Option { + let v = self.utxos_map.insert(outpoint, weighted_utxo); + if v.is_none() { + self.utxos.push(outpoint); + } + debug_assert!(self.utxos.len() == self.utxos_map.len()); + v + } + + fn order_weighted_utxos(&self) -> Vec { + self.utxos + .iter() + .map(|outpoint| self.utxos_map.get(outpoint).cloned().unwrap()) + .collect::>() + } +} + +impl FromIterator<(OutPoint, WeightedUtxo)> for OrderUtxos { + fn from_iter>(iter: T) -> Self { + let mut r = OrderUtxos::default(); + for (outpoint, weighted_utxo) in iter { + r.insert(outpoint, weighted_utxo); + } + r + } +} + +impl IntoIterator for OrderUtxos { + type Item = (OutPoint, WeightedUtxo); + type IntoIter = std::vec::IntoIter; + fn into_iter(mut self) -> Self::IntoIter { + self.utxos + .into_iter() + .map(|outpoint| (outpoint, self.utxos_map.remove(&outpoint).unwrap())) + .collect::>() + .into_iter() + } +} + +impl Extend<(OutPoint, WeightedUtxo)> for OrderUtxos { + fn extend>(&mut self, iter: T) { + for (outpoint, weighted_utxo) in iter { + self.insert(outpoint, weighted_utxo); + } + } +} + impl AsRef> for Wallet { fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { self.indexed_graph.graph() diff --git a/wallet/src/wallet/tx_builder.rs b/wallet/src/wallet/tx_builder.rs index 949cb792..a3de0d1c 100644 --- a/wallet/src/wallet/tx_builder.rs +++ b/wallet/src/wallet/tx_builder.rs @@ -52,8 +52,8 @@ use rand_core::RngCore; use super::coin_selection::CoinSelectionAlgorithm; use super::utils::shuffle_slice; use super::{CreateTxError, Wallet}; -use crate::collections::{BTreeMap, HashMap, HashSet}; -use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; +use crate::collections::{BTreeMap, HashSet}; +use crate::{KeychainKind, LocalOutput, OrderUtxos, Utxo, WeightedUtxo}; /// A transaction builder /// @@ -126,7 +126,7 @@ pub(crate) struct TxParams { pub(crate) fee_policy: Option, pub(crate) internal_policy_path: Option>>, pub(crate) external_policy_path: Option>>, - pub(crate) utxos: HashMap, + pub(crate) utxos: OrderUtxos, pub(crate) unspendable: HashSet, pub(crate) manually_selected_only: bool, pub(crate) sighash: Option, @@ -297,7 +297,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> { ) }) }) - .collect::, AddUtxoError>>()?; + .collect::>()?; self.params.utxos.extend(utxo_batch); Ok(self) diff --git a/wallet/tests/wallet.rs b/wallet/tests/wallet.rs index 8ed28159..b9a9011d 100644 --- a/wallet/tests/wallet.rs +++ b/wallet/tests/wallet.rs @@ -4699,3 +4699,68 @@ fn test_tx_details_method() { let tx_details_2_option = test_wallet.tx_details(txid_2); assert!(tx_details_2_option.is_none()); } + +#[test] +fn test_build_tx_untouch() { + let (mut wallet, txid) = get_funded_wallet_wpkh(); + let script_pubkey = wallet + .next_unused_address(KeychainKind::External) + .address + .script_pubkey(); + let tx1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint { txid, vout: 0 }, + ..Default::default() + }], + output: vec![ + TxOut { + value: Amount::from_sat(500), + script_pubkey: script_pubkey.clone(), + }; + 4 + ], + ..new_tx(0) + }; + + let txid1 = tx1.compute_txid(); + insert_tx(&mut wallet, tx1); + insert_anchor( + &mut wallet, + txid1, + ConfirmationBlockTime { + block_id: BlockId { + height: 2_100, + hash: BlockHash::all_zeros(), + }, + confirmation_time: 300, + }, + ); + let utxos = wallet + .list_unspent() + .map(|o| o.outpoint) + .collect::>(); + assert!(utxos.len() == 4); + + let mut builder = wallet.build_tx(); + builder + .ordering(bdk_wallet::TxOrdering::Untouched) + .add_utxos(&utxos) + .unwrap() + .add_recipient(script_pubkey.clone(), Amount::from_sat(400)) + .add_recipient(script_pubkey.clone(), Amount::from_sat(300)) + .add_recipient(script_pubkey.clone(), Amount::from_sat(500)); + let tx = builder.finish().unwrap().unsigned_tx; + let txins = tx + .input + .iter() + .map(|txin| txin.previous_output) + .collect::>(); + assert!(txins == utxos); + let txouts = tx + .output + .iter() + .take(3) + .map(|txout| txout.value.to_sat()) + .collect::>(); + assert!(txouts == vec![400, 300, 500]); +}