[wallet] Make coin_select take may/must use utxo lists
so that in the future you can add a UTXO that you *must* spend and let the coin selection fill in the rest. This partially addresses #121
This commit is contained in:
parent
64b4cfe308
commit
17f7294c8e
@ -51,24 +51,25 @@
|
|||||||
//! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
|
//! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
|
||||||
//! fn coin_select(
|
//! fn coin_select(
|
||||||
//! &self,
|
//! &self,
|
||||||
//! utxos: Vec<UTXO>,
|
//! must_use_utxos: Vec<UTXO>,
|
||||||
//! _use_all_utxos: bool,
|
//! may_use_utxos: Vec<UTXO>,
|
||||||
//! fee_rate: FeeRate,
|
//! fee_rate: FeeRate,
|
||||||
//! amount_needed: u64,
|
//! amount_needed: u64,
|
||||||
//! input_witness_weight: usize,
|
//! input_witness_weight: usize,
|
||||||
//! fee_amount: f32,
|
//! fee_amount: f32,
|
||||||
//! ) -> Result<CoinSelectionResult, bdk::Error> {
|
//! ) -> Result<CoinSelectionResult, bdk::Error> {
|
||||||
//! let selected_amount = utxos.iter().fold(0, |acc, utxo| acc + utxo.txout.value);
|
//! let mut selected_amount = 0;
|
||||||
//! let all_utxos_selected = utxos
|
//! let all_utxos_selected = must_use_utxos
|
||||||
//! .into_iter()
|
//! .into_iter().chain(may_use_utxos)
|
||||||
//! .map(|utxo| {
|
//! .scan(&mut selected_amount, |selected_amount, utxo| {
|
||||||
//! (
|
//! **selected_amount += utxo.txout.value;
|
||||||
|
//! Some((
|
||||||
//! TxIn {
|
//! TxIn {
|
||||||
//! previous_output: utxo.outpoint,
|
//! previous_output: utxo.outpoint,
|
||||||
//! ..Default::default()
|
//! ..Default::default()
|
||||||
//! },
|
//! },
|
||||||
//! utxo.txout.script_pubkey,
|
//! utxo.txout.script_pubkey,
|
||||||
//! )
|
//! ))
|
||||||
//! })
|
//! })
|
||||||
//! .collect::<Vec<_>>();
|
//! .collect::<Vec<_>>();
|
||||||
//! let additional_weight = all_utxos_selected.iter().fold(0, |acc, (txin, _)| {
|
//! let additional_weight = all_utxos_selected.iter().fold(0, |acc, (txin, _)| {
|
||||||
@ -132,16 +133,16 @@ pub struct CoinSelectionResult {
|
|||||||
pub trait CoinSelectionAlgorithm: std::fmt::Debug {
|
pub trait CoinSelectionAlgorithm: std::fmt::Debug {
|
||||||
/// Perform the coin selection
|
/// Perform the coin selection
|
||||||
///
|
///
|
||||||
/// - `utxos`: the list of spendable UTXOs
|
/// - `must_use_utxos`: the utxos that must be spent regardless of `amount_needed`
|
||||||
/// - `use_all_utxos`: if true all utxos should be spent unconditionally
|
/// - `may_be_spent`: the utxos that may be spent to satisfy `amount_needed`
|
||||||
/// - `fee_rate`: fee rate to use
|
/// - `fee_rate`: fee rate to use
|
||||||
/// - `amount_needed`: the amount in satoshi to select
|
/// - `amount_needed`: the amount in satoshi to select
|
||||||
/// - `input_witness_weight`: the weight of an input's witness to keep into account for the fees
|
/// - `input_witness_weight`: the weight of an input's witness to keep into account for the fees
|
||||||
/// - `fee_amount`: the amount of fees in satoshi already accumulated from adding outputs
|
/// - `fee_amount`: the amount of fees in satoshi already accumulated from adding outputs
|
||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
utxos: Vec<UTXO>,
|
must_use_utxos: Vec<UTXO>,
|
||||||
use_all_utxos: bool,
|
may_use_utxos: Vec<UTXO>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
input_witness_weight: usize,
|
input_witness_weight: usize,
|
||||||
@ -159,14 +160,13 @@ pub struct DumbCoinSelection;
|
|||||||
impl CoinSelectionAlgorithm for DumbCoinSelection {
|
impl CoinSelectionAlgorithm for DumbCoinSelection {
|
||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
mut utxos: Vec<UTXO>,
|
must_use_utxos: Vec<UTXO>,
|
||||||
use_all_utxos: bool,
|
mut may_use_utxos: Vec<UTXO>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
outgoing_amount: u64,
|
outgoing_amount: u64,
|
||||||
input_witness_weight: usize,
|
input_witness_weight: usize,
|
||||||
mut fee_amount: f32,
|
mut fee_amount: f32,
|
||||||
) -> Result<CoinSelectionResult, Error> {
|
) -> Result<CoinSelectionResult, Error> {
|
||||||
let mut txin = Vec::new();
|
|
||||||
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@ -176,35 +176,51 @@ impl CoinSelectionAlgorithm for DumbCoinSelection {
|
|||||||
fee_rate
|
fee_rate
|
||||||
);
|
);
|
||||||
|
|
||||||
// sort so that we pick them starting from the larger.
|
// We put the "must_use" UTXOs first and make sure the "may_use" are sorted largest to smallest
|
||||||
utxos.sort_by(|a, b| a.txout.value.partial_cmp(&b.txout.value).unwrap());
|
let utxos = {
|
||||||
|
may_use_utxos.sort_by(|a, b| b.txout.value.partial_cmp(&a.txout.value).unwrap());
|
||||||
let mut selected_amount: u64 = 0;
|
must_use_utxos
|
||||||
while use_all_utxos || selected_amount < outgoing_amount + (fee_amount.ceil() as u64) {
|
.into_iter()
|
||||||
let utxo = match utxos.pop() {
|
.map(|utxo| (true, utxo))
|
||||||
Some(utxo) => utxo,
|
.chain(may_use_utxos.into_iter().map(|utxo| (false, utxo)))
|
||||||
None if selected_amount < outgoing_amount + (fee_amount.ceil() as u64) => {
|
|
||||||
return Err(Error::InsufficientFunds)
|
|
||||||
}
|
|
||||||
None if use_all_utxos => break,
|
|
||||||
None => return Err(Error::InsufficientFunds),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep including inputs until we've got enough.
|
||||||
|
// Store the total input value in selected_amount and the total fee being paid in fee_amount
|
||||||
|
let mut selected_amount = 0;
|
||||||
|
let txin = utxos
|
||||||
|
.scan(
|
||||||
|
(&mut selected_amount, &mut fee_amount),
|
||||||
|
|(selected_amount, fee_amount), (must_use, utxo)| {
|
||||||
|
if must_use || **selected_amount < outgoing_amount + (fee_amount.ceil() as u64)
|
||||||
|
{
|
||||||
let new_in = TxIn {
|
let new_in = TxIn {
|
||||||
previous_output: utxo.outpoint,
|
previous_output: utxo.outpoint,
|
||||||
script_sig: Script::default(),
|
script_sig: Script::default(),
|
||||||
sequence: 0, // Let the caller choose the right nSequence
|
sequence: 0, // Let the caller choose the right nSequence
|
||||||
witness: vec![],
|
witness: vec![],
|
||||||
};
|
};
|
||||||
fee_amount += calc_fee_bytes(serialize(&new_in).len() * 4 + input_witness_weight);
|
|
||||||
|
**fee_amount +=
|
||||||
|
calc_fee_bytes(serialize(&new_in).len() * 4 + input_witness_weight);
|
||||||
|
**selected_amount += utxo.txout.value;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Selected {}, updated fee_amount = `{}`",
|
"Selected {}, updated fee_amount = `{}`",
|
||||||
new_in.previous_output,
|
new_in.previous_output,
|
||||||
fee_amount
|
fee_amount
|
||||||
);
|
);
|
||||||
|
|
||||||
txin.push((new_in, utxo.txout.script_pubkey));
|
Some((new_in, utxo.txout.script_pubkey))
|
||||||
selected_amount += utxo.txout.value;
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if selected_amount < outgoing_amount + (fee_amount.ceil() as u64) {
|
||||||
|
return Err(Error::InsufficientFunds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(CoinSelectionResult {
|
Ok(CoinSelectionResult {
|
||||||
@ -259,8 +275,8 @@ mod test {
|
|||||||
|
|
||||||
let result = DumbCoinSelection
|
let result = DumbCoinSelection
|
||||||
.coin_select(
|
.coin_select(
|
||||||
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
false,
|
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
250_000,
|
250_000,
|
||||||
P2WPKH_WITNESS_SIZE,
|
P2WPKH_WITNESS_SIZE,
|
||||||
@ -280,7 +296,7 @@ mod test {
|
|||||||
let result = DumbCoinSelection
|
let result = DumbCoinSelection
|
||||||
.coin_select(
|
.coin_select(
|
||||||
utxos,
|
utxos,
|
||||||
true,
|
vec![],
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
20_000,
|
20_000,
|
||||||
P2WPKH_WITNESS_SIZE,
|
P2WPKH_WITNESS_SIZE,
|
||||||
@ -299,8 +315,8 @@ mod test {
|
|||||||
|
|
||||||
let result = DumbCoinSelection
|
let result = DumbCoinSelection
|
||||||
.coin_select(
|
.coin_select(
|
||||||
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
false,
|
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
20_000,
|
20_000,
|
||||||
P2WPKH_WITNESS_SIZE,
|
P2WPKH_WITNESS_SIZE,
|
||||||
@ -320,8 +336,8 @@ mod test {
|
|||||||
|
|
||||||
DumbCoinSelection
|
DumbCoinSelection
|
||||||
.coin_select(
|
.coin_select(
|
||||||
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
false,
|
|
||||||
FeeRate::from_sat_per_vb(1.0),
|
FeeRate::from_sat_per_vb(1.0),
|
||||||
500_000,
|
500_000,
|
||||||
P2WPKH_WITNESS_SIZE,
|
P2WPKH_WITNESS_SIZE,
|
||||||
@ -337,8 +353,8 @@ mod test {
|
|||||||
|
|
||||||
DumbCoinSelection
|
DumbCoinSelection
|
||||||
.coin_select(
|
.coin_select(
|
||||||
|
vec![],
|
||||||
utxos,
|
utxos,
|
||||||
false,
|
|
||||||
FeeRate::from_sat_per_vb(1000.0),
|
FeeRate::from_sat_per_vb(1000.0),
|
||||||
250_000,
|
250_000,
|
||||||
P2WPKH_WITNESS_SIZE,
|
P2WPKH_WITNESS_SIZE,
|
||||||
|
@ -359,13 +359,18 @@ where
|
|||||||
&builder.unspendable,
|
&builder.unspendable,
|
||||||
builder.send_all,
|
builder.send_all,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let (must_use_utxos, may_use_utxos) = match use_all_utxos {
|
||||||
|
true => (available_utxos, vec![]),
|
||||||
|
false => (vec![], available_utxos),
|
||||||
|
};
|
||||||
let coin_selection::CoinSelectionResult {
|
let coin_selection::CoinSelectionResult {
|
||||||
txin,
|
txin,
|
||||||
selected_amount,
|
selected_amount,
|
||||||
mut fee_amount,
|
mut fee_amount,
|
||||||
} = builder.coin_selection.coin_select(
|
} = builder.coin_selection.coin_select(
|
||||||
available_utxos,
|
must_use_utxos,
|
||||||
use_all_utxos,
|
may_use_utxos,
|
||||||
fee_rate,
|
fee_rate,
|
||||||
outgoing,
|
outgoing,
|
||||||
input_witness_weight,
|
input_witness_weight,
|
||||||
@ -597,13 +602,19 @@ where
|
|||||||
self.database.borrow().deref(),
|
self.database.borrow().deref(),
|
||||||
available_utxos.into_iter(),
|
available_utxos.into_iter(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let (must_use_utxos, may_use_utxos) = match use_all_utxos {
|
||||||
|
true => (available_utxos, vec![]),
|
||||||
|
false => (vec![], available_utxos),
|
||||||
|
};
|
||||||
|
|
||||||
let coin_selection::CoinSelectionResult {
|
let coin_selection::CoinSelectionResult {
|
||||||
txin,
|
txin,
|
||||||
selected_amount,
|
selected_amount,
|
||||||
fee_amount,
|
fee_amount,
|
||||||
} = builder.coin_selection.coin_select(
|
} = builder.coin_selection.coin_select(
|
||||||
available_utxos,
|
must_use_utxos,
|
||||||
use_all_utxos,
|
may_use_utxos,
|
||||||
new_feerate,
|
new_feerate,
|
||||||
fee_difference.saturating_sub(removed_change_output.value),
|
fee_difference.saturating_sub(removed_change_output.value),
|
||||||
input_witness_weight,
|
input_witness_weight,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user