allow to definie static fees for transactions Fixes #137
This commit is contained in:
parent
872d55cb4c
commit
27890cfcff
@ -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() {
|
||||
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() {
|
||||
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/*)");
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user