use address_validator::AddressValidator;
use signer::{Signer, SignerId, SignerOrdering, SignersContainer};
-use tx_builder::{FeePolicy, TxBuilder};
+use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxBuilderContext};
use utils::{After, Older};
use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progress};
/// ```
pub fn create_tx<Cs: coin_selection::CoinSelectionAlgorithm<D>>(
&self,
- builder: TxBuilder<D, Cs>,
+ builder: TxBuilder<D, Cs, CreateTx>,
) -> Result<(PSBT, TransactionDetails), Error> {
- if builder.recipients.is_empty() {
- return Err(Error::NoAddressees);
- }
-
// TODO: fetch both internal and external policies
let policy = self
.descriptor
FeePolicy::FeeRate(rate) => (*rate, 0.0),
};
- if builder.send_all && builder.recipients.len() != 1 {
- return Err(Error::SendAllMultipleOutputs);
+ // try not to move from `builder` because we still need to use it later.
+ 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
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
fee_amount += calc_fee_bytes(tx.get_weight());
- for (index, (script_pubkey, satoshi)) in builder.recipients.iter().enumerate() {
- let value = match builder.send_all {
- true => 0,
- false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
- false => *satoshi,
+ for (index, (script_pubkey, satoshi)) in recipients.into_iter().enumerate() {
+ let value = match builder.single_recipient {
+ Some(_) => 0,
+ None if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
+ None => satoshi,
};
if self.is_mine(script_pubkey)? {
builder.change_policy,
&builder.unspendable,
&builder.utxos,
- builder.send_all,
+ builder.drain_wallet,
builder.manually_selected_only,
false, // we don't mind using unconfirmed outputs here, hopefully coin selection will sort this out?
)?;
tx.input = txin;
// prepare the change output
- let change_output = match builder.send_all {
- true => None,
- false => {
+ let change_output = match builder.single_recipient {
+ Some(_) => None,
+ None => {
let change_script = self.get_change_address()?;
let change_output = TxOut {
script_pubkey: change_script,
};
let mut fee_amount = fee_amount.ceil() as u64;
-
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();
- change_output.value = change_val;
- received += change_val;
-
- tx.output.push(change_output);
- } else if builder.send_all && !change_val.is_dust() {
- // there's only one output, send everything to it
- tx.output[0].value = change_val;
-
- // send_all to our address
- if self.is_mine(&tx.output[0].script_pubkey)? {
- received = change_val;
+
+ 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;
+ received += change_val;
+
+ tx.output.push(change_output);
+ }
+ None => {
+ // there's only one output, send everything to it
+ tx.output[0].value = change_val;
+
+ // the single recipient is our address
+ if self.is_mine(&tx.output[0].script_pubkey)? {
+ 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
///
/// 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
- /// option must be enabled when bumping its fees to correctly reduce the only output's value to
- /// increase the fees.
+ /// **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.
pub fn bump_fee<Cs: coin_selection::CoinSelectionAlgorithm<D>>(
&self,
txid: &Txid,
- builder: TxBuilder<D, Cs>,
+ builder: TxBuilder<D, Cs, BumpFee>,
) -> Result<(PSBT, TransactionDetails), Error> {
let mut details = match self.database.borrow().get_tx(&txid, true)? {
None => return Err(Error::TransactionNotFound),
let vbytes = tx.get_weight() as f32 / 4.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
- // it's `send_all`
- let updatable_output = match builder.send_all {
- true => Some(0),
- false => {
+ // 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 ScriptType. We use
})
.collect::<Result<Vec<_>, _>>()?;
+ if builder.manually_selected_only && builder.utxos.is_empty() {
+ return Err(Error::NoUtxosSelected);
+ }
+
let builder_extra_utxos = builder
.utxos
.iter()
builder.change_policy,
&builder.unspendable,
&builder_extra_utxos[..],
- false, // when doing bump_fee `send_all` does not mean use all available utxos
+ builder.drain_wallet,
builder.manually_selected_only,
true, // we only want confirmed transactions for RBF
)?;
let change_val = selected_amount - amount_needed - fee_amount;
let change_val_after_add = change_val.saturating_sub(removed_output_fee_cost);
- if !builder.send_all && !change_val_after_add.is_dust() {
- 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);
- } else if builder.send_all && !change_val_after_add.is_dust() {
- removed_updatable_output.value = change_val_after_add;
- fee_amount += removed_output_fee_cost;
-
- // send_all to our address
- if self.is_mine(&removed_updatable_output.script_pubkey)? {
- details.received = change_val_after_add;
+ 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;
+ 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;
- 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?
+ // 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);
+ }
}
// sort input/outputs according to the chosen algorithm
Ok((must_spend, may_spend))
}
- fn complete_transaction<Cs: coin_selection::CoinSelectionAlgorithm<D>>(
+ fn complete_transaction<
+ Cs: coin_selection::CoinSelectionAlgorithm<D>,
+ Ctx: TxBuilderContext,
+ >(
&self,
tx: Transaction,
prev_script_pubkeys: HashMap<OutPoint, Script>,
- builder: TxBuilder<D, Cs>,
+ builder: TxBuilder<D, Cs, Ctx>,
) -> Result<PSBT, Error> {
let mut psbt = PSBT::from_unsigned_tx(tx)?;
}
#[test]
- #[should_panic(expected = "NoAddressees")]
+ #[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![]).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();
}
}
#[test]
- #[should_panic(expected = "SendAllMultipleOutputs")]
- fn test_create_tx_send_all_multiple_outputs() {
+ fn test_create_tx_single_recipient_drain_wallet() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_new_address().unwrap();
- wallet
+ let (psbt, details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![
- (addr.script_pubkey(), 25_000),
- (addr.script_pubkey(), 10_000),
- ])
- .send_all(),
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet(),
)
.unwrap();
- }
-
- #[test]
- fn test_create_tx_send_all() {
- 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(), 0)]).send_all())
- .unwrap();
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
assert_eq!(
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(), 0)]).send_all())
+ .create_tx(TxBuilder::with_recipients(vec![(
+ addr.script_pubkey(),
+ 25_000,
+ )]))
.unwrap();
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature);
let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .fee_rate(FeeRate::from_sat_per_vb(5.0))
- .send_all(),
+ TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)])
+ .fee_rate(FeeRate::from_sat_per_vb(5.0)),
)
.unwrap();
let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .fee_absolute(100)
- .send_all(),
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
+ .fee_absolute(100),
)
.unwrap();
let addr = wallet.get_new_address().unwrap();
let (psbt, details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .fee_absolute(0)
- .send_all(),
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
+ .fee_absolute(0),
)
.unwrap();
let addr = wallet.get_new_address().unwrap();
let (_psbt, _details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .fee_absolute(60_000)
- .send_all(),
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
+ .fee_absolute(60_000),
)
.unwrap();
}
#[test]
#[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 addr = wallet.get_new_address().unwrap();
// very high fee rate, so that the only output would be below dust
wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .send_all()
- .fee_rate(crate::FeeRate::from_sat_per_vb(453.0)),
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
+ .fee_rate(FeeRate::from_sat_per_vb(453.0)),
)
.unwrap();
}
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::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all())
+ .create_tx(
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet(),
+ )
.unwrap();
assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1);
let addr = testutils!(@external descriptors, 5);
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();
assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1);
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap();
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();
assert_eq!(
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap();
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();
assert_eq!(psbt.inputs[0].redeem_script, None);
get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
let addr = wallet.get_new_address().unwrap();
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();
let script = Script::from(
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap();
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();
assert!(psbt.inputs[0].non_witness_utxo.is_some());
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap();
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();
assert!(psbt.inputs[0].non_witness_utxo.is_none());
let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .force_non_witness_utxo()
- .send_all(),
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
+ .force_non_witness_utxo(),
)
.unwrap();
}
#[test]
- fn test_bump_fee_reduce_send_all() {
+ 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::with_recipients(vec![(addr.script_pubkey(), 0)])
- .send_all()
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
.enable_rbf(),
)
.unwrap();
.bump_fee(
&txid,
TxBuilder::new()
- .send_all()
+ .maintain_single_recipient()
.fee_rate(FeeRate::from_sat_per_vb(2.5)),
)
.unwrap();
}
#[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 addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, mut original_details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .send_all()
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .drain_wallet()
.enable_rbf(),
)
.unwrap();
.unwrap();
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();
assert_eq!(details.sent, original_details.sent);
assert_eq!(details.fees, 300);
}
+ #[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(
+ testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
+ Some(100),
+ );
+ let outpoint = OutPoint {
+ txid: incoming_txid,
+ 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 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_send_all_output() {
+ fn test_bump_fee_remove_output_manually_selected_only() {
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
- // doesn't try to pick more inputs
+ // 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,
+ vout: 0,
+ };
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let (psbt, mut original_details) = wallet
.create_tx(
- TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
- .utxos(vec![OutPoint {
- txid: incoming_txid,
- vout: 0,
- }])
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
+ .utxos(vec![outpoint])
.manually_selected_only()
- .send_all()
.enable_rbf(),
)
.unwrap();
.bump_fee(
&txid,
TxBuilder::new()
- .send_all()
+ .utxos(vec![outpoint])
+ .manually_selected_only()
.fee_rate(FeeRate::from_sat_per_vb(225.0)),
)
.unwrap();
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::with_recipients(vec![(addr.script_pubkey(), 0)])
- .send_all()
+ TxBuilder::new()
+ .set_single_recipient(addr.script_pubkey())
.add_utxo(OutPoint {
txid: incoming_txid,
vout: 0,
.set_tx(&original_details)
.unwrap();
- // NOTE: we don't set "send_all" here. so we have a transaction with only one input, but
- // here we are allowed to add more, and we will also have to add a change
+ // 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,
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_new_address().unwrap();
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();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
let addr = wallet.get_new_address().unwrap();
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();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
let addr = wallet.get_new_address().unwrap();
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();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
let addr = wallet.get_new_address().unwrap();
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();
let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap();
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_new_address().unwrap();
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();
psbt.inputs[0].hd_keypaths.clear();
//! # use std::str::FromStr;
//! # use bitcoin::*;
//! # 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
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
//! .do_not_spend_change()
//! .enable_rbf();
-//! # let builder: TxBuilder<bdk::database::MemoryDatabase, _> = builder;
+//! # let builder: TxBuilder<bdk::database::MemoryDatabase, _, CreateTx> = builder;
//! ```
use std::collections::BTreeMap;
use crate::database::Database;
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
///
/// 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;
#[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) send_all: bool,
+ pub(crate) drain_wallet: bool,
+ pub(crate) single_recipient: Option<Script>,
pub(crate) fee_policy: Option<FeePolicy>,
pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) utxos: Vec<OutPoint>,
pub(crate) force_non_witness_utxo: bool,
pub(crate) coin_selection: Cs,
- phantom: PhantomData<D>,
+ phantom: PhantomData<(D, Ctx)>,
}
#[derive(Debug)]
-pub enum FeePolicy {
+pub(crate) enum FeePolicy {
FeeRate(FeeRate),
FeeAmount(u64),
}
}
// 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
Cs: Default,
{
fn default() -> Self {
TxBuilder {
recipients: Default::default(),
- send_all: Default::default(),
+ drain_wallet: Default::default(),
+ single_recipient: Default::default(),
fee_policy: Default::default(),
policy_path: Default::default(),
utxos: Default::default(),
}
}
-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
pub fn new() -> Self {
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> {
- /// 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
- }
-
- /// 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
- }
-
+// methods supported by both contexts, for any CoinSelectionAlgorithm
+impl<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext> TxBuilder<D, Cs, Ctx> {
/// Set a custom fee rate
pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self {
self.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
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
///
/// The `version` should always be greater than `0` and greater than `1` if the wallet's
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
///
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
self,
coin_selection: P,
- ) -> TxBuilder<D, P> {
+ ) -> TxBuilder<D, P, Ctx> {
TxBuilder {
recipients: self.recipients,
- send_all: self.send_all,
+ drain_wallet: self.drain_wallet,
+ single_recipient: self.single_recipient,
fee_policy: self.fee_policy,
policy_path: self.policy_path,
utxos: self.utxos,
}
}
+// 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
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
pub enum TxOrdering {