]> Untitled Git - bdk/commitdiff
Implement RBF and add a few tests
authorAlekos Filini <alekos.filini@gmail.com>
Thu, 13 Aug 2020 14:51:27 +0000 (16:51 +0200)
committerAlekos Filini <alekos.filini@gmail.com>
Fri, 14 Aug 2020 10:48:07 +0000 (12:48 +0200)
src/cli.rs
src/database/mod.rs
src/error.rs
src/wallet/mod.rs
src/wallet/rbf.rs [new file with mode: 0644]
src/wallet/utils.rs
testutils-macros/src/lib.rs

index 164ae4a2b6ffe3fbb1fb4706ba8e8fd344e9a0a7..3da4d49e9134b0c2c2328b86ac9403e3fb2f610c 100644 (file)
@@ -9,7 +9,7 @@ use log::{debug, error, info, trace, LevelFilter};
 use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
 use bitcoin::hashes::hex::{FromHex, ToHex};
 use bitcoin::util::psbt::PartiallySignedTransaction;
-use bitcoin::{Address, OutPoint};
+use bitcoin::{Address, OutPoint, Txid};
 
 use crate::error::Error;
 use crate::types::ScriptType;
@@ -83,6 +83,12 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
                         .long("send_all")
                         .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"),
                 )
+                .arg(
+                    Arg::with_name("enable_rbf")
+                        .short("rbf")
+                        .long("enable_rbf")
+                        .help("Enables Replace-By-Fee (BIP125)"),
+                )
                 .arg(
                     Arg::with_name("utxos")
                         .long("utxos")
@@ -120,6 +126,53 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
                         .number_of_values(1),
                 ),
         )
+        .subcommand(
+            SubCommand::with_name("bump_fee")
+                .about("Bumps the fees of an RBF transaction")
+                .arg(
+                    Arg::with_name("txid")
+                        .required(true)
+                        .takes_value(true)
+                        .short("txid")
+                        .long("txid")
+                        .help("TXID of the transaction to update"),
+                )
+                .arg(
+                    Arg::with_name("send_all")
+                        .short("all")
+                        .long("send_all")
+                        .help("Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all`"),
+                )
+                .arg(
+                    Arg::with_name("utxos")
+                        .long("utxos")
+                        .value_name("TXID:VOUT")
+                        .help("Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .multiple(true)
+                        .validator(outpoint_validator),
+                )
+                .arg(
+                    Arg::with_name("unspendable")
+                        .long("unspendable")
+                        .value_name("TXID:VOUT")
+                        .help("Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees")
+                        .takes_value(true)
+                        .number_of_values(1)
+                        .multiple(true)
+                        .validator(outpoint_validator),
+                )
+                .arg(
+                    Arg::with_name("fee_rate")
+                        .required(true)
+                        .short("fee")
+                        .long("fee_rate")
+                        .value_name("SATS_VBYTE")
+                        .help("The new targeted fee rate in sat/vbyte")
+                        .takes_value(true),
+                ),
+        )
         .subcommand(
             SubCommand::with_name("policies")
                 .about("Returns the available spending policies for the descriptor")
@@ -331,6 +384,9 @@ where
         if sub_matches.is_present("send_all") {
             tx_builder = tx_builder.send_all();
         }
+        if sub_matches.is_present("enable_rbf") {
+            tx_builder = tx_builder.enable_rbf();
+        }
 
         if let Some(fee_rate) = sub_matches.value_of("fee_rate") {
             let fee_rate = f32::from_str(fee_rate).map_err(|s| Error::Generic(s.to_string()))?;
@@ -363,6 +419,40 @@ where
             result.1,
             base64::encode(&serialize(&result.0))
         )))
+    } else if let Some(sub_matches) = matches.subcommand_matches("bump_fee") {
+        let txid = Txid::from_str(sub_matches.value_of("txid").unwrap())
+            .map_err(|s| Error::Generic(s.to_string()))?;
+
+        let fee_rate = f32::from_str(sub_matches.value_of("fee_rate").unwrap())
+            .map_err(|s| Error::Generic(s.to_string()))?;
+        let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate));
+
+        if sub_matches.is_present("send_all") {
+            tx_builder = tx_builder.send_all();
+        }
+
+        if let Some(utxos) = sub_matches.values_of("utxos") {
+            let utxos = utxos
+                .map(|i| parse_outpoint(i))
+                .collect::<Result<Vec<_>, _>>()
+                .map_err(|s| Error::Generic(s.to_string()))?;
+            tx_builder = tx_builder.utxos(utxos);
+        }
+
+        if let Some(unspendable) = sub_matches.values_of("unspendable") {
+            let unspendable = unspendable
+                .map(|i| parse_outpoint(i))
+                .collect::<Result<Vec<_>, _>>()
+                .map_err(|s| Error::Generic(s.to_string()))?;
+            tx_builder = tx_builder.unspendable(unspendable);
+        }
+
+        let result = wallet.bump_fee(&txid, tx_builder)?;
+        Ok(Some(format!(
+            "{:#?}\nPSBT: {}",
+            result.1,
+            base64::encode(&serialize(&result.0))
+        )))
     } else if let Some(_sub_matches) = matches.subcommand_matches("policies") {
         Ok(Some(format!(
             "External: {}\nInternal:{}",
index 4ffb134737155f5f6c696bf6726f49ea5c71721d..5bb108e2faa9eeebde599e146b0c496052bc38cd 100644 (file)
@@ -96,11 +96,11 @@ pub trait DatabaseUtils: Database {
 
     fn get_previous_output(&self, outpoint: &OutPoint) -> Result<Option<TxOut>, Error> {
         self.get_raw_tx(&outpoint.txid)?
-            .and_then(|previous_tx| {
+            .map(|previous_tx| {
                 if outpoint.vout as usize >= previous_tx.output.len() {
-                    Some(Err(Error::InvalidOutpoint(outpoint.clone())))
+                    Err(Error::InvalidOutpoint(outpoint.clone()))
                 } else {
-                    Some(Ok(previous_tx.output[outpoint.vout as usize].clone()))
+                    Ok(previous_tx.output[outpoint.vout as usize].clone())
                 }
             })
             .transpose()
index 22cbb09c3ea85416e38f822a598cc877e0ece373..f7641bddbb281a71e9d01e36c992768080b468c5 100644 (file)
@@ -14,6 +14,10 @@ pub enum Error {
     InvalidAddressNetwork(Address),
     UnknownUTXO,
     DifferentTransactions,
+    TransactionNotFound,
+    TransactionConfirmed,
+    IrreplaceableTransaction,
+    FeeRateTooLow(crate::wallet::utils::FeeRate),
 
     ChecksumMismatch,
     DifferentDescriptorStructure,
index fd868890e8ed84903b71d0b2e8097e6f5a2d99d0..edc803607de6f1a84f90c579fdd48d5478a58650 100644 (file)
@@ -1,7 +1,7 @@
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::collections::{BTreeMap, HashSet};
-use std::ops::DerefMut;
+use std::ops::{Deref, DerefMut};
 use std::str::FromStr;
 
 use bitcoin::blockdata::opcodes;
@@ -19,12 +19,13 @@ use log::{debug, error, info, trace};
 
 pub mod coin_selection;
 pub mod export;
+mod rbf;
 pub mod time;
 pub mod tx_builder;
 pub mod utils;
 
 use tx_builder::TxBuilder;
-use utils::IsDust;
+use utils::{FeeRate, IsDust};
 
 use crate::blockchain::{noop_progress, Blockchain, OfflineBlockchain, OnlineBlockchain};
 use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
@@ -308,79 +309,235 @@ where
         builder.ordering.modify_tx(&mut tx);
 
         let txid = tx.txid();
-        let mut psbt = PSBT::from_unsigned_tx(tx)?;
+        let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?;
 
-        // add metadata for the inputs
-        for (psbt_input, input) in psbt
-            .inputs
-            .iter_mut()
-            .zip(psbt.global.unsigned_tx.input.iter())
-        {
-            let prev_script = prev_script_pubkeys.get(&input.previous_output).unwrap();
+        let transaction_details = TransactionDetails {
+            transaction: None,
+            txid,
+            timestamp: time::get_timestamp(),
+            received,
+            sent: total_amount,
+            fees: fee_amount,
+            height: None,
+        };
 
-            // Add sighash, default is obviously "ALL"
-            psbt_input.sighash_type = builder.sighash.or(Some(SigHashType::All));
+        Ok((psbt, transaction_details))
+    }
 
-            // Try to find the prev_script in our db to figure out if this is internal or external,
-            // and the derivation index
-            let (script_type, child) = match self
-                .database
-                .borrow()
-                .get_path_from_script_pubkey(&prev_script)?
-            {
-                Some(x) => x,
-                None => continue,
-            };
+    // TODO: support for merging multiple transactions while bumping the fees
+    // TODO: option to force addition of an extra output? seems bad for privacy to update the
+    // change
+    pub fn bump_fee<Cs: coin_selection::CoinSelectionAlgorithm>(
+        &self,
+        txid: &Txid,
+        builder: TxBuilder<Cs>,
+    ) -> Result<(PSBT, TransactionDetails), Error> {
+        let mut details = match self.database.borrow().get_tx(&txid, true)? {
+            None => return Err(Error::TransactionNotFound),
+            Some(tx) if tx.transaction.is_none() => return Err(Error::TransactionNotFound),
+            Some(tx) if tx.height.is_some() => return Err(Error::TransactionConfirmed),
+            Some(tx) => tx,
+        };
+        let mut tx = details.transaction.take().unwrap();
+        if !tx.input.iter().any(|txin| txin.sequence <= 0xFFFFFFFD) {
+            return Err(Error::IrreplaceableTransaction);
+        }
 
-            let (desc, _) = self.get_descriptor_for(script_type);
-            psbt_input.hd_keypaths = desc.get_hd_keypaths(child)?;
-            let derived_descriptor = desc.derive(child)?;
+        // 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();
 
-            psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
-            psbt_input.witness_script = derived_descriptor.psbt_witness_script();
+        if new_feerate < required_feerate {
+            return Err(Error::FeeRateTooLow(required_feerate));
+        }
+        let mut fee_difference =
+            (new_feerate.as_sat_vb() * tx.get_weight() as f32 / 4.0).ceil() as u64 - details.fees;
 
-            let prev_output = input.previous_output;
-            if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
-                if derived_descriptor.is_witness() {
-                    psbt_input.witness_utxo =
-                        Some(prev_tx.output[prev_output.vout as usize].clone());
-                }
-                if !derived_descriptor.is_witness() || builder.force_non_witness_utxo {
-                    psbt_input.non_witness_utxo = Some(prev_tx);
+        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 = if builder.send_all {
+            0
+        } else {
+            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
+                // `get_deget_descriptor_for` to find what's the ScriptType for `Internal`
+                // addresses really is, because if there's no change_descriptor it's actually equal
+                // to "External"
+                let (_, change_type) = self.get_descriptor_for(ScriptType::Internal);
+                match self
+                    .database
+                    .borrow()
+                    .get_path_from_script_pubkey(&txout.script_pubkey)?
+                {
+                    Some((script_type, _)) if script_type == change_type => {
+                        change_output = Some(index);
+                        break;
+                    }
+                    _ => {}
                 }
             }
-        }
 
-        // probably redundant but it doesn't hurt...
-        self.add_input_hd_keypaths(&mut psbt)?;
+            // we need a change output, add one here and take into account the extra fees for it
+            if change_output.is_none() {
+                let change_script = self.get_change_address()?;
+                let change_txout = TxOut {
+                    script_pubkey: change_script,
+                    value: 0,
+                };
+                fee_difference +=
+                    (serialize(&change_txout).len() as f32 * new_feerate.as_sat_vb()).ceil() as u64;
+                tx.output.push(change_txout);
 
-        // add metadata for the outputs
-        for (psbt_output, tx_output) in psbt
-            .outputs
-            .iter_mut()
-            .zip(psbt.global.unsigned_tx.output.iter())
+                change_output = Some(tx.output.len() - 1);
+            }
+
+            change_output.unwrap()
+        };
+
+        // if `builder.utxos` is Some(_) we have to add inputs and we skip down to the last branch
+        match tx.output[updatable_output]
+            .value
+            .checked_sub(fee_difference)
         {
-            if let Some((script_type, child)) = self
-                .database
-                .borrow()
-                .get_path_from_script_pubkey(&tx_output.script_pubkey)?
-            {
-                let (desc, _) = self.get_descriptor_for(script_type);
-                psbt_output.hd_keypaths = desc.get_hd_keypaths(child)?;
+            Some(new_value) if !new_value.is_dust() && builder.utxos.is_none() => {
+                // try to reduce the "updatable output" amount
+                tx.output[updatable_output].value = new_value;
+                if self.is_mine(&tx.output[updatable_output].script_pubkey)? {
+                    details.received -= fee_difference;
+                }
+
+                details.fees += fee_difference;
             }
-        }
+            _ if builder.send_all && builder.utxos.is_none() => {
+                // if the tx is "send_all" it doesn't make sense to either remove the only output
+                // or add more inputs
+                return Err(Error::InsufficientFunds);
+            }
+            _ => {
+                // initially always remove the change output
+                let mut removed_change_output = tx.output.remove(updatable_output);
+                if self.is_mine(&removed_change_output.script_pubkey)? {
+                    details.received -= removed_change_output.value;
+                }
 
-        let transaction_details = TransactionDetails {
-            transaction: None,
-            txid,
-            timestamp: time::get_timestamp(),
-            received,
-            sent: total_amount,
-            fees: fee_amount,
-            height: None,
+                // we want to add more inputs if:
+                // - builder.utxos tells us to do so
+                // - the removed change value is lower than the fee_difference we want to add
+                let needs_more_inputs =
+                    builder.utxos.is_some() || removed_change_output.value <= fee_difference;
+                let added_amount = if needs_more_inputs {
+                    // TODO: use the right weight instead of the maximum, and only fall-back to it if the
+                    // script is unknown in the database
+                    let input_witness_weight = std::cmp::max(
+                        self.get_descriptor_for(ScriptType::Internal)
+                            .0
+                            .max_satisfaction_weight(),
+                        self.get_descriptor_for(ScriptType::External)
+                            .0
+                            .max_satisfaction_weight(),
+                    );
+
+                    let (available_utxos, use_all_utxos) = self.get_available_utxos(
+                        builder.change_policy,
+                        &builder.utxos,
+                        &builder.unspendable,
+                        false,
+                    )?;
+                    let available_utxos = rbf::filter_available(
+                        self.database.borrow().deref(),
+                        available_utxos.into_iter(),
+                    )?;
+                    let coin_selection::CoinSelectionResult {
+                        txin,
+                        total_amount,
+                        fee_amount,
+                    } = builder.coin_selection.coin_select(
+                        available_utxos,
+                        use_all_utxos,
+                        new_feerate.as_sat_vb(),
+                        fee_difference
+                            .checked_sub(removed_change_output.value)
+                            .unwrap_or(0),
+                        input_witness_weight,
+                        0.0,
+                    )?;
+                    fee_difference += fee_amount.ceil() as u64;
+
+                    // add the new inputs
+                    let (mut txin, _): (Vec<_>, Vec<_>) = txin.into_iter().unzip();
+
+                    // TODO: use tx_builder.sequence ??
+                    // copy the n_sequence from the inputs that were already in the transaction
+                    txin.iter_mut()
+                        .for_each(|i| i.sequence = tx.input[0].sequence);
+                    tx.input.extend_from_slice(&mut txin);
+
+                    details.sent += total_amount;
+                    total_amount
+                } else {
+                    // otherwise just remove the output and add 0 new coins
+                    0
+                };
+
+                match (removed_change_output.value + added_amount).checked_sub(fee_difference) {
+                    None => return Err(Error::InsufficientFunds),
+                    Some(new_value) if new_value.is_dust() => {
+                        // the change would be dust, add that to fees
+                        details.fees += fee_difference + new_value;
+                    }
+                    Some(new_value) => {
+                        // add the change back
+                        removed_change_output.value = new_value;
+                        tx.output.push(removed_change_output);
+
+                        details.received += new_value;
+                        details.fees += fee_difference;
+                    }
+                }
+            }
         };
 
-        Ok((psbt, transaction_details))
+        // clear witnesses
+        for input in &mut tx.input {
+            input.script_sig = Script::default();
+            input.witness = vec![];
+        }
+
+        // sort input/outputs according to the chosen algorithm
+        builder.ordering.modify_tx(&mut tx);
+
+        // TODO: check that we are not replacing more than 100 txs from mempool
+
+        details.txid = tx.txid();
+        details.timestamp = time::get_timestamp();
+
+        let prev_script_pubkeys = tx
+            .input
+            .iter()
+            .map(|txin| {
+                Ok((
+                    txin.previous_output.clone(),
+                    self.database
+                        .borrow()
+                        .get_previous_output(&txin.previous_output)?,
+                ))
+            })
+            .collect::<Result<Vec<_>, Error>>()?
+            .into_iter()
+            .filter_map(|(outpoint, txout)| match txout {
+                Some(txout) => Some((outpoint, txout.script_pubkey)),
+                None => None,
+            })
+            .collect();
+        let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?;
+
+        Ok((psbt, details))
     }
 
     // TODO: define an enum for signing errors
@@ -734,6 +891,80 @@ where
         }
     }
 
+    fn complete_transaction<Cs: coin_selection::CoinSelectionAlgorithm>(
+        &self,
+        tx: Transaction,
+        prev_script_pubkeys: HashMap<OutPoint, Script>,
+        builder: TxBuilder<Cs>,
+    ) -> Result<PSBT, Error> {
+        let mut psbt = PSBT::from_unsigned_tx(tx)?;
+
+        // add metadata for the inputs
+        for (psbt_input, input) in psbt
+            .inputs
+            .iter_mut()
+            .zip(psbt.global.unsigned_tx.input.iter())
+        {
+            let prev_script = match prev_script_pubkeys.get(&input.previous_output) {
+                Some(prev_script) => prev_script,
+                None => continue,
+            };
+
+            // Add sighash, default is obviously "ALL"
+            psbt_input.sighash_type = builder.sighash.or(Some(SigHashType::All));
+
+            // Try to find the prev_script in our db to figure out if this is internal or external,
+            // and the derivation index
+            let (script_type, child) = match self
+                .database
+                .borrow()
+                .get_path_from_script_pubkey(&prev_script)?
+            {
+                Some(x) => x,
+                None => continue,
+            };
+
+            let (desc, _) = self.get_descriptor_for(script_type);
+            psbt_input.hd_keypaths = desc.get_hd_keypaths(child)?;
+            let derived_descriptor = desc.derive(child)?;
+
+            psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
+            psbt_input.witness_script = derived_descriptor.psbt_witness_script();
+
+            let prev_output = input.previous_output;
+            if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
+                if derived_descriptor.is_witness() {
+                    psbt_input.witness_utxo =
+                        Some(prev_tx.output[prev_output.vout as usize].clone());
+                }
+                if !derived_descriptor.is_witness() || builder.force_non_witness_utxo {
+                    psbt_input.non_witness_utxo = Some(prev_tx);
+                }
+            }
+        }
+
+        // probably redundant but it doesn't hurt...
+        self.add_input_hd_keypaths(&mut psbt)?;
+
+        // add metadata for the outputs
+        for (psbt_output, tx_output) in psbt
+            .outputs
+            .iter_mut()
+            .zip(psbt.global.unsigned_tx.output.iter())
+        {
+            if let Some((script_type, child)) = self
+                .database
+                .borrow()
+                .get_path_from_script_pubkey(&tx_output.script_pubkey)?
+            {
+                let (desc, _) = self.get_descriptor_for(script_type);
+                psbt_output.hd_keypaths = desc.get_hd_keypaths(child)?;
+            }
+        }
+
+        Ok(psbt)
+    }
+
     fn add_input_hd_keypaths(&self, psbt: &mut PSBT) -> Result<(), Error> {
         let mut input_utxos = Vec::with_capacity(psbt.inputs.len());
         for n in 0..psbt.inputs.len() {
@@ -984,14 +1215,43 @@ mod test {
 
         let txid = wallet.database.borrow_mut().received_tx(
             testutils! {
-                @tx ( (@external descriptors, 0) => 50_000 )
+                @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
             },
-            None,
+            Some(100),
         );
 
         (wallet, descriptors, txid)
     }
 
+    macro_rules! assert_fee_rate {
+        ($tx:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
+            let mut tx = $tx.clone();
+            $(
+                $( $add_signature )*
+                for txin in &mut tx.input {
+                    txin.witness.push([0x00; 108].to_vec()); // fake signature
+                }
+            )*
+
+            #[allow(unused_mut)]
+            #[allow(unused_assignments)]
+            let mut dust_change = false;
+            $(
+                $( $dust_change )*
+                dust_change = true;
+            )*
+
+            let tx_fee_rate = $fees as f32 / (tx.get_weight() as f32 / 4.0);
+            let fee_rate = $fee_rate.as_sat_vb();
+
+            if !dust_change {
+                assert!((tx_fee_rate - fee_rate).abs() < 0.5, format!("Expected fee rate of {}, the tx has {}", fee_rate, tx_fee_rate));
+            } else {
+                assert!(tx_fee_rate >= fee_rate, format!("Expected fee rate of at least {}, the tx has {}", fee_rate, tx_fee_rate));
+            }
+        });
+    }
+
     #[test]
     #[should_panic(expected = "NoAddressees")]
     fn test_create_tx_empty_addressees() {
@@ -1214,6 +1474,32 @@ mod test {
         );
     }
 
+    #[test]
+    fn test_create_tx_default_fee_rate() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = wallet.get_new_address().unwrap();
+        let (psbt, details) = wallet
+            .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all())
+            .unwrap();
+
+        assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature);
+    }
+
+    #[test]
+    fn test_create_tx_custom_fee_rate() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = wallet.get_new_address().unwrap();
+        let (psbt, details) = wallet
+            .create_tx(
+                TxBuilder::from_addressees(vec![(addr.clone(), 0)])
+                    .fee_rate(FeeRate::from_sat_per_vb(5.0))
+                    .send_all(),
+            )
+            .unwrap();
+
+        assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature);
+    }
+
     #[test]
     fn test_create_tx_add_change() {
         use super::tx_builder::TxOrdering;
@@ -1465,4 +1751,489 @@ mod test {
         assert!(psbt.inputs[0].non_witness_utxo.is_some());
         assert!(psbt.inputs[0].witness_utxo.is_some());
     }
+
+    #[test]
+    #[should_panic(expected = "IrreplaceableTransaction")]
+    fn test_bump_fee_irreplaceable_tx() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = wallet.get_new_address().unwrap();
+        let (psbt, mut details) = wallet
+            .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]))
+            .unwrap();
+        let tx = psbt.extract_tx();
+        let txid = tx.txid();
+        // skip saving the utxos, we know they can't be used anyways
+        details.transaction = Some(tx);
+        wallet.database.borrow_mut().set_tx(&details).unwrap();
+
+        wallet.bump_fee(&txid, TxBuilder::new()).unwrap();
+    }
+
+    #[test]
+    #[should_panic(expected = "TransactionConfirmed")]
+    fn test_bump_fee_confirmed_tx() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = wallet.get_new_address().unwrap();
+        let (psbt, mut details) = wallet
+            .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]))
+            .unwrap();
+        let tx = psbt.extract_tx();
+        let txid = tx.txid();
+        // skip saving the utxos, we know they can't be used anyways
+        details.transaction = Some(tx);
+        details.height = Some(42);
+        wallet.database.borrow_mut().set_tx(&details).unwrap();
+
+        wallet.bump_fee(&txid, TxBuilder::new()).unwrap();
+    }
+
+    #[test]
+    #[should_panic(expected = "FeeRateTooLow")]
+    fn test_bump_fee_low_fee_rate() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = wallet.get_new_address().unwrap();
+        let (psbt, mut details) = wallet
+            .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).enable_rbf())
+            .unwrap();
+        let tx = psbt.extract_tx();
+        let txid = tx.txid();
+        // skip saving the utxos, we know they can't be used anyways
+        details.transaction = Some(tx);
+        wallet.database.borrow_mut().set_tx(&details).unwrap();
+
+        wallet
+            .bump_fee(
+                &txid,
+                TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(1.0)),
+            )
+            .unwrap();
+    }
+
+    #[test]
+    fn test_bump_fee_reduce_change() {
+        let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
+        let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
+        let (psbt, mut original_details) = wallet
+            .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 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_rate(FeeRate::from_sat_per_vb(2.5)),
+            )
+            .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);
+
+        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_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature);
+    }
+
+    #[test]
+    fn test_bump_fee_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::from_addressees(vec![(addr.clone(), 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_rate(FeeRate::from_sat_per_vb(2.5)),
+            )
+            .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_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature);
+    }
+
+    #[test]
+    #[should_panic(expected = "InsufficientFunds")]
+    fn test_bump_fee_remove_send_all_output() {
+        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
+        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::from_addressees(vec![(addr.clone(), 0)])
+                    .utxos(vec![OutPoint {
+                        txid: incoming_txid,
+                        vout: 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();
+        assert_eq!(original_details.sent, 25_000);
+
+        wallet
+            .bump_fee(
+                &txid,
+                TxBuilder::new()
+                    .send_all()
+                    .fee_rate(FeeRate::from_sat_per_vb(225.0)),
+            )
+            .unwrap();
+    }
+
+    #[test]
+    fn test_bump_fee_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::from_addressees(vec![(addr.clone(), 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_rate(FeeRate::from_sat_per_vb(50.0)),
+            )
+            .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_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature);
+    }
+
+    #[test]
+    fn test_bump_fee_no_change_add_input_and_change() {
+        let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
+        let incoming_txid = wallet.database.borrow_mut().received_tx(
+            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::from_addressees(vec![(addr.clone(), 0)])
+                    .send_all()
+                    .add_utxo(OutPoint {
+                        txid: incoming_txid,
+                        vout: 0,
+                    })
+                    .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();
+
+        // 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
+        let (psbt, details) = wallet
+            .bump_fee(
+                &txid,
+                TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(50.0)),
+            )
+            .unwrap();
+
+        let original_send_all_amount = original_details.sent - original_details.fees;
+        assert_eq!(details.sent, original_details.sent + 50_000);
+        assert_eq!(
+            details.received,
+            75_000 - original_send_all_amount - details.fees
+        );
+
+        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,
+            original_send_all_amount
+        );
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey != addr.script_pubkey())
+                .unwrap()
+                .value,
+            75_000 - original_send_all_amount - details.fees
+        );
+
+        assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature);
+    }
+
+    #[test]
+    fn test_bump_fee_add_input_change_dust() {
+        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::from_addressees(vec![(addr.clone(), 45_000)]).enable_rbf())
+            .unwrap();
+        let mut tx = psbt.extract_tx();
+        assert_eq!(tx.input.len(), 1);
+        assert_eq!(tx.output.len(), 2);
+        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_rate(FeeRate::from_sat_per_vb(140.0)),
+            )
+            .unwrap();
+
+        assert_eq!(original_details.received, 5_000 - original_details.fees);
+
+        assert_eq!(details.sent, original_details.sent + 25_000);
+        assert_eq!(details.fees, 30_000);
+        assert_eq!(details.received, 0);
+
+        let tx = &psbt.global.unsigned_tx;
+        assert_eq!(tx.input.len(), 2);
+        assert_eq!(tx.output.len(), 1);
+        assert_eq!(
+            tx.output
+                .iter()
+                .find(|txout| txout.script_pubkey == addr.script_pubkey())
+                .unwrap()
+                .value,
+            45_000
+        );
+
+        assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
+    }
+
+    #[test]
+    fn test_bump_fee_force_add_input() {
+        let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
+        let incoming_txid = wallet.database.borrow_mut().received_tx(
+            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::from_addressees(vec![(addr.clone(), 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_rate(FeeRate::from_sat_per_vb(5.0)),
+            )
+            .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_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature);
+    }
 }
diff --git a/src/wallet/rbf.rs b/src/wallet/rbf.rs
new file mode 100644 (file)
index 0000000..cd7327e
--- /dev/null
@@ -0,0 +1,103 @@
+use crate::database::Database;
+use crate::error::Error;
+use crate::types::*;
+
+/// Filters unspent utxos
+pub(super) fn filter_available<I: Iterator<Item = UTXO>, D: Database>(
+    database: &D,
+    iter: I,
+) -> Result<Vec<UTXO>, Error> {
+    Ok(iter
+        .map(|utxo| {
+            Ok(match database.get_tx(&utxo.outpoint.txid, true)? {
+                None => None,
+                Some(tx) if tx.height.is_none() => None,
+                Some(_) => Some(utxo),
+            })
+        })
+        .collect::<Result<Vec<_>, Error>>()?
+        .into_iter()
+        .filter_map(|x| x)
+        .collect())
+}
+
+#[cfg(test)]
+mod test {
+    use std::str::FromStr;
+
+    use bitcoin::{OutPoint, Transaction, TxIn, TxOut, Txid};
+
+    use super::*;
+    use crate::database::{BatchOperations, MemoryDatabase};
+
+    fn add_transaction(
+        database: &mut MemoryDatabase,
+        spend: Vec<OutPoint>,
+        outputs: Vec<u64>,
+    ) -> Txid {
+        let tx = Transaction {
+            version: 1,
+            lock_time: 0,
+            input: spend
+                .iter()
+                .cloned()
+                .map(|previous_output| TxIn {
+                    previous_output,
+                    ..Default::default()
+                })
+                .collect(),
+            output: outputs
+                .iter()
+                .cloned()
+                .map(|value| TxOut {
+                    value,
+                    ..Default::default()
+                })
+                .collect(),
+        };
+        let txid = tx.txid();
+
+        for input in &spend {
+            database.del_utxo(input).unwrap();
+        }
+        for vout in 0..outputs.len() {
+            database
+                .set_utxo(&UTXO {
+                    txout: tx.output[vout].clone(),
+                    outpoint: OutPoint {
+                        txid,
+                        vout: vout as u32,
+                    },
+                    is_internal: true,
+                })
+                .unwrap();
+        }
+        database
+            .set_tx(&TransactionDetails {
+                txid,
+                transaction: Some(tx),
+                height: None,
+                ..Default::default()
+            })
+            .unwrap();
+
+        txid
+    }
+
+    #[test]
+    fn test_filter_available() {
+        let mut database = MemoryDatabase::new();
+        add_transaction(
+            &mut database,
+            vec![OutPoint::from_str(
+                "aad194c72fd5cfd16d23da9462930ca91e35df1cfee05242b62f4034f50c3d41:5",
+            )
+            .unwrap()],
+            vec![50_000],
+        );
+
+        let filtered =
+            filter_available(&database, database.iter_utxos().unwrap().into_iter()).unwrap();
+        assert_eq!(filtered, &[]);
+    }
+}
index 465036f0e24e9a680e9839806f63bd29d0c01bd4..f6c287cf052af61f680439a51be94c65047f35c4 100644 (file)
@@ -14,7 +14,7 @@ impl IsDust for u64 {
     }
 }
 
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
 // Internally stored as satoshi/vbyte
 pub struct FeeRate(f32);
 
index 1a8d42a8558e6c3200b540a240ffcb229e8d54bc..b70a37ec1029502131dfd90cb71be1e1ea1cd1af 100644 (file)
@@ -363,6 +363,135 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt
                     wallet.sync(None).unwrap();
                     assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
                 }
+
+                #[test]
+                #[serial]
+                fn test_sync_bump_fee() {
+                    let (wallet, descriptors, mut test_client) = init_single_sig();
+                    let node_addr = test_client.get_node_address(None);
+
+                    test_client.receive(testutils! {
+                        @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
+                    });
+
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                    let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 5_000)]).enable_rbf()).unwrap();
+                    let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
+                    assert_eq!(wallet.get_balance().unwrap(), details.received);
+
+                    let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(2.1))).unwrap();
+                    let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
+                    assert_eq!(wallet.get_balance().unwrap(), new_details.received);
+
+                    assert!(new_details.fees > details.fees);
+                }
+
+                #[test]
+                #[serial]
+                fn test_sync_bump_fee_remove_change() {
+                    let (wallet, descriptors, mut test_client) = init_single_sig();
+                    let node_addr = test_client.get_node_address(None);
+
+                    test_client.receive(testutils! {
+                        @tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
+                    });
+
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 50_000);
+
+                    let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap();
+                    let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
+                    assert_eq!(wallet.get_balance().unwrap(), details.received);
+
+                    let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(5.0))).unwrap();
+
+                    let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 0);
+                    assert_eq!(new_details.received, 0);
+
+                    assert!(new_details.fees > details.fees);
+                }
+
+                #[test]
+                #[serial]
+                fn test_sync_bump_fee_add_input() {
+                    let (wallet, descriptors, mut test_client) = init_single_sig();
+                    let node_addr = test_client.get_node_address(None);
+
+                    test_client.receive(testutils! {
+                        @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
+                    });
+
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 75_000);
+
+                    let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap();
+                    let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
+                    assert_eq!(details.received, 1_000 - details.fees);
+
+                    let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(10.0))).unwrap();
+
+                    let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(new_details.sent, 75_000);
+                    assert_eq!(wallet.get_balance().unwrap(), new_details.received);
+                }
+
+                #[test]
+                #[serial]
+                fn test_sync_bump_fee_add_input_no_change() {
+                    let (wallet, descriptors, mut test_client) = init_single_sig();
+                    let node_addr = test_client.get_node_address(None);
+
+                    test_client.receive(testutils! {
+                        @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
+                    });
+
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 75_000);
+
+                    let (psbt, details) = wallet.create_tx(TxBuilder::from_addressees(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap();
+                    let (psbt, finalized) = wallet.sign(psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
+                    assert_eq!(details.received, 1_000 - details.fees);
+
+                    let (new_psbt, new_details) = wallet.bump_fee(&details.txid, TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(123.0))).unwrap();
+                    println!("{:#?}", new_details);
+
+                    let (new_psbt, finalized) = wallet.sign(new_psbt, None).unwrap();
+                    assert!(finalized, "Cannot finalize transaction");
+                    wallet.broadcast(new_psbt.extract_tx()).unwrap();
+                    wallet.sync(None).unwrap();
+                    assert_eq!(new_details.sent, 75_000);
+                    assert_eq!(wallet.get_balance().unwrap(), 0);
+                    assert_eq!(new_details.received, 0);
+                }
             }
 
                         };