Added add_foreign_utxo
To allow adding UTXOs external to the current wallet. The caller must provide the psbt::Input so we can create a coherent PSBT at the end and so this is compatible with existing PSBT workflows. Main changes: - There are now two types of UTXOs, local and foreign reflected in a `Utxo` enum. - `WeightedUtxo` now captures floating `(Utxo, usize)` tuples - `CoinSelectionResult` now has methods on it for distinguishing between local amount included vs total.
This commit is contained in:
parent
9a918f285d
commit
1fbfeabd77
@ -50,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
type to mark for a missing client.
|
type to mark for a missing client.
|
||||||
- Upgrade `tokio` to `1.0`.
|
- Upgrade `tokio` to `1.0`.
|
||||||
|
|
||||||
#### Transaction Creation Overhaul
|
### Transaction Creation Overhaul
|
||||||
|
|
||||||
The `TxBuilder` is now created from the `build_tx` or `build_fee_bump` functions on wallet and the
|
The `TxBuilder` is now created from the `build_tx` or `build_fee_bump` functions on wallet and the
|
||||||
final transaction is created by calling `finish` on the builder.
|
final transaction is created by calling `finish` on the builder.
|
||||||
@ -61,6 +61,13 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
- Added `Wallet::get_utxo`
|
- Added `Wallet::get_utxo`
|
||||||
- Added `Wallet::get_descriptor_for_keychain`
|
- Added `Wallet::get_descriptor_for_keychain`
|
||||||
|
|
||||||
|
### `add_foreign_utxo`
|
||||||
|
|
||||||
|
- Renamed `UTXO` to `LocalUtxo`
|
||||||
|
- Added `WeightedUtxo` to replace floating `(UTXO, usize)`.
|
||||||
|
- Added `Utxo` enum to incorporate both local utxos and foreign utxos
|
||||||
|
- Added `TxBuilder::add_foreign_utxo` which allows adding a utxo external to the wallet.
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
#### Changed
|
#### Changed
|
||||||
- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository
|
- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository
|
||||||
|
62
src/types.rs
62
src/types.rs
@ -25,7 +25,7 @@
|
|||||||
use std::convert::AsRef;
|
use std::convert::AsRef;
|
||||||
|
|
||||||
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
|
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
|
||||||
use bitcoin::hash_types::Txid;
|
use bitcoin::{hash_types::Txid, util::psbt};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -90,7 +90,9 @@ impl std::default::Default for FeeRate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wallet unspent output
|
/// An unspent output owned by a [`Wallet`].
|
||||||
|
///
|
||||||
|
/// [`Wallet`]: crate::Wallet
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LocalUtxo {
|
pub struct LocalUtxo {
|
||||||
/// Reference to a transaction output
|
/// Reference to a transaction output
|
||||||
@ -101,6 +103,62 @@ pub struct LocalUtxo {
|
|||||||
pub keychain: KeychainKind,
|
pub keychain: KeychainKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct WeightedUtxo {
|
||||||
|
/// The weight of the witness data or `scriptSig`.
|
||||||
|
/// This is used to properly maintain the feerate when doing coin selection.
|
||||||
|
pub satisfaction_weight: usize,
|
||||||
|
/// The UTXO
|
||||||
|
pub utxo: Utxo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
/// An unspent transaction output (UTXO).
|
||||||
|
pub enum Utxo {
|
||||||
|
/// A UTXO owned by the local wallet.
|
||||||
|
Local(LocalUtxo),
|
||||||
|
/// A UTXO owned by another wallet.
|
||||||
|
Foreign {
|
||||||
|
/// The location of the output.
|
||||||
|
outpoint: OutPoint,
|
||||||
|
/// The information about the input we require to add it to a PSBT.
|
||||||
|
// Box it to stop the type being too big.
|
||||||
|
psbt_input: Box<psbt::Input>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Utxo {
|
||||||
|
/// Get the location of the UTXO
|
||||||
|
pub fn outpoint(&self) -> OutPoint {
|
||||||
|
match &self {
|
||||||
|
Utxo::Local(local) => local.outpoint,
|
||||||
|
Utxo::Foreign { outpoint, .. } => *outpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `TxOut` of the UTXO
|
||||||
|
pub fn txout(&self) -> &TxOut {
|
||||||
|
match &self {
|
||||||
|
Utxo::Local(local) => &local.txout,
|
||||||
|
Utxo::Foreign {
|
||||||
|
outpoint,
|
||||||
|
psbt_input,
|
||||||
|
} => {
|
||||||
|
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||||
|
return &prev_tx.output[outpoint.vout as usize];
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(txout) = &psbt_input.witness_utxo {
|
||||||
|
return &txout;
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!("Foreign UTXOs will always have one of these set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A wallet transaction
|
/// A wallet transaction
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct TransactionDetails {
|
pub struct TransactionDetails {
|
||||||
|
@ -50,8 +50,8 @@
|
|||||||
//! fn coin_select(
|
//! fn coin_select(
|
||||||
//! &self,
|
//! &self,
|
||||||
//! database: &D,
|
//! database: &D,
|
||||||
//! required_utxos: Vec<(LocalUtxo, usize)>,
|
//! required_utxos: Vec<WeightedUtxo>,
|
||||||
//! optional_utxos: Vec<(LocalUtxo, usize)>,
|
//! optional_utxos: Vec<WeightedUtxo>,
|
||||||
//! fee_rate: FeeRate,
|
//! fee_rate: FeeRate,
|
||||||
//! amount_needed: u64,
|
//! amount_needed: u64,
|
||||||
//! fee_amount: f32,
|
//! fee_amount: f32,
|
||||||
@ -60,11 +60,10 @@
|
|||||||
//! let mut additional_weight = 0;
|
//! let mut additional_weight = 0;
|
||||||
//! let all_utxos_selected = required_utxos
|
//! let all_utxos_selected = required_utxos
|
||||||
//! .into_iter().chain(optional_utxos)
|
//! .into_iter().chain(optional_utxos)
|
||||||
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), (utxo, weight)| {
|
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), weighted_utxo| {
|
||||||
//! **selected_amount += utxo.txout.value;
|
//! **selected_amount += weighted_utxo.utxo.txout().value;
|
||||||
//! **additional_weight += TXIN_BASE_WEIGHT + weight;
|
//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
|
||||||
//!
|
//! Some(weighted_utxo.utxo)
|
||||||
//! Some(utxo)
|
|
||||||
//! })
|
//! })
|
||||||
//! .collect::<Vec<_>>();
|
//! .collect::<Vec<_>>();
|
||||||
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
|
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
|
||||||
@ -75,7 +74,6 @@
|
|||||||
//!
|
//!
|
||||||
//! Ok(CoinSelectionResult {
|
//! Ok(CoinSelectionResult {
|
||||||
//! selected: all_utxos_selected,
|
//! selected: all_utxos_selected,
|
||||||
//! selected_amount,
|
|
||||||
//! fee_amount: fee_amount + additional_fees,
|
//! fee_amount: fee_amount + additional_fees,
|
||||||
//! })
|
//! })
|
||||||
//! }
|
//! }
|
||||||
@ -97,9 +95,9 @@
|
|||||||
//! # Ok::<(), bdk::Error>(())
|
//! # Ok::<(), bdk::Error>(())
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::types::FeeRate;
|
||||||
use crate::error::Error;
|
use crate::{database::Database, WeightedUtxo};
|
||||||
use crate::types::{FeeRate, LocalUtxo};
|
use crate::{error::Error, Utxo};
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@ -122,13 +120,29 @@ pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CoinSelectionResult {
|
pub struct CoinSelectionResult {
|
||||||
/// List of outputs selected for use as inputs
|
/// List of outputs selected for use as inputs
|
||||||
pub selected: Vec<LocalUtxo>,
|
pub selected: Vec<Utxo>,
|
||||||
/// Sum of the selected inputs' value
|
|
||||||
pub selected_amount: u64,
|
|
||||||
/// Total fee amount in satoshi
|
/// Total fee amount in satoshi
|
||||||
pub fee_amount: f32,
|
pub fee_amount: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CoinSelectionResult {
|
||||||
|
/// The total value of the inputs selected.
|
||||||
|
pub fn selected_amount(&self) -> u64 {
|
||||||
|
self.selected.iter().map(|u| u.txout().value).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The total value of the inputs selected from the local wallet.
|
||||||
|
pub fn local_selected_amount(&self) -> u64 {
|
||||||
|
self.selected
|
||||||
|
.iter()
|
||||||
|
.filter_map(|u| match u {
|
||||||
|
Utxo::Local(_) => Some(u.txout().value),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait for generalized coin selection algorithms
|
/// Trait for generalized coin selection algorithms
|
||||||
///
|
///
|
||||||
/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
|
/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
|
||||||
@ -151,8 +165,8 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
|
|||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
database: &D,
|
database: &D,
|
||||||
required_utxos: Vec<(LocalUtxo, usize)>,
|
required_utxos: Vec<WeightedUtxo>,
|
||||||
optional_utxos: Vec<(LocalUtxo, usize)>,
|
optional_utxos: Vec<WeightedUtxo>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
fee_amount: f32,
|
fee_amount: f32,
|
||||||
@ -170,8 +184,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
_database: &D,
|
_database: &D,
|
||||||
required_utxos: Vec<(LocalUtxo, usize)>,
|
required_utxos: Vec<WeightedUtxo>,
|
||||||
mut optional_utxos: Vec<(LocalUtxo, usize)>,
|
mut optional_utxos: Vec<WeightedUtxo>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
mut fee_amount: f32,
|
mut fee_amount: f32,
|
||||||
@ -188,7 +202,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
|
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
|
||||||
// initially smallest to largest, before being reversed with `.rev()`.
|
// initially smallest to largest, before being reversed with `.rev()`.
|
||||||
let utxos = {
|
let utxos = {
|
||||||
optional_utxos.sort_unstable_by_key(|(utxo, _)| utxo.txout.value);
|
optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
|
||||||
required_utxos
|
required_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|utxo| (true, utxo))
|
.map(|utxo| (true, utxo))
|
||||||
@ -201,18 +215,19 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
let selected = utxos
|
let selected = utxos
|
||||||
.scan(
|
.scan(
|
||||||
(&mut selected_amount, &mut fee_amount),
|
(&mut selected_amount, &mut fee_amount),
|
||||||
|(selected_amount, fee_amount), (must_use, (utxo, weight))| {
|
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||||
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
|
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
|
||||||
**fee_amount += calc_fee_bytes(TXIN_BASE_WEIGHT + weight);
|
**fee_amount +=
|
||||||
**selected_amount += utxo.txout.value;
|
calc_fee_bytes(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
|
||||||
|
**selected_amount += weighted_utxo.utxo.txout().value;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Selected {}, updated fee_amount = `{}`",
|
"Selected {}, updated fee_amount = `{}`",
|
||||||
utxo.outpoint,
|
weighted_utxo.utxo.outpoint(),
|
||||||
fee_amount
|
fee_amount
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(utxo)
|
Some(weighted_utxo.utxo)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -231,7 +246,6 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
Ok(CoinSelectionResult {
|
Ok(CoinSelectionResult {
|
||||||
selected,
|
selected,
|
||||||
fee_amount,
|
fee_amount,
|
||||||
selected_amount,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,9 +253,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
// Adds fee information to an UTXO.
|
// Adds fee information to an UTXO.
|
||||||
struct OutputGroup {
|
struct OutputGroup {
|
||||||
utxo: LocalUtxo,
|
weighted_utxo: WeightedUtxo,
|
||||||
// weight needed to satisfy the UTXO, as described in `Descriptor::max_satisfaction_weight`
|
|
||||||
satisfaction_weight: usize,
|
|
||||||
// Amount of fees for spending a certain utxo, calculated using a certain FeeRate
|
// Amount of fees for spending a certain utxo, calculated using a certain FeeRate
|
||||||
fee: f32,
|
fee: f32,
|
||||||
// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
|
// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
|
||||||
@ -249,12 +261,12 @@ struct OutputGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OutputGroup {
|
impl OutputGroup {
|
||||||
fn new(utxo: LocalUtxo, satisfaction_weight: usize, fee_rate: FeeRate) -> Self {
|
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||||
let fee = (TXIN_BASE_WEIGHT + satisfaction_weight) as f32 / 4.0 * fee_rate.as_sat_vb();
|
let fee = (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as f32 / 4.0
|
||||||
let effective_value = utxo.txout.value as i64 - fee.ceil() as i64;
|
* fee_rate.as_sat_vb();
|
||||||
|
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
|
||||||
OutputGroup {
|
OutputGroup {
|
||||||
utxo,
|
weighted_utxo,
|
||||||
satisfaction_weight,
|
|
||||||
effective_value,
|
effective_value,
|
||||||
fee,
|
fee,
|
||||||
}
|
}
|
||||||
@ -291,8 +303,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
fn coin_select(
|
fn coin_select(
|
||||||
&self,
|
&self,
|
||||||
_database: &D,
|
_database: &D,
|
||||||
required_utxos: Vec<(LocalUtxo, usize)>,
|
required_utxos: Vec<WeightedUtxo>,
|
||||||
optional_utxos: Vec<(LocalUtxo, usize)>,
|
optional_utxos: Vec<WeightedUtxo>,
|
||||||
fee_rate: FeeRate,
|
fee_rate: FeeRate,
|
||||||
amount_needed: u64,
|
amount_needed: u64,
|
||||||
fee_amount: f32,
|
fee_amount: f32,
|
||||||
@ -300,7 +312,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
// Mapping every (UTXO, usize) to an output group
|
// Mapping every (UTXO, usize) to an output group
|
||||||
let required_utxos: Vec<OutputGroup> = required_utxos
|
let required_utxos: Vec<OutputGroup> = required_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Mapping every (UTXO, usize) to an output group.
|
// Mapping every (UTXO, usize) to an output group.
|
||||||
@ -308,7 +320,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
|
|||||||
// adding them is more than their value
|
// adding them is more than their value
|
||||||
let optional_utxos: Vec<OutputGroup> = optional_utxos
|
let optional_utxos: Vec<OutputGroup> = optional_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.filter(|u| u.effective_value > 0)
|
.filter(|u| u.effective_value > 0)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -507,14 +519,12 @@ impl BranchAndBoundCoinSelection {
|
|||||||
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>();
|
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>();
|
||||||
let selected = selected_utxos
|
let selected = selected_utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| u.utxo)
|
.map(|u| u.weighted_utxo.utxo)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let selected_amount = selected.iter().map(|u| u.txout.value).sum();
|
|
||||||
|
|
||||||
CoinSelectionResult {
|
CoinSelectionResult {
|
||||||
selected,
|
selected,
|
||||||
fee_amount,
|
fee_amount,
|
||||||
selected_amount,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,10 +545,11 @@ mod test {
|
|||||||
|
|
||||||
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
|
||||||
|
|
||||||
fn get_test_utxos() -> Vec<(LocalUtxo, usize)> {
|
fn get_test_utxos() -> Vec<WeightedUtxo> {
|
||||||
vec![
|
vec![
|
||||||
(
|
WeightedUtxo {
|
||||||
LocalUtxo {
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
outpoint: OutPoint::from_str(
|
outpoint: OutPoint::from_str(
|
||||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||||
)
|
)
|
||||||
@ -548,11 +559,11 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
},
|
}),
|
||||||
P2WPKH_WITNESS_SIZE,
|
},
|
||||||
),
|
WeightedUtxo {
|
||||||
(
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
LocalUtxo {
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
outpoint: OutPoint::from_str(
|
outpoint: OutPoint::from_str(
|
||||||
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
|
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
|
||||||
)
|
)
|
||||||
@ -562,17 +573,17 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::Internal,
|
keychain: KeychainKind::Internal,
|
||||||
},
|
}),
|
||||||
P2WPKH_WITNESS_SIZE,
|
},
|
||||||
),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<(LocalUtxo, usize)> {
|
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
for _ in 0..utxos_number {
|
for _ in 0..utxos_number {
|
||||||
res.push((
|
res.push(WeightedUtxo {
|
||||||
LocalUtxo {
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
outpoint: OutPoint::from_str(
|
outpoint: OutPoint::from_str(
|
||||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||||
)
|
)
|
||||||
@ -582,16 +593,16 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
},
|
}),
|
||||||
P2WPKH_WITNESS_SIZE,
|
});
|
||||||
));
|
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<(LocalUtxo, usize)> {
|
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||||
let utxo = (
|
let utxo = WeightedUtxo {
|
||||||
LocalUtxo {
|
satisfaction_weight: P2WPKH_WITNESS_SIZE,
|
||||||
|
utxo: Utxo::Local(LocalUtxo {
|
||||||
outpoint: OutPoint::from_str(
|
outpoint: OutPoint::from_str(
|
||||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
|
||||||
)
|
)
|
||||||
@ -601,18 +612,18 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
},
|
}),
|
||||||
P2WPKH_WITNESS_SIZE,
|
};
|
||||||
);
|
|
||||||
vec![utxo; utxos_number]
|
vec![utxo; utxos_number]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<(LocalUtxo, usize)>) -> u64 {
|
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
|
||||||
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2);
|
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2);
|
||||||
utxos.shuffle(&mut rng);
|
utxos.shuffle(&mut rng);
|
||||||
utxos[..utxos_picked_len]
|
utxos[..utxos_picked_len]
|
||||||
.iter()
|
.iter()
|
||||||
.fold(0, |acc, x| acc + x.0.txout.value)
|
.map(|u| u.utxo.txout().value)
|
||||||
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -632,7 +643,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 2);
|
assert_eq!(result.selected.len(), 2);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_000);
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 186.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,7 +664,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 2);
|
assert_eq!(result.selected.len(), 2);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_000);
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 186.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,7 +685,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 1);
|
assert_eq!(result.selected.len(), 1);
|
||||||
assert_eq!(result.selected_amount, 200_000);
|
assert_eq!(result.selected_amount(), 200_000);
|
||||||
assert_eq!(result.fee_amount, 118.0);
|
assert_eq!(result.fee_amount, 118.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,7 +745,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 3);
|
assert_eq!(result.selected.len(), 3);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_000);
|
||||||
assert_eq!(result.fee_amount, 254.0);
|
assert_eq!(result.fee_amount, 254.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -755,7 +766,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 2);
|
assert_eq!(result.selected.len(), 2);
|
||||||
assert_eq!(result.selected_amount, 300_000);
|
assert_eq!(result.selected_amount(), 300_000);
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 186.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -812,7 +823,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.selected.len(), 1);
|
assert_eq!(result.selected.len(), 1);
|
||||||
assert_eq!(result.selected_amount, 100_000);
|
assert_eq!(result.selected_amount(), 100_000);
|
||||||
let input_size = (TXIN_BASE_WEIGHT as f32) / 4.0 + P2WPKH_WITNESS_SIZE as f32 / 4.0;
|
let input_size = (TXIN_BASE_WEIGHT as f32) / 4.0 + P2WPKH_WITNESS_SIZE as f32 / 4.0;
|
||||||
let epsilon = 0.5;
|
let epsilon = 0.5;
|
||||||
assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon);
|
assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon);
|
||||||
@ -837,7 +848,7 @@ mod test {
|
|||||||
0.0,
|
0.0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.selected_amount, target_amount);
|
assert_eq!(result.selected_amount(), target_amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -847,7 +858,7 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||||
let utxos: Vec<OutputGroup> = get_test_utxos()
|
let utxos: Vec<OutputGroup> = get_test_utxos()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_available_value = utxos
|
let curr_available_value = utxos
|
||||||
@ -875,7 +886,7 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
let fee_rate = FeeRate::from_sat_per_vb(10.0);
|
||||||
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_available_value = utxos
|
let curr_available_value = utxos
|
||||||
@ -908,7 +919,7 @@ mod test {
|
|||||||
|
|
||||||
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_value = 0;
|
let curr_value = 0;
|
||||||
@ -933,7 +944,7 @@ mod test {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.fee_amount, 186.0);
|
assert_eq!(result.fee_amount, 186.0);
|
||||||
assert_eq!(result.selected_amount, 100_000);
|
assert_eq!(result.selected_amount(), 100_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: bnb() function should be optimized, and this test should be done with more utxos
|
// TODO: bnb() function should be optimized, and this test should be done with more utxos
|
||||||
@ -946,7 +957,7 @@ mod test {
|
|||||||
for _ in 0..200 {
|
for _ in 0..200 {
|
||||||
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let curr_value = 0;
|
let curr_value = 0;
|
||||||
@ -969,7 +980,7 @@ mod test {
|
|||||||
0.0,
|
0.0,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result.selected_amount, target_amount);
|
assert_eq!(result.selected_amount(), target_amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -983,7 +994,7 @@ mod test {
|
|||||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||||
let utxos: Vec<OutputGroup> = utxos
|
let utxos: Vec<OutputGroup> = utxos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| OutputGroup::new(u.0, u.1, fee_rate))
|
.map(|u| OutputGroup::new(u, fee_rate))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let result = BranchAndBoundCoinSelection::default().single_random_draw(
|
let result = BranchAndBoundCoinSelection::default().single_random_draw(
|
||||||
@ -994,7 +1005,7 @@ mod test {
|
|||||||
50.0,
|
50.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(result.selected_amount > target_amount);
|
assert!(result.selected_amount() > target_amount);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.fee_amount,
|
result.fee_amount,
|
||||||
50.0 + result.selected.len() as f32 * 68.0
|
50.0 + result.selected.len() as f32 * 68.0
|
||||||
|
@ -513,11 +513,7 @@ where
|
|||||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let coin_selection::CoinSelectionResult {
|
let coin_selection = coin_selection.coin_select(
|
||||||
selected,
|
|
||||||
selected_amount,
|
|
||||||
mut fee_amount,
|
|
||||||
} = coin_selection.coin_select(
|
|
||||||
self.database.borrow().deref(),
|
self.database.borrow().deref(),
|
||||||
required_utxos,
|
required_utxos,
|
||||||
optional_utxos,
|
optional_utxos,
|
||||||
@ -525,10 +521,13 @@ where
|
|||||||
outgoing,
|
outgoing,
|
||||||
fee_amount,
|
fee_amount,
|
||||||
)?;
|
)?;
|
||||||
tx.input = selected
|
let mut fee_amount = coin_selection.fee_amount;
|
||||||
|
|
||||||
|
tx.input = coin_selection
|
||||||
|
.selected
|
||||||
.iter()
|
.iter()
|
||||||
.map(|u| bitcoin::TxIn {
|
.map(|u| bitcoin::TxIn {
|
||||||
previous_output: u.outpoint,
|
previous_output: u.outpoint(),
|
||||||
script_sig: Script::default(),
|
script_sig: Script::default(),
|
||||||
sequence: n_sequence,
|
sequence: n_sequence,
|
||||||
witness: vec![],
|
witness: vec![],
|
||||||
@ -550,9 +549,8 @@ where
|
|||||||
Some(change_output)
|
Some(change_output)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut fee_amount = fee_amount.ceil() as u64;
|
let mut fee_amount = fee_amount.ceil() as u64;
|
||||||
let change_val = (selected_amount - outgoing).saturating_sub(fee_amount);
|
let change_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
||||||
|
|
||||||
match change_output {
|
match change_output {
|
||||||
None if change_val.is_dust() => {
|
None if change_val.is_dust() => {
|
||||||
@ -588,14 +586,15 @@ where
|
|||||||
params.ordering.sort_tx(&mut tx);
|
params.ordering.sort_tx(&mut tx);
|
||||||
|
|
||||||
let txid = tx.txid();
|
let txid = tx.txid();
|
||||||
let psbt = self.complete_transaction(tx, selected, params)?;
|
let sent = coin_selection.local_selected_amount();
|
||||||
|
let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
|
||||||
|
|
||||||
let transaction_details = TransactionDetails {
|
let transaction_details = TransactionDetails {
|
||||||
transaction: None,
|
transaction: None,
|
||||||
txid,
|
txid,
|
||||||
timestamp: time::get_timestamp(),
|
timestamp: time::get_timestamp(),
|
||||||
received,
|
received,
|
||||||
sent: selected_amount,
|
sent,
|
||||||
fees: fee_amount,
|
fees: fee_amount,
|
||||||
height: None,
|
height: None,
|
||||||
};
|
};
|
||||||
@ -705,7 +704,10 @@ where
|
|||||||
keychain,
|
keychain,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((utxo, weight))
|
Ok(WeightedUtxo {
|
||||||
|
satisfaction_weight: weight,
|
||||||
|
utxo: Utxo::Local(utxo),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
@ -1039,18 +1041,18 @@ where
|
|||||||
&self,
|
&self,
|
||||||
change_policy: tx_builder::ChangeSpendPolicy,
|
change_policy: tx_builder::ChangeSpendPolicy,
|
||||||
unspendable: &HashSet<OutPoint>,
|
unspendable: &HashSet<OutPoint>,
|
||||||
manually_selected: Vec<(LocalUtxo, usize)>,
|
manually_selected: Vec<WeightedUtxo>,
|
||||||
must_use_all_available: bool,
|
must_use_all_available: bool,
|
||||||
manual_only: bool,
|
manual_only: bool,
|
||||||
must_only_use_confirmed_tx: bool,
|
must_only_use_confirmed_tx: bool,
|
||||||
) -> Result<(Vec<(LocalUtxo, usize)>, Vec<(LocalUtxo, usize)>), Error> {
|
) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
|
||||||
// must_spend <- manually selected utxos
|
// must_spend <- manually selected utxos
|
||||||
// may_spend <- all other available utxos
|
// may_spend <- all other available utxos
|
||||||
let mut may_spend = self.get_available_utxos()?;
|
let mut may_spend = self.get_available_utxos()?;
|
||||||
may_spend.retain(|may_spend| {
|
may_spend.retain(|may_spend| {
|
||||||
manually_selected
|
manually_selected
|
||||||
.iter()
|
.iter()
|
||||||
.find(|manually_selected| manually_selected.0.outpoint == may_spend.0.outpoint)
|
.find(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
|
||||||
.is_none()
|
.is_none()
|
||||||
});
|
});
|
||||||
let mut must_spend = manually_selected;
|
let mut must_spend = manually_selected;
|
||||||
@ -1088,6 +1090,14 @@ where
|
|||||||
retain
|
retain
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut may_spend = may_spend
|
||||||
|
.into_iter()
|
||||||
|
.map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
|
||||||
|
satisfaction_weight,
|
||||||
|
utxo: Utxo::Local(local_utxo),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
if must_use_all_available {
|
if must_use_all_available {
|
||||||
must_spend.append(&mut may_spend);
|
must_spend.append(&mut may_spend);
|
||||||
}
|
}
|
||||||
@ -1098,7 +1108,7 @@ where
|
|||||||
fn complete_transaction(
|
fn complete_transaction(
|
||||||
&self,
|
&self,
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
selected: Vec<LocalUtxo>,
|
selected: Vec<Utxo>,
|
||||||
params: TxParams,
|
params: TxParams,
|
||||||
) -> Result<PSBT, Error> {
|
) -> Result<PSBT, Error> {
|
||||||
use bitcoin::util::psbt::serialize::Serialize;
|
use bitcoin::util::psbt::serialize::Serialize;
|
||||||
@ -1131,9 +1141,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lookup_output = selected
|
let mut lookup_output = selected
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|utxo| (utxo.outpoint, utxo))
|
.map(|utxo| (utxo.outpoint(), utxo))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
// add metadata for the inputs
|
// add metadata for the inputs
|
||||||
@ -1142,7 +1152,7 @@ where
|
|||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(psbt.global.unsigned_tx.input.iter())
|
.zip(psbt.global.unsigned_tx.input.iter())
|
||||||
{
|
{
|
||||||
let utxo = match lookup_output.get(&input.previous_output) {
|
let utxo = match lookup_output.remove(&input.previous_output) {
|
||||||
Some(utxo) => utxo,
|
Some(utxo) => utxo,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
@ -1153,32 +1163,50 @@ where
|
|||||||
psbt_input.sighash_type = Some(sighash_type);
|
psbt_input.sighash_type = Some(sighash_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find the prev_script in our db to figure out if this is internal or external,
|
match utxo {
|
||||||
// and the derivation index
|
Utxo::Local(utxo) => {
|
||||||
let (keychain, child) = match self
|
// Try to find the prev_script in our db to figure out if this is internal or external,
|
||||||
.database
|
// and the derivation index
|
||||||
.borrow()
|
let (keychain, child) = match self
|
||||||
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
|
.database
|
||||||
{
|
.borrow()
|
||||||
Some(x) => x,
|
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
|
||||||
None => continue,
|
{
|
||||||
};
|
Some(x) => x,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
let (desc, _) = self._get_descriptor_for_keychain(keychain);
|
let desc = self.get_descriptor_for_keychain(keychain);
|
||||||
let derived_descriptor = desc.as_derived(child, &self.secp);
|
let derived_descriptor = desc.as_derived(child, &self.secp);
|
||||||
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?;
|
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?;
|
||||||
|
|
||||||
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
|
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
|
||||||
psbt_input.witness_script = derived_descriptor.psbt_witness_script();
|
psbt_input.witness_script = derived_descriptor.psbt_witness_script();
|
||||||
|
|
||||||
let prev_output = input.previous_output;
|
let prev_output = input.previous_output;
|
||||||
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
|
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
|
||||||
if desc.is_witness() {
|
if desc.is_witness() {
|
||||||
psbt_input.witness_utxo =
|
psbt_input.witness_utxo =
|
||||||
Some(prev_tx.output[prev_output.vout as usize].clone());
|
Some(prev_tx.output[prev_output.vout as usize].clone());
|
||||||
|
}
|
||||||
|
if !desc.is_witness() || params.force_non_witness_utxo {
|
||||||
|
psbt_input.non_witness_utxo = Some(prev_tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !desc.is_witness() || params.force_non_witness_utxo {
|
Utxo::Foreign {
|
||||||
psbt_input.non_witness_utxo = Some(prev_tx);
|
psbt_input: foreign_psbt_input,
|
||||||
|
outpoint,
|
||||||
|
} => {
|
||||||
|
if params.force_non_witness_utxo
|
||||||
|
&& foreign_psbt_input.non_witness_utxo.is_none()
|
||||||
|
{
|
||||||
|
return Err(Error::Generic(format!(
|
||||||
|
"Missing non_witness_utxo on foreign utxo {}",
|
||||||
|
outpoint
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
*psbt_input = *foreign_psbt_input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1348,7 +1376,7 @@ where
|
|||||||
mod test {
|
mod test {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitcoin::Network;
|
use bitcoin::{util::psbt, Network};
|
||||||
|
|
||||||
use crate::database::memory::MemoryDatabase;
|
use crate::database::memory::MemoryDatabase;
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
@ -2237,6 +2265,182 @@ mod test {
|
|||||||
assert_eq!(psbt.global.unknown.get(&psbt_key), Some(&value_bytes));
|
assert_eq!(psbt.global.unknown.get(&psbt_key), Some(&value_bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_foreign_utxo() {
|
||||||
|
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let (wallet2, _, _) =
|
||||||
|
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||||
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
|
let utxo = wallet2.list_unspent().unwrap().remove(0);
|
||||||
|
let foreign_utxo_satisfaction = wallet2
|
||||||
|
.get_descriptor_for_keychain(KeychainKind::External)
|
||||||
|
.max_satisfaction_weight()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let psbt_input = psbt::Input {
|
||||||
|
witness_utxo: Some(utxo.txout.clone()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = wallet1.build_tx();
|
||||||
|
builder
|
||||||
|
.add_recipient(addr.script_pubkey(), 60_000)
|
||||||
|
.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
|
||||||
|
.unwrap();
|
||||||
|
let (psbt, details) = builder.finish().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
details.sent - details.received,
|
||||||
|
10_000 + details.fees,
|
||||||
|
"we should have only net spent ~10_000"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
psbt.global
|
||||||
|
.unsigned_tx
|
||||||
|
.input
|
||||||
|
.iter()
|
||||||
|
.find(|input| input.previous_output == utxo.outpoint)
|
||||||
|
.is_some(),
|
||||||
|
"foreign_utxo should be in there"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (psbt, finished) = wallet1.sign(psbt, None).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!finished,
|
||||||
|
"only one of the inputs should have been signed so far"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, finished) = wallet2.sign(psbt, None).unwrap();
|
||||||
|
assert!(finished, "all the inputs should have been signed now");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
|
||||||
|
fn test_add_foreign_utxo_invalid_psbt_input() {
|
||||||
|
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
let outpoint = wallet.list_unspent().unwrap()[0].outpoint;
|
||||||
|
let foreign_utxo_satisfaction = wallet
|
||||||
|
.get_descriptor_for_keychain(KeychainKind::External)
|
||||||
|
.max_satisfaction_weight()
|
||||||
|
.unwrap();
|
||||||
|
builder
|
||||||
|
.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
|
||||||
|
let (wallet1, _, txid1) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let (wallet2, _, txid2) =
|
||||||
|
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||||
|
|
||||||
|
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
|
||||||
|
let tx1 = wallet1
|
||||||
|
.database
|
||||||
|
.borrow()
|
||||||
|
.get_tx(&txid1, true)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.transaction
|
||||||
|
.unwrap();
|
||||||
|
let tx2 = wallet2
|
||||||
|
.database
|
||||||
|
.borrow()
|
||||||
|
.get_tx(&txid2, true)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.transaction
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let satisfaction_weight = wallet2
|
||||||
|
.get_descriptor_for_keychain(KeychainKind::External)
|
||||||
|
.max_satisfaction_weight()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let psbt_input1 = psbt::Input {
|
||||||
|
non_witness_utxo: Some(tx1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let psbt_input2 = psbt::Input {
|
||||||
|
non_witness_utxo: Some(tx2),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = wallet1.build_tx();
|
||||||
|
assert!(
|
||||||
|
builder
|
||||||
|
.add_foreign_utxo(utxo2.outpoint, psbt_input1, satisfaction_weight)
|
||||||
|
.is_err(),
|
||||||
|
"should fail when outpoint doesn't match psbt_input"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
builder
|
||||||
|
.add_foreign_utxo(utxo2.outpoint, psbt_input2, satisfaction_weight)
|
||||||
|
.is_ok(),
|
||||||
|
"shoulld be ok when outpoing does match psbt_input"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_foreign_utxo_force_non_witness_utxo() {
|
||||||
|
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
|
||||||
|
let (wallet2, _, txid2) =
|
||||||
|
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
|
||||||
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||||
|
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
|
||||||
|
|
||||||
|
let satisfaction_weight = wallet2
|
||||||
|
.get_descriptor_for_keychain(KeychainKind::External)
|
||||||
|
.max_satisfaction_weight()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut builder = wallet1.build_tx();
|
||||||
|
builder
|
||||||
|
.add_recipient(addr.script_pubkey(), 60_000)
|
||||||
|
.force_non_witness_utxo();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut builder = builder.clone();
|
||||||
|
let psbt_input = psbt::Input {
|
||||||
|
witness_utxo: Some(utxo2.txout.clone()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
builder
|
||||||
|
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
builder.finish().is_err(),
|
||||||
|
"psbt_input with witness_utxo should succeed with witness_utxo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut builder = builder.clone();
|
||||||
|
let tx2 = wallet2
|
||||||
|
.database
|
||||||
|
.borrow()
|
||||||
|
.get_tx(&txid2, true)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.transaction
|
||||||
|
.unwrap();
|
||||||
|
let psbt_input = psbt::Input {
|
||||||
|
non_witness_utxo: Some(tx2),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
builder
|
||||||
|
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
builder.finish().is_ok(),
|
||||||
|
"psbt_input with non_witness_utxo should succeed with force_non_witness_utxo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(
|
#[should_panic(
|
||||||
expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
|
expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
|
||||||
|
@ -54,15 +54,15 @@ use std::collections::HashSet;
|
|||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT;
|
use bitcoin::util::psbt::{self, PartiallySignedTransaction as PSBT};
|
||||||
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
use bitcoin::{OutPoint, Script, SigHashType, Transaction};
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorTrait;
|
use miniscript::descriptor::DescriptorTrait;
|
||||||
|
|
||||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||||
use crate::{database::BatchDatabase, Error, Wallet};
|
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{FeeRate, KeychainKind, LocalUtxo},
|
types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
|
||||||
TransactionDetails,
|
TransactionDetails,
|
||||||
};
|
};
|
||||||
/// Context in which the [`TxBuilder`] is valid
|
/// Context in which the [`TxBuilder`] is valid
|
||||||
@ -150,7 +150,7 @@ pub(crate) struct TxParams {
|
|||||||
pub(crate) fee_policy: Option<FeePolicy>,
|
pub(crate) fee_policy: Option<FeePolicy>,
|
||||||
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||||
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||||
pub(crate) utxos: Vec<(LocalUtxo, usize)>,
|
pub(crate) utxos: Vec<WeightedUtxo>,
|
||||||
pub(crate) unspendable: HashSet<OutPoint>,
|
pub(crate) unspendable: HashSet<OutPoint>,
|
||||||
pub(crate) manually_selected_only: bool,
|
pub(crate) manually_selected_only: bool,
|
||||||
pub(crate) sighash: Option<SigHashType>,
|
pub(crate) sighash: Option<SigHashType>,
|
||||||
@ -297,7 +297,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
|||||||
for utxo in utxos {
|
for utxo in utxos {
|
||||||
let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain);
|
let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||||
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
||||||
self.params.utxos.push((utxo, satisfaction_weight));
|
self.params.utxos.push(WeightedUtxo {
|
||||||
|
satisfaction_weight,
|
||||||
|
utxo: Utxo::Local(utxo),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(self)
|
Ok(self)
|
||||||
@ -311,6 +314,84 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
|
|||||||
self.add_utxos(&[outpoint])
|
self.add_utxos(&[outpoint])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a foreign UTXO i.e. A UTXO not owned by this wallet.
|
||||||
|
///
|
||||||
|
/// At a minimum to add a foreign UTXO we need:
|
||||||
|
///
|
||||||
|
/// 1. `outpoint`: To add it to the raw transaction.
|
||||||
|
/// 2. `psbt_input`: To know the value.
|
||||||
|
/// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation.
|
||||||
|
///
|
||||||
|
/// There are several security concerns about adding foregin UTXOs that application
|
||||||
|
/// developers should consider. First, how do you know the value of the input is correct? If a
|
||||||
|
/// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the
|
||||||
|
/// value by checking it against the transaction. If only a `wintess_utxo` is provided then this
|
||||||
|
/// method doesn't verify the value but just takes it as a given -- it is up to you to check
|
||||||
|
/// that whoever sent you the `input_psbt` was not lying!
|
||||||
|
///
|
||||||
|
/// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your
|
||||||
|
/// application it may be important that this be known precisely. If not, a malicious
|
||||||
|
/// counterparty may fool you into putting in a value that is too low, giving the transaction a
|
||||||
|
/// lower than expected feerate. They could also fool you into putting a value that is too high
|
||||||
|
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
|
||||||
|
/// of course check the real input weight matches the expected weight prior to broadcasting.
|
||||||
|
///
|
||||||
|
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
|
||||||
|
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
|
||||||
|
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
|
||||||
|
///
|
||||||
|
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This method returns errors in the following circumstances:
|
||||||
|
///
|
||||||
|
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
|
||||||
|
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
|
||||||
|
///
|
||||||
|
/// Note if you set [`force_non_witness_utxo`] any `psbt_input` you pass to this method must
|
||||||
|
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
|
||||||
|
///
|
||||||
|
/// [`force_non_witness_utxo`]: Self::force_non_witness_utxo
|
||||||
|
/// [`finish`]: Self::finish
|
||||||
|
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
||||||
|
pub fn add_foreign_utxo(
|
||||||
|
&mut self,
|
||||||
|
outpoint: OutPoint,
|
||||||
|
psbt_input: psbt::Input,
|
||||||
|
satisfaction_weight: usize,
|
||||||
|
) -> Result<&mut Self, Error> {
|
||||||
|
if psbt_input.witness_utxo.is_none() {
|
||||||
|
match psbt_input.non_witness_utxo.as_ref() {
|
||||||
|
Some(tx) => {
|
||||||
|
if tx.txid() != outpoint.txid {
|
||||||
|
return Err(Error::Generic(
|
||||||
|
"Foreign utxo outpoint does not match PSBT input".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if tx.output.len() <= outpoint.vout as usize {
|
||||||
|
return Err(Error::InvalidOutpoint(outpoint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(Error::Generic(
|
||||||
|
"Foreign utxo missing witness_utxo or non_witness_utxo".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.params.utxos.push(WeightedUtxo {
|
||||||
|
satisfaction_weight,
|
||||||
|
utxo: Utxo::Foreign {
|
||||||
|
outpoint,
|
||||||
|
psbt_input: Box::new(psbt_input),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
/// Only spend utxos added by [`add_utxo`].
|
/// Only spend utxos added by [`add_utxo`].
|
||||||
///
|
///
|
||||||
/// The wallet will **not** add additional utxos to the transaction even if they are needed to
|
/// The wallet will **not** add additional utxos to the transaction even if they are needed to
|
||||||
|
Loading…
x
Reference in New Issue
Block a user