diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 67032cd3c..7992d86c7 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -334,9 +334,9 @@ impl Wallet { } /// Return the list of unspent outputs of this wallet - pub fn list_unspent(&self) -> Vec { + pub fn list_unspent(&self, include_reserved_utxos: bool) -> Vec { self.keychain_tracker - .full_utxos() + .full_utxos(include_reserved_utxos) .map(|(&(keychain, derivation_index), utxo)| LocalUtxo { outpoint: utxo.outpoint, txout: utxo.txout, @@ -388,9 +388,9 @@ impl Wallet { /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the /// wallet's database. - pub fn get_utxo(&self, op: OutPoint) -> Option { + pub fn get_utxo(&self, op: OutPoint, include_reserved_utxos: bool) -> Option { self.keychain_tracker - .full_utxos() + .full_utxos(include_reserved_utxos) .find_map(|(&(keychain, derivation_index), txo)| { if op == txo.outpoint { Some(LocalUtxo { @@ -527,11 +527,12 @@ impl Wallet { /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// values. - pub fn get_balance(&self) -> Balance { - self.keychain_tracker.balance(|keychain| match keychain { - KeychainKind::External => false, - KeychainKind::Internal => true, - }) + pub fn get_balance(&self, include_reserved_utxos: bool) -> Balance { + self.keychain_tracker + .balance(include_reserved_utxos, |keychain| match keychain { + KeychainKind::External => false, + KeychainKind::Internal => true, + }) } /// Add an external signer @@ -866,6 +867,7 @@ impl Wallet { params.drain_wallet, params.manually_selected_only, params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee + params.include_reserved_utxos, current_height.map(LockTime::to_consensus_u32), ); @@ -909,6 +911,10 @@ impl Wallet { }) .collect(); + coin_selection.selected.iter().for_each(|u| { + let _ = self.keychain_tracker.mark_reserved(u.outpoint()); + }); + if tx.output.is_empty() { // Uh oh, our transaction has no outputs. // We allow this when: @@ -1370,6 +1376,13 @@ impl Wallet { txout_index.unmark_used(&keychain, index); } } + + // release all the UTXOs that were reserved by this transaction + tx.input.iter().for_each(|input| { + let _ = self + .keychain_tracker + .unmark_reserved(&input.previous_output); + }); } fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { @@ -1391,8 +1404,8 @@ impl Wallet { Some(descriptor.at_derivation_index(child)) } - fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> { - self.list_unspent() + fn get_available_utxos(&self, include_reserved_utxos: bool) -> Vec<(LocalUtxo, usize)> { + self.list_unspent(include_reserved_utxos) .into_iter() .map(|utxo| { let keychain = utxo.keychain; @@ -1417,11 +1430,12 @@ impl Wallet { must_use_all_available: bool, manual_only: bool, must_only_use_confirmed_tx: bool, + include_reserved_utxos: bool, current_height: Option, ) -> (Vec, Vec) { // must_spend <- manually selected utxos // may_spend <- all other available utxos - let mut may_spend = self.get_available_utxos(); + let mut may_spend = self.get_available_utxos(include_reserved_utxos); may_spend.retain(|may_spend| { !manually_selected diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index dbd4811c1..6c999757f 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -97,6 +97,7 @@ impl TxBuilderContext for BumpFee {} /// // non-chaining /// let (psbt2, details) = { /// let mut builder = wallet.build_tx(); +/// builder.include_reserved_utxos(true); /// builder.ordering(TxOrdering::Untouched); /// for addr in &[addr1, addr2] { /// builder.add_recipient(addr.script_pubkey(), 50_000); @@ -150,6 +151,7 @@ pub(crate) struct TxParams { pub(crate) bumping_fee: Option, pub(crate) current_height: Option, pub(crate) allow_dust: bool, + pub(crate) include_reserved_utxos: bool, } #[derive(Clone, Copy, Debug)] @@ -274,12 +276,20 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in /// the "utxos" and the "unspendable" list, it will be spent. - pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> { + pub fn add_utxos( + &mut self, + outpoints: &[OutPoint], + include_reserved_utxos: bool, + ) -> Result<&mut Self, Error> { { let wallet = self.wallet.borrow(); let utxos = outpoints .iter() - .map(|outpoint| wallet.get_utxo(*outpoint).ok_or(Error::UnknownUtxo)) + .map(|outpoint| { + wallet + .get_utxo(*outpoint, include_reserved_utxos) + .ok_or(Error::UnknownUtxo) + }) .collect::, _>>()?; for utxo in utxos { @@ -299,8 +309,12 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in /// the "utxos" and the "unspendable" list, it will be spent. - pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, Error> { - self.add_utxos(&[outpoint]) + pub fn add_utxo( + &mut self, + outpoint: OutPoint, + include_reserved_utxos: bool, + ) -> Result<&mut Self, Error> { + self.add_utxos(&[outpoint], include_reserved_utxos) } /// Add a foreign UTXO i.e. a UTXO not owned by this wallet. @@ -576,6 +590,13 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, self.params.allow_dust = allow_dust; self } + + /// Specifies whether UTXOs used by previously built but not yet broadcasted + /// transactions can be used in the current transaction building process + pub fn include_reserved_utxos(&mut self, include_reserved_utxos: bool) -> &mut Self { + self.params.include_reserved_utxos = include_reserved_utxos; + self + } } impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 9b25223e4..319f21951 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -89,7 +89,7 @@ fn test_descriptor_checksum() { #[test] fn test_get_funded_wallet_balance() { let (wallet, _) = get_funded_wallet(get_test_wpkh()); - assert_eq!(wallet.get_balance().confirmed, 50000); + assert_eq!(wallet.get_balance(false).confirmed, 50000); } macro_rules! assert_fee_rate { @@ -435,14 +435,14 @@ fn test_create_tx_drain_to_and_utxos() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New); let utxos: Vec<_> = wallet - .list_unspent() + .list_unspent(false) .into_iter() .map(|u| u.outpoint) .collect(); let mut builder = wallet.build_tx(); builder .drain_to(addr.script_pubkey()) - .add_utxos(&utxos) + .add_utxos(&utxos, true) .unwrap(); let (psbt, details) = builder.finish().unwrap(); @@ -818,10 +818,13 @@ fn test_create_tx_add_utxo() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 30_000) - .add_utxo(OutPoint { - txid: small_output_tx.txid(), - vout: 0, - }) + .add_utxo( + OutPoint { + txid: small_output_tx.txid(), + vout: 0, + }, + false, + ) .unwrap(); let (psbt, details) = builder.finish().unwrap(); @@ -855,10 +858,13 @@ fn test_create_tx_manually_selected_insufficient() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), 30_000) - .add_utxo(OutPoint { - txid: small_output_tx.txid(), - vout: 0, - }) + .add_utxo( + OutPoint { + txid: small_output_tx.txid(), + vout: 0, + }, + false, + ) .unwrap() .manually_selected_only(); builder.finish().unwrap(); @@ -953,7 +959,7 @@ fn test_add_foreign_utxo() { get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo = wallet2.list_unspent().remove(0); + let utxo = wallet2.list_unspent(false).remove(0); let foreign_utxo_satisfaction = wallet2 .get_descriptor_for_keychain(KeychainKind::External) .max_satisfaction_weight() @@ -1017,7 +1023,7 @@ fn test_add_foreign_utxo() { #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] fn test_add_foreign_utxo_invalid_psbt_input() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let outpoint = wallet.list_unspent()[0].outpoint; + let outpoint = wallet.list_unspent(false)[0].outpoint; let foreign_utxo_satisfaction = wallet .get_descriptor_for_keychain(KeychainKind::External) .max_satisfaction_weight() @@ -1035,7 +1041,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { let (wallet2, txid2) = get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - let utxo2 = wallet2.list_unspent().remove(0); + let utxo2 = wallet2.list_unspent(false).remove(0); let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); @@ -1079,7 +1085,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { let (wallet2, txid2) = get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo2 = wallet2.list_unspent().remove(0); + let utxo2 = wallet2.list_unspent(false).remove(0); let satisfaction_weight = wallet2 .get_descriptor_for_keychain(KeychainKind::External) @@ -1087,6 +1093,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { .unwrap(); let mut builder = wallet1.build_tx(); + builder.include_reserved_utxos(true); builder.add_recipient(addr.script_pubkey(), 60_000); { @@ -1141,7 +1148,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { fn test_get_psbt_input() { // this should grab a known good utxo and set the input let (wallet, _) = get_funded_wallet(get_test_wpkh()); - for utxo in wallet.list_unspent() { + for utxo in wallet.list_unspent(false) { let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); } @@ -1459,10 +1466,13 @@ fn test_bump_fee_drain_wallet() { let mut builder = wallet.build_tx(); builder .drain_to(addr.script_pubkey()) - .add_utxo(OutPoint { - txid: tx.txid(), - vout: 0, - }) + .add_utxo( + OutPoint { + txid: tx.txid(), + vout: 0, + }, + false, + ) .unwrap() .manually_selected_only() .enable_rbf(); @@ -1514,7 +1524,7 @@ fn test_bump_fee_remove_output_manually_selected_only() { let mut builder = wallet.build_tx(); builder .drain_to(addr.script_pubkey()) - .add_utxo(outpoint) + .add_utxo(outpoint, false) .unwrap() .manually_selected_only() .enable_rbf(); @@ -1641,7 +1651,7 @@ fn test_bump_fee_no_change_add_input_and_change() { let mut builder = wallet.build_tx(); builder .drain_to(addr.script_pubkey()) - .add_utxo(op) + .add_utxo(op, false) .unwrap() .manually_selected_only() .enable_rbf(); @@ -1771,7 +1781,7 @@ fn test_bump_fee_force_add_input() { // the addition of an extra input with `add_utxo()` let mut builder = wallet.build_fee_bump(txid).unwrap(); builder - .add_utxo(incoming_op) + .add_utxo(incoming_op, false) .unwrap() .fee_rate(FeeRate::from_sat_per_vb(5.0)); let (psbt, details) = builder.finish().unwrap(); @@ -1826,7 +1836,10 @@ fn test_bump_fee_absolute_force_add_input() { // the new fee_rate is low enough that just reducing the change would be fine, but we force // the addition of an extra input with `add_utxo()` let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.add_utxo(incoming_op).unwrap().fee_absolute(250); + builder + .add_utxo(incoming_op, false) + .unwrap() + .fee_absolute(250); let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent + 25_000); @@ -1937,7 +1950,7 @@ fn test_fee_amount_negative_drain_val() { let mut builder = wallet.build_tx(); builder .add_recipient(send_to.script_pubkey(), 8630) - .add_utxo(incoming_op) + .add_utxo(incoming_op, false) .unwrap() .enable_rbf() .fee_rate(fee_rate); @@ -2109,6 +2122,7 @@ fn test_remove_partial_sigs_after_finalize_sign_option() { for remove_partial_sigs in &[true, false] { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); + builder.include_reserved_utxos(true); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap().0; @@ -2139,6 +2153,7 @@ fn test_try_finalize_sign_option() { for try_finalize in &[true, false] { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); + builder.include_reserved_utxos(true); builder.drain_to(addr.script_pubkey()).drain_wallet(); let mut psbt = builder.finish().unwrap().0; @@ -2641,7 +2656,7 @@ fn test_taproot_foreign_utxo() { let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo = wallet2.list_unspent().remove(0); + let utxo = wallet2.list_unspent(false).remove(0); let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); let foreign_utxo_satisfaction = wallet2 .get_descriptor_for_keychain(KeychainKind::External) @@ -3009,7 +3024,7 @@ fn test_spend_coinbase() { let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; let maturity_time = confirmation_height + COINBASE_MATURITY; - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); assert_eq!( balance, Balance { @@ -3054,7 +3069,7 @@ fn test_spend_coinbase() { hash: BlockHash::all_zeros(), }) .unwrap(); - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); assert_eq!( balance, Balance { @@ -3247,6 +3262,7 @@ fn test_tx_cancellation() { let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); let mut builder = $wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 10_000); + builder.include_reserved_utxos(true); let (psbt, _) = builder.finish().unwrap(); @@ -3307,3 +3323,91 @@ fn test_tx_cancellation() { .unwrap(); assert_eq!(change_derivation_4, (KeychainKind::Internal, 2)); } + +#[test] +fn test_utxo_reservation() { + // create wallet and fund it. + let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_address(New); + + // this transaction creates more utxos necessary for testing utxo reservation + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 10000); + let (psbt, _) = builder.finish().unwrap(); + + let tx1 = psbt.extract_tx(); + + wallet + .insert_checkpoint(BlockId { + height: 1_001, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + tx1.clone(), + ConfirmationTime::Confirmed { + height: 1_001, + time: 110, + }, + ) + .unwrap(); + + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 1000); + let (psbt, _) = builder.finish().unwrap(); + let tx2 = psbt.extract_tx(); + + let available_utxos = wallet + .list_unspent(false) + .iter() + .map(|u| u.outpoint) + .collect::>(); + + //check that every input is not in available utxos + for input in &tx2.input { + assert!(!available_utxos.contains(&input.previous_output)); + } + + let addr = wallet.get_address(New); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 1000); + let (psbt, _) = builder.finish().unwrap(); + let tx3 = psbt.extract_tx(); + + let available_utxos = wallet + .list_unspent(false) + .iter() + .map(|u| u.outpoint) + .collect::>(); + + //check that every input is not in available utxos + for input in &tx3.input { + assert!(!available_utxos.contains(&input.previous_output)); + } + + wallet.cancel_tx(&tx2); + let available_utxos = wallet + .list_unspent(false) + .iter() + .map(|u| u.outpoint) + .collect::>(); + + // check if reserved utxos are returned to utxo set after transaction cancellation + for input in &tx2.input { + assert!(available_utxos.contains(&input.previous_output)); + } + + wallet.cancel_tx(&tx3); + let available_utxos = wallet + .list_unspent(false) + .iter() + .map(|u| u.outpoint) + .collect::>(); + + // check if reserved utxos are returned to utxo set after transaction cancellation + for input in &tx3.input { + assert!(available_utxos.contains(&input.previous_output)); + } +} diff --git a/crates/chain/src/keychain/tracker.rs b/crates/chain/src/keychain/tracker.rs index fff5ee2b4..8f9bd9361 100644 --- a/crates/chain/src/keychain/tracker.rs +++ b/crates/chain/src/keychain/tracker.rs @@ -1,4 +1,4 @@ -use bitcoin::Transaction; +use bitcoin::{OutPoint, Transaction}; use miniscript::{Descriptor, DescriptorPublicKey}; use crate::{ @@ -21,6 +21,8 @@ pub struct KeychainTracker { /// Index between script pubkeys to transaction outputs pub txout_index: KeychainTxOutIndex, chain_graph: ChainGraph

, + /// UTXOs reserved by transactions as inputs + reserved: HashSet, } impl KeychainTracker @@ -126,9 +128,17 @@ where /// Refer to [`full_txouts`] for more. /// /// [`full_txouts`]: Self::full_txouts - pub fn full_utxos(&self) -> impl Iterator)> + '_ { - self.full_txouts() - .filter(|(_, txout)| txout.spent_by.is_none()) + pub fn full_utxos( + &self, + include_reserved_utxos: bool, + ) -> impl Iterator)> + '_ { + self.full_txouts().filter(move |(_, txout)| { + if include_reserved_utxos { + txout.spent_by.is_none() + } else { + txout.spent_by.is_none() && !self.reserved.contains(&txout.outpoint) + } + }) } /// Returns a reference to the internal [`ChainGraph`]. @@ -228,13 +238,17 @@ where /// /// When in doubt set `should_trust` to return false. This doesn't do anything other than change /// where the unconfirmed output's value is accounted for in `Balance`. - pub fn balance(&self, mut should_trust: impl FnMut(&K) -> bool) -> Balance { + pub fn balance( + &self, + include_reserved_utxos: bool, + mut should_trust: impl FnMut(&K) -> bool, + ) -> Balance { let mut immature = 0; let mut trusted_pending = 0; let mut untrusted_pending = 0; let mut confirmed = 0; let last_sync_height = self.chain().latest_checkpoint().map(|latest| latest.height); - for ((keychain, _), utxo) in self.full_utxos() { + for ((keychain, _), utxo) in self.full_utxos(include_reserved_utxos) { let chain_position = &utxo.chain_position; match chain_position.height() { @@ -272,12 +286,43 @@ where /// Returns the balance of all spendable confirmed unspent outputs of this tracker at a /// particular height. - pub fn balance_at(&self, height: u32) -> u64 { + pub fn balance_at(&self, include_reserved_utxos: bool, height: u32) -> u64 { self.full_txouts() - .filter(|(_, full_txout)| full_txout.is_spendable_at(height)) + .filter(|(_, full_txout)| { + if include_reserved_utxos { + full_txout.is_spendable_at(height) + } else { + full_txout.is_spendable_at(height) + && !self.reserved.contains(&full_txout.outpoint) + } + }) .map(|(_, full_txout)| full_txout.txout.value) .sum() } + + /// Returns boolean saying if or not the outpoint has been added to reserved UTXOs. + /// + /// Marks a UTXO(`outpoint`) as reserved by adding it to a set of utxos used + /// by transactions that have been created but not yet broadcasted. + /// Once a utxo is marked as reserved by a particular transaction either + /// during/after the transaction building process, it cannot be used for + /// building another transaction. + /// + /// This is useful in a scenario where you are creating a sequence of + /// transactions and you don't want utxos to be re-used as inputs + /// across transactions. + /// + /// A UTXO will be considered reserved until it is released by [`Self::unmark_reserved`] + /// to the wallet's UTXO pool. + pub fn mark_reserved(&mut self, outpoint: OutPoint) -> bool { + self.reserved.insert(outpoint) + } + + /// Undoes the effect of [`Self::mark_reserved`]. Returns whether `outpoint` + /// has been removed from `reserved` + pub fn unmark_reserved(&mut self, outpoint: &OutPoint) -> bool { + self.reserved.remove(outpoint) + } } impl Default for KeychainTracker { @@ -285,6 +330,7 @@ impl Default for KeychainTracker { Self { txout_index: Default::default(), chain_graph: Default::default(), + reserved: Default::default(), } } } diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs index 3bf0a1d50..6319ab750 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -63,14 +63,14 @@ fn test_balance() { use core::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] enum Keychain { - One, - Two, + External, + Internal, } let mut tracker = KeychainTracker::::default(); - let one = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#rg247h69").unwrap(); - let two = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1/*)#ju05rz2a").unwrap(); - tracker.add_keychain(Keychain::One, one); - tracker.add_keychain(Keychain::Two, two); + let external = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#rg247h69").unwrap(); + let internal = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1/*)#ju05rz2a").unwrap(); + tracker.add_keychain(Keychain::External, external); + tracker.add_keychain(Keychain::Internal, internal); let tx1 = Transaction { version: 0x01, @@ -80,7 +80,7 @@ fn test_balance() { value: 13_000, script_pubkey: tracker .txout_index - .reveal_next_spk(&Keychain::One) + .reveal_next_spk(&Keychain::External) .0 .1 .clone(), @@ -95,7 +95,7 @@ fn test_balance() { value: 7_000, script_pubkey: tracker .txout_index - .reveal_next_spk(&Keychain::Two) + .reveal_next_spk(&Keychain::Internal) .0 .1 .clone(), @@ -110,7 +110,7 @@ fn test_balance() { value: 11_000, script_pubkey: tracker .txout_index - .reveal_next_spk(&Keychain::Two) + .reveal_next_spk(&Keychain::Internal) .0 .1 .clone(), @@ -127,18 +127,18 @@ fn test_balance() { .unwrap(); let should_trust = |keychain: &Keychain| match *keychain { - Keychain::One => false, - Keychain::Two => true, + Keychain::External => false, + Keychain::Internal => true, }; - assert_eq!(tracker.balance(should_trust), Balance::default()); + assert_eq!(tracker.balance(true, should_trust), Balance::default()); let _ = tracker .insert_tx(tx1.clone(), TxHeight::Unconfirmed) .unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { untrusted_pending: 13_000, ..Default::default() @@ -150,7 +150,7 @@ fn test_balance() { .unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { trusted_pending: 7_000, untrusted_pending: 13_000, @@ -163,7 +163,7 @@ fn test_balance() { .unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { trusted_pending: 7_000, untrusted_pending: 13_000, @@ -175,7 +175,7 @@ fn test_balance() { let _ = tracker.insert_tx(tx1, TxHeight::Confirmed(1)).unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { trusted_pending: 7_000, untrusted_pending: 0, @@ -187,7 +187,7 @@ fn test_balance() { let _ = tracker.insert_tx(tx2, TxHeight::Confirmed(2)).unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { trusted_pending: 0, untrusted_pending: 0, @@ -204,7 +204,7 @@ fn test_balance() { .unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { trusted_pending: 0, untrusted_pending: 0, @@ -221,7 +221,7 @@ fn test_balance() { .unwrap(); assert_eq!( - tracker.balance(should_trust), + tracker.balance(true, should_trust), Balance { trusted_pending: 0, untrusted_pending: 0, @@ -230,10 +230,10 @@ fn test_balance() { } ); - assert_eq!(tracker.balance_at(0), 0); - assert_eq!(tracker.balance_at(1), 13_000); - assert_eq!(tracker.balance_at(2), 20_000); - assert_eq!(tracker.balance_at(98), 20_000); - assert_eq!(tracker.balance_at(99), 31_000); - assert_eq!(tracker.balance_at(100), 31_000); + assert_eq!(tracker.balance_at(true, 0), 0); + assert_eq!(tracker.balance_at(true, 1), 13_000); + assert_eq!(tracker.balance_at(true, 2), 20_000); + assert_eq!(tracker.balance_at(true, 98), 20_000); + assert_eq!(tracker.balance_at(true, 99), 31_000); + assert_eq!(tracker.balance_at(true, 100), 31_000); } diff --git a/example-crates/keychain_tracker_electrum/src/main.rs b/example-crates/keychain_tracker_electrum/src/main.rs index c8b9e0684..9157aeec5 100644 --- a/example-crates/keychain_tracker_electrum/src/main.rs +++ b/example-crates/keychain_tracker_electrum/src/main.rs @@ -173,7 +173,7 @@ fn main() -> anyhow::Result<()> { if utxos { let utxos = tracker - .full_utxos() + .full_utxos(false) .map(|(_, utxo)| utxo) .collect::>(); outpoints = Box::new( diff --git a/example-crates/keychain_tracker_esplora/src/main.rs b/example-crates/keychain_tracker_esplora/src/main.rs index cae5e9601..d12508abe 100644 --- a/example-crates/keychain_tracker_esplora/src/main.rs +++ b/example-crates/keychain_tracker_esplora/src/main.rs @@ -181,7 +181,7 @@ fn main() -> anyhow::Result<()> { if utxos { let utxos = tracker - .full_utxos() + .full_utxos(false) .map(|(_, utxo)| utxo) .collect::>(); outpoints = Box::new( diff --git a/example-crates/keychain_tracker_example_cli/src/lib.rs b/example-crates/keychain_tracker_example_cli/src/lib.rs index df42df1ac..af83945b0 100644 --- a/example-crates/keychain_tracker_example_cli/src/lib.rs +++ b/example-crates/keychain_tracker_example_cli/src/lib.rs @@ -57,7 +57,10 @@ pub enum Commands { addr_cmd: AddressCmd, }, /// Get the wallet balance. - Balance, + Balance { + #[clap(long)] + include_reserved_utxos: bool, + }, /// TxOut related commands. #[clap(name = "txout")] TxOut { @@ -67,6 +70,8 @@ pub enum Commands { /// Send coins to an address. Send { value: u64, + #[clap(long)] + include_reserved_utxos: bool, address: Address, #[clap(short, default_value = "largest-first")] coin_select: CoinSelectionAlgo, @@ -241,18 +246,21 @@ where } } -pub fn run_balance_cmd(tracker: &Mutex>) { +pub fn run_balance_cmd( + tracker: &Mutex>, + include_reserved_utxos: bool, +) { let tracker = tracker.lock().unwrap(); - let (confirmed, unconfirmed) = - tracker - .full_utxos() - .fold((0, 0), |(confirmed, unconfirmed), (_, utxo)| { - if utxo.chain_position.height().is_confirmed() { - (confirmed + utxo.txout.value, unconfirmed) - } else { - (confirmed, unconfirmed + utxo.txout.value) - } - }); + let (confirmed, unconfirmed) = tracker.full_utxos(include_reserved_utxos).fold( + (0, 0), + |(confirmed, unconfirmed), (_, utxo)| { + if utxo.chain_position.height().is_confirmed() { + (confirmed + utxo.txout.value, unconfirmed) + } else { + (confirmed, unconfirmed + utxo.txout.value) + } + }, + ); println!("confirmed: {}", confirmed); println!("unconfirmed: {}", unconfirmed); @@ -274,7 +282,7 @@ pub fn run_txo_cmd( #[allow(clippy::type_complexity)] // FIXME let txouts: Box)>> = match (unspent, spent) { - (true, false) => Box::new(tracker.full_utxos()), + (true, false) => Box::new(tracker.full_utxos(false)), (false, true) => Box::new( tracker .full_txouts() @@ -315,6 +323,7 @@ pub fn run_txo_cmd( #[allow(clippy::type_complexity)] // FIXME pub fn create_tx( value: u64, + include_reserved_utxos: bool, address: Address, coin_select: CoinSelectionAlgo, keychain_tracker: &mut KeychainTracker, @@ -331,7 +340,8 @@ pub fn create_tx( }; // TODO use planning module - let mut candidates = planned_utxos(keychain_tracker, &assets).collect::>(); + let mut candidates = + planned_utxos(keychain_tracker, &assets, include_reserved_utxos).collect::>(); // apply coin selection algorithm match coin_select { @@ -543,8 +553,10 @@ where match command { // TODO: Make these functions return stuffs Commands::Address { addr_cmd } => run_address_cmd(tracker, store, addr_cmd, network), - Commands::Balance => { - run_balance_cmd(tracker); + Commands::Balance { + include_reserved_utxos, + } => { + run_balance_cmd(tracker, include_reserved_utxos); Ok(()) } Commands::TxOut { txout_cmd } => { @@ -553,14 +565,21 @@ where } Commands::Send { value, + include_reserved_utxos, address, coin_select, } => { let (transaction, change_index) = { // take mutable ref to construct tx -- it is only open for a short time while building it. let tracker = &mut *tracker.lock().unwrap(); - let (transaction, change_info) = - create_tx(value, address, coin_select, tracker, keymap)?; + let (transaction, change_info) = create_tx( + value, + include_reserved_utxos, + address, + coin_select, + tracker, + keymap, + )?; if let Some((change_derivation_changes, (change_keychain, index))) = change_info { // We must first persist to disk the fact that we've got a new address from the @@ -672,10 +691,10 @@ where pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, P: ChainPosition>( tracker: &'a KeychainTracker, assets: &'a bdk_tmp_plan::Assets, + include_reserved_utxos: bool, ) -> impl Iterator, FullTxOut

)> + 'a { - tracker - .full_utxos() - .filter_map(move |((keychain, derivation_index), full_txout)| { + tracker.full_utxos(include_reserved_utxos).filter_map( + move |((keychain, derivation_index), full_txout)| { Some(( bdk_tmp_plan::plan_satisfaction( &tracker @@ -688,5 +707,6 @@ pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, P: ChainPosition>( )?, full_txout, )) - }) + }, + ) } diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 5145d593b..0e7e4daa0 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { let address = wallet.get_address(bdk::wallet::AddressIndex::New); println!("Generated Address: {}", address); - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); println!("Wallet balance before syncing: {} sats", balance.total()); print!("Syncing..."); @@ -74,7 +74,7 @@ fn main() -> Result<(), Box> { wallet.apply_update(update)?; wallet.commit()?; - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); println!("Wallet balance after syncing: {} sats", balance.total()); if balance.total() < SEND_AMOUNT { diff --git a/example-crates/wallet_esplora/src/main.rs b/example-crates/wallet_esplora/src/main.rs index d8eda32a2..494dcbad4 100644 --- a/example-crates/wallet_esplora/src/main.rs +++ b/example-crates/wallet_esplora/src/main.rs @@ -28,7 +28,7 @@ fn main() -> Result<(), Box> { let address = wallet.get_address(AddressIndex::New); println!("Generated Address: {}", address); - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); println!("Wallet balance before syncing: {} sats", balance.total()); print!("Syncing..."); @@ -66,7 +66,7 @@ fn main() -> Result<(), Box> { wallet.apply_update(update)?; wallet.commit()?; - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); println!("Wallet balance after syncing: {} sats", balance.total()); if balance.total() < SEND_AMOUNT { diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index b78b09dfa..4c50a7001 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -29,7 +29,7 @@ async fn main() -> Result<(), Box> { let address = wallet.get_address(AddressIndex::New); println!("Generated Address: {}", address); - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); println!("Wallet balance before syncing: {} sats", balance.total()); print!("Syncing..."); @@ -69,7 +69,7 @@ async fn main() -> Result<(), Box> { wallet.apply_update(update)?; wallet.commit()?; - let balance = wallet.get_balance(); + let balance = wallet.get_balance(false); println!("Wallet balance after syncing: {} sats", balance.total()); if balance.total() < SEND_AMOUNT {