diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5251388..9afcaa49d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 type to mark for a missing client. - Upgrade `tokio` to `1.0`. +#### Transaction Creation Overhaul + +The `TxBuilder` is now created from the `build_tx` or `build_fee_bump` functions on wallet and the +final transaction is created by calling `finish` on the builder. + +- Removed `TxBuilder::utxos` in favor of `TxBuilder::add_utxos` +- Added `Wallet::build_tx` to replace `Wallet::create_tx` +- Added `Wallet::build_fee_bump` to replace `Wallet::bump_fee` +- Added `Wallet::get_utxo` +- Added `Wallet::get_descriptor_for_keychain` + ### CLI #### Changed - Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository diff --git a/Cargo.toml b/Cargo.toml index 15dfbd126..12b0fd1a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,8 +60,8 @@ test-electrum = ["electrum"] test-md-docs = ["electrum"] [dev-dependencies] -bdk-testutils = "0.2" -bdk-testutils-macros = "0.2" +bdk-testutils = { path = "./testutils" } +bdk-testutils-macros = { path = "./testutils-macros" } serial_test = "0.4" lazy_static = "1.4" env_logger = "0.7" diff --git a/README.md b/README.md index f13e2d324..ef491d033 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ fn main() -> Result<(), bdk::Error> { ### Create a transaction ```rust,no_run -use bdk::{FeeRate, TxBuilder, Wallet}; +use bdk::{FeeRate, Wallet}; use bdk::database::MemoryDatabase; use bdk::blockchain::{noop_progress, ElectrumBlockchain}; @@ -108,12 +108,15 @@ fn main() -> Result<(), bdk::Error> { wallet.sync(noop_progress(), None)?; let send_to = wallet.get_new_address()?; - let (psbt, details) = wallet.create_tx( - TxBuilder::with_recipients(vec![(send_to.script_pubkey(), 50_000)]) + let (psbt, details) = { + let mut builder = wallet.build_tx(); + builder + .add_recipient(send_to.script_pubkey(), 50_000) .enable_rbf() .do_not_spend_change() - .fee_rate(FeeRate::from_sat_per_vb(5.0)) - )?; + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + builder.finish()? + }; println!("Transaction details: {:#?}", details); println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); diff --git a/examples/address_validator.rs b/examples/address_validator.rs index d00a239ae..8730fb794 100644 --- a/examples/address_validator.rs +++ b/examples/address_validator.rs @@ -35,6 +35,7 @@ use bitcoin::hashes::hex::FromHex; use bitcoin::util::bip32::Fingerprint; use bitcoin::{Network, Script}; +#[derive(Debug)] struct DummyValidator; impl AddressValidator for DummyValidator { fn validate( diff --git a/src/database/memory.rs b/src/database/memory.rs index cda3775e9..84ecf30a6 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -458,16 +458,17 @@ impl ConfigurableDatabase for MemoryDatabase { } } -#[cfg(test)] -impl MemoryDatabase { - // Artificially insert a tx in the database, as if we had found it with a `sync` - pub fn received_tx( - &mut self, - tx_meta: testutils::TestIncomingTx, - current_height: Option, - ) -> bitcoin::Txid { - use std::str::FromStr; - +#[macro_export] +#[doc(hidden)] +/// Artificially insert a tx in the database, as if we had found it with a `sync`. This is a hidden +/// macro and not a `[cfg(test)]` function so it can be called within the context of doctests which +/// don't have `test` set. +macro_rules! populate_test_db { + ($db:expr, $tx_meta:expr, $current_height:expr$(,)?) => {{ + use $crate::database::BatchOperations; + let mut db = $db; + let tx_meta = $tx_meta; + let current_height: Option = $current_height; let tx = Transaction { version: 1, lock_time: 0, @@ -499,9 +500,9 @@ impl MemoryDatabase { fees: 0, }; - self.set_tx(&tx_details).unwrap(); + db.set_tx(&tx_details).unwrap(); for (vout, out) in tx.output.iter().enumerate() { - self.set_utxo(&UTXO { + db.set_utxo(&UTXO { txout: out.clone(), outpoint: OutPoint { txid, @@ -513,7 +514,37 @@ impl MemoryDatabase { } txid - } + }}; +} + +#[macro_export] +#[doc(hidden)] +/// Macro for getting a wallet for use in a doctest +macro_rules! doctest_wallet { + () => {{ + use $crate::bitcoin::Network; + use $crate::database::MemoryDatabase; + use testutils::testutils; + let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; + let descriptors = testutils!(@descriptors (descriptor) (descriptor)); + + let mut db = MemoryDatabase::new(); + let txid = populate_test_db!( + &mut db, + testutils! { + @tx ( (@external descriptors, 0) => 500_000 ) (@confirmations 1) + }, + Some(100), + ); + + $crate::Wallet::new_offline( + &descriptors.0, + descriptors.1.as_ref(), + Network::Regtest, + db + ) + .unwrap() + }} } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index d2737b474..3221dde79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,7 +115,7 @@ //! ### Example //! ```ignore //! use base64::decode; -//! use bdk::{FeeRate, TxBuilder, Wallet}; +//! use bdk::{FeeRate, Wallet}; //! use bdk::database::MemoryDatabase; //! use bdk::blockchain::{noop_progress, ElectrumBlockchain}; //! @@ -136,12 +136,12 @@ //! wallet.sync(noop_progress(), None)?; //! //! let send_to = wallet.get_new_address()?; -//! let (psbt, details) = wallet.create_tx( -//! TxBuilder::with_recipients(vec![(send_to.script_pubkey(), 50_000)]) -//! .enable_rbf() -//! .do_not_spend_change() -//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) -//! )?; +//! let (psbt, details) = wallet.build_tx() +//! .add_recipient(send_to.script_pubkey(), 50_000) +//! .enable_rbf() +//! .do_not_spend_change() +//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) +//! .finish()?; //! //! println!("Transaction details: {:#?}", details); //! println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); diff --git a/src/wallet/address_validator.rs b/src/wallet/address_validator.rs index c1555f6cd..4484efef0 100644 --- a/src/wallet/address_validator.rs +++ b/src/wallet/address_validator.rs @@ -45,6 +45,7 @@ //! # use bdk::address_validator::*; //! # use bdk::database::*; //! # use bdk::*; +//! #[derive(Debug)] //! struct PrintAddressAndContinue; //! //! impl AddressValidator for PrintAddressAndContinue { @@ -111,7 +112,7 @@ impl std::error::Error for AddressValidatorError {} /// validator will be propagated up to the original caller that triggered the address generation. /// /// For a usage example see [this module](crate::address_validator)'s documentation. -pub trait AddressValidator: Send + Sync { +pub trait AddressValidator: Send + Sync + fmt::Debug { /// Validate or inspect an address fn validate( &self, @@ -127,8 +128,8 @@ mod test { use super::*; use crate::wallet::test::{get_funded_wallet, get_test_wpkh}; - use crate::wallet::TxBuilder; + #[derive(Debug)] struct TestValidator; impl AddressValidator for TestValidator { fn validate( @@ -157,11 +158,8 @@ mod test { wallet.add_address_validator(Arc::new(TestValidator)); let addr = testutils!(@external descriptors, 10); - wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + builder.finish().unwrap(); } } diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index d4fcd02af..c57f251de 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -27,19 +27,16 @@ //! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to //! define custom coin selection algorithms. //! -//! The coin selection algorithm is not globally part of a [`Wallet`](super::Wallet), instead it -//! is selected whenever a [`Wallet::create_tx`](super::Wallet::create_tx) call is made, through -//! the use of the [`TxBuilder`] structure, specifically with -//! [`TxBuilder::coin_selection`](super::tx_builder::TxBuilder::coin_selection) method. -//! -//! The [`DefaultCoinSelectionAlgorithm`] selects the default coin selection algorithm that -//! [`TxBuilder`] uses, if it's not explicitly overridden. +//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on +//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will +//! be used if it is not explicitly set. //! //! [`TxBuilder`]: super::tx_builder::TxBuilder +//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection //! //! ## Example //! -//! ```no_run +//! ``` //! # use std::str::FromStr; //! # use bitcoin::*; //! # use bdk::wallet::coin_selection::*; @@ -84,14 +81,16 @@ //! } //! } //! -//! # let wallet = Wallet::new_offline("", None, Network::Testnet, bdk::database::MemoryDatabase::default())?; +//! # let wallet = doctest_wallet!(); //! // create wallet, sync, ... //! //! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); -//! let (psbt, details) = wallet.create_tx( -//! TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)]) -//! .coin_selection(AlwaysSpendEverything), -//! )?; +//! let (psbt, details) = { +//! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything); +//! builder +//! .add_recipient(to_address.script_pubkey(), 50_000); +//! builder.finish()? +//! }; //! //! // inspect, sign, broadcast, ... //! diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index cabb9b422..19dfff3cb 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -57,8 +57,9 @@ pub(crate) mod utils; pub use utils::IsDust; use address_validator::AddressValidator; +use coin_selection::DefaultCoinSelectionAlgorithm; use signer::{Signer, SignerOrdering, SignersContainer}; -use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxBuilderContext}; +use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; use utils::{ check_nlocktime, check_nsequence_rbf, descriptor_to_pk_ctx, After, Older, SecpCtx, DUST_LIMIT_SATOSHI, @@ -81,11 +82,12 @@ const CACHE_ADDR_BATCH_SIZE: u32 = 100; /// A wallet takes descriptors, a [`database`](trait@crate::database::Database) and a /// [`blockchain`](trait@crate::blockchain::Blockchain) and implements the basic functions that a Bitcoin wallets /// needs to operate, like [generating addresses](Wallet::get_new_address), [returning the balance](Wallet::get_balance), -/// [creating transactions](Wallet::create_tx), etc. +/// [creating transactions](Wallet::build_tx), etc. /// /// A wallet can be either "online" if the [`blockchain`](crate::blockchain) type provided /// implements [`Blockchain`], or "offline" if it is the unit type `()`. Offline wallets only expose /// methods that don't need any interaction with the blockchain to work. +#[derive(Debug)] pub struct Wallet { descriptor: ExtendedDescriptor, change_descriptor: Option, @@ -200,6 +202,12 @@ where self.database.borrow().iter_utxos() } + /// Returns the `UTXO` owned by this wallet corresponding to `outpoint` if it exists in the + /// wallet's database. + pub fn get_utxo(&self, outpoint: OutPoint) -> Result, Error> { + self.database.borrow().get_utxo(&outpoint) + } + /// Return the list of transactions made and received by the wallet /// /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if @@ -246,27 +254,45 @@ where self.address_validators.push(validator); } - /// Create a new transaction following the options specified in the `builder` + /// Start building a transaction. + /// + /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. /// /// ## Example /// - /// ```no_run + /// ``` /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; /// # use bdk::database::*; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; + /// # let wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); - /// let (psbt, details) = wallet.create_tx( - /// TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)]) - /// )?; + /// let (psbt, details) = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), 50_000); + /// builder.finish()? + /// }; + /// /// // sign and broadcast ... /// # Ok::<(), bdk::Error>(()) /// ``` - pub fn create_tx>( + /// + /// [`TxBuilder`]: crate::TxBuilder + pub fn build_tx(&self) -> TxBuilder<'_, B, D, DefaultCoinSelectionAlgorithm, CreateTx> { + TxBuilder { + wallet: &self, + params: TxParams::default(), + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + } + } + + pub(crate) fn create_tx>( &self, - builder: TxBuilder, + coin_selection: Cs, + params: TxParams, ) -> Result<(PSBT, TransactionDetails), Error> { let external_policy = self .descriptor @@ -285,24 +311,24 @@ where // The policy allows spending external outputs, but it requires a policy path that hasn't been // provided - if builder.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange + if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange && external_policy.requires_path() - && builder.external_policy_path.is_none() + && params.external_policy_path.is_none() { return Err(Error::SpendingPolicyRequired(KeychainKind::External)); }; // Same for the internal_policy path, if present if let Some(internal_policy) = &internal_policy { - if builder.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden && internal_policy.requires_path() - && builder.internal_policy_path.is_none() + && params.internal_policy_path.is_none() { return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); }; } let external_requirements = external_policy.get_condition( - builder + params .external_policy_path .as_ref() .unwrap_or(&BTreeMap::new()), @@ -311,7 +337,7 @@ where .map(|policy| { Ok::<_, Error>( policy.get_condition( - builder + params .internal_policy_path .as_ref() .unwrap_or(&BTreeMap::new()), @@ -325,7 +351,7 @@ where .merge(&internal_requirements.unwrap_or_default())?; debug!("Policy requirements: {:?}", requirements); - let version = match builder.version { + let version = match params.version { Some(tx_builder::Version(0)) => { return Err(Error::Generic("Invalid version `0`".into())) } @@ -340,7 +366,7 @@ where _ => 1, }; - let lock_time = match builder.locktime { + let lock_time = match params.locktime { // No nLockTime, default to 0 None => requirements.timelock.unwrap_or(0), // Specific nLockTime required and we have no constraints, so just set to that value @@ -351,7 +377,7 @@ where Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{}`, but at least `{}` is required to spend from this script", x, requirements.timelock.unwrap()))) }; - let n_sequence = match (builder.rbf, requirements.csv) { + let n_sequence = match (params.rbf, requirements.csv) { // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final (None, None) if lock_time != 0 => 0xFFFFFFFE, // No RBF, CSV or nLockTime, make the transaction final @@ -385,6 +411,35 @@ where (Some(rbf), _) => rbf.get_value(), }; + let (fee_rate, mut fee_amount) = match params + .fee_policy + .as_ref() + .unwrap_or(&FeePolicy::FeeRate(FeeRate::default())) + { + //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 + FeePolicy::FeeAmount(fee) => { + if let Some(previous_fee) = params.bumping_fee { + if *fee < previous_fee.absolute { + return Err(Error::FeeTooLow { + required: previous_fee.absolute, + }); + } + } + (FeeRate::from_sat_per_vb(0.0), *fee as f32) + } + FeePolicy::FeeRate(rate) => { + if let Some(previous_fee) = params.bumping_fee { + let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0); + if *rate < required_feerate { + return Err(Error::FeeRateTooLow { + required: required_feerate, + }); + } + } + (*rate, 0.0) + } + }; + let mut tx = Transaction { version, lock_time, @@ -392,23 +447,14 @@ where output: vec![], }; - let (fee_rate, mut fee_amount) = match builder - .fee_policy - .as_ref() - .unwrap_or(&FeePolicy::FeeRate(FeeRate::default())) - { - FeePolicy::FeeAmount(amount) => (FeeRate::from_sat_per_vb(0.0), *amount as f32), - FeePolicy::FeeRate(rate) => (*rate, 0.0), - }; - - // try not to move from `builder` because we still need to use it later. - let recipients = match &builder.single_recipient { + let recipients = match ¶ms.single_recipient { Some(recipient) => vec![(recipient, 0)], - None => builder.recipients.iter().map(|(r, v)| (r, *v)).collect(), + None => params.recipients.iter().map(|(r, v)| (r, *v)).collect(), }; - if builder.single_recipient.is_some() - && !builder.manually_selected_only - && !builder.drain_wallet + if params.single_recipient.is_some() + && !params.manually_selected_only + && !params.drain_wallet + && params.bumping_fee.is_none() { return Err(Error::SingleRecipientNoInputs); } @@ -416,7 +462,7 @@ where return Err(Error::NoRecipients); } - if builder.manually_selected_only && builder.utxos.is_empty() { + if params.manually_selected_only && params.utxos.is_empty() { return Err(Error::NoUtxosSelected); } @@ -428,7 +474,7 @@ where fee_amount += calc_fee_bytes(tx.get_weight()); for (index, (script_pubkey, satoshi)) in recipients.into_iter().enumerate() { - let value = match builder.single_recipient { + let value = match params.single_recipient { Some(_) => 0, None if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)), None => satoshi, @@ -449,7 +495,7 @@ where outgoing += value; } - if builder.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed && self.change_descriptor.is_none() { return Err(Error::Generic( @@ -458,19 +504,19 @@ where } let (required_utxos, optional_utxos) = self.preselect_utxos( - builder.change_policy, - &builder.unspendable, - &builder.utxos, - builder.drain_wallet, - builder.manually_selected_only, - false, // we don't mind using unconfirmed outputs here, hopefully coin selection will sort this out? + params.change_policy, + ¶ms.unspendable, + params.utxos.clone(), + params.drain_wallet, + params.manually_selected_only, + params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee )?; let coin_selection::CoinSelectionResult { selected, selected_amount, mut fee_amount, - } = builder.coin_selection.coin_select( + } = coin_selection.coin_select( self.database.borrow().deref(), required_utxos, optional_utxos, @@ -489,7 +535,7 @@ where .collect(); // prepare the change output - let change_output = match builder.single_recipient { + let change_output = match params.single_recipient { Some(_) => None, None => { let change_script = self.get_change_address()?; @@ -538,10 +584,10 @@ where } // sort input/outputs according to the chosen algorithm - builder.ordering.sort_tx(&mut tx); + params.ordering.sort_tx(&mut tx); let txid = tx.txid(); - let psbt = self.complete_transaction(tx, selected, builder)?; + let psbt = self.complete_transaction(tx, selected, params)?; let transaction_details = TransactionDetails { transaction: None, @@ -556,42 +602,56 @@ where Ok((psbt, transaction_details)) } - /// Bump the fee of a transaction following the options specified in the `builder` + /// Bump the fee of a transaction previously created with this wallet. /// - /// Return an error if the transaction is already confirmed or doesn't explicitly signal RBF. + /// Returns an error if the transaction is already confirmed or doesn't explicitly signal + /// *repalce by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] + /// pre-populated with the inputs and outputs of the original transaction. /// /// **NOTE**: if the original transaction was made with [`TxBuilder::set_single_recipient`], /// the [`TxBuilder::maintain_single_recipient`] flag should be enabled to correctly reduce the /// only output's value in order to increase the fees. /// - /// If the `builder` specifies some `utxos` that must be spent, they will be added to the - /// transaction regardless of whether they are necessary or not to cover additional fees. - /// /// ## Example /// /// ```no_run + /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; /// # use bdk::database::*; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; - /// let txid = Txid::from_str("faff0a466b70f5d5f92bd757a92c1371d4838bdd5bc53a06764e2488e51ce8f8").unwrap(); - /// let (psbt, details) = wallet.bump_fee( - /// &txid, - /// TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(5.0)), - /// )?; - /// // sign and broadcast ... + /// # let wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (psbt, _) = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), 50_000) + /// .enable_rbf(); + /// builder.finish()? + /// }; + /// let (psbt, _) = wallet.sign(psbt, None)?; + /// let tx = psbt.extract_tx(); + /// // broadcast tx but it's taking too long to confirm so we want to bump the fee + /// let (psbt, _) = { + /// let mut builder = wallet.build_fee_bump(tx.txid())?; + /// builder + /// .fee_rate(FeeRate::from_sat_per_vb(5.0)); + /// builder.finish()? + /// }; + /// + /// let (psbt, _) = wallet.sign(psbt, None)?; + /// let fee_bumped_tx = psbt.extract_tx(); + /// // broadcast fee_bumped_tx to replace original /// # Ok::<(), bdk::Error>(()) /// ``` // TODO: support for merging multiple transactions while bumping the fees // TODO: option to force addition of an extra output? seems bad for privacy to update the // change - pub fn bump_fee>( + pub fn build_fee_bump( &self, - txid: &Txid, - builder: TxBuilder, - ) -> Result<(PSBT, TransactionDetails), Error> { + txid: Txid, + ) -> Result, Error> { let mut details = match self.database.borrow().get_tx(&txid, true)? { None => return Err(Error::TransactionNotFound), Some(tx) if tx.transaction.is_none() => return Err(Error::TransactionNotFound), @@ -603,68 +663,15 @@ where return Err(Error::IrreplaceableTransaction); } - // the new tx must "pay for its bandwidth" let vbytes = tx.get_weight() as f32 / 4.0; - let required_feerate = FeeRate::from_sat_per_vb(details.fees as f32 / vbytes + 1.0); - - // find the index of the output that we can update. either the change or the only one if - // it's `single_recipient` - let updatable_output = match builder.single_recipient { - Some(_) if tx.output.len() != 1 => return Err(Error::SingleRecipientMultipleOutputs), - Some(_) => Some(0), - None => { - let mut change_output = None; - for (index, txout) in tx.output.iter().enumerate() { - // look for an output that we know and that has the right KeychainKind. We use - // `get_descriptor_for` to find what's the KeychainKind for `Internal` - // addresses really is, because if there's no change_descriptor it's actually equal - // to "External" - let (_, change_type) = self.get_descriptor_for_keychain(KeychainKind::Internal); - match self - .database - .borrow() - .get_path_from_script_pubkey(&txout.script_pubkey)? - { - Some((keychain, _)) if keychain == change_type => { - change_output = Some(index); - break; - } - _ => {} - } - } - - change_output - } - }; - let updatable_output = match updatable_output { - Some(updatable_output) => updatable_output, - None => { - // we need a change output, add one here and take into account the extra fees for it - let change_script = self.get_change_address()?; - let change_txout = TxOut { - script_pubkey: change_script, - value: 0, - }; - tx.output.push(change_txout); - - tx.output.len() - 1 - } - }; - - // initially always remove the output we can change - let mut removed_updatable_output = tx.output.remove(updatable_output); - if self.is_mine(&removed_updatable_output.script_pubkey)? { - details.received -= removed_updatable_output.value; - } + let feerate = details.fees as f32 / vbytes; let deriv_ctx = descriptor_to_pk_ctx(&self.secp); - let original_sequence = tx.input[0].sequence; - // remove the inputs from the tx and process them let original_txin = tx.input.drain(..).collect::>(); - let mut original_utxos = original_txin + let original_utxos = original_txin .iter() - .map(|txin| -> Result<(UTXO, usize), Error> { + .map(|txin| -> Result<_, Error> { let txout = self .database .borrow() @@ -677,7 +684,7 @@ where .get_path_from_script_pubkey(&txout.script_pubkey)? { Some((keychain, _)) => ( - self.get_descriptor_for_keychain(keychain) + self._get_descriptor_for_keychain(keychain) .0 .max_satisfaction_weight(deriv_ctx) .unwrap(), @@ -702,134 +709,47 @@ where }) .collect::, _>>()?; - if builder.manually_selected_only && builder.utxos.is_empty() { - return Err(Error::NoUtxosSelected); - } - - let builder_extra_utxos = builder - .utxos - .iter() - .filter(|utxo| { - !original_txin - .iter() - .any(|txin| &&txin.previous_output == utxo) - }) - .cloned() - .collect::>(); - - let (mut required_utxos, optional_utxos) = self.preselect_utxos( - builder.change_policy, - &builder.unspendable, - &builder_extra_utxos[..], - builder.drain_wallet, - builder.manually_selected_only, - true, // we only want confirmed transactions for RBF - )?; - - required_utxos.append(&mut original_utxos); - - let amount_needed = tx.output.iter().fold(0, |acc, out| acc + out.value); - let (new_feerate, initial_fee) = match builder - .fee_policy - .as_ref() - .unwrap_or(&FeePolicy::FeeRate(FeeRate::default())) - { - FeePolicy::FeeAmount(amount) => { - if *amount < details.fees { - return Err(Error::FeeTooLow { - required: details.fees, - }); - } - (FeeRate::from_sat_per_vb(0.0), *amount as f32) - } - FeePolicy::FeeRate(rate) => { - if *rate < required_feerate { - return Err(Error::FeeRateTooLow { - required: required_feerate, - }); + if tx.output.len() > 1 { + let mut change_index = None; + for (index, txout) in tx.output.iter().enumerate() { + let (_, change_type) = self._get_descriptor_for_keychain(KeychainKind::Internal); + match self + .database + .borrow() + .get_path_from_script_pubkey(&txout.script_pubkey)? + { + Some((keychain, _)) if keychain == change_type => change_index = Some(index), + _ => {} } - (*rate, tx.get_weight() as f32 / 4.0 * rate.as_sat_vb()) } - }; - let coin_selection::CoinSelectionResult { - selected, - selected_amount, - fee_amount, - } = builder.coin_selection.coin_select( - self.database.borrow().deref(), - required_utxos, - optional_utxos, - new_feerate, - amount_needed, - initial_fee, - )?; - - tx.input = selected - .iter() - .map(|u| bitcoin::TxIn { - previous_output: u.outpoint, - script_sig: Script::default(), - // TODO: use builder.n_sequence?? - sequence: original_sequence, - witness: vec![], - }) - .collect(); - - details.sent = selected_amount; - - let mut fee_amount = fee_amount.ceil() as u64; - let removed_output_fee_cost = (serialize(&removed_updatable_output).len() as f32 - * new_feerate.as_sat_vb()) - .ceil() as u64; - - let change_val = selected_amount - amount_needed - fee_amount; - let change_val_after_add = change_val.saturating_sub(removed_output_fee_cost); - match builder.single_recipient { - None if change_val_after_add.is_dust() => { - // skip the change output because it's dust, this adds up to the fees - fee_amount += change_val; - } - Some(_) if change_val_after_add.is_dust() => { - // single_recipient but the only output would be below dust limit - // TODO: or OutputBelowDustLimit? - return Err(Error::InsufficientFunds { - needed: DUST_LIMIT_SATOSHI, - available: change_val_after_add, - }); - } - None => { - removed_updatable_output.value = change_val_after_add; - fee_amount += removed_output_fee_cost; - details.received += change_val_after_add; - - tx.output.push(removed_updatable_output); - } - Some(_) => { - removed_updatable_output.value = change_val_after_add; - fee_amount += removed_output_fee_cost; - - // single recipient and it's our address - if self.is_mine(&removed_updatable_output.script_pubkey)? { - details.received = change_val_after_add; - } - - tx.output.push(removed_updatable_output); + if let Some(change_index) = change_index { + tx.output.remove(change_index); } } - // sort input/outputs according to the chosen algorithm - builder.ordering.sort_tx(&mut tx); - - // TODO: check that we are not replacing more than 100 txs from mempool - - details.txid = tx.txid(); - details.fees = fee_amount; - details.timestamp = time::get_timestamp(); - - let psbt = self.complete_transaction(tx, selected, builder)?; + let params = TxParams { + // TODO: figure out what rbf option should be? + version: Some(tx_builder::Version(tx.version)), + recipients: tx + .output + .into_iter() + .map(|txout| (txout.script_pubkey, txout.value)) + .collect(), + utxos: original_utxos, + bumping_fee: Some(tx_builder::PreviousFee { + absolute: details.fees, + rate: feerate, + }), + ..Default::default() + }; - Ok((psbt, details)) + Ok(TxBuilder { + wallet: &self, + params, + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + }) } /// Sign a transaction with all the wallet's signers, in the order specified by every signer's @@ -837,15 +757,21 @@ where /// /// ## Example /// - /// ```no_run + /// ``` /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; /// # use bdk::database::*; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; - /// # let (psbt, _) = wallet.create_tx(TxBuilder::new())?; + /// # let wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (psbt, _) = { + /// let mut builder = wallet.build_tx(); + /// builder.add_recipient(to_address.script_pubkey(), 50_000); + /// builder.finish()? + /// }; /// let (signed_psbt, finalized) = wallet.sign(psbt, None)?; + /// assert!(finalized, "we should have signed all the inputs"); /// # Ok::<(), bdk::Error>(()) pub fn sign(&self, mut psbt: PSBT, assume_height: Option) -> Result<(PSBT, bool), Error> { // this helps us doing our job later @@ -986,9 +912,15 @@ where &self.secp } + /// Returns the descriptor used to create adddresses for a particular `keychain`. + pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor { + let (descriptor, _) = self._get_descriptor_for_keychain(keychain); + descriptor + } + // Internals - fn get_descriptor_for_keychain( + fn _get_descriptor_for_keychain( &self, keychain: KeychainKind, ) -> (&ExtendedDescriptor, KeychainKind) { @@ -1006,14 +938,14 @@ where .database .borrow() .get_path_from_script_pubkey(&txout.script_pubkey)? - .map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain).0, child)) + .map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child)) .map(|(desc, child)| desc.derive(ChildNumber::from_normal_idx(child).unwrap()))) } fn get_change_address(&self) -> Result { let deriv_ctx = descriptor_to_pk_ctx(&self.secp); - let (desc, keychain) = self.get_descriptor_for_keychain(KeychainKind::Internal); + let (desc, keychain) = self._get_descriptor_for_keychain(KeychainKind::Internal); let index = self.fetch_and_increment_index(keychain)?; Ok(desc @@ -1022,7 +954,7 @@ where } fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result { - let (descriptor, keychain) = self.get_descriptor_for_keychain(keychain); + let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); let index = match descriptor.is_fixed() { true => 0, false => self.database.borrow_mut().increment_last_index(keychain)?, @@ -1056,7 +988,7 @@ where from: u32, mut count: u32, ) -> Result<(), Error> { - let (descriptor, keychain) = self.get_descriptor_for_keychain(keychain); + let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); if descriptor.is_fixed() { if from > 0 { return Ok(()); @@ -1102,7 +1034,6 @@ where ( utxo, self.get_descriptor_for_keychain(keychain) - .0 .max_satisfaction_weight(deriv_ctx) .unwrap(), ) @@ -1117,7 +1048,7 @@ where &self, change_policy: tx_builder::ChangeSpendPolicy, unspendable: &HashSet, - manually_selected: &[OutPoint], + manually_selected: Vec<(UTXO, usize)>, must_use_all_available: bool, manual_only: bool, must_only_use_confirmed_tx: bool, @@ -1125,22 +1056,13 @@ where // must_spend <- manually selected utxos // may_spend <- all other available utxos let mut may_spend = self.get_available_utxos()?; - let mut must_spend = { - let must_spend_idx = manually_selected + may_spend.retain(|may_spend| { + manually_selected .iter() - .map(|manually_selected| { - may_spend - .iter() - .position(|available| available.0.outpoint == *manually_selected) - .ok_or(Error::UnknownUTXO) - }) - .collect::, _>>()?; - - must_spend_idx - .into_iter() - .map(|i| may_spend.remove(i)) - .collect() - }; + .find(|manually_selected| manually_selected.0.outpoint == may_spend.0.outpoint) + .is_none() + }); + let mut must_spend = manually_selected; // NOTE: we are intentionally ignoring `unspendable` here. i.e manual // selection overrides unspendable. @@ -1182,20 +1104,17 @@ where Ok((must_spend, may_spend)) } - fn complete_transaction< - Cs: coin_selection::CoinSelectionAlgorithm, - Ctx: TxBuilderContext, - >( + fn complete_transaction( &self, tx: Transaction, selected: Vec, - builder: TxBuilder, + params: TxParams, ) -> Result { use bitcoin::util::psbt::serialize::Serialize; let mut psbt = PSBT::from_unsigned_tx(tx)?; - if builder.add_global_xpubs { + if params.add_global_xpubs { let mut all_xpubs = self.descriptor.get_extended_keys()?; if let Some(change_descriptor) = &self.change_descriptor { all_xpubs.extend(change_descriptor.get_extended_keys()?); @@ -1237,9 +1156,9 @@ where None => continue, }; - // Only set it if the builder has a custom one, otherwise leave blank which defaults to + // Only set it if the params has a custom one, otherwise leave blank which defaults to // SIGHASH_ALL - if let Some(sighash_type) = builder.sighash { + if let Some(sighash_type) = params.sighash { psbt_input.sighash_type = Some(sighash_type); } @@ -1254,7 +1173,7 @@ where None => continue, }; - let (desc, _) = self.get_descriptor_for_keychain(keychain); + let (desc, _) = self._get_descriptor_for_keychain(keychain); psbt_input.hd_keypaths = desc.get_hd_keypaths(child, &self.secp)?; let derived_descriptor = desc.derive(ChildNumber::from_normal_idx(child)?); @@ -1267,7 +1186,7 @@ where psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); } - if !derived_descriptor.is_witness() || builder.force_non_witness_utxo { + if !derived_descriptor.is_witness() || params.force_non_witness_utxo { psbt_input.non_witness_utxo = Some(prev_tx); } } @@ -1287,9 +1206,9 @@ where .borrow() .get_path_from_script_pubkey(&tx_output.script_pubkey)? { - let (desc, _) = self.get_descriptor_for_keychain(keychain); + let (desc, _) = self._get_descriptor_for_keychain(keychain); psbt_output.hd_keypaths = desc.get_hd_keypaths(child, &self.secp)?; - if builder.include_output_redeem_witness_script { + if params.include_output_redeem_witness_script { let derived_descriptor = desc.derive(ChildNumber::from_normal_idx(child)?); psbt_output.witness_script = derived_descriptor.psbt_witness_script(&self.secp); psbt_output.redeem_script = derived_descriptor.psbt_redeem_script(&self.secp); @@ -1317,7 +1236,7 @@ where debug!("Found descriptor {:?}/{}", keychain, child); // merge hd_keypaths - let (desc, _) = self.get_descriptor_for_keychain(keychain); + let desc = self.get_descriptor_for_keychain(keychain); let mut hd_keypaths = desc.get_hd_keypaths(child, &self.secp)?; psbt_input.hd_keypaths.append(&mut hd_keypaths); } @@ -1568,11 +1487,12 @@ mod test { ) .unwrap(); - let txid = wallet.database.borrow_mut().received_tx( + let txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! { @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1) }, - Some(100), + Some(100) ); (wallet, descriptors, txid) @@ -1611,9 +1531,7 @@ mod test { #[should_panic(expected = "NoRecipients")] fn test_create_tx_empty_recipients() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); - wallet - .create_tx(TxBuilder::with_recipients(vec![])) - .unwrap(); + wallet.build_tx().finish().unwrap(); } #[test] @@ -1621,13 +1539,11 @@ mod test { fn test_create_tx_manually_selected_empty_utxos() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .manually_selected_only() - .utxos(vec![]), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .manually_selected_only(); + builder.finish().unwrap(); } #[test] @@ -1635,9 +1551,11 @@ mod test { fn test_create_tx_version_0() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(0)) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .version(0); + builder.finish().unwrap(); } #[test] @@ -1647,18 +1565,22 @@ mod test { fn test_create_tx_version_1_csv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(1)) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .version(1); + builder.finish().unwrap(); } #[test] fn test_create_tx_custom_version() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(42)) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .version(42); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.version, 42); } @@ -1667,12 +1589,9 @@ mod test { fn test_create_tx_default_locktime() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 0); } @@ -1681,12 +1600,9 @@ mod test { fn test_create_tx_default_locktime_cltv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 100_000); } @@ -1695,11 +1611,11 @@ mod test { fn test_create_tx_custom_locktime() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(630_000), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .nlocktime(630_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000); } @@ -1708,11 +1624,11 @@ mod test { fn test_create_tx_custom_locktime_compatible_with_cltv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(630_000), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .nlocktime(630_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000); } @@ -1724,23 +1640,20 @@ mod test { fn test_create_tx_custom_locktime_incompatible_with_cltv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(50000), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .nlocktime(50000); + builder.finish().unwrap(); } #[test] fn test_create_tx_no_rbf_csv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 6); } @@ -1749,12 +1662,11 @@ mod test { fn test_create_tx_with_default_rbf_csv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), - ) - .unwrap(); - + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, _) = builder.finish().unwrap(); // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). // It will be set to the OP_CSV value, in this case 6 assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 6); @@ -1767,24 +1679,20 @@ mod test { fn test_create_tx_with_custom_rbf_csv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .enable_rbf_with_sequence(3), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf_with_sequence(3); + builder.finish().unwrap(); } #[test] fn test_create_tx_no_rbf_cltv() { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFE); } @@ -1794,24 +1702,22 @@ mod test { fn test_create_tx_invalid_rbf_sequence() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .enable_rbf_with_sequence(0xFFFFFFFE), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf_with_sequence(0xFFFFFFFE); + builder.finish().unwrap(); } #[test] fn test_create_tx_custom_rbf_sequence() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .enable_rbf_with_sequence(0xDEADBEEF), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf_with_sequence(0xDEADBEEF); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xDEADBEEF); } @@ -1820,12 +1726,9 @@ mod test { fn test_create_tx_default_sequence() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFF); } @@ -1837,25 +1740,22 @@ mod test { fn test_create_tx_change_policy_no_internal() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .do_not_spend_change(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .do_not_spend_change(); + builder.finish().unwrap(); } #[test] fn test_create_tx_single_recipient_drain_wallet() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.output.len(), 1); assert_eq!( @@ -1868,12 +1768,9 @@ mod test { fn test_create_tx_default_fee_rate() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, details) = builder.finish().unwrap(); assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature); } @@ -1882,12 +1779,11 @@ mod test { fn test_create_tx_custom_fee_rate() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .fee_rate(FeeRate::from_sat_per_vb(5.0)), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (psbt, details) = builder.finish().unwrap(); assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature); } @@ -1896,14 +1792,12 @@ mod test { fn test_create_tx_absolute_fee() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(100), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(100); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.fees, 100); assert_eq!(psbt.global.unsigned_tx.output.len(), 1); @@ -1917,14 +1811,12 @@ mod test { fn test_create_tx_absolute_zero_fee() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(0), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(0); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.fees, 0); assert_eq!(psbt.global.unsigned_tx.output.len(), 1); @@ -1939,14 +1831,12 @@ mod test { fn test_create_tx_absolute_high_fee() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (_psbt, _details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .fee_absolute(60_000), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .fee_absolute(60_000); + let (_psbt, _details) = builder.finish().unwrap(); } #[test] @@ -1955,12 +1845,11 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) - .ordering(TxOrdering::Untouched), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .ordering(TxOrdering::Untouched); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.output.len(), 2); assert_eq!(psbt.global.unsigned_tx.output[0].value, 25_000); @@ -1974,12 +1863,9 @@ mod test { fn test_create_tx_skip_change_dust() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 49_800, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 49_800); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.output.len(), 1); assert_eq!(psbt.global.unsigned_tx.output[0].value, 49_800); @@ -1992,29 +1878,24 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); // very high fee rate, so that the only output would be below dust - wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .fee_rate(FeeRate::from_sat_per_vb(453.0)), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .fee_rate(FeeRate::from_sat_per_vb(453.0)); + builder.finish().unwrap(); } #[test] fn test_create_tx_ordering_respected() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![ - (addr.script_pubkey(), 30_000), - (addr.script_pubkey(), 10_000), - ]) - .ordering(super::tx_builder::TxOrdering::BIP69Lexicographic), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .add_recipient(addr.script_pubkey(), 10_000) + .ordering(super::tx_builder::TxOrdering::BIP69Lexicographic); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.output.len(), 3); assert_eq!( @@ -2029,12 +1910,9 @@ mod test { fn test_create_tx_default_sighash() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 30_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 30_000); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.inputs[0].sighash_type, None); } @@ -2043,12 +1921,11 @@ mod test { fn test_create_tx_custom_sighash() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)]) - .sighash(bitcoin::SigHashType::Single), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .sighash(bitcoin::SigHashType::Single); + let (psbt, _) = builder.finish().unwrap(); assert_eq!( psbt.inputs[0].sighash_type, @@ -2063,13 +1940,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1); assert_eq!( @@ -2091,13 +1966,11 @@ mod test { wallet.get_new_address().unwrap(); let addr = testutils!(@external descriptors, 5); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1); assert_eq!( @@ -2116,13 +1989,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert_eq!( psbt.inputs[0].redeem_script, @@ -2143,13 +2014,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.inputs[0].redeem_script, None); assert_eq!( @@ -2170,13 +2039,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); let script = Script::from( Vec::::from_hex( @@ -2194,13 +2061,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].witness_utxo.is_none()); @@ -2211,13 +2076,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_none()); assert!(psbt.inputs[0].witness_utxo.is_some()); @@ -2228,13 +2091,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_none()); assert!(psbt.inputs[0].witness_utxo.is_some()); @@ -2245,14 +2106,12 @@ mod test { let (wallet, _, _) = get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .force_non_witness_utxo(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .force_non_witness_utxo(); + let (psbt, _) = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].witness_utxo.is_some()); @@ -2261,22 +2120,22 @@ mod test { #[test] fn test_create_tx_add_utxo() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - let small_output_txid = wallet.database.borrow_mut().received_tx( + let small_output_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)]).add_utxo( - OutPoint { - txid: small_output_txid, - vout: 0, - }, - ), - ) + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .add_utxo(OutPoint { + txid: small_output_txid, + vout: 0, + }) .unwrap(); + let (psbt, details) = builder.finish().unwrap(); assert_eq!( psbt.global.unsigned_tx.input.len(), @@ -2290,22 +2149,23 @@ mod test { #[should_panic(expected = "InsufficientFunds")] fn test_create_tx_manually_selected_insufficient() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - let small_output_txid = wallet.database.borrow_mut().received_tx( + let small_output_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)]) - .add_utxo(OutPoint { - txid: small_output_txid, - vout: 0, - }) - .manually_selected_only(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .add_utxo(OutPoint { + txid: small_output_txid, + vout: 0, + }) + .unwrap() + .manually_selected_only(); + builder.finish().unwrap(); } #[test] @@ -2314,12 +2174,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 30_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 30_000); + builder.finish().unwrap(); } #[test] @@ -2332,12 +2189,11 @@ mod test { let path = vec![(root_id, vec![0])].into_iter().collect(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)]) - .policy_path(path, KeychainKind::External), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .policy_path(path, KeychainKind::External); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFF); } @@ -2352,12 +2208,11 @@ mod test { let path = vec![(root_id, vec![1])].into_iter().collect(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)]) - .policy_path(path, KeychainKind::External), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 30_000) + .policy_path(path, KeychainKind::External); + let (psbt, _) = builder.finish().unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 144); } @@ -2370,11 +2225,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).add_global_xpubs(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .add_global_xpubs(); + let (psbt, _) = builder.finish().unwrap(); let type_value = 0x01; let key = base58::from_check("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); @@ -2395,11 +2250,11 @@ mod test { fn test_create_tx_global_xpubs_origin_missing() { let (wallet, _, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); let addr = wallet.get_new_address().unwrap(); - wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).add_global_xpubs(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .add_global_xpubs(); + builder.finish().unwrap(); } #[test] @@ -2410,11 +2265,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).add_global_xpubs(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .add_global_xpubs(); + let (psbt, _) = builder.finish().unwrap(); let type_value = 0x01; let key = base58::from_check("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); @@ -2434,19 +2289,17 @@ mod test { fn test_bump_fee_irreplaceable_tx() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, mut details) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, mut details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the utxos, we know they can't be used anyways details.transaction = Some(tx); wallet.database.borrow_mut().set_tx(&details).unwrap(); - wallet.bump_fee(&txid, TxBuilder::new()).unwrap(); + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); } #[test] @@ -2454,12 +2307,10 @@ mod test { fn test_bump_fee_confirmed_tx() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, mut details) = wallet - .create_tx(TxBuilder::with_recipients(vec![( - addr.script_pubkey(), - 25_000, - )])) - .unwrap(); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, mut details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the utxos, we know they can't be used anyways @@ -2467,7 +2318,7 @@ mod test { details.height = Some(42); wallet.database.borrow_mut().set_tx(&details).unwrap(); - wallet.bump_fee(&txid, TxBuilder::new()).unwrap(); + wallet.build_fee_bump(txid).unwrap().finish().unwrap(); } #[test] @@ -2475,23 +2326,21 @@ mod test { fn test_bump_fee_low_fee_rate() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, mut details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, mut details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the utxos, we know they can't be used anyways details.transaction = Some(tx); wallet.database.borrow_mut().set_tx(&details).unwrap(); - wallet - .bump_fee( - &txid, - TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(1.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(1.0)); + builder.finish().unwrap(); } #[test] @@ -2499,20 +2348,21 @@ mod test { fn test_bump_fee_low_abs() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, mut details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, mut details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the utxos, we know they can't be used anyways details.transaction = Some(tx); wallet.database.borrow_mut().set_tx(&details).unwrap(); - wallet - .bump_fee(&txid, TxBuilder::new().fee_absolute(10)) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(10); + builder.finish().unwrap(); } #[test] @@ -2520,31 +2370,32 @@ mod test { fn test_bump_fee_zero_abs() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); - let (psbt, mut details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, mut details) = builder.finish().unwrap(); + let tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the utxos, we know they can't be used anyways details.transaction = Some(tx); wallet.database.borrow_mut().set_tx(&details).unwrap(); - wallet - .bump_fee(&txid, TxBuilder::new().fee_absolute(0)) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(0); + builder.finish().unwrap(); } #[test] fn test_bump_fee_reduce_change() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -2563,12 +2414,9 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(2.5)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(2.5)); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent); assert_eq!( @@ -2603,11 +2451,11 @@ mod test { fn test_bump_fee_absolute_reduce_change() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -2626,9 +2474,9 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee(&txid, TxBuilder::new().fee_absolute(200)) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(200); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent); assert_eq!( @@ -2668,14 +2516,12 @@ mod test { fn test_bump_fee_reduce_single_recipient() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); for txin in &mut tx.input { @@ -2693,14 +2539,12 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new() - .maintain_single_recipient() - .fee_rate(FeeRate::from_sat_per_vb(2.5)), - ) + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .fee_rate(FeeRate::from_sat_per_vb(2.5)) + .maintain_single_recipient() .unwrap(); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent); assert!(details.fees > original_details.fees); @@ -2716,14 +2560,12 @@ mod test { fn test_bump_fee_absolute_reduce_single_recipient() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet() + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); for txin in &mut tx.input { @@ -2741,14 +2583,12 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new() - .maintain_single_recipient() - .fee_absolute(300), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .maintain_single_recipient() + .unwrap() + .fee_absolute(300); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent); assert!(details.fees > original_details.fees); @@ -2764,7 +2604,8 @@ mod test { fn test_bump_fee_drain_wallet() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); // receive an extra tx so that our wallet has two utxos. - let incoming_txid = wallet.database.borrow_mut().received_tx( + let incoming_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); @@ -2773,15 +2614,14 @@ mod test { vout: 0, }; let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .utxos(vec![outpoint]) - .manually_selected_only() - .enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .add_utxo(outpoint) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); for txin in &mut tx.input { @@ -2802,15 +2642,13 @@ mod test { // for the new feerate, it should be enough to reduce the output, but since we specify // `drain_wallet` we expect to spend everything - let (_, details) = wallet - .bump_fee( - &txid, - TxBuilder::new() - .drain_wallet() - .maintain_single_recipient() - .fee_rate(FeeRate::from_sat_per_vb(5.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .drain_wallet() + .maintain_single_recipient() + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (_, details) = builder.finish().unwrap(); assert_eq!(details.sent, 75_000); } @@ -2819,10 +2657,12 @@ mod test { fn test_bump_fee_remove_output_manually_selected_only() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); // receive an extra tx so that our wallet has two utxos. then we manually pick only one of - // them, and make sure that `bump_fee` doesn't try to add more. eventually, it should fail - // because the fee rate is too high and the single utxo isn't enough to create a non-dust - // output - let incoming_txid = wallet.database.borrow_mut().received_tx( + // them, and make sure that `bump_fee` doesn't try to add more. This fails because we've + // told the wallet it's not allowed to add more inputs AND it can't reduce the value of the + // existing output. In other words, bump_fee + manually_selected_only is always an error + // unless you've also set "maintain_single_recipient". + let incoming_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); @@ -2831,15 +2671,14 @@ mod test { vout: 0, }; let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .utxos(vec![outpoint]) - .manually_selected_only() - .enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .add_utxo(outpoint) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); for txin in &mut tx.input { @@ -2858,31 +2697,28 @@ mod test { .unwrap(); assert_eq!(original_details.sent, 25_000); - wallet - .bump_fee( - &txid, - TxBuilder::new() - .utxos(vec![outpoint]) - .manually_selected_only() - .fee_rate(FeeRate::from_sat_per_vb(225.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .manually_selected_only() + .fee_rate(FeeRate::from_sat_per_vb(255.0)); + builder.finish().unwrap(); } #[test] fn test_bump_fee_add_input() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - wallet.database.borrow_mut().received_tx( + crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -2901,12 +2737,9 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(50.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent + 25_000); assert_eq!(details.fees + details.received, 30_000); @@ -2937,17 +2770,18 @@ mod test { #[test] fn test_bump_fee_absolute_add_input() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - wallet.database.borrow_mut().received_tx( + crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -2966,9 +2800,9 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee(&txid, TxBuilder::new().fee_absolute(6_000)) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_absolute(6_000); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent + 25_000); assert_eq!(details.fees + details.received, 30_000); @@ -2999,25 +2833,26 @@ mod test { #[test] fn test_bump_fee_no_change_add_input_and_change() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - let incoming_txid = wallet.database.borrow_mut().received_tx( + let incoming_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); // initially make a tx without change by using `set_single_recipient` let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .add_utxo(OutPoint { - txid: incoming_txid, - vout: 0, - }) - .manually_selected_only() - .enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .add_utxo(OutPoint { + txid: incoming_txid, + vout: 0, + }) + .unwrap() + .manually_selected_only() + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); + let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -3038,12 +2873,9 @@ mod test { // now bump the fees without using `maintain_single_recipient`. the wallet should add an // extra input and a change output, and leave the original output untouched - let (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(50.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); + let (psbt, details) = builder.finish().unwrap(); let original_send_all_amount = original_details.sent - original_details.fees; assert_eq!(details.sent, original_details.sent + 50_000); @@ -3078,17 +2910,18 @@ mod test { #[test] fn test_bump_fee_add_input_change_dust() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - wallet.database.borrow_mut().received_tx( + crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); assert_eq!(tx.input.len(), 1); assert_eq!(tx.output.len(), 2); @@ -3109,12 +2942,9 @@ mod test { .set_tx(&original_details) .unwrap(); - let (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(140.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(FeeRate::from_sat_per_vb(140.0)); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(original_details.received, 5_000 - original_details.fees); @@ -3140,17 +2970,18 @@ mod test { #[test] fn test_bump_fee_force_add_input() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - let incoming_txid = wallet.database.borrow_mut().received_tx( + let incoming_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -3171,17 +3002,15 @@ mod test { // 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 (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new() - .add_utxo(OutPoint { - txid: incoming_txid, - vout: 0, - }) - .fee_rate(FeeRate::from_sat_per_vb(5.0)), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .add_utxo(OutPoint { + txid: incoming_txid, + vout: 0, + }) + .unwrap() + .fee_rate(FeeRate::from_sat_per_vb(5.0)); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent + 25_000); assert_eq!(details.fees + details.received, 30_000); @@ -3212,17 +3041,18 @@ mod test { #[test] fn test_bump_fee_absolute_force_add_input() { let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); - let incoming_txid = wallet.database.borrow_mut().received_tx( + let incoming_txid = crate::populate_test_db!( + wallet.database.borrow_mut(), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), Some(100), ); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, mut original_details) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .enable_rbf(); + let (psbt, mut original_details) = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways @@ -3243,17 +3073,15 @@ mod test { // 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 (psbt, details) = wallet - .bump_fee( - &txid, - TxBuilder::new() - .add_utxo(OutPoint { - txid: incoming_txid, - vout: 0, - }) - .fee_absolute(250), - ) - .unwrap(); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder + .add_utxo(OutPoint { + txid: incoming_txid, + vout: 0, + }) + .unwrap() + .fee_absolute(250); + let (psbt, details) = builder.finish().unwrap(); assert_eq!(details.sent, original_details.sent + 25_000); assert_eq!(details.fees + details.received, 30_000); @@ -3285,13 +3113,11 @@ mod test { fn test_sign_single_xprv() { let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert_eq!(finalized, true); @@ -3304,13 +3130,11 @@ mod test { fn test_sign_single_xprv_bip44_path() { let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert_eq!(finalized, true); @@ -3323,13 +3147,11 @@ mod test { fn test_sign_single_xprv_sh_wpkh() { let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert_eq!(finalized, true); @@ -3343,13 +3165,11 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); let addr = wallet.get_new_address().unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert_eq!(finalized, true); @@ -3362,13 +3182,11 @@ mod test { fn test_sign_single_xprv_no_hd_keypaths() { let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_new_address().unwrap(); - let (mut psbt, _) = wallet - .create_tx( - TxBuilder::new() - .set_single_recipient(addr.script_pubkey()) - .drain_wallet(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .set_single_recipient(addr.script_pubkey()) + .drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); psbt.inputs[0].hd_keypaths.clear(); assert_eq!(psbt.inputs[0].hd_keypaths.len(), 0); @@ -3384,12 +3202,11 @@ mod test { fn test_include_output_redeem_witness_script() { let (wallet, _, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]) - .include_output_redeem_witness_script(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .include_output_redeem_witness_script(); + let (psbt, _) = builder.finish().unwrap(); // p2sh-p2wsh transaction should contain both witness and redeem scripts assert!(psbt @@ -3402,12 +3219,11 @@ mod test { fn test_signing_only_one_of_multiple_inputs() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let (mut psbt, _) = wallet - .create_tx( - TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]) - .include_output_redeem_witness_script(), - ) - .unwrap(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 45_000) + .include_output_redeem_witness_script(); + let (mut psbt, _) = builder.finish().unwrap(); // add another input to the psbt that is at least passable. let mut dud_input = bitcoin::util::psbt::Input::default(); diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 921081a3c..a4848096b 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -32,14 +32,21 @@ //! # use bdk::*; //! # use bdk::wallet::tx_builder::CreateTx; //! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); -//! // Create a transaction with one output to `to_address` of 50_000 satoshi, with a custom fee rate -//! // of 5.0 satoshi/vbyte, only spending non-change outputs and with RBF signaling -//! // enabled -//! let builder = TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)]) +//! # let wallet = doctest_wallet!(); +//! // create a TxBuilder from a wallet +//! let mut tx_builder = wallet.build_tx(); +//! +//! tx_builder +//! // Create a transaction with one output to `to_address` of 50_000 satoshi +//! .add_recipient(to_address.script_pubkey(), 50_000) +//! // With a custom fee rate of 5.0 satoshi/vbyte //! .fee_rate(FeeRate::from_sat_per_vb(5.0)) +//! // Only spend non-change outputs //! .do_not_spend_change() +//! // Turn on RBF signaling //! .enable_rbf(); -//! # let builder: TxBuilder = builder; +//! let (psbt, tx_details) = tx_builder.finish()?; +//! # Ok::<(), bdk::Error>(()) //! ``` use std::collections::BTreeMap; @@ -47,39 +54,101 @@ use std::collections::HashSet; use std::default::Default; use std::marker::PhantomData; +use bitcoin::util::psbt::PartiallySignedTransaction as PSBT; use bitcoin::{OutPoint, Script, SigHashType, Transaction}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; -use crate::database::Database; -use crate::types::{FeeRate, KeychainKind, UTXO}; - +use crate::{database::BatchDatabase, Error, Wallet}; +use crate::{ + types::{FeeRate, KeychainKind, UTXO}, + TransactionDetails, +}; /// Context in which the [`TxBuilder`] is valid pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {} -/// [`Wallet::create_tx`](super::Wallet::create_tx) context +/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed +/// to bumping the fee of an existing one). #[derive(Debug, Default, Clone)] pub struct CreateTx; impl TxBuilderContext for CreateTx {} -/// [`Wallet::bump_fee`](super::Wallet::bump_fee) context +/// Marker type to indicate the [`TxBuilder`] is being used to bump the fee of an existing transaction. #[derive(Debug, Default, Clone)] pub struct BumpFee; impl TxBuilderContext for BumpFee {} /// A transaction builder /// -/// This structure contains the configuration that the wallet must follow to build a transaction. +/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After +/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and +/// generate the transaction. +/// +/// Each option setting method on `TxBuilder` takes and returns `&mut self` so you can chain calls +/// as in the following example: +/// +/// ``` +/// # use bdk::*; +/// # use bdk::wallet::tx_builder::*; +/// # use bitcoin::*; +/// # use core::str::FromStr; +/// # let wallet = doctest_wallet!(); +/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); +/// # let addr2 = addr1.clone(); +/// // chaining +/// let (psbt1, details) = { +/// let mut builder = wallet.build_tx(); +/// builder +/// .ordering(TxOrdering::Untouched) +/// .add_recipient(addr1.script_pubkey(), 50_000) +/// .add_recipient(addr2.script_pubkey(), 50_000); +/// builder.finish()? +/// }; +/// +/// // non-chaining +/// let (psbt2, details) = { +/// let mut builder = wallet.build_tx(); +/// builder.ordering(TxOrdering::Untouched); +/// for addr in &[addr1, addr2] { +/// builder.add_recipient(addr.script_pubkey(), 50_000); +/// } +/// builder.finish()? +/// }; /// -/// For an example see [this module](super::tx_builder)'s documentation; -#[derive(Debug)] -pub struct TxBuilder, Ctx: TxBuilderContext> { +/// assert_eq!(psbt1.global.unsigned_tx.output[..2], psbt2.global.unsigned_tx.output[..2]); +/// # Ok::<(), bdk::Error>(()) +/// ``` +/// +/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`. +/// This means it is usually best to call [`coin_selection`] on the return value of `build_tx` before assigning it. +/// +/// For further examples see [this module](super::tx_builder)'s documentation; +/// +/// [`build_tx`]: Wallet::build_tx +/// [`build_fee_bump`]: Wallet::build_fee_bump +/// [`finish`]: Self::finish +/// [`coin_selection`]: Self::coin_selection +#[derive(Clone, Debug)] +pub struct TxBuilder<'a, B, D, Cs, Ctx> { + pub(crate) wallet: &'a Wallet, + // params and coin_selection are Options not becasue they are optionally set (they are always + // there) but because `.finish()` uses `Option::take` to get an owned value from a &mut self. + // They are only `None` after `.finish()` is called. + pub(crate) params: TxParams, + pub(crate) coin_selection: Cs, + pub(crate) phantom: PhantomData, +} + +/// The parameters for transaction creation sans coin selection algorithm. +//TODO: TxParams should eventually be exposed publicly. +#[derive(Default, Debug, Clone)] +pub(crate) struct TxParams { pub(crate) recipients: Vec<(Script, u64)>, pub(crate) drain_wallet: bool, pub(crate) single_recipient: Option