]> Untitled Git - bdk/commitdiff
allow to definie static fees for transactions Fixes #137
authorRichard Ulrich <richi@paraeasy.ch>
Tue, 20 Oct 2020 16:10:59 +0000 (18:10 +0200)
committerRichard Ulrich <richi@paraeasy.ch>
Tue, 20 Oct 2020 16:10:59 +0000 (18:10 +0200)
src/wallet/mod.rs
src/wallet/tx_builder.rs

index 89b740d22c5ff85c3f734880352ee04310eb73a6..8f9a3a066ee0bae0b77ec22c2563970d57970fc2 100644 (file)
@@ -55,7 +55,7 @@ pub use utils::IsDust;
 
 use address_validator::AddressValidator;
 use signer::{Signer, SignerId, SignerOrdering, SignersContainer};
-use tx_builder::TxBuilder;
+use tx_builder::{FeePolicy, TxBuilder};
 use utils::{After, Older};
 
 use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progress};
@@ -299,7 +299,7 @@ where
             output: vec![],
         };
 
-        let fee_rate = builder.fee_rate.unwrap_or_default();
+        let fee_rate = get_fee_rate(&builder.fee_policy);
         if builder.send_all && builder.recipients.len() != 1 {
             return Err(Error::SendAllMultipleOutputs);
         }
@@ -393,6 +393,14 @@ where
         };
 
         let mut fee_amount = fee_amount.ceil() as u64;
+
+        if builder.has_absolute_fee() {
+            fee_amount = match builder.fee_policy.as_ref().unwrap() {
+                FeePolicy::FeeAmount(amount) => *amount,
+                _ => 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();
@@ -485,9 +493,9 @@ where
         // the new tx must "pay for its bandwidth"
         let vbytes = tx.get_weight() as f32 / 4.0;
         let required_feerate = FeeRate::from_sat_per_vb(details.fees as f32 / vbytes + 1.0);
-        let new_feerate = builder.fee_rate.unwrap_or_default();
+        let new_feerate = get_fee_rate(&builder.fee_policy);
 
-        if new_feerate < required_feerate {
+        if new_feerate < required_feerate && !builder.has_absolute_fee() {
             return Err(Error::FeeRateTooLow {
                 required: required_feerate,
             });
@@ -623,6 +631,15 @@ where
 
         let amount_needed = tx.output.iter().fold(0, |acc, out| acc + out.value);
         let initial_fee = tx.get_weight() as f32 / 4.0 * new_feerate.as_sat_vb();
+        let initial_fee = if builder.has_absolute_fee() {
+            match builder.fee_policy.as_ref().unwrap() {
+                FeePolicy::FeeAmount(amount) => *amount as f32,
+                _ => initial_fee,
+            }
+        } else {
+            initial_fee
+        };
+
         let coin_selection::CoinSelectionResult {
             txin,
             selected_amount,
@@ -652,6 +669,12 @@ where
         details.sent = selected_amount;
 
         let mut fee_amount = fee_amount.ceil() as u64;
+        if builder.has_absolute_fee() {
+            fee_amount = match builder.fee_policy.as_ref().unwrap() {
+                FeePolicy::FeeAmount(amount) => *amount,
+                _ => fee_amount,
+            }
+        };
         let removed_output_fee_cost = (serialize(&removed_updatable_output).len() as f32
             * new_feerate.as_sat_vb())
         .ceil() as u64;
@@ -659,14 +682,23 @@ where
         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;
+            if builder.has_absolute_fee() {
+                removed_updatable_output.value = change_val_after_add + removed_output_fee_cost;
+                details.received += change_val_after_add + removed_output_fee_cost;
+            } else {
+                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;
+            if builder.has_absolute_fee() {
+                removed_updatable_output.value = change_val_after_add + removed_output_fee_cost;
+            } else {
+                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)? {
@@ -1210,6 +1242,17 @@ where
     }
 }
 
+/// get the fee rate if specified or a default
+fn get_fee_rate(fee_policy: &Option<FeePolicy>) -> FeeRate {
+    if fee_policy.is_none() {
+        return FeeRate::default();
+    }
+    match fee_policy.as_ref().unwrap() {
+        FeePolicy::FeeRate(fr) => *fr,
+        _ => FeeRate::default(),
+    }
+}
+
 #[cfg(test)]
 mod test {
     use std::str::FromStr;
@@ -1660,6 +1703,21 @@ mod test {
         assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature);
     }
 
+    #[test]
+    fn test_create_tx_absolute_fee() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = wallet.get_new_address().unwrap();
+        let (psbt, details) = wallet
+            .create_tx(
+                TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)])
+                    .fee_absolute(100)
+                    .send_all(),
+            )
+            .unwrap();
+
+        assert_eq!(details.fees, 100);
+    }
+
     #[test]
     fn test_create_tx_add_change() {
         use super::tx_builder::TxOrdering;
@@ -2049,6 +2107,71 @@ mod test {
         assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature);
     }
 
+    #[test]
+    fn test_bump_fee_absolute_reduce_change() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
+        let (psbt, mut original_details) = wallet
+            .create_tx(
+                TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(),
+            )
+            .unwrap();
+        let mut tx = psbt.extract_tx();
+        let txid = tx.txid();
+        // skip saving the new utxos, we know they can't be used anyways
+        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();
+
+        let (psbt, details) = wallet
+            .bump_fee(&txid, TxBuilder::new().fee_absolute(200))
+            .unwrap();
+
+        assert_eq!(details.sent, original_details.sent);
+        assert_eq!(
+            details.received + details.fees,
+            original_details.received + original_details.fees
+        );
+        assert!(
+            details.fees > original_details.fees,
+            "{} > {}",
+            details.fees,
+            original_details.fees
+        );
+
+        let tx = &psbt.global.unsigned_tx;
+        assert_eq!(tx.output.len(), 2);
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey == addr.script_pubkey())
+                .unwrap()
+                .value,
+            25_000
+        );
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey != addr.script_pubkey())
+                .unwrap()
+                .value,
+            details.received
+        );
+
+        assert_eq!(details.fees, 200);
+    }
+
     #[test]
     fn test_bump_fee_reduce_send_all() {
         let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
@@ -2096,6 +2219,48 @@ mod test {
         assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature);
     }
 
+    #[test]
+    fn test_bump_fee_absolute_reduce_send_all() {
+        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()
+                    .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();
+
+        let (psbt, details) = wallet
+            .bump_fee(&txid, TxBuilder::new().send_all().fee_absolute(300))
+            .unwrap();
+
+        assert_eq!(details.sent, original_details.sent);
+        assert!(details.fees > original_details.fees);
+
+        let tx = &psbt.global.unsigned_tx;
+        assert_eq!(tx.output.len(), 1);
+        assert_eq!(tx.output[0].value + details.fees, details.sent);
+
+        assert_eq!(details.fees, 300);
+    }
+
     #[test]
     #[should_panic(expected = "InsufficientFunds")]
     fn test_bump_fee_remove_send_all_output() {
@@ -2211,6 +2376,68 @@ mod test {
         assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature);
     }
 
+    #[test]
+    fn test_bump_fee_absolute_add_input() {
+        let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
+        wallet.database.borrow_mut().received_tx(
+            testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
+            Some(100),
+        );
+
+        let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
+        let (psbt, mut original_details) = wallet
+            .create_tx(
+                TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(),
+            )
+            .unwrap();
+        let mut tx = psbt.extract_tx();
+        let txid = tx.txid();
+        // skip saving the new utxos, we know they can't be used anyways
+        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();
+
+        let (psbt, details) = wallet
+            .bump_fee(&txid, TxBuilder::new().fee_absolute(6_000))
+            .unwrap();
+
+        assert_eq!(details.sent, original_details.sent + 25_000);
+        assert_eq!(details.fees + details.received, 30_000);
+
+        let tx = &psbt.global.unsigned_tx;
+        assert_eq!(tx.input.len(), 2);
+        assert_eq!(tx.output.len(), 2);
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey == addr.script_pubkey())
+                .unwrap()
+                .value,
+            45_000
+        );
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey != addr.script_pubkey())
+                .unwrap()
+                .value,
+            details.received
+        );
+
+        assert_eq!(details.fees, 6_000);
+    }
+
     #[test]
     fn test_bump_fee_no_change_add_input_and_change() {
         let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
@@ -2422,6 +2649,78 @@ mod test {
         assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature);
     }
 
+    #[test]
+    fn test_bump_fee_absolute_force_add_input() {
+        let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
+        let incoming_txid = wallet.database.borrow_mut().received_tx(
+            testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
+            Some(100),
+        );
+
+        let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
+        let (psbt, mut original_details) = wallet
+            .create_tx(
+                TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(),
+            )
+            .unwrap();
+        let mut tx = psbt.extract_tx();
+        let txid = tx.txid();
+        // skip saving the new utxos, we know they can't be used anyways
+        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();
+
+        // the new fee_rate is low enough that just reducing the change would be fine, but we force
+        // the addition of an extra input with `add_utxo()`
+        let (psbt, details) = wallet
+            .bump_fee(
+                &txid,
+                TxBuilder::new()
+                    .add_utxo(OutPoint {
+                        txid: incoming_txid,
+                        vout: 0,
+                    })
+                    .fee_absolute(250),
+            )
+            .unwrap();
+
+        assert_eq!(details.sent, original_details.sent + 25_000);
+        assert_eq!(details.fees + details.received, 30_000);
+
+        let tx = &psbt.global.unsigned_tx;
+        assert_eq!(tx.input.len(), 2);
+        assert_eq!(tx.output.len(), 2);
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey == addr.script_pubkey())
+                .unwrap()
+                .value,
+            45_000
+        );
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey != addr.script_pubkey())
+                .unwrap()
+                .value,
+            details.received
+        );
+
+        assert_eq!(details.fees, 250);
+    }
+
     #[test]
     fn test_sign_single_xprv() {
         let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
index 309825bcd08c9606e58ffa10e1567756d05e4a4c..d693f7714c0d41e2a7fd880596666298208812fe 100644 (file)
@@ -60,7 +60,7 @@ use crate::types::{FeeRate, UTXO};
 pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>> {
     pub(crate) recipients: Vec<(Script, u64)>,
     pub(crate) send_all: bool,
-    pub(crate) fee_rate: Option<FeeRate>,
+    pub(crate) fee_policy: Option<FeePolicy>,
     pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>,
     pub(crate) utxos: Option<Vec<OutPoint>>,
     pub(crate) unspendable: Option<Vec<OutPoint>>,
@@ -76,6 +76,12 @@ pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>> {
     phantom: PhantomData<D>,
 }
 
+#[derive(Debug)]
+pub 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>
 where
@@ -85,7 +91,7 @@ where
         TxBuilder {
             recipients: Default::default(),
             send_all: Default::default(),
-            fee_rate: Default::default(),
+            fee_policy: Default::default(),
             policy_path: Default::default(),
             utxos: Default::default(),
             unspendable: Default::default(),
@@ -140,7 +146,13 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> {
 
     /// Set a custom fee rate
     pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self {
-        self.fee_rate = Some(fee_rate);
+        self.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
+        self
+    }
+
+    /// Set an absolute fee
+    pub fn fee_absolute(mut self, fee_amount: u64) -> Self {
+        self.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
         self
     }
 
@@ -287,7 +299,7 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> {
         TxBuilder {
             recipients: self.recipients,
             send_all: self.send_all,
-            fee_rate: self.fee_rate,
+            fee_policy: self.fee_policy,
             policy_path: self.policy_path,
             utxos: self.utxos,
             unspendable: self.unspendable,
@@ -303,6 +315,17 @@ impl<D: Database, Cs: CoinSelectionAlgorithm<D>> TxBuilder<D, Cs> {
             phantom: PhantomData,
         }
     }
+
+    /// Returns true if an absolute fee was specified
+    pub fn has_absolute_fee(&self) -> bool {
+        if self.fee_policy.is_none() {
+            return false;
+        };
+        match self.fee_policy.as_ref().unwrap() {
+            FeePolicy::FeeAmount(_) => true,
+            _ => false,
+        }
+    }
 }
 
 /// Ordering of the transaction's inputs and outputs