Implement RBF and add a few tests
This commit is contained in:
parent
8f8c393f6f
commit
c12aa3d327
92
src/cli.rs
92
src/cli.rs
@ -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:{}",
|
||||
|
@ -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()
|
||||
|
@ -14,6 +14,10 @@ pub enum Error {
|
||||
InvalidAddressNetwork(Address),
|
||||
UnknownUTXO,
|
||||
DifferentTransactions,
|
||||
TransactionNotFound,
|
||||
TransactionConfirmed,
|
||||
IrreplaceableTransaction,
|
||||
FeeRateTooLow(crate::wallet::utils::FeeRate),
|
||||
|
||||
ChecksumMismatch,
|
||||
DifferentDescriptorStructure,
|
||||
|
@ -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,67 +309,7 @@ where
|
||||
builder.ordering.modify_tx(&mut tx);
|
||||
|
||||
let txid = tx.txid();
|
||||
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 = prev_script_pubkeys.get(&input.previous_output).unwrap();
|
||||
|
||||
// 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)?;
|
||||
}
|
||||
}
|
||||
let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?;
|
||||
|
||||
let transaction_details = TransactionDetails {
|
||||
transaction: None,
|
||||
@ -383,6 +324,222 @@ where
|
||||
Ok((psbt, transaction_details))
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
pub fn sign(&self, mut psbt: PSBT, assume_height: Option<u32>) -> Result<(PSBT, bool), Error> {
|
||||
// this helps us doing our job later
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
103
src/wallet/rbf.rs
Normal file
103
src/wallet/rbf.rs
Normal file
@ -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, &[]);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user