use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::consensus::encode::serialize;
use bitcoin::psbt;
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{
absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid,
Weight, Witness,
};
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
#[allow(unused_imports)]
use log::{debug, error, info, trace};
pub mod coin_selection;
pub mod export;
pub mod signer;
pub mod time;
pub mod tx_builder;
pub(crate) mod utils;
#[cfg(feature = "verify")]
#[cfg_attr(docsrs, doc(cfg(feature = "verify")))]
pub mod verify;
#[cfg(feature = "hardware-signer")]
#[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))]
pub mod hardwaresigner;
pub use utils::IsDust;
use coin_selection::DefaultCoinSelectionAlgorithm;
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
use utils::{check_nsequence_rbf, After, Older, SecpCtx};
use crate::blockchain::{GetHeight, NoopProgress, Progress, WalletSync};
use crate::database::memory::MemoryDatabase;
use crate::database::{AnyDatabase, BatchDatabase, BatchOperations, DatabaseUtils, SyncTime};
use crate::descriptor::checksum::calc_checksum_bytes_internal;
use crate::descriptor::policy::BuildSatisfaction;
use crate::descriptor::{
calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
};
use crate::error::{Error, MiniscriptPsbtError};
use crate::psbt::PsbtUtils;
use crate::signer::SignerError;
use crate::testutils;
use crate::types::*;
use crate::wallet::coin_selection::Excess::{Change, NoChange};
const CACHE_ADDR_BATCH_SIZE: u32 = 100;
const COINBASE_MATURITY: u32 = 100;
#[derive(Debug)]
pub struct Wallet<D> {
descriptor: ExtendedDescriptor,
change_descriptor: Option<ExtendedDescriptor>,
signers: Arc<SignersContainer>,
change_signers: Arc<SignersContainer>,
network: Network,
database: RefCell<D>,
secp: SecpCtx,
}
#[derive(Debug)]
pub enum AddressIndex {
New,
LastUnused,
Peek(u32),
Reset(u32),
}
#[derive(Debug, PartialEq, Eq)]
pub struct AddressInfo {
pub index: u32,
pub address: Address,
pub keychain: KeychainKind,
}
impl Deref for AddressInfo {
type Target = Address;
fn deref(&self) -> &Self::Target {
&self.address
}
}
impl fmt::Display for AddressInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.address)
}
}
#[derive(Debug, Default)]
pub struct SyncOptions {
pub progress: Option<Box<dyn Progress>>,
}
impl<D> Wallet<D>
where
D: BatchDatabase,
{
#[deprecated = "Just use Wallet::new -- all wallets are offline now!"]
pub fn new_offline<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
database: D,
) -> Result<Self, Error> {
Self::new(descriptor, change_descriptor, network, database)
}
pub fn new<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
mut database: D,
) -> Result<Self, Error> {
let secp = Secp256k1::new();
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?;
Self::db_checksum(
&mut database,
&descriptor.to_string(),
KeychainKind::External,
)?;
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
let (change_descriptor, change_signers) = match change_descriptor {
Some(desc) => {
let (change_descriptor, change_keymap) =
into_wallet_descriptor_checked(desc, &secp, network)?;
Self::db_checksum(
&mut database,
&change_descriptor.to_string(),
KeychainKind::Internal,
)?;
let change_signers = Arc::new(SignersContainer::build(
change_keymap,
&change_descriptor,
&secp,
));
(Some(change_descriptor), change_signers)
}
None => (None, Arc::new(SignersContainer::new())),
};
Ok(Wallet {
descriptor,
change_descriptor,
signers,
change_signers,
network,
database: RefCell::new(database),
secp,
})
}
fn db_checksum(db: &mut D, desc: &str, kind: KeychainKind) -> Result<(), Error> {
let checksum = calc_checksum_bytes_internal(desc, true)?;
if db.check_descriptor_checksum(kind, checksum).is_ok() {
return Ok(());
}
let checksum_inception = calc_checksum_bytes_internal(desc, false)?;
db.check_descriptor_checksum(kind, checksum_inception)
}
pub fn network(&self) -> Network {
self.network
}
fn get_new_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> {
let incremented_index = self.fetch_and_increment_index(keychain)?;
let address_result = self
.get_descriptor_for_keychain(keychain)
.at_derivation_index(incremented_index)
.expect("can't be hardened")
.address(self.network);
address_result
.map(|address| AddressInfo {
address,
index: incremented_index,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
}
fn get_unused_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> {
let current_index = self.fetch_index(keychain)?;
let derived_key = self
.get_descriptor_for_keychain(keychain)
.at_derivation_index(current_index)
.expect("can't be hardened");
let script_pubkey = derived_key.script_pubkey();
let found_used = self
.list_transactions(true)?
.iter()
.flat_map(|tx_details| tx_details.transaction.as_ref())
.flat_map(|tx| tx.output.iter())
.any(|o| o.script_pubkey == script_pubkey);
if found_used {
self.get_new_address(keychain)
} else {
derived_key
.address(self.network)
.map(|address| AddressInfo {
address,
index: current_index,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
}
}
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
self.get_descriptor_for_keychain(keychain)
.at_derivation_index(index)
.map_err(|_| Error::HardenedIndex)?
.address(self.network)
.map(|address| AddressInfo {
index,
address,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
}
fn reset_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
self.set_index(keychain, index)?;
self.get_descriptor_for_keychain(keychain)
.at_derivation_index(index)
.map_err(|_| Error::HardenedIndex)?
.address(self.network)
.map(|address| AddressInfo {
index,
address,
keychain,
})
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
}
pub fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
self._get_address(address_index, KeychainKind::External)
}
pub fn get_internal_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
self._get_address(address_index, KeychainKind::Internal)
}
fn _get_address(
&self,
address_index: AddressIndex,
keychain: KeychainKind,
) -> Result<AddressInfo, Error> {
match address_index {
AddressIndex::New => self.get_new_address(keychain),
AddressIndex::LastUnused => self.get_unused_address(keychain),
AddressIndex::Peek(index) => self.peek_address(index, keychain),
AddressIndex::Reset(index) => self.reset_address(index, keychain),
}
}
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
let mut new_addresses_cached = false;
let max_address = match self.descriptor.has_wildcard() {
false => 0,
true => max_addresses,
};
debug!("max_address {}", max_address);
if self
.database
.borrow()
.get_script_pubkey_from_path(KeychainKind::External, max_address.saturating_sub(1))?
.is_none()
{
debug!("caching external addresses");
new_addresses_cached = true;
self.cache_addresses(KeychainKind::External, 0, max_address)?;
}
if let Some(change_descriptor) = &self.change_descriptor {
let max_address = match change_descriptor.has_wildcard() {
false => 0,
true => max_addresses,
};
if self
.database
.borrow()
.get_script_pubkey_from_path(KeychainKind::Internal, max_address.saturating_sub(1))?
.is_none()
{
debug!("caching internal addresses");
new_addresses_cached = true;
self.cache_addresses(KeychainKind::Internal, 0, max_address)?;
}
}
Ok(new_addresses_cached)
}
pub fn is_mine(&self, script: &Script) -> Result<bool, Error> {
self.database.borrow().is_mine(script)
}
pub fn list_unspent(&self) -> Result<Vec<LocalUtxo>, Error> {
Ok(self
.database
.borrow()
.iter_utxos()?
.into_iter()
.filter(|l| !l.is_spent)
.collect())
}
pub fn get_utxo(&self, outpoint: OutPoint) -> Result<Option<LocalUtxo>, Error> {
self.database.borrow().get_utxo(&outpoint)
}
pub fn get_tx(
&self,
txid: &Txid,
include_raw: bool,
) -> Result<Option<TransactionDetails>, Error> {
self.database.borrow().get_tx(txid, include_raw)
}
pub fn list_transactions(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error> {
self.database.borrow().iter_txs(include_raw)
}
pub fn get_balance(&self) -> Result<Balance, Error> {
let mut immature = 0;
let mut trusted_pending = 0;
let mut untrusted_pending = 0;
let mut confirmed = 0;
let utxos = self.list_unspent()?;
let database = self.database.borrow();
let last_sync_height = match database
.get_sync_time()?
.map(|sync_time| sync_time.block_time.height)
{
Some(height) => height,
None => return Ok(Balance::default()),
};
for u in utxos {
let tx = database
.get_tx(&u.outpoint.txid, true)?
.expect("Transaction not found in database");
if let Some(tx_conf_time) = &tx.confirmation_time {
if tx.transaction.expect("No transaction").is_coin_base()
&& (last_sync_height - tx_conf_time.height) < COINBASE_MATURITY
{
immature += u.txout.value;
} else {
confirmed += u.txout.value;
}
} else if u.keychain == KeychainKind::Internal {
trusted_pending += u.txout.value;
} else {
untrusted_pending += u.txout.value;
}
}
Ok(Balance {
immature,
trusted_pending,
untrusted_pending,
confirmed,
})
}
pub fn add_signer(
&mut self,
keychain: KeychainKind,
ordering: SignerOrdering,
signer: Arc<dyn TransactionSigner>,
) {
let signers = match keychain {
KeychainKind::External => Arc::make_mut(&mut self.signers),
KeychainKind::Internal => Arc::make_mut(&mut self.change_signers),
};
signers.add_external(signer.id(&self.secp), ordering, signer);
}
pub fn get_signers(&self, keychain: KeychainKind) -> Arc<SignersContainer> {
match keychain {
KeychainKind::External => Arc::clone(&self.signers),
KeychainKind::Internal => Arc::clone(&self.change_signers),
}
}
pub fn build_tx(&self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> {
TxBuilder {
wallet: self,
params: TxParams::default(),
coin_selection: DefaultCoinSelectionAlgorithm::default(),
phantom: core::marker::PhantomData,
}
}
pub(crate) fn create_tx<Cs: coin_selection::CoinSelectionAlgorithm<D>>(
&self,
coin_selection: Cs,
params: TxParams,
) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> {
let external_policy = self
.descriptor
.extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)?
.unwrap();
let internal_policy = self
.change_descriptor
.as_ref()
.map(|desc| {
Ok::<_, Error>(
desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
.unwrap(),
)
})
.transpose()?;
if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange
&& external_policy.requires_path()
&& params.external_policy_path.is_none()
{
return Err(Error::SpendingPolicyRequired(KeychainKind::External));
};
if let Some(internal_policy) = &internal_policy {
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden
&& internal_policy.requires_path()
&& params.internal_policy_path.is_none()
{
return Err(Error::SpendingPolicyRequired(KeychainKind::Internal));
};
}
let external_requirements = external_policy.get_condition(
params
.external_policy_path
.as_ref()
.unwrap_or(&BTreeMap::new()),
)?;
let internal_requirements = internal_policy
.map(|policy| {
Ok::<_, Error>(
policy.get_condition(
params
.internal_policy_path
.as_ref()
.unwrap_or(&BTreeMap::new()),
)?,
)
})
.transpose()?;
let requirements =
external_requirements.merge(&internal_requirements.unwrap_or_default())?;
debug!("Policy requirements: {:?}", requirements);
let version = match params.version {
Some(tx_builder::Version(0)) => {
return Err(Error::Generic("Invalid version `0`".into()))
}
Some(tx_builder::Version(1)) if requirements.csv.is_some() => {
return Err(Error::Generic(
"TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
.into(),
))
}
Some(tx_builder::Version(x)) => x,
None if requirements.csv.is_some() => 2,
_ => 1,
};
let current_height = match params.current_height {
None => self.database().get_sync_time()?.map(|sync_time| {
absolute::LockTime::from_height(sync_time.block_time.height)
.expect("Invalid height")
}),
h => h,
};
let lock_time = match params.locktime {
None => {
let fee_sniping_height = current_height.unwrap_or(absolute::LockTime::ZERO);
match requirements.timelock {
None => fee_sniping_height,
Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => fee_sniping_height,
Some(value) => value,
}
}
Some(x) if requirements.timelock.is_none() => x,
Some(x) if requirements.timelock.unwrap().is_same_unit(x) && x >= requirements.timelock.unwrap() => x,
Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", x, requirements.timelock.unwrap())))
};
let n_sequence = match (params.rbf, requirements.csv) {
(None, None) if lock_time != absolute::LockTime::ZERO => {
Sequence::ENABLE_LOCKTIME_NO_RBF
}
(None, None) => Sequence::MAX,
(None, Some(csv)) => csv,
(Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => {
return Err(Error::Generic(
"Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(),
))
}
(Some(tx_builder::RbfValue::Value(rbf)), Some(csv))
if !check_nsequence_rbf(rbf, csv) =>
{
return Err(Error::Generic(format!(
"Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
rbf, csv
)))
}
(Some(tx_builder::RbfValue::Default), Some(csv)) => csv,
(Some(rbf), _) => rbf.get_value(),
};
let (fee_rate, mut fee_amount) = match params
.fee_policy
.as_ref()
.unwrap_or(&FeePolicy::FeeRate(FeeRate::default()))
{
FeePolicy::FeeAmount(fee) => {
if let Some(previous_fee) = params.bumping_fee {
if *fee < previous_fee.absolute {
return Err(Error::FeeTooLow {
required: previous_fee.absolute,
});
}
}
(FeeRate::from_sat_per_vb(0.0), *fee)
}
FeePolicy::FeeRate(rate) => {
if let Some(previous_fee) = params.bumping_fee {
let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0);
if *rate < required_feerate {
return Err(Error::FeeRateTooLow {
required: required_feerate,
});
}
}
(*rate, 0)
}
};
let mut tx = Transaction {
version,
lock_time,
input: vec![],
output: vec![],
};
if params.manually_selected_only && params.utxos.is_empty() {
return Err(Error::NoUtxosSelected);
}
let mut outgoing: u64 = 0;
let mut received: u64 = 0;
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
for (index, (script_pubkey, value)) in recipients.enumerate() {
if !params.allow_dust
&& value.is_dust(script_pubkey)
&& !script_pubkey.is_provably_unspendable()
{
return Err(Error::OutputBelowDustLimit(index));
}
if self.is_mine(script_pubkey)? {
received += value;
}
let new_out = TxOut {
script_pubkey: script_pubkey.clone(),
value,
};
tx.output.push(new_out);
outgoing += value;
}
fee_amount += fee_rate.fee_wu(tx.weight());
fee_amount += fee_rate.fee_wu(Weight::from_wu(2));
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
&& self.change_descriptor.is_none()
{
return Err(Error::Generic(
"The `change_policy` can be set only if the wallet has a change_descriptor".into(),
));
}
let (required_utxos, optional_utxos) = self.preselect_utxos(
params.change_policy,
¶ms.unspendable,
params.utxos.clone(),
params.drain_wallet,
params.manually_selected_only,
params.bumping_fee.is_some(), current_height.map(absolute::LockTime::to_consensus_u32),
)?;
let drain_script = match params.drain_to {
Some(ref drain_recipient) => drain_recipient.clone(),
None => self
.get_internal_address(AddressIndex::New)?
.address
.script_pubkey(),
};
let coin_selection = coin_selection.coin_select(
self.database.borrow().deref(),
required_utxos,
optional_utxos,
fee_rate,
outgoing + fee_amount,
&drain_script,
)?;
fee_amount += coin_selection.fee_amount;
let excess = &coin_selection.excess;
tx.input = coin_selection
.selected
.iter()
.map(|u| bitcoin::TxIn {
previous_output: u.outpoint(),
script_sig: ScriptBuf::default(),
sequence: n_sequence,
witness: Witness::new(),
})
.collect();
if tx.output.is_empty() {
if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) {
if let NoChange {
dust_threshold,
remaining_amount,
change_fee,
} = excess
{
return Err(Error::InsufficientFunds {
needed: *dust_threshold,
available: remaining_amount.saturating_sub(*change_fee),
});
}
} else {
return Err(Error::NoRecipients);
}
}
match excess {
NoChange {
remaining_amount, ..
} => fee_amount += remaining_amount,
Change { amount, fee } => {
if self.is_mine(&drain_script)? {
received += amount;
}
fee_amount += fee;
let drain_output = TxOut {
value: *amount,
script_pubkey: drain_script,
};
tx.output.push(drain_output);
}
};
params.ordering.sort_tx(&mut tx);
let txid = tx.txid();
let sent = coin_selection.local_selected_amount();
let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
let transaction_details = TransactionDetails {
transaction: None,
txid,
confirmation_time: None,
received,
sent,
fee: Some(fee_amount),
};
Ok((psbt, transaction_details))
}
pub fn build_fee_bump(
&self,
txid: Txid,
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, 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.confirmation_time.is_some() => return Err(Error::TransactionConfirmed),
Some(tx) => tx,
};
let mut tx = details.transaction.take().unwrap();
if !tx
.input
.iter()
.any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
{
return Err(Error::IrreplaceableTransaction);
}
let feerate = FeeRate::from_wu(details.fee.ok_or(Error::FeeRateUnavailable)?, tx.weight());
let original_txin = tx.input.drain(..).collect::<Vec<_>>();
let original_utxos = original_txin
.iter()
.map(|txin| -> Result<_, Error> {
let txout = self
.database
.borrow()
.get_previous_output(&txin.previous_output)?
.ok_or(Error::UnknownUtxo)?;
let (weight, keychain) = match self
.database
.borrow()
.get_path_from_script_pubkey(&txout.script_pubkey)?
{
#[allow(deprecated)]
Some((keychain, _)) => (
self._get_descriptor_for_keychain(keychain)
.0
.max_satisfaction_weight()
.unwrap(),
keychain,
),
None => {
let weight =
serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len();
(weight, KeychainKind::External)
}
};
let utxo = LocalUtxo {
outpoint: txin.previous_output,
txout,
keychain,
is_spent: true,
};
Ok(WeightedUtxo {
satisfaction_weight: weight,
utxo: Utxo::Local(utxo),
})
})
.collect::<Result<Vec<_>, _>>()?;
if tx.output.len() > 1 {
let mut change_index = None;
for (index, txout) in tx.output.iter().enumerate() {
let (_, change_type) = self._get_descriptor_for_keychain(KeychainKind::Internal);
match self
.database
.borrow()
.get_path_from_script_pubkey(&txout.script_pubkey)?
{
Some((keychain, _)) if keychain == change_type => change_index = Some(index),
_ => {}
}
}
if let Some(change_index) = change_index {
tx.output.remove(change_index);
}
}
let params = TxParams {
version: Some(tx_builder::Version(tx.version)),
recipients: tx
.output
.into_iter()
.map(|txout| (txout.script_pubkey, txout.value))
.collect(),
utxos: original_utxos,
bumping_fee: Some(tx_builder::PreviousFee {
absolute: details.fee.ok_or(Error::FeeRateUnavailable)?,
rate: feerate.as_sat_per_vb(),
}),
..Default::default()
};
Ok(TxBuilder {
wallet: self,
params,
coin_selection: DefaultCoinSelectionAlgorithm::default(),
phantom: core::marker::PhantomData,
})
}
pub fn sign(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions,
) -> Result<bool, Error> {
self.update_psbt_with_descriptor(psbt)?;
if !sign_options.trust_witness_utxo
&& psbt
.inputs
.iter()
.filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none())
.filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none())
.any(|i| i.non_witness_utxo.is_none())
{
return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo));
}
if !sign_options.allow_all_sighashes
&& !psbt.inputs.iter().all(|i| {
i.sighash_type.is_none()
|| i.sighash_type == Some(EcdsaSighashType::All.into())
|| i.sighash_type == Some(TapSighashType::All.into())
|| i.sighash_type == Some(TapSighashType::Default.into())
})
{
return Err(Error::Signer(signer::SignerError::NonStandardSighash));
}
for signer in self
.signers
.signers()
.iter()
.chain(self.change_signers.signers().iter())
{
signer.sign_transaction(psbt, &sign_options, &self.secp)?;
}
if sign_options.try_finalize {
self.finalize_psbt(psbt, sign_options)
} else {
Ok(false)
}
}
pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, Error> {
match (keychain, self.change_descriptor.as_ref()) {
(KeychainKind::External, _) => Ok(self.descriptor.extract_policy(
&self.signers,
BuildSatisfaction::None,
&self.secp,
)?),
(KeychainKind::Internal, None) => Ok(None),
(KeychainKind::Internal, Some(desc)) => Ok(desc.extract_policy(
&self.change_signers,
BuildSatisfaction::None,
&self.secp,
)?),
}
}
pub fn public_descriptor(
&self,
keychain: KeychainKind,
) -> Result<Option<ExtendedDescriptor>, Error> {
match (keychain, self.change_descriptor.as_ref()) {
(KeychainKind::External, _) => Ok(Some(self.descriptor.clone())),
(KeychainKind::Internal, None) => Ok(None),
(KeychainKind::Internal, Some(desc)) => Ok(Some(desc.clone())),
}
}
pub fn finalize_psbt(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions,
) -> Result<bool, Error> {
let tx = &psbt.unsigned_tx;
let mut finished = true;
for (n, input) in tx.input.iter().enumerate() {
let psbt_input = &psbt
.inputs
.get(n)
.ok_or(Error::Signer(SignerError::InputIndexOutOfRange))?;
if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
continue;
}
let create_height = self
.database
.borrow()
.get_tx(&input.previous_output.txid, false)?
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(u32::MAX));
let last_sync_height = self
.database()
.get_sync_time()?
.map(|sync_time| sync_time.block_time.height);
let current_height = sign_options.assume_height.or(last_sync_height);
debug!(
"Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}",
n, input.previous_output, create_height, current_height
);
let desc = psbt
.get_utxo_for(n)
.map(|txout| self.get_descriptor_for_txout(&txout))
.transpose()?
.flatten()
.or_else(|| {
self.descriptor.derive_from_psbt_input(
psbt_input,
psbt.get_utxo_for(n),
&self.secp,
)
})
.or_else(|| {
self.change_descriptor.as_ref().and_then(|desc| {
desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp)
})
});
match desc {
Some(desc) => {
let mut tmp_input = bitcoin::TxIn::default();
match desc.satisfy(
&mut tmp_input,
(
PsbtInputSatisfier::new(psbt, n),
After::new(current_height, false),
Older::new(current_height, create_height, false),
),
) {
Ok(_) => {
let psbt_input = &mut psbt.inputs[n];
psbt_input.final_script_sig = Some(tmp_input.script_sig);
psbt_input.final_script_witness = Some(tmp_input.witness);
if sign_options.remove_partial_sigs {
psbt_input.partial_sigs.clear();
}
}
Err(e) => {
debug!("satisfy error {:?} for input {}", e, n);
finished = false
}
}
}
None => finished = false,
}
}
Ok(finished)
}
pub fn secp_ctx(&self) -> &SecpCtx {
&self.secp
}
pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor {
let (descriptor, _) = self._get_descriptor_for_keychain(keychain);
descriptor
}
fn _get_descriptor_for_keychain(
&self,
keychain: KeychainKind,
) -> (&ExtendedDescriptor, KeychainKind) {
match keychain {
KeychainKind::Internal if self.change_descriptor.is_some() => (
self.change_descriptor.as_ref().unwrap(),
KeychainKind::Internal,
),
_ => (&self.descriptor, KeychainKind::External),
}
}
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Result<Option<DerivedDescriptor>, Error> {
Ok(self
.database
.borrow()
.get_path_from_script_pubkey(&txout.script_pubkey)?
.map(|(keychain, child)| (self.get_descriptor_for_keychain(keychain), child))
.map(|(desc, child)| {
desc.at_derivation_index(child)
.expect("child is not hardened")
}))
}
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.has_wildcard() {
false => 0,
true => self.database.borrow_mut().increment_last_index(keychain)?,
};
if self
.database
.borrow()
.get_script_pubkey_from_path(keychain, index)?
.is_none()
{
self.cache_addresses(keychain, index, CACHE_ADDR_BATCH_SIZE)?;
}
Ok(index)
}
fn fetch_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.has_wildcard() {
false => Some(0),
true => self.database.borrow_mut().get_last_index(keychain)?,
};
if let Some(i) = index {
Ok(i)
} else {
self.fetch_and_increment_index(keychain)
}
}
fn set_index(&self, keychain: KeychainKind, index: u32) -> Result<(), Error> {
self.database.borrow_mut().set_last_index(keychain, index)?;
Ok(())
}
fn cache_addresses(
&self,
keychain: KeychainKind,
from: u32,
mut count: u32,
) -> Result<(), Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
if !descriptor.has_wildcard() {
if from > 0 {
return Ok(());
}
count = 1;
}
let mut address_batch = self.database.borrow().begin_batch();
let start_time = time::Instant::new();
for i in from..(from + count) {
address_batch.set_script_pubkey(
&descriptor
.at_derivation_index(i)
.expect("i is not hardened")
.script_pubkey(),
keychain,
i,
)?;
}
info!(
"Derivation of {} addresses from {} took {} ms",
count,
from,
start_time.elapsed().as_millis()
);
self.database.borrow_mut().commit_batch(address_batch)?;
Ok(())
}
fn get_available_utxos(&self) -> Result<Vec<(LocalUtxo, usize)>, Error> {
Ok(self
.list_unspent()?
.into_iter()
.map(|utxo| {
let keychain = utxo.keychain;
(
utxo,
#[allow(deprecated)]
self.get_descriptor_for_keychain(keychain)
.max_satisfaction_weight()
.unwrap(),
)
})
.collect())
}
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
fn preselect_utxos(
&self,
change_policy: tx_builder::ChangeSpendPolicy,
unspendable: &HashSet<OutPoint>,
manually_selected: Vec<WeightedUtxo>,
must_use_all_available: bool,
manual_only: bool,
must_only_use_confirmed_tx: bool,
current_height: Option<u32>,
) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
let mut may_spend = self.get_available_utxos()?;
may_spend.retain(|may_spend| {
!manually_selected
.iter()
.any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
});
let mut must_spend = manually_selected;
if manual_only {
return Ok((must_spend, vec![]));
}
let database = self.database.borrow();
let satisfies_confirmed = may_spend
.iter()
.map(|u| {
database
.get_tx(&u.0.outpoint.txid, true)
.map(|tx| match tx {
None => false,
Some(tx) => {
let mut spendable = true;
if must_only_use_confirmed_tx && tx.confirmation_time.is_none() {
return false;
}
if tx
.transaction
.expect("We specifically ask for the transaction above")
.is_coin_base()
{
if let Some(current_height) = current_height {
match &tx.confirmation_time {
Some(t) => {
spendable &= (current_height.saturating_sub(t.height))
>= COINBASE_MATURITY;
}
None => spendable = false,
}
}
}
spendable
}
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut i = 0;
may_spend.retain(|u| {
let retain = change_policy.is_satisfied_by(&u.0)
&& !unspendable.contains(&u.0.outpoint)
&& satisfies_confirmed[i];
i += 1;
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 {
must_spend.append(&mut may_spend);
}
Ok((must_spend, may_spend))
}
fn complete_transaction(
&self,
tx: Transaction,
selected: Vec<Utxo>,
params: TxParams,
) -> Result<psbt::PartiallySignedTransaction, Error> {
let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?;
if params.add_global_xpubs {
let mut all_xpubs = self.descriptor.get_extended_keys()?;
if let Some(change_descriptor) = &self.change_descriptor {
all_xpubs.extend(change_descriptor.get_extended_keys()?);
}
for xpub in all_xpubs {
let origin = match xpub.origin {
Some(origin) => origin,
None if xpub.xkey.depth == 0 => {
(xpub.root_fingerprint(&self.secp), vec![].into())
}
_ => return Err(Error::MissingKeyOrigin(xpub.xkey.to_string())),
};
psbt.xpub.insert(xpub.xkey, origin);
}
}
let mut lookup_output = selected
.into_iter()
.map(|utxo| (utxo.outpoint(), utxo))
.collect::<HashMap<_, _>>();
for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) {
let utxo = match lookup_output.remove(&input.previous_output) {
Some(utxo) => utxo,
None => continue,
};
match utxo {
Utxo::Local(utxo) => {
*psbt_input =
match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) {
Ok(psbt_input) => psbt_input,
Err(e) => match e {
Error::UnknownUtxo => psbt::Input {
sighash_type: params.sighash,
..psbt::Input::default()
},
_ => return Err(e),
},
}
}
Utxo::Foreign {
psbt_input: foreign_psbt_input,
outpoint,
} => {
let is_taproot = foreign_psbt_input
.witness_utxo
.as_ref()
.map(|txout| txout.script_pubkey.is_v1_p2tr())
.unwrap_or(false);
if !is_taproot
&& !params.only_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;
}
}
}
self.update_psbt_with_descriptor(&mut psbt)?;
Ok(psbt)
}
pub fn get_psbt_input(
&self,
utxo: LocalUtxo,
sighash_type: Option<psbt::PsbtSighashType>,
only_witness_utxo: bool,
) -> Result<psbt::Input, Error> {
let (keychain, child) = self
.database
.borrow()
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
.ok_or(Error::UnknownUtxo)?;
let mut psbt_input = psbt::Input {
sighash_type,
..psbt::Input::default()
};
let desc = self.get_descriptor_for_keychain(keychain);
let derived_descriptor = desc
.at_derivation_index(child)
.expect("child can't be hardened");
psbt_input
.update_with_descriptor_unchecked(&derived_descriptor)
.map_err(MiniscriptPsbtError::Conversion)?;
let prev_output = utxo.outpoint;
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
if desc.is_witness() || desc.is_taproot() {
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
}
if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
psbt_input.non_witness_utxo = Some(prev_tx);
}
}
Ok(psbt_input)
}
fn update_psbt_with_descriptor(
&self,
psbt: &mut psbt::PartiallySignedTransaction,
) -> Result<(), Error> {
#[allow(clippy::needless_collect)]
let utxos = (0..psbt.inputs.len())
.filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
.chain(
psbt.unsigned_tx
.output
.iter()
.enumerate()
.map(|(i, out)| (false, i, out.clone())),
)
.collect::<Vec<_>>();
for (is_input, index, out) in utxos.into_iter() {
if let Some((keychain, child)) = self
.database
.borrow()
.get_path_from_script_pubkey(&out.script_pubkey)?
{
debug!(
"Found descriptor for input #{} {:?}/{}",
index, keychain, child
);
let desc = self.get_descriptor_for_keychain(keychain);
let desc = desc
.at_derivation_index(child)
.expect("child can't be hardened");
if is_input {
psbt.update_input_with_descriptor(index, &desc)
.map_err(MiniscriptPsbtError::UtxoUpdate)?;
} else {
psbt.update_output_with_descriptor(index, &desc)
.map_err(MiniscriptPsbtError::OutputUpdate)?;
}
}
}
Ok(())
}
pub fn database(&self) -> impl std::ops::Deref<Target = D> + '_ {
self.database.borrow()
}
#[maybe_async]
pub fn sync<B: WalletSync + GetHeight>(
&self,
blockchain: &B,
sync_opts: SyncOptions,
) -> Result<(), Error> {
debug!("Begin sync...");
let mut progress_iter = sync_opts.progress.into_iter();
let mut new_progress = || {
progress_iter
.next()
.unwrap_or_else(|| Box::new(NoopProgress))
};
let run_setup = self.ensure_addresses_cached(CACHE_ADDR_BATCH_SIZE)?;
debug!("run_setup: {}", run_setup);
let has_wildcard = self.descriptor.has_wildcard()
&& (self.change_descriptor.is_none()
|| self.change_descriptor.as_ref().unwrap().has_wildcard());
let max_rounds = if has_wildcard { 100 } else { 1 };
for _ in 0..max_rounds {
let sync_res = if run_setup {
maybe_await!(blockchain.wallet_setup(&self.database, new_progress()))
} else {
maybe_await!(blockchain.wallet_sync(&self.database, new_progress()))
};
let ensure_cache = sync_res.map_or_else(
|e| match e {
Error::MissingCachedScripts(inner) => {
let extra =
std::cmp::max(inner.missing_count as u32, CACHE_ADDR_BATCH_SIZE);
let last = inner.last_count as u32;
Ok(extra + last)
}
_ => Err(e),
},
|_| Ok(0_u32),
)?;
if !self.ensure_addresses_cached(ensure_cache)? {
break;
}
}
let sync_time = SyncTime {
block_time: BlockTime {
height: maybe_await!(blockchain.get_height())?,
timestamp: time::get_timestamp(),
},
};
debug!("Saving `sync_time` = {:?}", sync_time);
self.database.borrow_mut().set_sync_time(sync_time)?;
Ok(())
}
pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String {
self.get_descriptor_for_keychain(keychain)
.to_string()
.split_once('#')
.unwrap()
.1
.to_string()
}
}
pub fn wallet_name_from_descriptor<T>(
descriptor: T,
change_descriptor: Option<T>,
network: Network,
secp: &SecpCtx,
) -> Result<String, Error>
where
T: IntoWalletDescriptor,
{
let descriptor = descriptor
.into_wallet_descriptor(secp, network)?
.0
.to_string();
let mut wallet_name = calc_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
if let Some(change_descriptor) = change_descriptor {
let change_descriptor = change_descriptor
.into_wallet_descriptor(secp, network)?
.0
.to_string();
wallet_name.push_str(
calc_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
);
}
Ok(wallet_name)
}
pub fn get_funded_wallet(
descriptor: &str,
) -> (Wallet<AnyDatabase>, (String, Option<String>), bitcoin::Txid) {
let descriptors = testutils!(@descriptors (descriptor));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let funding_address_kix = 0;
let tx_meta = testutils! {
@tx ( (@external descriptors, funding_address_kix) => 50_000 ) (@confirmations 1)
};
wallet
.database
.borrow_mut()
.set_script_pubkey(
&bitcoin::Address::from_str(&tx_meta.output.get(0).unwrap().to_address)
.unwrap()
.assume_checked()
.script_pubkey(),
KeychainKind::External,
funding_address_kix,
)
.unwrap();
wallet
.database
.borrow_mut()
.set_last_index(KeychainKind::External, funding_address_kix)
.unwrap();
let txid = crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, Some(100));
(wallet, descriptors, txid)
}
#[cfg(test)]
pub(crate) mod test {
use assert_matches::assert_matches;
use bitcoin::{absolute, blockdata::script::PushBytes, psbt, Network, Sequence};
use crate::database::Database;
use crate::types::KeychainKind;
use super::*;
use crate::signer::{SignOptions, SignerError};
use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset};
const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
#[test]
fn test_descriptor_checksum() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let checksum = wallet.descriptor_checksum(KeychainKind::External);
assert_eq!(checksum.len(), 8);
assert_eq!(
calc_checksum(&wallet.descriptor.to_string()).unwrap(),
checksum
);
}
#[test]
fn test_db_checksum() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let desc = wallet.descriptor.to_string();
let checksum = calc_checksum_bytes_internal(&desc, true).unwrap();
let checksum_inception = calc_checksum_bytes_internal(&desc, false).unwrap();
let checksum_invalid = [b'q'; 8];
let mut db = MemoryDatabase::new();
db.check_descriptor_checksum(KeychainKind::External, checksum)
.expect("failed to save actual checksum");
Wallet::db_checksum(&mut db, &desc, KeychainKind::External)
.expect("db that uses actual checksum should be supported");
let mut db = MemoryDatabase::new();
db.check_descriptor_checksum(KeychainKind::External, checksum_inception)
.expect("failed to save checksum inception");
Wallet::db_checksum(&mut db, &desc, KeychainKind::External)
.expect("db that uses checksum inception should be supported");
let mut db = MemoryDatabase::new();
db.check_descriptor_checksum(KeychainKind::External, checksum_invalid)
.expect("failed to save invalid checksum");
Wallet::db_checksum(&mut db, &desc, KeychainKind::External)
.expect_err("db that uses invalid checksum should fail");
}
#[test]
fn test_get_funded_wallet_balance() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
assert_eq!(wallet.get_balance().unwrap().confirmed, 50000);
}
#[test]
fn test_cache_addresses_fixed() {
let db = MemoryDatabase::new();
let wallet = Wallet::new(
"wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
None,
Network::Testnet,
db,
)
.unwrap();
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1qj08ys4ct2hzzc2hcz6h2hgrvlmsjynaw43s835"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1qj08ys4ct2hzzc2hcz6h2hgrvlmsjynaw43s835"
);
assert!(wallet
.database
.borrow_mut()
.get_script_pubkey_from_path(KeychainKind::External, 0)
.unwrap()
.is_some());
assert!(wallet
.database
.borrow_mut()
.get_script_pubkey_from_path(KeychainKind::Internal, 0)
.unwrap()
.is_none());
}
#[test]
fn test_cache_addresses() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
assert!(wallet
.database
.borrow_mut()
.get_script_pubkey_from_path(KeychainKind::External, CACHE_ADDR_BATCH_SIZE - 1)
.unwrap()
.is_some());
assert!(wallet
.database
.borrow_mut()
.get_script_pubkey_from_path(KeychainKind::External, CACHE_ADDR_BATCH_SIZE)
.unwrap()
.is_none());
}
#[test]
fn test_cache_addresses_refill() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
assert!(wallet
.database
.borrow_mut()
.get_script_pubkey_from_path(KeychainKind::External, CACHE_ADDR_BATCH_SIZE - 1)
.unwrap()
.is_some());
for _ in 0..CACHE_ADDR_BATCH_SIZE {
wallet.get_address(New).unwrap();
}
assert!(wallet
.database
.borrow_mut()
.get_script_pubkey_from_path(KeychainKind::External, CACHE_ADDR_BATCH_SIZE * 2 - 1)
.unwrap()
.is_some());
}
pub(crate) fn get_test_wpkh() -> &'static str {
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
}
pub(crate) fn get_test_single_sig_csv() -> &'static str {
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
}
pub(crate) fn get_test_a_or_b_plus_csv() -> &'static str {
"wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
}
pub(crate) fn get_test_single_sig_cltv() -> &'static str {
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
}
pub(crate) fn get_test_tr_single_sig() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)"
}
pub(crate) fn get_test_tr_with_taptree() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
pub(crate) fn get_test_tr_with_taptree_both_priv() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
}
pub(crate) fn get_test_tr_repeated_key() -> &'static str {
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
}
pub(crate) fn get_test_tr_single_sig_xprv() -> &'static str {
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
}
pub(crate) fn get_test_tr_with_taptree_xprv() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
pub(crate) fn get_test_tr_dup_keys() -> &'static str {
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
}
macro_rules! assert_fee_rate {
($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
let psbt = $psbt.clone();
#[allow(unused_mut)]
let mut tx = $psbt.clone().extract_tx();
$(
$( $add_signature )*
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); }
)*
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut dust_change = false;
$(
$( $dust_change )*
dust_change = true;
)*
let fee_amount = psbt
.inputs
.iter()
.fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value)
- psbt
.unsigned_tx
.output
.iter()
.fold(0, |acc, o| acc + o.value);
assert_eq!(fee_amount, $fees);
let tx_fee_rate = FeeRate::from_wu($fees, tx.weight());
let fee_rate = $fee_rate;
if !dust_change {
assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_per_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
} else {
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
}
});
}
macro_rules! from_str {
($e:expr, $t:ty) => {{
use std::str::FromStr;
<$t>::from_str($e).unwrap()
}};
($e:expr) => {
from_str!($e, _)
};
}
#[test]
#[should_panic(expected = "NoRecipients")]
fn test_create_tx_empty_recipients() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
wallet.build_tx().finish().unwrap();
}
#[test]
#[should_panic(expected = "NoUtxosSelected")]
fn test_create_tx_manually_selected_empty_utxos() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.manually_selected_only();
builder.finish().unwrap();
}
#[test]
#[should_panic(expected = "Invalid version `0`")]
fn test_create_tx_version_0() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.version(0);
builder.finish().unwrap();
}
#[test]
#[should_panic(
expected = "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
)]
fn test_create_tx_version_1_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.version(1);
builder.finish().unwrap();
}
#[test]
fn test_create_tx_custom_version() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.version(42);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.version, 42);
}
#[test]
fn test_create_tx_default_locktime() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let tx_meta = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, None);
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.lock_time, absolute::LockTime::ZERO);
}
#[test]
fn test_create_tx_fee_sniping_locktime_provided_height() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let sync_time = SyncTime {
block_time: BlockTime {
height: 24,
timestamp: 0,
},
};
wallet
.database
.borrow_mut()
.set_sync_time(sync_time)
.unwrap();
let current_height = 25;
builder.current_height(current_height);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.unsigned_tx.lock_time,
absolute::LockTime::from_height(current_height).unwrap()
);
}
#[test]
fn test_create_tx_fee_sniping_locktime_last_sync() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let sync_time = SyncTime {
block_time: BlockTime {
height: 25,
timestamp: 0,
},
};
wallet
.database
.borrow_mut()
.set_sync_time(sync_time.clone())
.unwrap();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.unsigned_tx.lock_time,
absolute::LockTime::from_height(sync_time.block_time.height).unwrap()
);
}
#[test]
fn test_create_tx_default_locktime_cltv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.unsigned_tx.lock_time,
absolute::LockTime::from_height(100_000).unwrap()
);
}
#[test]
fn test_create_tx_custom_locktime() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.current_height(630_001)
.nlocktime(absolute::LockTime::from_height(630_000).unwrap());
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.unsigned_tx.lock_time,
absolute::LockTime::from_height(630_000).unwrap()
);
}
#[test]
fn test_create_tx_custom_locktime_compatible_with_cltv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(absolute::LockTime::from_height(630_000).unwrap());
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.unsigned_tx.lock_time,
absolute::LockTime::from_height(630_000).unwrap()
);
}
#[test]
#[should_panic(
expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script"
)]
fn test_create_tx_custom_locktime_incompatible_with_cltv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(absolute::LockTime::from_height(50000).unwrap());
builder.finish().unwrap();
}
#[test]
fn test_create_tx_no_rbf_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
}
#[test]
fn test_create_tx_with_default_rbf_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6));
}
#[test]
#[should_panic(
expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`"
)]
fn test_create_tx_with_custom_rbf_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(3));
builder.finish().unwrap();
}
#[test]
fn test_create_tx_no_rbf_cltv() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
}
#[test]
#[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")]
fn test_create_tx_invalid_rbf_sequence() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
builder.finish().unwrap();
}
#[test]
fn test_create_tx_custom_rbf_sequence() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(0xDEADBEEF));
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF));
}
#[test]
fn test_create_tx_default_sequence() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let tx_meta = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, None);
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
}
#[test]
#[should_panic(
expected = "The `change_policy` can be set only if the wallet has a change_descriptor"
)]
fn test_create_tx_change_policy_no_internal() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.do_not_spend_change();
builder.finish().unwrap();
}
#[test]
fn test_create_tx_drain_wallet_and_drain_to() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.unsigned_tx.output[0].value,
50_000 - details.fee.unwrap_or(0)
);
}
#[test]
fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
.unwrap()
.assume_checked();
let drain_addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 20_000)
.drain_to(drain_addr.script_pubkey())
.drain_wallet();
let (psbt, details) = builder.finish().unwrap();
let outputs = psbt.unsigned_tx.output;
assert_eq!(outputs.len(), 2);
let main_output = outputs
.iter()
.find(|x| x.script_pubkey == addr.script_pubkey())
.unwrap();
let drain_output = outputs
.iter()
.find(|x| x.script_pubkey == drain_addr.script_pubkey())
.unwrap();
assert_eq!(main_output.value, 20_000,);
assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0));
}
#[test]
fn test_create_tx_drain_to_and_utxos() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let utxos: Vec<_> = wallet
.get_available_utxos()
.unwrap()
.into_iter()
.map(|(u, _)| u.outpoint)
.collect();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.add_utxos(&utxos)
.unwrap();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.unsigned_tx.output[0].value,
50_000 - details.fee.unwrap_or(0)
);
}
#[test]
#[should_panic(expected = "NoRecipients")]
fn test_create_tx_drain_to_no_drain_wallet_no_utxos() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let drain_addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(drain_addr.script_pubkey());
builder.finish().unwrap();
}
#[test]
fn test_create_tx_default_fee_rate() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, details) = builder.finish().unwrap();
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::default(), @add_signature);
}
#[test]
fn test_create_tx_custom_fee_rate() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (psbt, details) = builder.finish().unwrap();
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
}
#[test]
fn test_create_tx_absolute_fee() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_absolute(100);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.fee.unwrap_or(0), 100);
assert_eq!(psbt.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.unsigned_tx.output[0].value,
50_000 - details.fee.unwrap_or(0)
);
}
#[test]
fn test_create_tx_absolute_zero_fee() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_absolute(0);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.fee.unwrap_or(0), 0);
assert_eq!(psbt.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.unsigned_tx.output[0].value,
50_000 - details.fee.unwrap_or(0)
);
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_create_tx_absolute_high_fee() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_absolute(60_000);
let (_psbt, _details) = builder.finish().unwrap();
}
#[test]
fn test_create_tx_add_change() {
use super::tx_builder::TxOrdering;
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.ordering(TxOrdering::Untouched);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.output.len(), 2);
assert_eq!(psbt.unsigned_tx.output[0].value, 25_000);
assert_eq!(
psbt.unsigned_tx.output[1].value,
25_000 - details.fee.unwrap_or(0)
);
}
#[test]
fn test_create_tx_skip_change_dust() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 49_800);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.output.len(), 1);
assert_eq!(psbt.unsigned_tx.output[0].value, 49_800);
assert_eq!(details.fee.unwrap_or(0), 200);
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_create_tx_drain_to_dust_amount() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_rate(FeeRate::from_sat_per_vb(453.0));
builder.finish().unwrap();
}
#[test]
fn test_create_tx_ordering_respected() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.add_recipient(addr.script_pubkey(), 10_000)
.ordering(super::tx_builder::TxOrdering::Bip69Lexicographic);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.output.len(), 3);
assert_eq!(
psbt.unsigned_tx.output[0].value,
10_000 - details.fee.unwrap_or(0)
);
assert_eq!(psbt.unsigned_tx.output[1].value, 10_000);
assert_eq!(psbt.unsigned_tx.output[2].value, 30_000);
}
#[test]
fn test_create_tx_default_sighash() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 30_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.inputs[0].sighash_type, None);
}
#[test]
fn test_create_tx_custom_sighash() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.sighash(bitcoin::sighash::EcdsaSighashType::Single.into());
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.inputs[0].sighash_type,
Some(bitcoin::sighash::EcdsaSighashType::Single.into())
);
}
#[test]
fn test_create_tx_input_hd_keypaths() {
use bitcoin::bip32::{DerivationPath, Fingerprint};
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1);
assert_eq!(
psbt.inputs[0].bip32_derivation.values().next().unwrap(),
&(
Fingerprint::from_str("d34db33f").unwrap(),
DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap()
)
);
}
#[test]
fn test_create_tx_output_hd_keypaths() {
use bitcoin::bip32::{DerivationPath, Fingerprint};
let (wallet, descriptors, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)");
wallet.get_address(New).unwrap();
let addr = testutils!(@external descriptors, 5);
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1);
assert_eq!(
psbt.outputs[0].bip32_derivation.values().next().unwrap(),
&(
Fingerprint::from_str("d34db33f").unwrap(),
DerivationPath::from_str("m/44'/0'/0'/0/5").unwrap()
)
);
}
#[test]
fn test_create_tx_set_redeem_script_p2sh() {
use bitcoin::hashes::hex::FromHex;
let (wallet, _, _) =
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.inputs[0].redeem_script,
Some(ScriptBuf::from(
Vec::<u8>::from_hex(
"21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac"
)
.unwrap()
))
);
assert_eq!(psbt.inputs[0].witness_script, None);
}
#[test]
fn test_create_tx_set_witness_script_p2wsh() {
use bitcoin::hashes::hex::FromHex;
let (wallet, _, _) =
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.inputs[0].redeem_script, None);
assert_eq!(
psbt.inputs[0].witness_script,
Some(ScriptBuf::from(
Vec::<u8>::from_hex(
"21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac"
)
.unwrap()
))
);
}
#[test]
fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() {
use bitcoin::hashes::hex::FromHex;
let (wallet, _, _) =
get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
let script = ScriptBuf::from(
Vec::<u8>::from_hex(
"21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac",
)
.unwrap(),
);
assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_v0_p2wsh()));
assert_eq!(psbt.inputs[0].witness_script, Some(script));
}
#[test]
fn test_create_tx_non_witness_utxo() {
let (wallet, _, _) =
get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_some());
assert!(psbt.inputs[0].witness_utxo.is_none());
}
#[test]
fn test_create_tx_only_witness_utxo() {
let (wallet, _, _) =
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.only_witness_utxo()
.drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_none());
assert!(psbt.inputs[0].witness_utxo.is_some());
}
#[test]
fn test_create_tx_shwpkh_has_witness_utxo() {
let (wallet, _, _) =
get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert!(psbt.inputs[0].witness_utxo.is_some());
}
#[test]
fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() {
let (wallet, _, _) =
get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert!(psbt.inputs[0].non_witness_utxo.is_some());
assert!(psbt.inputs[0].witness_utxo.is_some());
}
#[test]
fn test_create_tx_add_utxo() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let small_output_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.add_utxo(OutPoint {
txid: small_output_txid,
vout: 0,
})
.unwrap();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(
psbt.unsigned_tx.input.len(),
2,
"should add an additional input since 25_000 < 30_000"
);
assert_eq!(details.sent, 75_000, "total should be sum of both inputs");
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_create_tx_manually_selected_insufficient() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let small_output_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.add_utxo(OutPoint {
txid: small_output_txid,
vout: 0,
})
.unwrap()
.manually_selected_only();
builder.finish().unwrap();
}
#[test]
#[should_panic(expected = "SpendingPolicyRequired(External)")]
fn test_create_tx_policy_path_required() {
let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 30_000);
builder.finish().unwrap();
}
#[test]
fn test_create_tx_policy_path_no_csv() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let tx_meta = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
};
crate::populate_test_db!(wallet.database.borrow_mut(), tx_meta, None);
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
let path = vec![(root_id, vec![0])].into_iter().collect();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF));
}
#[test]
fn test_create_tx_policy_path_use_csv() {
let (wallet, _, _) = get_funded_wallet(get_test_a_or_b_plus_csv());
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
let path = vec![(root_id, vec![1])].into_iter().collect();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144));
}
#[test]
fn test_create_tx_policy_path_ignored_subtree_with_csv() {
let (wallet, _, _) = get_funded_wallet("wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))");
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
let path = vec![(root_id, vec![0])].into_iter().collect();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 30_000)
.policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE));
}
#[test]
fn test_create_tx_global_xpubs_with_origin() {
use bitcoin::bip32;
use bitcoin::hashes::hex::FromHex;
let (wallet, _, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.add_global_xpubs();
let (psbt, _) = builder.finish().unwrap();
let key = bip32::ExtendedPubKey::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap();
let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap();
let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap();
assert_eq!(psbt.xpub.len(), 1);
assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path)));
}
#[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()
.assume_checked();
let utxo = wallet2.list_unspent().unwrap().remove(0);
#[allow(deprecated)]
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)
.only_witness_utxo()
.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
.unwrap();
let (mut psbt, details) = builder.finish().unwrap();
assert_eq!(
details.sent - details.received,
10_000 + details.fee.unwrap_or(0),
"we should have only net spent ~10_000"
);
assert!(
psbt.unsigned_tx
.input
.iter()
.any(|input| input.previous_output == utxo.outpoint),
"foreign_utxo should be in there"
);
let finished = wallet1
.sign(
&mut psbt,
SignOptions {
trust_witness_utxo: true,
..Default::default()
},
)
.unwrap();
assert!(
!finished,
"only one of the inputs should have been signed so far"
);
let finished = wallet2
.sign(
&mut psbt,
SignOptions {
trust_witness_utxo: true,
..Default::default()
},
)
.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;
#[allow(deprecated)]
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();
#[allow(deprecated)]
let satisfaction_weight = wallet2
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
.unwrap();
let mut builder = wallet1.build_tx();
assert!(
builder
.add_foreign_utxo(
utxo2.outpoint,
psbt::Input {
non_witness_utxo: Some(tx1),
..Default::default()
},
satisfaction_weight
)
.is_err(),
"should fail when outpoint doesn't match psbt_input"
);
assert!(
builder
.add_foreign_utxo(
utxo2.outpoint,
psbt::Input {
non_witness_utxo: Some(tx2),
..Default::default()
},
satisfaction_weight
)
.is_ok(),
"shoulld be ok when outpoint does match psbt_input"
);
}
#[test]
fn test_add_foreign_utxo_only_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()
.assume_checked();
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
#[allow(deprecated)]
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);
{
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 fail with only witness_utxo"
);
}
{
let mut builder = builder.clone();
let psbt_input = psbt::Input {
witness_utxo: Some(utxo2.txout.clone()),
..Default::default()
};
builder
.only_witness_utxo()
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
.unwrap();
assert!(
builder.finish().is_ok(),
"psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled"
);
}
{
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 by default"
);
}
}
#[test]
fn test_get_psbt_input() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
for utxo in wallet.list_unspent().unwrap() {
let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap();
assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some());
}
}
#[test]
#[should_panic(
expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
)]
fn test_create_tx_global_xpubs_origin_missing() {
let (wallet, _, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.add_global_xpubs();
builder.finish().unwrap();
}
#[test]
fn test_create_tx_global_xpubs_master_without_origin() {
use bitcoin::bip32;
use bitcoin::hashes::hex::FromHex;
let (wallet, _, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.add_global_xpubs();
let (psbt, _) = builder.finish().unwrap();
let key = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap();
let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap();
assert_eq!(psbt.xpub.len(), 1);
assert_eq!(
psbt.xpub.get(&key),
Some(&(fingerprint, bip32::DerivationPath::default()))
);
}
#[test]
#[should_panic(expected = "IrreplaceableTransaction")]
fn test_bump_fee_irreplaceable_tx() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, mut details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
let txid = tx.txid();
details.transaction = Some(tx);
wallet.database.borrow_mut().set_tx(&details).unwrap();
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
#[test]
#[should_panic(expected = "TransactionConfirmed")]
fn test_bump_fee_confirmed_tx() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, mut details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
let txid = tx.txid();
details.transaction = Some(tx);
details.confirmation_time = Some(BlockTime {
timestamp: 12345678,
height: 42,
});
wallet.database.borrow_mut().set_tx(&details).unwrap();
wallet.build_fee_bump(txid).unwrap().finish().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_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf();
let (psbt, mut details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
let txid = tx.txid();
details.transaction = Some(tx);
wallet.database.borrow_mut().set_tx(&details).unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(1.0));
builder.finish().unwrap();
}
#[test]
#[should_panic(expected = "FeeTooLow")]
fn test_bump_fee_low_abs() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf();
let (psbt, mut details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
let txid = tx.txid();
details.transaction = Some(tx);
wallet.database.borrow_mut().set_tx(&details).unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(10);
builder.finish().unwrap();
}
#[test]
#[should_panic(expected = "FeeTooLow")]
fn test_bump_fee_zero_abs() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf();
let (psbt, mut details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
let txid = tx.txid();
details.transaction = Some(tx);
wallet.database.borrow_mut().set_tx(&details).unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(0);
builder.finish().unwrap();
}
#[test]
fn test_bump_fee_reduce_change() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent);
assert_eq!(
details.received + details.fee.unwrap_or(0),
original_details.received + original_details.fee.unwrap_or(0)
);
assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0));
let tx = &psbt.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, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
}
#[test]
fn test_bump_fee_absolute_reduce_change() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(200);
builder.enable_rbf();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent);
assert_eq!(
details.received + details.fee.unwrap_or(0),
original_details.received + original_details.fee.unwrap_or(0)
);
assert!(
details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0),
"{} > {}",
details.fee.unwrap_or(0),
original_details.fee.unwrap_or(0)
);
let tx = &psbt.unsigned_tx;
assert_eq!(tx.output.len(), 2);
assert_eq!(
tx.output
.iter()
.find(|txout| txout.script_pubkey == addr.script_pubkey())
.unwrap()
.value,
25_000
);
assert_eq!(
tx.output
.iter()
.find(|txout| txout.script_pubkey != addr.script_pubkey())
.unwrap()
.value,
details.received
);
assert_eq!(details.fee.unwrap_or(0), 200);
}
#[test]
fn test_bump_fee_reduce_single_recipient() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.fee_rate(FeeRate::from_sat_per_vb(2.5))
.allow_shrinking(addr.script_pubkey())
.unwrap();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent);
assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0));
let tx = &psbt.unsigned_tx;
assert_eq!(tx.output.len(), 1);
assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent);
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
}
#[test]
fn test_bump_fee_absolute_reduce_single_recipient() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.allow_shrinking(addr.script_pubkey())
.unwrap()
.fee_absolute(300);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent);
assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0));
let tx = &psbt.unsigned_tx;
assert_eq!(tx.output.len(), 1);
assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent);
assert_eq!(details.fee.unwrap_or(0), 300);
}
#[test]
fn test_bump_fee_drain_wallet() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let incoming_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let outpoint = OutPoint {
txid: incoming_txid,
vout: 0,
};
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.add_utxo(outpoint)
.unwrap()
.manually_selected_only()
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.drain_wallet()
.allow_shrinking(addr.script_pubkey())
.unwrap()
.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (_, details) = builder.finish().unwrap();
assert_eq!(details.sent, 75_000);
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_bump_fee_remove_output_manually_selected_only() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let incoming_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let outpoint = OutPoint {
txid: incoming_txid,
vout: 0,
};
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.add_utxo(outpoint)
.unwrap()
.manually_selected_only()
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.manually_selected_only()
.fee_rate(FeeRate::from_sat_per_vb(255.0));
builder.finish().unwrap();
}
#[test]
fn test_bump_fee_add_input() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.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, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
}
#[test]
fn test_bump_fee_absolute_add_input() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(6_000);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.unsigned_tx;
assert_eq!(tx.input.len(), 2);
assert_eq!(tx.output.len(), 2);
assert_eq!(
tx.output
.iter()
.find(|txout| txout.script_pubkey == addr.script_pubkey())
.unwrap()
.value,
45_000
);
assert_eq!(
tx.output
.iter()
.find(|txout| txout.script_pubkey != addr.script_pubkey())
.unwrap()
.value,
details.received
);
assert_eq!(details.fee.unwrap_or(0), 6_000);
}
#[test]
fn test_bump_fee_no_change_add_input_and_change() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let incoming_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.add_utxo(OutPoint {
txid: incoming_txid,
vout: 0,
})
.unwrap()
.manually_selected_only()
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
let (psbt, details) = builder.finish().unwrap();
let original_send_all_amount = original_details.sent - original_details.fee.unwrap_or(0);
assert_eq!(details.sent, original_details.sent + 50_000);
assert_eq!(
details.received,
75_000 - original_send_all_amount - details.fee.unwrap_or(0)
);
let tx = &psbt.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.fee.unwrap_or(0)
);
assert_fee_rate!(psbt, details.fee.unwrap_or(0), 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());
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
assert_eq!(tx.input.len(), 1);
assert_eq!(tx.output.len(), 2);
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); wallet
.database
.borrow_mut()
.del_utxo(&txin.previous_output)
.unwrap();
}
let original_tx_weight = tx.weight();
original_details.transaction = Some(tx);
wallet
.database
.borrow_mut()
.set_tx(&original_details)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap();
let new_tx_weight = original_tx_weight + Weight::from_wu(160 + 112 - 124);
let fee_abs = 50_000 + 25_000 - 45_000 - 10;
builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight));
let (psbt, details) = builder.finish().unwrap();
assert_eq!(
original_details.received,
5_000 - original_details.fee.unwrap_or(0)
);
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fee.unwrap_or(0), 30_000);
assert_eq!(details.received, 0);
let tx = &psbt.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, details.fee.unwrap_or(0), 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 = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.add_utxo(OutPoint {
txid: incoming_txid,
vout: 0,
})
.unwrap()
.fee_rate(FeeRate::from_sat_per_vb(5.0));
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.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, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
}
#[test]
fn test_bump_fee_absolute_force_add_input() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let incoming_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.add_utxo(OutPoint {
txid: incoming_txid,
vout: 0,
})
.unwrap()
.fee_absolute(250);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.unsigned_tx;
assert_eq!(tx.input.len(), 2);
assert_eq!(tx.output.len(), 2);
assert_eq!(
tx.output
.iter()
.find(|txout| txout.script_pubkey == addr.script_pubkey())
.unwrap()
.value,
45_000
);
assert_eq!(
tx.output
.iter()
.find(|txout| txout.script_pubkey != addr.script_pubkey())
.unwrap()
.value,
details.received
);
assert_eq!(details.fee.unwrap_or(0), 250);
}
#[test]
#[should_panic(expected = "InsufficientFunds")]
fn test_bump_fee_unconfirmed_inputs_only() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.drain_wallet()
.drain_to(addr.script_pubkey())
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 0)),
Some(100),
);
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(25.0));
builder.finish().unwrap();
}
#[test]
fn test_bump_fee_unconfirmed_input() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 0)),
Some(100),
);
let mut builder = wallet.build_tx();
builder
.drain_wallet()
.drain_to(addr.script_pubkey())
.enable_rbf();
let (psbt, mut original_details) = builder.finish().unwrap();
let mut tx = psbt.extract_tx();
let txid = tx.txid();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); 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 mut builder = wallet.build_fee_bump(txid).unwrap();
builder
.fee_rate(FeeRate::from_sat_per_vb(15.0))
.allow_shrinking(addr.script_pubkey())
.unwrap();
builder.finish().unwrap();
}
#[test]
fn test_fee_amount_negative_drain_val() {
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt")
.unwrap()
.assume_checked();
let fee_rate = FeeRate::from_sat_per_vb(2.01);
let incoming_txid = crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 8859 ) (@confirmations 1)),
Some(100),
);
let mut builder = wallet.build_tx();
builder
.add_recipient(send_to.script_pubkey(), 8630)
.add_utxo(OutPoint::new(incoming_txid, 0))
.unwrap()
.enable_rbf()
.fee_rate(fee_rate);
let (psbt, details) = builder.finish().unwrap();
assert!(psbt.inputs.len() == 1);
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate, @add_signature);
}
#[test]
fn test_sign_single_xprv() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2);
}
#[test]
fn test_sign_single_xprv_with_master_fingerprint_and_path() {
let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2);
}
#[test]
fn test_sign_single_xprv_bip44_path() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2);
}
#[test]
fn test_sign_single_xprv_sh_wpkh() {
let (wallet, _, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2);
}
#[test]
fn test_sign_single_wif() {
let (wallet, _, _) =
get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2);
}
#[test]
fn test_sign_single_xprv_no_hd_keypaths() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs[0].bip32_derivation.clear();
assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0);
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
let extracted = psbt.extract_tx();
assert_eq!(extracted.input[0].witness.len(), 2);
}
#[test]
fn test_include_output_redeem_witness_script() {
let (wallet, _, _) = get_funded_wallet("sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))");
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.include_output_redeem_witness_script();
let (psbt, _) = builder.finish().unwrap();
assert!(psbt
.outputs
.iter()
.any(|output| output.redeem_script.is_some() && output.witness_script.is_some()));
}
#[test]
fn test_signing_only_one_of_multiple_inputs() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 45_000)
.include_output_redeem_witness_script();
let (mut psbt, _) = builder.finish().unwrap();
let dud_input = bitcoin::psbt::Input {
witness_utxo: Some(TxOut {
value: 100_000,
script_pubkey: miniscript::Descriptor::<bitcoin::PublicKey>::from_str(
"wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)",
)
.unwrap()
.script_pubkey(),
}),
..Default::default()
};
psbt.inputs.push(dud_input);
psbt.unsigned_tx.input.push(bitcoin::TxIn::default());
let is_final = wallet
.sign(
&mut psbt,
SignOptions {
trust_witness_utxo: true,
..Default::default()
},
)
.unwrap();
assert!(
!is_final,
"shouldn't be final since we can't sign one of the inputs"
);
assert!(
psbt.inputs[0].final_script_witness.is_some(),
"should finalized input it signed"
)
}
#[test]
fn test_remove_partial_sigs_after_finalize_sign_option() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
for remove_partial_sigs in &[true, false] {
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let mut psbt = builder.finish().unwrap().0;
assert!(wallet
.sign(
&mut psbt,
SignOptions {
remove_partial_sigs: *remove_partial_sigs,
..Default::default()
},
)
.unwrap());
psbt.inputs.iter().for_each(|input| {
if *remove_partial_sigs {
assert!(input.partial_sigs.is_empty())
} else {
assert!(!input.partial_sigs.is_empty())
}
});
}
}
#[test]
fn test_try_finalize_sign_option() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
for try_finalize in &[true, false] {
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let mut psbt = builder.finish().unwrap().0;
let finalized = wallet
.sign(
&mut psbt,
SignOptions {
try_finalize: *try_finalize,
..Default::default()
},
)
.unwrap();
psbt.inputs.iter().for_each(|input| {
if *try_finalize {
assert!(finalized);
assert!(input.final_script_sig.is_some());
assert!(input.final_script_witness.is_some());
} else {
assert!(!finalized);
assert!(input.final_script_sig.is_none());
assert!(input.final_script_witness.is_none());
}
});
}
}
#[test]
fn test_sign_nonstandard_sighash() {
let sighash = EcdsaSighashType::NonePlusAnyoneCanPay;
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.sighash(sighash.into())
.drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let result = wallet.sign(&mut psbt, Default::default());
assert!(
result.is_err(),
"Signing should have failed because the TX uses non-standard sighashes"
);
assert_matches!(
result,
Err(Error::Signer(SignerError::NonStandardSighash)),
"Signing failed with the wrong error type"
);
let result = wallet.sign(
&mut psbt,
SignOptions {
allow_all_sighashes: true,
..Default::default()
},
);
assert!(result.is_ok(), "Signing should have worked");
assert!(
result.unwrap(),
"Should finalize the input since we can produce signatures"
);
let extracted = psbt.extract_tx();
assert_eq!(
*extracted.input[0].witness.to_vec()[0].last().unwrap(),
sighash.to_u32() as u8,
"The signature should have been made with the right sighash"
);
}
#[test]
fn test_unused_address() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(LastUnused).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
assert_eq!(
wallet.get_address(LastUnused).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
}
#[test]
fn test_next_unused_address() {
let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let descriptors = testutils!(@descriptors (descriptor));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Testnet,
MemoryDatabase::new(),
)
.unwrap();
assert_eq!(
wallet.get_address(LastUnused).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(100),
);
assert_eq!(
wallet.get_address(LastUnused).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
}
#[test]
fn test_peek_address_at_index() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(Peek(1)).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
assert_eq!(
wallet.get_address(Peek(0)).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
assert_eq!(
wallet.get_address(Peek(2)).unwrap().to_string(),
"tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
}
#[test]
fn test_peek_address_at_index_not_derivable() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(Peek(1)).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
assert_eq!(
wallet.get_address(Peek(0)).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
assert_eq!(
wallet.get_address(Peek(2)).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
}
#[test]
fn test_reset_address_index() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2"
);
assert_eq!(
wallet.get_address(Reset(1)).unwrap().to_string(),
"tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7"
);
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
"tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2"
);
}
#[test]
fn test_returns_index_and_address() {
let db = MemoryDatabase::new();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 0,
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 1,
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
assert_eq!(
wallet.get_address(Peek(25)).unwrap(),
AddressInfo {
index: 25,
address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
assert_eq!(
wallet.get_address(Reset(1)).unwrap(),
AddressInfo {
index: 1,
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
assert_eq!(
wallet.get_address(New).unwrap(),
AddressInfo {
index: 2,
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
}
#[test]
fn test_sending_to_bip350_bech32m_address() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let addr =
Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 45_000);
builder.finish().unwrap();
}
#[test]
fn test_get_address() {
use crate::descriptor::template::Bip84;
let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let wallet = Wallet::new(
Bip84(key, KeychainKind::External),
Some(Bip84(key, KeychainKind::Internal)),
Network::Regtest,
MemoryDatabase::default(),
)
.unwrap();
assert_eq!(
wallet.get_address(AddressIndex::New).unwrap(),
AddressInfo {
index: 0,
address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w")
.unwrap()
.assume_checked(),
keychain: KeychainKind::External,
}
);
assert_eq!(
wallet.get_internal_address(AddressIndex::New).unwrap(),
AddressInfo {
index: 0,
address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79")
.unwrap()
.assume_checked(),
keychain: KeychainKind::Internal,
}
);
let wallet = Wallet::new(
Bip84(key, KeychainKind::External),
None,
Network::Regtest,
MemoryDatabase::default(),
)
.unwrap();
assert_eq!(
wallet.get_internal_address(AddressIndex::New).unwrap(),
AddressInfo {
index: 0,
address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w")
.unwrap()
.assume_checked(),
keychain: KeychainKind::Internal,
},
"when there's no internal descriptor it should just use external"
);
}
#[test]
fn test_get_address_no_reuse_single_descriptor() {
use crate::descriptor::template::Bip84;
use std::collections::HashSet;
let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let wallet = Wallet::new(
Bip84(key, KeychainKind::External),
None,
Network::Regtest,
MemoryDatabase::default(),
)
.unwrap();
let mut used_set = HashSet::new();
(0..3).for_each(|_| {
let external_addr = wallet.get_address(AddressIndex::New).unwrap().address;
assert!(used_set.insert(external_addr));
let internal_addr = wallet
.get_internal_address(AddressIndex::New)
.unwrap()
.address;
assert!(used_set.insert(internal_addr));
});
}
#[test]
fn test_taproot_psbt_populate_tap_key_origins() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.inputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>(),
vec![(
from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"),
(vec![], (from_str!("f6a5cb8b"), from_str!("m/0")))
)],
"Wrong input tap_key_origins"
);
assert_eq!(
psbt.outputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>(),
vec![(
from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"),
(vec![], (from_str!("f6a5cb8b"), from_str!("m/1")))
)],
"Wrong output tap_key_origins"
);
}
#[test]
fn test_taproot_psbt_populate_tap_key_origins_repeated_key() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let path = vec![("rn4nre9c".to_string(), vec![0])]
.into_iter()
.collect();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), 25_000)
.policy_path(path, KeychainKind::External);
let (psbt, _) = builder.finish().unwrap();
let mut input_key_origins = psbt.inputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>();
input_key_origins.sort();
assert_eq!(
input_key_origins,
vec![
(
from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"),
(
vec![
from_str!(
"858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e"
),
from_str!(
"f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903"
),
],
(FromStr::from_str("ece52657").unwrap(), vec![].into())
)
),
(
from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"),
(
vec![],
(FromStr::from_str("871fd295").unwrap(), vec![].into())
)
),
],
"Wrong input tap_key_origins"
);
let mut output_key_origins = psbt.outputs[0]
.tap_key_origins
.clone()
.into_iter()
.collect::<Vec<_>>();
output_key_origins.sort();
assert_eq!(
input_key_origins, output_key_origins,
"Wrong output tap_key_origins"
);
}
#[test]
fn test_taproot_psbt_input_tap_tree() {
use bitcoin::hashes::hex::FromHex;
use bitcoin::taproot;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree());
let addr = wallet.get_address(AddressIndex::Peek(0)).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (psbt, _) = builder.finish().unwrap();
assert_eq!(
psbt.inputs[0].tap_merkle_root,
Some(
taproot::TapNodeHash::from_str(
"61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986"
)
.unwrap()
),
);
assert_eq!(
psbt.inputs[0].tap_scripts.clone().into_iter().collect::<Vec<_>>(),
vec![
(taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (ScriptBuf::from_hex("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac").unwrap(), taproot::LeafVersion::TapScript)),
(taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (ScriptBuf::from_hex("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac").unwrap(), taproot::LeafVersion::TapScript)),
],
);
assert_eq!(
psbt.inputs[0].tap_internal_key,
Some(from_str!(
"b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"
))
);
assert_eq!(
psbt.inputs[0].tap_internal_key,
psbt.outputs[0].tap_internal_key
);
let tap_tree: bitcoin::taproot::TapTree = serde_json::from_str(r#"[1,{"Script":["2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",192]},1,{"Script":["208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac",192]}]"#).unwrap();
assert_eq!(psbt.outputs[0].tap_tree, Some(tap_tree));
}
#[test]
fn test_taproot_sign_missing_witness_utxo() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let witness_utxo = psbt.inputs[0].witness_utxo.take();
let result = wallet.sign(
&mut psbt,
SignOptions {
allow_all_sighashes: true,
..Default::default()
},
);
assert_matches!(
result,
Err(Error::Signer(SignerError::MissingWitnessUtxo)),
"Signing should have failed with the correct error because the witness_utxo is missing"
);
psbt.inputs[0].witness_utxo = witness_utxo;
let result = wallet.sign(
&mut psbt,
SignOptions {
allow_all_sighashes: true,
..Default::default()
},
);
assert_matches!(
result,
Ok(true),
"Should finalize the input since we can produce signatures"
);
}
#[test]
fn test_taproot_sign_using_non_witness_utxo() {
let (wallet, _, prev_txid) = get_funded_wallet(get_test_tr_single_sig());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
psbt.inputs[0].witness_utxo = None;
psbt.inputs[0].non_witness_utxo = wallet.database().get_raw_tx(&prev_txid).unwrap();
assert!(
psbt.inputs[0].non_witness_utxo.is_some(),
"Previous tx should be present in the database"
);
let result = wallet.sign(&mut psbt, Default::default());
assert!(result.is_ok(), "Signing should have worked");
assert!(
result.unwrap(),
"Should finalize the input since we can produce signatures"
);
}
#[test]
fn test_taproot_foreign_utxo() {
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
let (wallet2, _, _) = get_funded_wallet(get_test_tr_single_sig());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let utxo = wallet2.list_unspent().unwrap().remove(0);
let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap();
#[allow(deprecated)]
let foreign_utxo_satisfaction = wallet2
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
.unwrap();
assert!(
psbt_input.non_witness_utxo.is_none(),
"`non_witness_utxo` should never be populated for taproot"
);
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.fee.unwrap_or(0),
"we should have only net spent ~10_000"
);
assert!(
psbt.unsigned_tx
.input
.iter()
.any(|input| input.previous_output == utxo.outpoint),
"foreign_utxo should be in there"
);
}
fn test_spend_from_wallet(wallet: Wallet<AnyDatabase>) {
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
assert!(
wallet.sign(&mut psbt, Default::default()).unwrap(),
"Unable to finalize tx"
);
}
#[test]
fn test_taproot_key_spend() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig());
test_spend_from_wallet(wallet);
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
test_spend_from_wallet(wallet);
}
#[test]
fn test_taproot_no_key_spend() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
assert!(
wallet
.sign(
&mut psbt,
SignOptions {
sign_with_tap_internal_key: false,
..Default::default()
},
)
.unwrap(),
"Unable to finalize tx"
);
assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none()));
}
#[test]
fn test_taproot_script_spend() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree());
test_spend_from_wallet(wallet);
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_xprv());
test_spend_from_wallet(wallet);
}
#[test]
fn test_taproot_script_spend_sign_all_leaves() {
use crate::signer::TapLeavesOptions;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
assert!(
wallet
.sign(
&mut psbt,
SignOptions {
tap_leaves_options: TapLeavesOptions::All,
..Default::default()
},
)
.unwrap(),
"Unable to finalize tx"
);
assert!(psbt
.inputs
.iter()
.all(|i| i.tap_script_sigs.len() == i.tap_scripts.len()));
}
#[test]
fn test_taproot_script_spend_sign_include_some_leaves() {
use crate::signer::TapLeavesOptions;
use bitcoin::taproot::TapLeafHash;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
let mut script_leaves: Vec<_> = psbt.inputs[0]
.tap_scripts
.clone()
.values()
.map(|(script, version)| TapLeafHash::from_script(script, *version))
.collect();
let included_script_leaves = vec![script_leaves.pop().unwrap()];
let excluded_script_leaves = script_leaves;
assert!(
wallet
.sign(
&mut psbt,
SignOptions {
tap_leaves_options: TapLeavesOptions::Include(
included_script_leaves.clone()
),
..Default::default()
},
)
.unwrap(),
"Unable to finalize tx"
);
assert!(psbt.inputs[0]
.tap_script_sigs
.iter()
.all(|s| included_script_leaves.contains(&s.0 .1)
&& !excluded_script_leaves.contains(&s.0 .1)));
}
#[test]
fn test_taproot_script_spend_sign_exclude_some_leaves() {
use crate::signer::TapLeavesOptions;
use bitcoin::taproot::TapLeafHash;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
let mut script_leaves: Vec<_> = psbt.inputs[0]
.tap_scripts
.clone()
.values()
.map(|(script, version)| TapLeafHash::from_script(script, *version))
.collect();
let included_script_leaves = vec![script_leaves.pop().unwrap()];
let excluded_script_leaves = script_leaves;
assert!(
wallet
.sign(
&mut psbt,
SignOptions {
tap_leaves_options: TapLeavesOptions::Exclude(
excluded_script_leaves.clone()
),
..Default::default()
},
)
.unwrap(),
"Unable to finalize tx"
);
assert!(psbt.inputs[0]
.tap_script_sigs
.iter()
.all(|s| included_script_leaves.contains(&s.0 .1)
&& !excluded_script_leaves.contains(&s.0 .1)));
}
#[test]
fn test_taproot_script_spend_sign_no_leaves() {
use crate::signer::TapLeavesOptions;
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
wallet
.sign(
&mut psbt,
SignOptions {
tap_leaves_options: TapLeavesOptions::None,
..Default::default()
},
)
.unwrap();
assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty()));
}
#[test]
fn test_taproot_sign_derive_index_from_psbt() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
let addr = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 25_000);
let (mut psbt, _) = builder.finish().unwrap();
let wallet_empty = Wallet::new(
get_test_tr_single_sig_xprv(),
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
assert!(
wallet_empty.sign(&mut psbt, Default::default()).unwrap(),
"Unable to finalize tx"
);
}
#[test]
fn test_taproot_sign_explicit_sighash_all() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.sighash(TapSighashType::All.into())
.drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let result = wallet.sign(&mut psbt, Default::default());
assert!(
result.is_ok(),
"Signing should work because SIGHASH_ALL is safe"
)
}
#[test]
fn test_taproot_sign_non_default_sighash() {
let sighash = TapSighashType::NonePlusAnyoneCanPay;
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.sighash(sighash.into())
.drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let witness_utxo = psbt.inputs[0].witness_utxo.take();
let result = wallet.sign(&mut psbt, Default::default());
assert!(
result.is_err(),
"Signing should have failed because the TX uses non-standard sighashes"
);
assert_matches!(
result,
Err(Error::Signer(SignerError::NonStandardSighash)),
"Signing failed with the wrong error type"
);
let result = wallet.sign(
&mut psbt,
SignOptions {
allow_all_sighashes: true,
..Default::default()
},
);
assert!(
result.is_err(),
"Signing should have failed because the witness_utxo is missing"
);
assert_matches!(
result,
Err(Error::Signer(SignerError::MissingWitnessUtxo)),
"Signing failed with the wrong error type"
);
psbt.inputs[0].witness_utxo = witness_utxo;
let result = wallet.sign(
&mut psbt,
SignOptions {
allow_all_sighashes: true,
..Default::default()
},
);
assert!(result.is_ok(), "Signing should have worked");
assert!(
result.unwrap(),
"Should finalize the input since we can produce signatures"
);
let extracted = psbt.extract_tx();
assert_eq!(
*extracted.input[0].witness.to_vec()[0].last().unwrap(),
sighash as u8,
"The signature should have been made with the right sighash"
);
}
#[test]
fn test_spend_coinbase() {
let descriptors = testutils!(@descriptors (get_test_wpkh()));
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
AnyDatabase::Memory(MemoryDatabase::new()),
)
.unwrap();
let confirmation_time = 5;
crate::populate_test_db!(
wallet.database.borrow_mut(),
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
Some(confirmation_time),
(@coinbase true)
);
let sync_time = SyncTime {
block_time: BlockTime {
height: confirmation_time,
timestamp: 0,
},
};
wallet
.database
.borrow_mut()
.set_sync_time(sync_time)
.unwrap();
let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1;
let maturity_time = confirmation_time + COINBASE_MATURITY;
let balance = wallet.get_balance().unwrap();
assert_eq!(
balance,
Balance {
immature: 25_000,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 0
}
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance.immature / 2)
.current_height(confirmation_time);
assert_matches!(
builder.finish(),
Err(Error::InsufficientFunds {
needed: _,
available: 0
})
);
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance.immature / 2)
.current_height(not_yet_mature_time);
assert_matches!(
builder.finish(),
Err(Error::InsufficientFunds {
needed: _,
available: 0
})
);
let sync_time = SyncTime {
block_time: BlockTime {
height: maturity_time,
timestamp: 0,
},
};
wallet
.database
.borrow_mut()
.set_sync_time(sync_time)
.unwrap();
let balance = wallet.get_balance().unwrap();
assert_eq!(
balance,
Balance {
immature: 0,
trusted_pending: 0,
untrusted_pending: 0,
confirmed: 25_000
}
);
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), balance.confirmed / 2)
.current_height(maturity_time);
builder.finish().unwrap();
}
#[test]
fn test_allow_dust_limit() {
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New).unwrap();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), 0);
assert_matches!(builder.finish(), Err(Error::OutputBelowDustLimit(0)));
let mut builder = wallet.build_tx();
builder
.allow_dust(true)
.add_recipient(addr.script_pubkey(), 0);
assert!(builder.finish().is_ok());
}
#[test]
fn test_fee_rate_sign_no_grinding_high_r() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let fee_rate = FeeRate::from_sat_per_vb(1.0);
let mut builder = wallet.build_tx();
let data: &PushBytes = From::<&[u8; 1]>::from(&[0; 1]);
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_rate(fee_rate)
.add_data(&data);
let (mut psbt, details) = builder.finish().unwrap();
let (op_return_vout, _) = psbt
.unsigned_tx
.output
.iter()
.enumerate()
.find(|(_n, i)| i.script_pubkey.is_op_return())
.unwrap();
let mut sig_len: usize = 0;
while sig_len < 71 {
let data: &PushBytes = From::<&[u8; 1]>::from(&[1; 1]);
psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data);
psbt.inputs[0].partial_sigs.clear();
wallet
.sign(
&mut psbt,
SignOptions {
remove_partial_sigs: false,
try_finalize: false,
allow_grinding: false,
..Default::default()
},
)
.unwrap();
let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
}
wallet
.sign(
&mut psbt,
SignOptions {
remove_partial_sigs: false,
allow_grinding: false,
..Default::default()
},
)
.unwrap();
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
}
#[test]
fn test_fee_rate_sign_grinding_low_r() {
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
let addr = wallet.get_address(New).unwrap();
let fee_rate = FeeRate::from_sat_per_vb(1.0);
let mut builder = wallet.build_tx();
builder
.drain_to(addr.script_pubkey())
.drain_wallet()
.fee_rate(fee_rate);
let (mut psbt, details) = builder.finish().unwrap();
wallet
.sign(
&mut psbt,
SignOptions {
remove_partial_sigs: false,
allow_grinding: true,
..Default::default()
},
)
.unwrap();
let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
let sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
assert_eq!(sig_len, 70);
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
}
#[cfg(feature = "test-hardware-signer")]
#[test]
fn test_create_signer() {
use crate::wallet::hardwaresigner::HWISigner;
use hwi::HWIClient;
let mut devices = HWIClient::enumerate().unwrap();
if devices.is_empty() {
panic!("No devices found!");
}
let device = devices.remove(0).unwrap();
let client = HWIClient::get_client(&device, true, Network::Regtest.into()).unwrap();
let descriptors = client.get_descriptors::<String>(None).unwrap();
let custom_signer = HWISigner::from_device(&device, Network::Regtest.into()).unwrap();
let (mut wallet, _, _) = get_funded_wallet(&descriptors.internal[0]);
wallet.add_signer(
KeychainKind::External,
SignerOrdering(200),
Arc::new(custom_signer),
);
let addr = wallet.get_address(LastUnused).unwrap();
let mut builder = wallet.build_tx();
builder.drain_to(addr.script_pubkey()).drain_wallet();
let (mut psbt, _) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);
}
#[test]
fn test_taproot_load_descriptor_duplicated_keys() {
let (wallet, _, _) = get_funded_wallet(get_test_tr_dup_keys());
let addr = wallet.get_address(New).unwrap();
assert_eq!(
addr.to_string(),
"bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5"
);
}
}