]> Untitled Git - bdk/commitdiff
Replace set_single_recipient with drain_to
authorLLFourn <lloyd.fourn@gmail.com>
Wed, 16 Jun 2021 02:43:32 +0000 (12:43 +1000)
committerLLFourn <lloyd.fourn@gmail.com>
Mon, 12 Jul 2021 06:21:53 +0000 (16:21 +1000)
What set_single_recipient does turns out to be useful with multiple
recipients.
Effectively, set_single_recipient was simply creating a change
output that was arbitrarily required to be the only output.
But what if you want to send excess funds to one address but still have
additional recipients who receive a fixed value?
Generalizing this to `drain_to` simplifies the logic and removes several
error cases while also allowing new use cases.

"maintain_single_recipient" is also replaced with "allow_shrinking"
which has more general semantics.

CHANGELOG.md
src/error.rs
src/wallet/mod.rs
src/wallet/tx_builder.rs

index 58c404b2c80cc1c5d9bffefc7e23127be6c3eeea..8ae2ff95c6442d6ba978102524a23da469aa1d00 100644 (file)
@@ -7,9 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 
 ### Wallet
-#### Added
-- Bitcoin core RPC added as blockchain backend
-- Add a `verify` feature that can be enable to verify the unconfirmed txs we download against the consensus rules
+
+- Added Bitcoin core RPC added as blockchain backend
+- Added a `verify` feature that can be enable to verify the unconfirmed txs we download against the consensus rules
+- Removed and replaced `set_single_recipient` with more general `allow_shrinking`.
 
 ## [v0.8.0] - [v0.7.0]
 
index 8d62227638eb07111bda01b8b5fca29df5cf2aa1..540f7419025de39035ea52f9b761004da227bac3 100644 (file)
@@ -24,10 +24,6 @@ pub enum Error {
     Generic(String),
     /// This error is thrown when trying to convert Bare and Public key script to address
     ScriptDoesntHaveAddressForm,
-    /// Found multiple outputs when `single_recipient` option has been specified
-    SingleRecipientMultipleOutputs,
-    /// `single_recipient` option is selected but neither `drain_wallet` nor `manually_selected_only` are
-    SingleRecipientNoInputs,
     /// Cannot build a tx without recipients
     NoRecipients,
     /// `manually_selected_only` option is selected but no utxo has been passed
index 55dd711029168956fe80d5b6ceb18234aeb30c18..29f430875e2cdbfd4d3ad944588db6e22d77ed7d 100644 (file)
@@ -565,21 +565,6 @@ where
             output: vec![],
         };
 
-        let recipients = match &params.single_recipient {
-            Some(recipient) => vec![(recipient, 0)],
-            None => params.recipients.iter().map(|(r, v)| (r, *v)).collect(),
-        };
-        if params.single_recipient.is_some()
-            && !params.manually_selected_only
-            && !params.drain_wallet
-            && params.bumping_fee.is_none()
-        {
-            return Err(Error::SingleRecipientNoInputs);
-        }
-        if recipients.is_empty() {
-            return Err(Error::NoRecipients);
-        }
-
         if params.manually_selected_only && params.utxos.is_empty() {
             return Err(Error::NoUtxosSelected);
         }
@@ -591,12 +576,12 @@ where
         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 recipients.into_iter().enumerate() {
-            let value = match params.single_recipient {
-                Some(_) => 0,
-                None if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
-                None => satoshi,
-            };
+        let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
+
+        for (index, (script_pubkey, value)) in recipients.enumerate() {
+            if value.is_dust() {
+                return Err(Error::OutputBelowDustLimit(index));
+            }
 
             if self.is_mine(script_pubkey)? {
                 received += value;
@@ -651,54 +636,47 @@ where
             })
             .collect();
 
-        // prepare the change output
-        let change_output = match params.single_recipient {
-            Some(_) => None,
-            None => {
-                let change_script = self.get_change_address()?;
-                let change_output = TxOut {
-                    script_pubkey: change_script,
-                    value: 0,
-                };
+        // prepare the drain output
+        let mut drain_output = {
+            let script_pubkey = match params.drain_to {
+                Some(ref drain_recipient) => drain_recipient.clone(),
+                None => self.get_change_address()?,
+            };
 
-                // take the change into account for fees
-                fee_amount += calc_fee_bytes(serialize(&change_output).len() * 4);
-                Some(change_output)
+            TxOut {
+                script_pubkey,
+                value: 0,
             }
         };
-        let mut fee_amount = fee_amount.ceil() as u64;
-        let change_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
-
-        match change_output {
-            None if change_val.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,
-                });
-            }
-            Some(_) if change_val.is_dust() => {
-                // skip the change output because it's dust -- just include it in the fee.
-                fee_amount += change_val;
-            }
-            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;
+        fee_amount += calc_fee_bytes(serialize(&drain_output).len() * 4);
 
-                // the single recipient is our address
-                if self.is_mine(&tx.output[0].script_pubkey)? {
-                    received = change_val;
+        let mut fee_amount = fee_amount.ceil() as u64;
+        let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
+
+        if tx.output.is_empty() {
+            if params.drain_to.is_some() {
+                if drain_val.is_dust() {
+                    return Err(Error::InsufficientFunds {
+                        needed: DUST_LIMIT_SATOSHI,
+                        available: drain_val,
+                    });
                 }
+            } else {
+                return Err(Error::NoRecipients);
             }
         }
 
+        if drain_val.is_dust() {
+            fee_amount += drain_val;
+        } else {
+            drain_output.value = drain_val;
+            if self.is_mine(&drain_output.script_pubkey)? {
+                received += drain_val;
+            }
+            tx.output.push(drain_output);
+        }
+
         // sort input/outputs according to the chosen algorithm
         params.ordering.sort_tx(&mut tx);
 
@@ -725,10 +703,6 @@ where
     /// *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.
-    ///
     /// ## Example
     ///
     /// ```no_run
@@ -2008,13 +1982,11 @@ pub(crate) mod test {
     }
 
     #[test]
-    fn test_create_tx_single_recipient_drain_wallet() {
+    fn test_create_tx_drain_wallet_and_drain_to() {
         let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, details) = builder.finish().unwrap();
 
         assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
@@ -2024,6 +1996,33 @@ pub(crate) mod test {
         );
     }
 
+    #[test]
+    fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
+        let drain_addr = wallet.get_address(New).unwrap();
+        let mut builder = wallet.build_tx();
+        builder
+            .add_recipient(addr.script_pubkey(), 20_000)
+            .drain_to(drain_addr.script_pubkey())
+            .drain_wallet();
+        let (psbt, details) = builder.finish().unwrap();
+        dbg!(&psbt);
+        let outputs = psbt.global.unsigned_tx.output;
+
+        assert_eq!(outputs.len(), 2);
+        let main_output = outputs
+            .iter()
+            .find(|x| x.script_pubkey == addr.script_pubkey())
+            .unwrap();
+        let drain_output = outputs
+            .iter()
+            .find(|x| x.script_pubkey == drain_addr.script_pubkey())
+            .unwrap();
+        assert_eq!(main_output.value, 20_000,);
+        assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0));
+    }
+
     #[test]
     fn test_create_tx_default_fee_rate() {
         let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
@@ -2054,7 +2053,7 @@ pub(crate) mod test {
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .drain_wallet()
             .fee_absolute(100);
         let (psbt, details) = builder.finish().unwrap();
@@ -2073,7 +2072,7 @@ pub(crate) mod test {
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .drain_wallet()
             .fee_absolute(0);
         let (psbt, details) = builder.finish().unwrap();
@@ -2093,7 +2092,7 @@ pub(crate) mod test {
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .drain_wallet()
             .fee_absolute(60_000);
         let (_psbt, _details) = builder.finish().unwrap();
@@ -2134,13 +2133,13 @@ pub(crate) mod test {
 
     #[test]
     #[should_panic(expected = "InsufficientFunds")]
-    fn test_create_tx_single_recipient_dust_amount() {
+    fn test_create_tx_drain_to_dust_amount() {
         let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
         let addr = wallet.get_address(New).unwrap();
         // very high fee rate, so that the only output would be below dust
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .drain_wallet()
             .fee_rate(FeeRate::from_sat_per_vb(453.0));
         builder.finish().unwrap();
@@ -2201,9 +2200,7 @@ pub(crate) mod test {
         let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1);
@@ -2227,9 +2224,7 @@ pub(crate) mod test {
 
         let addr = testutils!(@external descriptors, 5);
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1);
@@ -2250,9 +2245,7 @@ pub(crate) mod test {
             get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert_eq!(
@@ -2275,9 +2268,7 @@ pub(crate) mod test {
             get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert_eq!(psbt.inputs[0].redeem_script, None);
@@ -2300,9 +2291,7 @@ pub(crate) mod test {
             get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         let script = Script::from(
@@ -2322,9 +2311,7 @@ pub(crate) mod test {
             get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert!(psbt.inputs[0].non_witness_utxo.is_some());
@@ -2338,7 +2325,7 @@ pub(crate) mod test {
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .only_witness_utxo()
             .drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
@@ -2353,9 +2340,7 @@ pub(crate) mod test {
             get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert!(psbt.inputs[0].witness_utxo.is_some());
@@ -2367,9 +2352,7 @@ pub(crate) mod test {
             get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (psbt, _) = builder.finish().unwrap();
 
         assert!(psbt.inputs[0].non_witness_utxo.is_some());
@@ -3003,7 +2986,7 @@ pub(crate) mod test {
         let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .drain_wallet()
             .enable_rbf();
         let (psbt, mut original_details) = builder.finish().unwrap();
@@ -3027,7 +3010,7 @@ pub(crate) mod test {
         let mut builder = wallet.build_fee_bump(txid).unwrap();
         builder
             .fee_rate(FeeRate::from_sat_per_vb(2.5))
-            .maintain_single_recipient()
+            .allow_shrinking(addr.script_pubkey())
             .unwrap();
         let (psbt, details) = builder.finish().unwrap();
 
@@ -3047,7 +3030,7 @@ pub(crate) mod test {
         let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .drain_wallet()
             .enable_rbf();
         let (psbt, mut original_details) = builder.finish().unwrap();
@@ -3070,7 +3053,7 @@ pub(crate) mod test {
 
         let mut builder = wallet.build_fee_bump(txid).unwrap();
         builder
-            .maintain_single_recipient()
+            .allow_shrinking(addr.script_pubkey())
             .unwrap()
             .fee_absolute(300);
         let (psbt, details) = builder.finish().unwrap();
@@ -3101,7 +3084,7 @@ pub(crate) mod test {
         let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .add_utxo(outpoint)
             .unwrap()
             .manually_selected_only()
@@ -3130,7 +3113,7 @@ pub(crate) mod test {
         let mut builder = wallet.build_fee_bump(txid).unwrap();
         builder
             .drain_wallet()
-            .maintain_single_recipient()
+            .allow_shrinking(addr.script_pubkey())
             .unwrap()
             .fee_rate(FeeRate::from_sat_per_vb(5.0));
         let (_, details) = builder.finish().unwrap();
@@ -3145,7 +3128,7 @@ pub(crate) mod test {
         // 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".
+        // unless you've also set "allow_shrinking OR there is a change output".
         let incoming_txid = crate::populate_test_db!(
             wallet.database.borrow_mut(),
             testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
@@ -3158,7 +3141,7 @@ pub(crate) mod test {
         let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .add_utxo(outpoint)
             .unwrap()
             .manually_selected_only()
@@ -3324,11 +3307,11 @@ pub(crate) mod test {
             Some(100),
         );
 
-        // initially make a tx without change by using `set_single_recipient`
+        // initially make a tx without change by using `set_drain_recipient`
         let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .add_utxo(OutPoint {
                 txid: incoming_txid,
                 vout: 0,
@@ -3356,7 +3339,7 @@ pub(crate) mod test {
             .set_tx(&original_details)
             .unwrap();
 
-        // now bump the fees without using `maintain_single_recipient`. the wallet should add an
+        // now bump the fees without using `allow_shrinking`. the wallet should add an
         // extra input and a change output, and leave the original output untouched
         let mut builder = wallet.build_fee_bump(txid).unwrap();
         builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
@@ -3602,9 +3585,7 @@ pub(crate) mod test {
         let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
 
         let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
@@ -3619,9 +3600,7 @@ pub(crate) mod test {
         let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
 
         let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
@@ -3636,9 +3615,7 @@ pub(crate) mod test {
         let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
 
         let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
@@ -3653,9 +3630,7 @@ pub(crate) mod test {
         let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
 
         let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
@@ -3671,9 +3646,7 @@ pub(crate) mod test {
             get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
 
         let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
@@ -3688,9 +3661,7 @@ pub(crate) mod test {
         let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
-        builder
-            .set_single_recipient(addr.script_pubkey())
-            .drain_wallet();
+        builder.drain_to(addr.script_pubkey()).drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
 
         psbt.inputs[0].bip32_derivation.clear();
@@ -3772,7 +3743,7 @@ pub(crate) mod test {
         let addr = wallet.get_address(New).unwrap();
         let mut builder = wallet.build_tx();
         builder
-            .set_single_recipient(addr.script_pubkey())
+            .drain_to(addr.script_pubkey())
             .sighash(sighash)
             .drain_wallet();
         let (mut psbt, _) = builder.finish().unwrap();
index 92718c8817cef2d7efb6a0bc7b4a2411027e3d25..20ea19553ee270d59f4252e30d6b0ff2f5999109 100644 (file)
@@ -133,7 +133,7 @@ pub struct TxBuilder<'a, B, D, Cs, Ctx> {
 pub(crate) struct TxParams {
     pub(crate) recipients: Vec<(Script, u64)>,
     pub(crate) drain_wallet: bool,
-    pub(crate) single_recipient: Option<Script>,
+    pub(crate) drain_to: Option<Script>,
     pub(crate) fee_policy: Option<FeePolicy>,
     pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
     pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
@@ -560,49 +560,82 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
         self
     }
 
-    /// Set a single recipient that will get all the selected funds minus the fee. No change will
-    /// be created
+    /// Sets the address to *drain* excess coins to.
     ///
-    /// 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 or [`add_utxo`](Self::add_utxo).
+    /// Usually, when there is excess coins they are sent to a change address generated by the
+    /// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
+    /// your choosing. Just as with a change output, if the drain output is not needed (the excess
+    /// coins are too small) it will not be included in the resulting transaction. The only
+    /// difference is that it is valid to use `drain_to` without setting any ordinary recipients
+    /// with [`add_recipient`] (but it is perfectly find to add recipients as well).
     ///
     /// 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
+    /// add [`allow_shrinking`] to correctly update the
     /// single output instead of adding one more for the change.
-    pub fn set_single_recipient(&mut self, recipient: Script) -> &mut Self {
-        self.params.single_recipient = Some(recipient);
-        self.params.recipients.clear();
-
+    ///
+    /// # Example
+    ///
+    /// `drain_to` is very useful for draining all the coins in a wallet with [`drain_wallet`] to a
+    /// single address.
+    ///
+    /// ```
+    /// # use std::str::FromStr;
+    /// # use bitcoin::*;
+    /// # use bdk::*;
+    /// # use bdk::wallet::tx_builder::CreateTx;
+    /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
+    /// # let wallet = doctest_wallet!();
+    /// let mut tx_builder = wallet.build_tx();
+    ///
+    /// tx_builder
+    ///     // spend all outputs in this wallet
+    ///     .drain_wallet()
+    ///     // send the excess (which is all the coins minus the fee) to this address
+    ///     .drain_to(to_address.script_pubkey())
+    ///     .fee_rate(FeeRate::from_sat_per_vb(5.0))
+    ///     .enable_rbf();
+    /// let (psbt, tx_details) = tx_builder.finish()?;
+    /// # Ok::<(), bdk::Error>(())
+    /// ```
+    ///
+    /// [`allow_shrinking`]: Self::allow_shrinking
+    /// [`add_recipient`]: Self::add_recipient
+    /// [`drain_wallet`]: Self::drain_wallet
+    pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
+        self.params.drain_to = Some(script_pubkey);
         self
     }
 }
 
 // methods supported only by bump_fee
 impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, 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`], 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
-    pub fn maintain_single_recipient(&mut self) -> Result<&mut Self, Error> {
-        let mut recipients = self.params.recipients.drain(..).collect::<Vec<_>>();
-        if recipients.len() != 1 {
-            return Err(Error::SingleRecipientMultipleOutputs);
+    /// Explicitly tells the wallet that it is allowed to reduce the fee of the output matching this
+    /// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
+    /// will attempt to find a change output to shrink instead.
+    ///
+    /// **Note** that the output may shrunk to below the dust limit and therefore removed. If it is
+    /// preserved then it is currently not guaranteed to be in the same position as it was
+    /// originally.
+    ///
+    /// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
+    /// transaction we are bumping.
+    pub fn allow_shrinking(&mut self, script_pubkey: Script) -> Result<&mut Self, Error> {
+        match self
+            .params
+            .recipients
+            .iter()
+            .position(|(recipient_script, _)| *recipient_script == script_pubkey)
+        {
+            Some(position) => {
+                self.params.recipients.remove(position);
+                self.params.drain_to = Some(script_pubkey);
+                Ok(self)
+            }
+            None => Err(Error::Generic(format!(
+                "{} was not in the original transaction",
+                script_pubkey
+            ))),
         }
-        self.params.single_recipient = Some(recipients.pop().unwrap().0);
-        Ok(self)
     }
 }