[wallet] Split send_all into set_single_recipient and drain_wallet

Previously `send_all` was particularly confusing, because when used on a
`create_tx` it implied two things:
- spend everything that's in the wallet (if no utxos are specified)
- don't create a change output

But when used on a `bump_fee` it only meant to not add a change output
and instead reduce the only existing output to increase the fee.

This has now been split into two separate options that should hopefully
make it more clear to use, as described in #142.

Additionally, `TxBuilder` now has a "context", that basically allows to
make some flags available only when they are actually meaningful, either
for `create_tx` or `bump_fee`.

Closes #142.
This commit is contained in:
Alekos Filini 2020-10-28 10:37:47 +01:00
parent f67bfe7bfc
commit 36c5a4dc0c
No known key found for this signature in database
GPG Key ID: 5E8AFC3034FDFA4F
4 changed files with 426 additions and 216 deletions

View File

@ -397,11 +397,16 @@ where
.map(|s| parse_recipient(s)) .map(|s| parse_recipient(s))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(Error::Generic)?; .map_err(Error::Generic)?;
let mut tx_builder = TxBuilder::with_recipients(recipients); let mut tx_builder = TxBuilder::new();
if sub_matches.is_present("send_all") { if sub_matches.is_present("send_all") {
tx_builder = tx_builder.send_all(); tx_builder = tx_builder
.drain_wallet()
.set_single_recipient(recipients[0].0.clone());
} else {
tx_builder = tx_builder.set_recipients(recipients);
} }
if sub_matches.is_present("enable_rbf") { if sub_matches.is_present("enable_rbf") {
tx_builder = tx_builder.enable_rbf(); tx_builder = tx_builder.enable_rbf();
} }
@ -445,7 +450,7 @@ where
let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate)); let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate));
if sub_matches.is_present("send_all") { if sub_matches.is_present("send_all") {
tx_builder = tx_builder.send_all(); tx_builder = tx_builder.maintain_single_recipient();
} }
if let Some(utxos) = sub_matches.values_of("utxos") { if let Some(utxos) = sub_matches.values_of("utxos") {

View File

@ -34,8 +34,10 @@ pub enum Error {
InvalidU32Bytes(Vec<u8>), InvalidU32Bytes(Vec<u8>),
Generic(String), Generic(String),
ScriptDoesntHaveAddressForm, ScriptDoesntHaveAddressForm,
SendAllMultipleOutputs, SingleRecipientMultipleOutputs,
NoAddressees, SingleRecipientNoInputs,
NoRecipients,
NoUtxosSelected,
OutputBelowDustLimit(usize), OutputBelowDustLimit(usize),
InsufficientFunds, InsufficientFunds,
InvalidAddressNetwork(Address), InvalidAddressNetwork(Address),

View File

@ -54,7 +54,7 @@ pub use utils::IsDust;
use address_validator::AddressValidator; use address_validator::AddressValidator;
use signer::{Signer, SignerId, SignerOrdering, SignersContainer}; use signer::{Signer, SignerId, SignerOrdering, SignersContainer};
use tx_builder::{FeePolicy, TxBuilder}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxBuilderContext};
use utils::{After, Older}; use utils::{After, Older};
use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progress}; use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progress};
@ -242,12 +242,8 @@ where
/// ``` /// ```
pub fn create_tx<Cs: coin_selection::CoinSelectionAlgorithm<D>>( pub fn create_tx<Cs: coin_selection::CoinSelectionAlgorithm<D>>(
&self, &self,
builder: TxBuilder<D, Cs>, builder: TxBuilder<D, Cs, CreateTx>,
) -> Result<(PSBT, TransactionDetails), Error> { ) -> Result<(PSBT, TransactionDetails), Error> {
if builder.recipients.is_empty() {
return Err(Error::NoAddressees);
}
// TODO: fetch both internal and external policies // TODO: fetch both internal and external policies
let policy = self let policy = self
.descriptor .descriptor
@ -307,8 +303,23 @@ where
FeePolicy::FeeRate(rate) => (*rate, 0.0), FeePolicy::FeeRate(rate) => (*rate, 0.0),
}; };
if builder.send_all && builder.recipients.len() != 1 { // try not to move from `builder` because we still need to use it later.
return Err(Error::SendAllMultipleOutputs); let recipients = match &builder.single_recipient {
Some(recipient) => vec![(recipient, 0)],
None => builder.recipients.iter().map(|(r, v)| (r, *v)).collect(),
};
if builder.single_recipient.is_some()
&& !builder.manually_selected_only
&& !builder.drain_wallet
{
return Err(Error::SingleRecipientNoInputs);
}
if recipients.is_empty() {
return Err(Error::NoRecipients);
}
if builder.manually_selected_only && builder.utxos.is_empty() {
return Err(Error::NoUtxosSelected);
} }
// we keep it as a float while we accumulate it, and only round it at the end // we keep it as a float while we accumulate it, and only round it at the end
@ -318,11 +329,11 @@ where
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0; let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
fee_amount += calc_fee_bytes(tx.get_weight()); fee_amount += calc_fee_bytes(tx.get_weight());
for (index, (script_pubkey, satoshi)) in builder.recipients.iter().enumerate() { for (index, (script_pubkey, satoshi)) in recipients.into_iter().enumerate() {
let value = match builder.send_all { let value = match builder.single_recipient {
true => 0, Some(_) => 0,
false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)), None if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
false => *satoshi, None => satoshi,
}; };
if self.is_mine(script_pubkey)? { if self.is_mine(script_pubkey)? {
@ -352,7 +363,7 @@ where
builder.change_policy, builder.change_policy,
&builder.unspendable, &builder.unspendable,
&builder.utxos, &builder.utxos,
builder.send_all, builder.drain_wallet,
builder.manually_selected_only, builder.manually_selected_only,
false, // we don't mind using unconfirmed outputs here, hopefully coin selection will sort this out? false, // we don't mind using unconfirmed outputs here, hopefully coin selection will sort this out?
)?; )?;
@ -381,9 +392,9 @@ where
tx.input = txin; tx.input = txin;
// prepare the change output // prepare the change output
let change_output = match builder.send_all { let change_output = match builder.single_recipient {
true => None, Some(_) => None,
false => { None => {
let change_script = self.get_change_address()?; let change_script = self.get_change_address()?;
let change_output = TxOut { let change_output = TxOut {
script_pubkey: change_script, script_pubkey: change_script,
@ -397,28 +408,32 @@ where
}; };
let mut fee_amount = fee_amount.ceil() as u64; let mut fee_amount = fee_amount.ceil() as u64;
let change_val = (selected_amount - outgoing).saturating_sub(fee_amount); let change_val = (selected_amount - outgoing).saturating_sub(fee_amount);
if !builder.send_all && !change_val.is_dust() {
let mut change_output = change_output.unwrap(); match change_output {
None if change_val.is_dust() => {
// single recipient, but the only output would be below dust limit
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit?
}
Some(_) if change_val.is_dust() => {
// skip the change output because it's dust, this adds up to the fees
fee_amount += selected_amount - outgoing;
}
Some(mut change_output) => {
change_output.value = change_val; change_output.value = change_val;
received += change_val; received += change_val;
tx.output.push(change_output); tx.output.push(change_output);
} else if builder.send_all && !change_val.is_dust() { }
None => {
// there's only one output, send everything to it // there's only one output, send everything to it
tx.output[0].value = change_val; tx.output[0].value = change_val;
// send_all to our address // the single recipient is our address
if self.is_mine(&tx.output[0].script_pubkey)? { if self.is_mine(&tx.output[0].script_pubkey)? {
received = change_val; received = change_val;
} }
} else if !builder.send_all && change_val.is_dust() { }
// skip the change output because it's dust, this adds up to the fees
fee_amount += selected_amount - outgoing;
} else if builder.send_all {
// send_all but the only output would be below dust limit
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit?
} }
// sort input/outputs according to the chosen algorithm // sort input/outputs according to the chosen algorithm
@ -444,9 +459,9 @@ where
/// ///
/// Return an error if the transaction is already confirmed or doesn't explicitly signal RBF. /// Return an error if the transaction is already confirmed or doesn't explicitly signal RBF.
/// ///
/// **NOTE**: if the original transaction was made with [`TxBuilder::send_all`], the same /// **NOTE**: if the original transaction was made with [`TxBuilder::set_single_recipient`],
/// option must be enabled when bumping its fees to correctly reduce the only output's value to /// the [`TxBuilder::maintain_single_recipient`] flag should be enabled to correctly reduce the
/// increase the fees. /// 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 /// 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. /// transaction regardless of whether they are necessary or not to cover additional fees.
@ -474,7 +489,7 @@ where
pub fn bump_fee<Cs: coin_selection::CoinSelectionAlgorithm<D>>( pub fn bump_fee<Cs: coin_selection::CoinSelectionAlgorithm<D>>(
&self, &self,
txid: &Txid, txid: &Txid,
builder: TxBuilder<D, Cs>, builder: TxBuilder<D, Cs, BumpFee>,
) -> Result<(PSBT, TransactionDetails), Error> { ) -> Result<(PSBT, TransactionDetails), Error> {
let mut details = match self.database.borrow().get_tx(&txid, true)? { let mut details = match self.database.borrow().get_tx(&txid, true)? {
None => return Err(Error::TransactionNotFound), None => return Err(Error::TransactionNotFound),
@ -491,15 +506,12 @@ where
let vbytes = tx.get_weight() as f32 / 4.0; let vbytes = tx.get_weight() as f32 / 4.0;
let required_feerate = FeeRate::from_sat_per_vb(details.fees as f32 / vbytes + 1.0); let required_feerate = FeeRate::from_sat_per_vb(details.fees as f32 / vbytes + 1.0);
if builder.send_all && tx.output.len() > 1 {
return Err(Error::SendAllMultipleOutputs);
}
// find the index of the output that we can update. either the change or the only one if // find the index of the output that we can update. either the change or the only one if
// it's `send_all` // it's `single_recipient`
let updatable_output = match builder.send_all { let updatable_output = match builder.single_recipient {
true => Some(0), Some(_) if tx.output.len() != 1 => return Err(Error::SingleRecipientMultipleOutputs),
false => { Some(_) => Some(0),
None => {
let mut change_output = None; let mut change_output = None;
for (index, txout) in tx.output.iter().enumerate() { for (index, txout) in tx.output.iter().enumerate() {
// look for an output that we know and that has the right ScriptType. We use // look for an output that we know and that has the right ScriptType. We use
@ -593,6 +605,10 @@ where
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
if builder.manually_selected_only && builder.utxos.is_empty() {
return Err(Error::NoUtxosSelected);
}
let builder_extra_utxos = builder let builder_extra_utxos = builder
.utxos .utxos
.iter() .iter()
@ -608,7 +624,7 @@ where
builder.change_policy, builder.change_policy,
&builder.unspendable, &builder.unspendable,
&builder_extra_utxos[..], &builder_extra_utxos[..],
false, // when doing bump_fee `send_all` does not mean use all available utxos builder.drain_wallet,
builder.manually_selected_only, builder.manually_selected_only,
true, // we only want confirmed transactions for RBF true, // we only want confirmed transactions for RBF
)?; )?;
@ -674,28 +690,33 @@ where
let change_val = selected_amount - amount_needed - fee_amount; let change_val = selected_amount - amount_needed - fee_amount;
let change_val_after_add = change_val.saturating_sub(removed_output_fee_cost); let change_val_after_add = change_val.saturating_sub(removed_output_fee_cost);
if !builder.send_all && !change_val_after_add.is_dust() { 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
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit?
}
None => {
removed_updatable_output.value = change_val_after_add; removed_updatable_output.value = change_val_after_add;
fee_amount += removed_output_fee_cost; fee_amount += removed_output_fee_cost;
details.received += change_val_after_add; details.received += change_val_after_add;
tx.output.push(removed_updatable_output); tx.output.push(removed_updatable_output);
} else if builder.send_all && !change_val_after_add.is_dust() { }
Some(_) => {
removed_updatable_output.value = change_val_after_add; removed_updatable_output.value = change_val_after_add;
fee_amount += removed_output_fee_cost; fee_amount += removed_output_fee_cost;
// send_all to our address // single recipient and it's our address
if self.is_mine(&removed_updatable_output.script_pubkey)? { if self.is_mine(&removed_updatable_output.script_pubkey)? {
details.received = change_val_after_add; details.received = change_val_after_add;
} }
tx.output.push(removed_updatable_output); tx.output.push(removed_updatable_output);
} else if !builder.send_all && change_val_after_add.is_dust() { }
// skip the change output because it's dust, this adds up to the fees
fee_amount += change_val;
} else if builder.send_all {
// send_all but the only output would be below dust limit
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit?
} }
// sort input/outputs according to the chosen algorithm // sort input/outputs according to the chosen algorithm
@ -1054,11 +1075,14 @@ where
Ok((must_spend, may_spend)) Ok((must_spend, may_spend))
} }
fn complete_transaction<Cs: coin_selection::CoinSelectionAlgorithm<D>>( fn complete_transaction<
Cs: coin_selection::CoinSelectionAlgorithm<D>,
Ctx: TxBuilderContext,
>(
&self, &self,
tx: Transaction, tx: Transaction,
prev_script_pubkeys: HashMap<OutPoint, Script>, prev_script_pubkeys: HashMap<OutPoint, Script>,
builder: TxBuilder<D, Cs>, builder: TxBuilder<D, Cs, Ctx>,
) -> Result<PSBT, Error> { ) -> Result<PSBT, Error> {
let mut psbt = PSBT::from_unsigned_tx(tx)?; let mut psbt = PSBT::from_unsigned_tx(tx)?;
@ -1430,11 +1454,25 @@ mod test {
} }
#[test] #[test]
#[should_panic(expected = "NoAddressees")] #[should_panic(expected = "NoRecipients")]
fn test_create_tx_empty_recipients() { fn test_create_tx_empty_recipients() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
wallet wallet
.create_tx(TxBuilder::with_recipients(vec![]).version(0)) .create_tx(TxBuilder::with_recipients(vec![]))
.unwrap();
}
#[test]
#[should_panic(expected = "NoUtxosSelected")]
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(); .unwrap();
} }
@ -1652,27 +1690,15 @@ mod test {
} }
#[test] #[test]
#[should_panic(expected = "SendAllMultipleOutputs")] fn test_create_tx_single_recipient_drain_wallet() {
fn test_create_tx_send_all_multiple_outputs() {
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),
(addr.script_pubkey(), 10_000),
])
.send_all(),
)
.unwrap();
}
#[test]
fn test_create_tx_send_all() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet let (psbt, details) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert_eq!(psbt.global.unsigned_tx.output.len(), 1); assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
@ -1687,7 +1713,10 @@ mod test {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet let (psbt, details) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(TxBuilder::with_recipients(vec![(
addr.script_pubkey(),
25_000,
)]))
.unwrap(); .unwrap();
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature); assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature);
@ -1699,9 +1728,8 @@ mod test {
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet let (psbt, details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
.fee_rate(FeeRate::from_sat_per_vb(5.0)) .fee_rate(FeeRate::from_sat_per_vb(5.0)),
.send_all(),
) )
.unwrap(); .unwrap();
@ -1714,9 +1742,10 @@ mod test {
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet let (psbt, details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.fee_absolute(100) .set_single_recipient(addr.script_pubkey())
.send_all(), .drain_wallet()
.fee_absolute(100),
) )
.unwrap(); .unwrap();
@ -1734,9 +1763,10 @@ mod test {
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet let (psbt, details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.fee_absolute(0) .set_single_recipient(addr.script_pubkey())
.send_all(), .drain_wallet()
.fee_absolute(0),
) )
.unwrap(); .unwrap();
@ -1755,9 +1785,10 @@ mod test {
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (_psbt, _details) = wallet let (_psbt, _details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.fee_absolute(60_000) .set_single_recipient(addr.script_pubkey())
.send_all(), .drain_wallet()
.fee_absolute(60_000),
) )
.unwrap(); .unwrap();
} }
@ -1800,15 +1831,16 @@ mod test {
#[test] #[test]
#[should_panic(expected = "InsufficientFunds")] #[should_panic(expected = "InsufficientFunds")]
fn test_create_tx_send_all_dust_amount() { fn test_create_tx_single_recipient_dust_amount() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
// very high fee rate, so that the only output would be below dust // very high fee rate, so that the only output would be below dust
wallet wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.send_all() .set_single_recipient(addr.script_pubkey())
.fee_rate(crate::FeeRate::from_sat_per_vb(453.0)), .drain_wallet()
.fee_rate(FeeRate::from_sat_per_vb(453.0)),
) )
.unwrap(); .unwrap();
} }
@ -1875,7 +1907,11 @@ mod test {
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1); assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1);
@ -1899,7 +1935,11 @@ mod test {
let addr = testutils!(@external descriptors, 5); let addr = testutils!(@external descriptors, 5);
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1); assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1);
@ -1920,7 +1960,11 @@ mod test {
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -1943,7 +1987,11 @@ mod test {
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert_eq!(psbt.inputs[0].redeem_script, None); assert_eq!(psbt.inputs[0].redeem_script, None);
@ -1966,7 +2014,11 @@ mod test {
get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
let script = Script::from( let script = Script::from(
@ -1986,7 +2038,11 @@ mod test {
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].non_witness_utxo.is_some());
@ -1999,7 +2055,11 @@ mod test {
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_none()); assert!(psbt.inputs[0].non_witness_utxo.is_none());
@ -2013,9 +2073,10 @@ mod test {
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.force_non_witness_utxo() .set_single_recipient(addr.script_pubkey())
.send_all(), .drain_wallet()
.force_non_witness_utxo(),
) )
.unwrap(); .unwrap();
@ -2309,13 +2370,14 @@ mod test {
} }
#[test] #[test]
fn test_bump_fee_reduce_send_all() { fn test_bump_fee_reduce_single_recipient() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, mut original_details) = wallet let (psbt, mut original_details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.send_all() .set_single_recipient(addr.script_pubkey())
.drain_wallet()
.enable_rbf(), .enable_rbf(),
) )
.unwrap(); .unwrap();
@ -2340,7 +2402,7 @@ mod test {
.bump_fee( .bump_fee(
&txid, &txid,
TxBuilder::new() TxBuilder::new()
.send_all() .maintain_single_recipient()
.fee_rate(FeeRate::from_sat_per_vb(2.5)), .fee_rate(FeeRate::from_sat_per_vb(2.5)),
) )
.unwrap(); .unwrap();
@ -2356,13 +2418,14 @@ mod test {
} }
#[test] #[test]
fn test_bump_fee_absolute_reduce_send_all() { fn test_bump_fee_absolute_reduce_single_recipient() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, mut original_details) = wallet let (psbt, mut original_details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.send_all() .set_single_recipient(addr.script_pubkey())
.drain_wallet()
.enable_rbf(), .enable_rbf(),
) )
.unwrap(); .unwrap();
@ -2384,7 +2447,12 @@ mod test {
.unwrap(); .unwrap();
let (psbt, details) = wallet let (psbt, details) = wallet
.bump_fee(&txid, TxBuilder::new().send_all().fee_absolute(300)) .bump_fee(
&txid,
TxBuilder::new()
.maintain_single_recipient()
.fee_absolute(300),
)
.unwrap(); .unwrap();
assert_eq!(details.sent, original_details.sent); assert_eq!(details.sent, original_details.sent);
@ -2398,25 +2466,82 @@ mod test {
} }
#[test] #[test]
#[should_panic(expected = "InsufficientFunds")] fn test_bump_fee_drain_wallet() {
fn test_bump_fee_remove_send_all_output() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh()); let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
// receive an extra tx, to make sure that in case of "send_all" we get an error and it // receive an extra tx so that our wallet has two utxos.
// doesn't try to pick more inputs
let incoming_txid = wallet.database.borrow_mut().received_tx( let incoming_txid = wallet.database.borrow_mut().received_tx(
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)), testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100), Some(100),
); );
let outpoint = OutPoint {
txid: incoming_txid,
vout: 0,
};
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, mut original_details) = wallet let (psbt, mut original_details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.utxos(vec![OutPoint { .set_single_recipient(addr.script_pubkey())
.utxos(vec![outpoint])
.manually_selected_only()
.enable_rbf(),
)
.unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; 108].to_vec()); // fake signature
wallet
.database
.borrow_mut()
.del_utxo(&txin.previous_output)
.unwrap();
}
original_details.transaction = Some(tx);
wallet
.database
.borrow_mut()
.set_tx(&original_details)
.unwrap();
assert_eq!(original_details.sent, 25_000);
// 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();
assert_eq!(details.sent, 75_000);
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
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(
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let outpoint = OutPoint {
txid: incoming_txid, txid: incoming_txid,
vout: 0, 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() .manually_selected_only()
.send_all()
.enable_rbf(), .enable_rbf(),
) )
.unwrap(); .unwrap();
@ -2442,7 +2567,8 @@ mod test {
.bump_fee( .bump_fee(
&txid, &txid,
TxBuilder::new() TxBuilder::new()
.send_all() .utxos(vec![outpoint])
.manually_selected_only()
.fee_rate(FeeRate::from_sat_per_vb(225.0)), .fee_rate(FeeRate::from_sat_per_vb(225.0)),
) )
.unwrap(); .unwrap();
@ -2583,11 +2709,12 @@ mod test {
Some(100), Some(100),
); );
// initially make a tx without change by using `set_single_recipient`
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, mut original_details) = wallet let (psbt, mut original_details) = wallet
.create_tx( .create_tx(
TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) TxBuilder::new()
.send_all() .set_single_recipient(addr.script_pubkey())
.add_utxo(OutPoint { .add_utxo(OutPoint {
txid: incoming_txid, txid: incoming_txid,
vout: 0, vout: 0,
@ -2614,8 +2741,8 @@ mod test {
.set_tx(&original_details) .set_tx(&original_details)
.unwrap(); .unwrap();
// NOTE: we don't set "send_all" here. so we have a transaction with only one input, but // now bump the fees without using `maintain_single_recipient`. the wallet should add an
// here we are allowed to add more, and we will also have to add a change // extra input and a change output, and leave the original output untouched
let (psbt, details) = wallet let (psbt, details) = wallet
.bump_fee( .bump_fee(
&txid, &txid,
@ -2864,7 +2991,11 @@ mod test {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
@ -2879,7 +3010,11 @@ mod test {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
@ -2894,7 +3029,11 @@ mod test {
let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
@ -2910,7 +3049,11 @@ mod test {
get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet let (psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
@ -2925,7 +3068,11 @@ mod test {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_new_address().unwrap(); let addr = wallet.get_new_address().unwrap();
let (mut psbt, _) = wallet let (mut psbt, _) = wallet
.create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap(); .unwrap();
psbt.inputs[0].hd_keypaths.clear(); psbt.inputs[0].hd_keypaths.clear();

View File

@ -30,6 +30,7 @@
//! # use std::str::FromStr; //! # use std::str::FromStr;
//! # use bitcoin::*; //! # use bitcoin::*;
//! # use bdk::*; //! # use bdk::*;
//! # use bdk::wallet::tx_builder::CreateTx;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); //! # 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 //! // 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 //! // of 5.0 satoshi/vbyte, only spending non-change outputs and with RBF signaling
@ -38,7 +39,7 @@
//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) //! .fee_rate(FeeRate::from_sat_per_vb(5.0))
//! .do_not_spend_change() //! .do_not_spend_change()
//! .enable_rbf(); //! .enable_rbf();
//! # let builder: TxBuilder<bdk::database::MemoryDatabase, _> = builder; //! # let builder: TxBuilder<bdk::database::MemoryDatabase, _, CreateTx> = builder;
//! ``` //! ```
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -52,15 +53,29 @@ use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorith
use crate::database::Database; use crate::database::Database;
use crate::types::{FeeRate, UTXO}; use crate::types::{FeeRate, UTXO};
/// Context in which the [`TxBuilder`] is valid
pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {}
/// [`Wallet::create_tx`](super::Wallet::create_tx) context
#[derive(Debug, Default, Clone)]
pub struct CreateTx;
impl TxBuilderContext for CreateTx {}
/// [`Wallet::bump_fee`](super::Wallet::bump_fee) context
#[derive(Debug, Default, Clone)]
pub struct BumpFee;
impl TxBuilderContext for BumpFee {}
/// A transaction builder /// A transaction builder
/// ///
/// This structure contains the configuration that the wallet must follow to build a transaction. /// This structure contains the configuration that the wallet must follow to build a transaction.
/// ///
/// For an example see [this module](super::tx_builder)'s documentation; /// For an example see [this module](super::tx_builder)'s documentation;
#[derive(Debug)] #[derive(Debug)]
pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>> { pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> {
pub(crate) recipients: Vec<(Script, u64)>, pub(crate) recipients: Vec<(Script, u64)>,
pub(crate) send_all: bool, pub(crate) drain_wallet: bool,
pub(crate) single_recipient: Option<Script>,
pub(crate) fee_policy: Option<FeePolicy>, pub(crate) fee_policy: Option<FeePolicy>,
pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>, pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) utxos: Vec<OutPoint>, pub(crate) utxos: Vec<OutPoint>,
@ -75,11 +90,11 @@ pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>> {
pub(crate) force_non_witness_utxo: bool, pub(crate) force_non_witness_utxo: bool,
pub(crate) coin_selection: Cs, pub(crate) coin_selection: Cs,
phantom: PhantomData<D>, phantom: PhantomData<(D, Ctx)>,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum FeePolicy { pub(crate) enum FeePolicy {
FeeRate(FeeRate), FeeRate(FeeRate),
FeeAmount(u64), FeeAmount(u64),
} }
@ -91,14 +106,16 @@ impl std::default::Default for FeePolicy {
} }
// Unfortunately derive doesn't work with `PhantomData`: https://github.com/rust-lang/rust/issues/26925 // Unfortunately derive doesn't work with `PhantomData`: https://github.com/rust-lang/rust/issues/26925
impl<D: Database, Cs: CoinSelectionAlgorithm<D>> Default for TxBuilder<D, Cs> impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> Default
for TxBuilder<D, Cs, Ctx>
where where
Cs: Default, Cs: Default,
{ {
fn default() -> Self { fn default() -> Self {
TxBuilder { TxBuilder {
recipients: Default::default(), recipients: Default::default(),
send_all: Default::default(), drain_wallet: Default::default(),
single_recipient: Default::default(),
fee_policy: Default::default(), fee_policy: Default::default(),
policy_path: Default::default(), policy_path: Default::default(),
utxos: Default::default(), utxos: Default::default(),
@ -118,49 +135,16 @@ where
} }
} }
impl<D: Database> TxBuilder<D, DefaultCoinSelectionAlgorithm> { // methods supported by both contexts, but only for `DefaultCoinSelectionAlgorithm`
impl<D: Database, Ctx: TxBuilderContext> TxBuilder<D, DefaultCoinSelectionAlgorithm, Ctx> {
/// Create an empty builder /// Create an empty builder
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
/// Create a builder starting from a list of recipients
pub fn with_recipients(recipients: Vec<(Script, u64)>) -> Self {
Self::default().set_recipients(recipients)
}
} }
impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> { // methods supported by both contexts, for any CoinSelectionAlgorithm
/// Replace the recipients already added with a new list impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilder<D, Cs, Ctx> {
pub fn set_recipients(mut self, recipients: Vec<(Script, u64)>) -> Self {
self.recipients = recipients;
self
}
/// Add a recipient to the internal list
pub fn add_recipient(mut self, script_pubkey: Script, amount: u64) -> Self {
self.recipients.push((script_pubkey, amount));
self
}
/// Send all inputs to a single output.
///
/// The semantics of `send_all` depend on whether you are using [`create_tx`] or [`bump_fee`].
/// In `create_tx` it (by default) **selects all the wallets inputs** and sends them to a single
/// output. In `bump_fee` it means to send the original inputs and any additional manually
/// selected intputs to a single output.
///
/// Adding more than one recipients with this option enabled will result in an error.
///
/// The value associated with the only recipient is irrelevant and will be replaced by the wallet.
///
/// [`bump_fee`]: crate::wallet::Wallet::bump_fee
/// [`create_tx`]: crate::wallet::Wallet::create_tx
pub fn send_all(mut self) -> Self {
self.send_all = true;
self
}
/// Set a custom fee rate /// Set a custom fee rate
pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self { pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self {
self.fee_policy = Some(FeePolicy::FeeRate(fee_rate)); self.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
@ -256,25 +240,6 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> {
self self
} }
/// Enable signaling RBF
///
/// This will use the default nSequence value of `0xFFFFFFFD`.
pub fn enable_rbf(self) -> Self {
self.enable_rbf_with_sequence(0xFFFFFFFD)
}
/// Enable signaling RBF with a specific nSequence value
///
/// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator
/// and the given `nsequence` is lower than the CSV value.
///
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
/// be a valid nSequence to signal RBF.
pub fn enable_rbf_with_sequence(mut self, nsequence: u32) -> Self {
self.rbf = Some(nsequence);
self
}
/// Build a transaction with a specific version /// Build a transaction with a specific version
/// ///
/// The `version` should always be greater than `0` and greater than `1` if the wallet's /// The `version` should always be greater than `0` and greater than `1` if the wallet's
@ -318,16 +283,23 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> {
self self
} }
/// Spend all the available inputs. This respects filters like [`unspendable`] and the change policy.
pub fn drain_wallet(mut self) -> Self {
self.drain_wallet = true;
self
}
/// Choose the coin selection algorithm /// Choose the coin selection algorithm
/// ///
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm). /// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>( pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
self, self,
coin_selection: P, coin_selection: P,
) -> TxBuilder<D, P> { ) -> TxBuilder<D, P, Ctx> {
TxBuilder { TxBuilder {
recipients: self.recipients, recipients: self.recipients,
send_all: self.send_all, drain_wallet: self.drain_wallet,
single_recipient: self.single_recipient,
fee_policy: self.fee_policy, fee_policy: self.fee_policy,
policy_path: self.policy_path, policy_path: self.policy_path,
utxos: self.utxos, utxos: self.utxos,
@ -347,6 +319,90 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> {
} }
} }
// methods supported only by create_tx, and only for `DefaultCoinSelectionAlgorithm`
impl<D: Database> TxBuilder<D, DefaultCoinSelectionAlgorithm, CreateTx> {
/// Create a builder starting from a list of recipients
pub fn with_recipients(recipients: Vec<(Script, u64)>) -> Self {
Self::default().set_recipients(recipients)
}
}
// methods supported only by create_tx, for any `CoinSelectionAlgorithm`
impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs, CreateTx> {
/// Replace the recipients already added with a new list
pub fn set_recipients(mut self, recipients: Vec<(Script, u64)>) -> Self {
self.recipients = recipients;
self
}
/// Add a recipient to the internal list
pub fn add_recipient(mut self, script_pubkey: Script, amount: u64) -> Self {
self.recipients.push((script_pubkey, amount));
self
}
/// Set a single recipient that will get all the selected funds minus the fee. No change will
/// be created
///
/// This method overrides any recipient set with [`set_recipients`](Self::set_recipients) or
/// [`add_recipient`](Self::add_recipient).
///
/// It can only be used in conjunction with [`drain_wallet`](Self::drain_wallet) to send the
/// entire content of the wallet (minus filters) to a single recipient or with a
/// list of manually selected UTXOs by enabling [`manually_selected_only`](Self::manually_selected_only)
/// and selecting them with [`utxos`](Self::utxos) or [`add_utxo`](Self::add_utxo).
///
/// When bumping the fees of a transaction made with this option, the user should remeber to
/// add [`maintain_single_recipient`](Self::maintain_single_recipient) to correctly update the
/// single output instead of adding one more for the change.
pub fn set_single_recipient(mut self, recipient: Script) -> Self {
self.single_recipient = Some(recipient);
self.recipients.clear();
self
}
/// Enable signaling RBF
///
/// This will use the default nSequence value of `0xFFFFFFFD`.
pub fn enable_rbf(self) -> Self {
self.enable_rbf_with_sequence(0xFFFFFFFD)
}
/// Enable signaling RBF with a specific nSequence value
///
/// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator
/// and the given `nsequence` is lower than the CSV value.
///
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
/// be a valid nSequence to signal RBF.
pub fn enable_rbf_with_sequence(mut self, nsequence: u32) -> Self {
self.rbf = Some(nsequence);
self
}
}
// methods supported only by bump_fee
impl<D: Database> TxBuilder<D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// Bump the fees of a transaction made with [`set_single_recipient`](Self::set_single_recipient)
///
/// Unless extra inputs are specified with [`add_utxo`] or [`utxos`], this flag will make
/// `bump_fee` reduce the value of the existing output, or fail if it would be consumed
/// entirely given the higher new fee rate.
///
/// If extra inputs are added and they are not entirely consumed in fees, a change output will not
/// be added; the existing output will simply grow in value.
///
/// Fails if the transaction has more than one outputs.
///
/// [`add_utxo`]: Self::add_utxo
/// [`utxos`]: Self::utxos
pub fn maintain_single_recipient(mut self) -> Self {
self.single_recipient = Some(Script::default());
self
}
}
/// Ordering of the transaction's inputs and outputs /// Ordering of the transaction's inputs and outputs
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub enum TxOrdering { pub enum TxOrdering {