From b4c31cd5bad4fea18044aab2cffd657b16ec185b Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 26 Jul 2023 19:46:40 -0500 Subject: [PATCH 1/6] feat(wallet)!: remove TransactionDetails from bdk::Wallet API Added - Wallet::sent_and_received function - Wallet::calculate_fee and Wallet::calculate_fee_rate functions - Wallet::error::CalculateFeeError BREAKING CHANGES: Removed - TransactionDetails struct Changed - Wallet::get_tx now returns CanonicalTx instead of TransactionDetails - TxBuilder::finish now returns only a PartiallySignedTransaction --- crates/bdk/src/error.rs | 14 + crates/bdk/src/keys/mod.rs | 2 +- crates/bdk/src/lib.rs | 2 +- crates/bdk/src/types.rs | 38 +- crates/bdk/src/wallet/coin_selection.rs | 2 +- crates/bdk/src/wallet/mod.rs | 132 ++--- crates/bdk/src/wallet/tx_builder.rs | 15 +- crates/bdk/tests/common.rs | 72 ++- crates/bdk/tests/psbt.rs | 16 +- crates/bdk/tests/wallet.rs | 509 +++++++++++------- example-crates/wallet_electrum/src/main.rs | 2 +- .../wallet_esplora_async/src/main.rs | 2 +- .../wallet_esplora_blocking/src/main.rs | 2 +- 13 files changed, 445 insertions(+), 363 deletions(-) diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs index fcb5a6f7b..f0e33fea6 100644 --- a/crates/bdk/src/error.rs +++ b/crates/bdk/src/error.rs @@ -9,6 +9,10 @@ // You may not use this file except in accordance with one or both of these // licenses. +//! Errors +//! +//! This module defines the errors that can be thrown by [`crate`] functions. + use crate::bitcoin::Network; use crate::{descriptor, wallet}; use alloc::{string::String, vec::Vec}; @@ -89,7 +93,17 @@ pub enum Error { Psbt(bitcoin::psbt::Error), } +/// Errors returned by `Wallet::calculate_fee`. +#[derive(Debug)] +pub enum CalculateFeeError { + /// Missing `TxOut` for one of the inputs of the tx + MissingTxOut, + /// When the transaction is invalid according to the graph it has a negative fee + NegativeFee(i64), +} + /// Errors returned by miniscript when updating inconsistent PSBTs +#[allow(missing_docs)] // TODO add docs #[derive(Debug, Clone)] pub enum MiniscriptPsbtError { Conversion(miniscript::descriptor::ConversionError), diff --git a/crates/bdk/src/keys/mod.rs b/crates/bdk/src/keys/mod.rs index c91625932..b47c4b86d 100644 --- a/crates/bdk/src/keys/mod.rs +++ b/crates/bdk/src/keys/mod.rs @@ -754,7 +754,7 @@ fn expand_multi_keys, Ctx: ScriptContext>( let (key_map, valid_networks) = key_maps_networks.into_iter().fold( (KeyMap::default(), any_network()), |(mut keys_acc, net_acc), (key, net)| { - keys_acc.extend(key.into_iter()); + keys_acc.extend(key); let net_acc = merge_networks(&net_acc, &net); (keys_acc, net_acc) diff --git a/crates/bdk/src/lib.rs b/crates/bdk/src/lib.rs index 012a868a6..93ed400b1 100644 --- a/crates/bdk/src/lib.rs +++ b/crates/bdk/src/lib.rs @@ -29,7 +29,7 @@ extern crate bip39; #[allow(unused_imports)] #[macro_use] -pub(crate) mod error; +pub mod error; pub mod descriptor; pub mod keys; pub mod psbt; diff --git a/crates/bdk/src/types.rs b/crates/bdk/src/types.rs index 1b96ef0ff..2a88cc374 100644 --- a/crates/bdk/src/types.rs +++ b/crates/bdk/src/types.rs @@ -14,8 +14,8 @@ use core::convert::AsRef; use core::ops::Sub; use bdk_chain::ConfirmationTime; -use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; -use bitcoin::{hash_types::Txid, psbt, Weight}; +use bitcoin::blockdata::transaction::{OutPoint, TxOut}; +use bitcoin::{psbt, Weight}; use serde::{Deserialize, Serialize}; @@ -234,40 +234,6 @@ impl Utxo { } } -/// A wallet transaction -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct TransactionDetails { - /// Optional transaction - pub transaction: Option, - /// Transaction id - pub txid: Txid, - /// Received value (sats) - /// Sum of owned outputs of this transaction. - pub received: u64, - /// Sent value (sats) - /// Sum of owned inputs of this transaction. - pub sent: u64, - /// Fee value in sats if it was available. - pub fee: Option, - /// If the transaction is confirmed, contains height and Unix timestamp of the block containing the - /// transaction, unconfirmed transaction contains `None`. - pub confirmation_time: ConfirmationTime, -} - -impl PartialOrd for TransactionDetails { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for TransactionDetails { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.confirmation_time - .cmp(&other.confirmation_time) - .then_with(|| self.txid.cmp(&other.txid)) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index 6f30fd14f..c3e84af2b 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -86,7 +86,7 @@ //! .unwrap() //! .require_network(Network::Testnet) //! .unwrap(); -//! let (psbt, details) = { +//! let psbt = { //! let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything); //! builder.add_recipient(to_address.script_pubkey(), 50_000); //! builder.finish()? diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 1ca78a775..c939db1c8 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -66,7 +66,7 @@ use crate::descriptor::{ calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, }; -use crate::error::{Error, MiniscriptPsbtError}; +use crate::error::{CalculateFeeError, Error, MiniscriptPsbtError}; use crate::psbt::PsbtUtils; use crate::signer::SignerError; use crate::types::*; @@ -430,27 +430,52 @@ impl Wallet { .next() } - /// Return a single transactions made and received by the wallet + /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. /// - /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if - /// `include_raw` is `true`. - pub fn get_tx(&self, txid: Txid, include_raw: bool) -> Option { + /// Note `tx` does not have to be in the graph for this to work. + pub fn calculate_fee(&self, tx: &Transaction) -> Result { + match self.indexed_graph.graph().calculate_fee(tx) { + None => Err(CalculateFeeError::MissingTxOut), + Some(fee) if fee < 0 => Err(CalculateFeeError::NegativeFee(fee)), + Some(fee) => Ok(u64::try_from(fee).unwrap()), + } + } + + /// Calculate the `FeeRate` for a given transaction. + /// + /// Note `tx` does not have to be in the graph for this to work. + pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result { + self.calculate_fee(tx).map(|fee| { + let weight = tx.weight(); + FeeRate::from_wu(fee, weight) + }) + } + + /// Computes total input value going from script pubkeys in the index (sent) and the total output + /// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed + /// correctly, the output being spent must have already been scanned by the index. Calculating + /// received just uses the transaction outputs directly, so it will be correct even if it has not + /// been scanned. + pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) { + self.indexed_graph.index.sent_and_received(tx) + } + + /// Return a single `CanonicalTx` made and received by the wallet or `None` if it doesn't + /// exist in the wallet + pub fn get_tx( + &self, + txid: Txid, + ) -> Option> { let graph = self.indexed_graph.graph(); - let canonical_tx = CanonicalTx { + Some(CanonicalTx { chain_position: graph.get_chain_position( &self.chain, self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), txid, )?, tx_node: graph.get_tx_node(txid)?, - }; - - Some(new_tx_details( - &self.indexed_graph, - canonical_tx, - include_raw, - )) + }) } /// Add a new checkpoint to the wallet's internal view of the chain. @@ -603,7 +628,7 @@ impl Wallet { /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let (psbt, details) = { + /// let psbt = { /// let mut builder = wallet.build_tx(); /// builder /// .add_recipient(to_address.script_pubkey(), 50_000); @@ -628,7 +653,7 @@ impl Wallet { &mut self, coin_selection: Cs, params: TxParams, - ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> + ) -> Result where D: PersistBackend, { @@ -976,20 +1001,8 @@ impl Wallet { // sort input/outputs according to the chosen algorithm 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: ConfirmationTime::Unconfirmed { last_seen: 0 }, - received, - sent, - fee: Some(fee_amount), - }; - - Ok((psbt, transaction_details)) + Ok(psbt) } /// Bump the fee of a transaction previously created with this wallet. @@ -1008,7 +1021,7 @@ impl Wallet { /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let (mut psbt, _) = { + /// let mut psbt = { /// let mut builder = wallet.build_tx(); /// builder /// .add_recipient(to_address.script_pubkey(), 50_000) @@ -1018,7 +1031,7 @@ impl Wallet { /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; /// let tx = psbt.extract_tx(); /// // broadcast tx but it's taking too long to confirm so we want to bump the fee - /// let (mut psbt, _) = { + /// let mut psbt = { /// let mut builder = wallet.build_fee_bump(tx.txid())?; /// builder /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)); @@ -1179,7 +1192,7 @@ impl Wallet { /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let (mut psbt, _) = { + /// let mut psbt = { /// let mut builder = wallet.build_tx(); /// builder.add_recipient(to_address.script_pubkey(), 50_000); /// builder.finish()? @@ -1735,7 +1748,7 @@ impl Wallet { Ok(()) } - /// Commits all curently [`staged`] changed to the persistence backend returning and error when + /// Commits all currently [`staged`] changed to the persistence backend returning and error when /// this fails. /// /// This returns whether the `update` resulted in any changes. @@ -1826,61 +1839,6 @@ fn new_local_utxo( } } -fn new_tx_details( - indexed_graph: &IndexedTxGraph>, - canonical_tx: CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>, - include_raw: bool, -) -> TransactionDetails { - let graph = indexed_graph.graph(); - let index = &indexed_graph.index; - let tx = canonical_tx.tx_node.tx; - - let received = tx - .output - .iter() - .map(|txout| { - if index.index_of_spk(&txout.script_pubkey).is_some() { - txout.value - } else { - 0 - } - }) - .sum(); - - let sent = tx - .input - .iter() - .map(|txin| { - if let Some((_, txout)) = index.txout(txin.previous_output) { - txout.value - } else { - 0 - } - }) - .sum(); - - let inputs = tx - .input - .iter() - .map(|txin| { - graph - .get_txout(txin.previous_output) - .map(|txout| txout.value) - }) - .sum::>(); - let outputs = tx.output.iter().map(|txout| txout.value).sum(); - let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); - - TransactionDetails { - transaction: if include_raw { Some(tx.clone()) } else { None }, - txid: canonical_tx.tx_node.txid, - received, - sent, - fee, - confirmation_time: canonical_tx.chain_position.cloned().into(), - } -} - #[macro_export] #[doc(hidden)] /// Macro for getting a wallet for use in a doctest diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index d7bcd7113..4ccb74d9f 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -32,7 +32,7 @@ //! .do_not_spend_change() //! // Turn on RBF signaling //! .enable_rbf(); -//! let (psbt, tx_details) = tx_builder.finish()?; +//! let psbt = tx_builder.finish()?; //! # Ok::<(), bdk::Error>(()) //! ``` @@ -48,10 +48,7 @@ use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transa use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::ChangeSet; -use crate::{ - types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}, - TransactionDetails, -}; +use crate::types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}; use crate::{Error, Utxo, Wallet}; /// Context in which the [`TxBuilder`] is valid pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} @@ -85,7 +82,7 @@ impl TxBuilderContext for BumpFee {} /// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let addr2 = addr1.clone(); /// // chaining -/// let (psbt1, details) = { +/// let psbt1 = { /// let mut builder = wallet.build_tx(); /// builder /// .ordering(TxOrdering::Untouched) @@ -95,7 +92,7 @@ impl TxBuilderContext for BumpFee {} /// }; /// /// // non-chaining -/// let (psbt2, details) = { +/// let psbt2 = { /// let mut builder = wallet.build_tx(); /// builder.ordering(TxOrdering::Untouched); /// for addr in &[addr1, addr2] { @@ -531,7 +528,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// Returns the [`BIP174`] "PSBT" and summary details about the transaction. /// /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki - pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> + pub fn finish(self) -> Result where D: PersistBackend, { @@ -645,7 +642,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { /// .drain_to(to_address.script_pubkey()) /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)) /// .enable_rbf(); - /// let (psbt, tx_details) = tx_builder.finish()?; + /// let psbt = tx_builder.finish()?; /// # Ok::<(), bdk::Error>(()) /// ``` /// diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index 002724d7f..65e5bc3e7 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -1,8 +1,11 @@ #![allow(unused)] -use bdk::{wallet::AddressIndex, Wallet}; + +use bdk::{wallet::AddressIndex, KeychainKind, LocalUtxo, Wallet}; +use bdk_chain::indexed_tx_graph::Indexer; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; -use bitcoin::{BlockHash, Network, Transaction, TxOut}; +use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid}; +use std::str::FromStr; /// Return a fake wallet that appears to be funded for testing. pub fn get_funded_wallet_with_change( @@ -10,16 +13,52 @@ pub fn get_funded_wallet_with_change( change: Option<&str>, ) -> (Wallet, bitcoin::Txid) { let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap(); - let address = wallet.get_address(AddressIndex::New).address; + let change_address = wallet.get_address(AddressIndex::New).address; + let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") + .expect("address") + .require_network(Network::Regtest) + .unwrap(); - let tx = Transaction { + let tx0 = Transaction { version: 1, lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], + input: vec![TxIn { + previous_output: OutPoint { + txid: Txid::all_zeros(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], output: vec![TxOut { - value: 50_000, - script_pubkey: address.script_pubkey(), + value: 76_000, + script_pubkey: change_address.script_pubkey(), + }], + }; + + let tx1 = Transaction { + version: 1, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: tx0.txid(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), }], + output: vec![ + TxOut { + value: 50_000, + script_pubkey: change_address.script_pubkey(), + }, + TxOut { + value: 25_000, + script_pubkey: sendto_address.script_pubkey(), + }, + ], }; wallet @@ -28,17 +67,32 @@ pub fn get_funded_wallet_with_change( hash: BlockHash::all_zeros(), }) .unwrap(); + wallet + .insert_checkpoint(BlockId { + height: 2_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); wallet .insert_tx( - tx.clone(), + tx0, ConfirmationTime::Confirmed { height: 1_000, time: 100, }, ) .unwrap(); + wallet + .insert_tx( + tx1.clone(), + ConfirmationTime::Confirmed { + height: 2_000, + time: 200, + }, + ) + .unwrap(); - (wallet, tx.txid()) + (wallet, tx1.txid()) } pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { diff --git a/crates/bdk/tests/psbt.rs b/crates/bdk/tests/psbt.rs index 22e34f9de..602c37dbd 100644 --- a/crates/bdk/tests/psbt.rs +++ b/crates/bdk/tests/psbt.rs @@ -18,7 +18,7 @@ fn test_psbt_malformed_psbt_input_legacy() { let send_to = wallet.get_address(AddressIndex::New); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); psbt.inputs.push(psbt_bip.inputs[0].clone()); let options = SignOptions { trust_witness_utxo: true, @@ -35,7 +35,7 @@ fn test_psbt_malformed_psbt_input_segwit() { let send_to = wallet.get_address(AddressIndex::New); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); psbt.inputs.push(psbt_bip.inputs[1].clone()); let options = SignOptions { trust_witness_utxo: true, @@ -51,7 +51,7 @@ fn test_psbt_malformed_tx_input() { let send_to = wallet.get_address(AddressIndex::New); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); psbt.unsigned_tx.input.push(TxIn::default()); let options = SignOptions { trust_witness_utxo: true, @@ -67,7 +67,7 @@ fn test_psbt_sign_with_finalized() { let send_to = wallet.get_address(AddressIndex::New); let mut builder = wallet.build_tx(); builder.add_recipient(send_to.script_pubkey(), 10_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); // add a finalized input psbt.inputs.push(psbt_bip.inputs[0].clone()); @@ -89,7 +89,7 @@ fn test_psbt_fee_rate_with_witness_utxo() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let fee_amount = psbt.fee_amount(); assert!(fee_amount.is_some()); @@ -114,7 +114,7 @@ fn test_psbt_fee_rate_with_nonwitness_utxo() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let fee_amount = psbt.fee_amount(); assert!(fee_amount.is_some()); let unfinalized_fee_rate = psbt.fee_rate().unwrap(); @@ -138,7 +138,7 @@ fn test_psbt_fee_rate_with_missing_txout() { let mut builder = wpkh_wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut wpkh_psbt, _) = builder.finish().unwrap(); + let mut wpkh_psbt = builder.finish().unwrap(); wpkh_psbt.inputs[0].witness_utxo = None; wpkh_psbt.inputs[0].non_witness_utxo = None; @@ -150,7 +150,7 @@ fn test_psbt_fee_rate_with_missing_txout() { let mut builder = pkh_wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate)); - let (mut pkh_psbt, _) = builder.finish().unwrap(); + let mut pkh_psbt = builder.finish().unwrap(); pkh_psbt.inputs[0].non_witness_utxo = None; assert!(pkh_psbt.fee_amount().is_none()); diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 14167ba8d..8ef921e12 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -1,24 +1,22 @@ use assert_matches::assert_matches; use bdk::descriptor::calc_checksum; +use bdk::psbt::PsbtUtils; use bdk::signer::{SignOptions, SignerError}; use bdk::wallet::coin_selection::LargestFirstCoinSelection; use bdk::wallet::AddressIndex::*; use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; -use bdk::Error; -use bdk::FeeRate; -use bdk::KeychainKind; -use bdk_chain::BlockId; -use bdk_chain::ConfirmationTime; +use bdk::{Error, FeeRate, KeychainKind}; use bdk_chain::COINBASE_MATURITY; +use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; -use bitcoin::BlockHash; use bitcoin::ScriptBuf; use bitcoin::{ absolute, script::PushBytesBuf, taproot::TapNodeHash, Address, OutPoint, Sequence, Transaction, TxIn, TxOut, Weight, }; use bitcoin::{psbt, Network}; +use bitcoin::{BlockHash, Txid}; use core::str::FromStr; mod common; @@ -88,6 +86,62 @@ fn test_get_funded_wallet_balance() { assert_eq!(wallet.get_balance().confirmed, 50000); } +#[test] +fn test_get_funded_wallet_sent_and_received() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + assert_eq!(wallet.get_balance().confirmed, 50000); + let mut tx_amounts: Vec<(Txid, (u64, u64))> = wallet + .transactions() + .map(|ct| (ct.node.txid, wallet.sent_and_received(ct.node.tx))) + .collect(); + tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + + assert_eq!(tx_amounts.len(), 2); + assert_matches!(tx_amounts.get(0), Some((_, (76_000, 50_000)))) +} + +#[test] +fn test_get_funded_wallet_tx_fees() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + assert_eq!(wallet.get_balance().confirmed, 50000); + let mut tx_fee_amounts: Vec<(Txid, Result)> = wallet + .transactions() + .map(|ct| { + let fee = wallet.calculate_fee(ct.node.tx); + (ct.node.txid, fee) + }) + .collect(); + tx_fee_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + + assert_eq!(tx_fee_amounts.len(), 2); + assert_matches!( + tx_fee_amounts.get(1), + Some((_, Err(bdk::error::CalculateFeeError::MissingTxOut))) + ); + assert_matches!(tx_fee_amounts.get(0), Some((_, Ok(1000)))) +} + +#[test] +fn test_get_funded_wallet_tx_fee_rate() { + let (wallet, _) = get_funded_wallet(get_test_wpkh()); + assert_eq!(wallet.get_balance().confirmed, 50000); + let mut tx_fee_rates: Vec<(Txid, Result)> = wallet + .transactions() + .map(|ct| { + let fee_rate = wallet.calculate_fee_rate(ct.node.tx); + (ct.node.txid, fee_rate) + }) + .collect(); + tx_fee_rates.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + + assert_eq!(tx_fee_rates.len(), 2); + assert_matches!( + tx_fee_rates.get(1), + Some((_, Err(bdk::error::CalculateFeeError::MissingTxOut))) + ); + assert_matches!(tx_fee_rates.get(0), Some((_, Ok(_)))) +} + 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(); @@ -195,7 +249,7 @@ fn test_create_tx_custom_version() { builder .add_recipient(addr.script_pubkey(), 25_000) .version(42); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.version, 42); } @@ -207,11 +261,11 @@ fn test_create_tx_default_locktime_is_last_sync_height() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); // Since we never synced the wallet we don't have a last_sync_height // we could use to try to prevent fee sniping. We default to 0. - assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 1_000); + assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 2_000); } #[test] @@ -221,7 +275,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); // If there's no current_height we're left with using the last sync height assert_eq!( @@ -236,7 +290,7 @@ fn test_create_tx_default_locktime_cltv() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 100_000); } @@ -250,7 +304,7 @@ fn test_create_tx_custom_locktime() { .add_recipient(addr.script_pubkey(), 25_000) .current_height(630_001) .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); // When we explicitly specify a nlocktime // we don't try any fee sniping prevention trick @@ -266,7 +320,7 @@ fn test_create_tx_custom_locktime_compatible_with_cltv() { builder .add_recipient(addr.script_pubkey(), 25_000) .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); } @@ -291,7 +345,7 @@ fn test_create_tx_no_rbf_csv() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); } @@ -304,7 +358,7 @@ fn test_create_tx_with_default_rbf_csv() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). // It will be set to the OP_CSV value, in this case 6 assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); @@ -330,7 +384,7 @@ fn test_create_tx_no_rbf_cltv() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); } @@ -355,7 +409,7 @@ fn test_create_tx_custom_rbf_sequence() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); } @@ -366,7 +420,7 @@ fn test_create_tx_default_sequence() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); } @@ -391,13 +445,11 @@ fn test_create_tx_drain_wallet_and_drain_to() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 50_000 - details.fee.unwrap_or(0) - ); + assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); } #[test] @@ -412,7 +464,8 @@ fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { .add_recipient(addr.script_pubkey(), 20_000) .drain_to(drain_addr.script_pubkey()) .drain_wallet(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); let outputs = psbt.unsigned_tx.output; assert_eq!(outputs.len(), 2); @@ -425,7 +478,7 @@ fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { .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)); + assert_eq!(drain_output.value, 30_000 - fee.unwrap_or(0)); } #[test] @@ -438,13 +491,11 @@ fn test_create_tx_drain_to_and_utxos() { .drain_to(addr.script_pubkey()) .add_utxos(&utxos) .unwrap(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); assert_eq!(psbt.unsigned_tx.output.len(), 1); - assert_eq!( - psbt.unsigned_tx.output[0].value, - 50_000 - details.fee.unwrap_or(0) - ); + assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); } #[test] @@ -463,9 +514,10 @@ fn test_create_tx_default_fee_rate() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::default(), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::default(), @add_signature); } #[test] @@ -476,9 +528,10 @@ fn test_create_tx_custom_fee_rate() { builder .add_recipient(addr.script_pubkey(), 25_000) .fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); } #[test] @@ -490,14 +543,12 @@ fn test_create_tx_absolute_fee() { .drain_to(addr.script_pubkey()) .drain_wallet() .fee_absolute(100); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); - assert_eq!(details.fee.unwrap_or(0), 100); + assert_eq!(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) - ); + assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); } #[test] @@ -509,14 +560,12 @@ fn test_create_tx_absolute_zero_fee() { .drain_to(addr.script_pubkey()) .drain_wallet() .fee_absolute(0); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); - assert_eq!(details.fee.unwrap_or(0), 0); + assert_eq!(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) - ); + assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); } #[test] @@ -529,7 +578,7 @@ fn test_create_tx_absolute_high_fee() { .drain_to(addr.script_pubkey()) .drain_wallet() .fee_absolute(60_000); - let (_psbt, _details) = builder.finish().unwrap(); + let _ = builder.finish().unwrap(); } #[test] @@ -542,14 +591,12 @@ fn test_create_tx_add_change() { builder .add_recipient(addr.script_pubkey(), 25_000) .ordering(TxOrdering::Untouched); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); 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) - ); + assert_eq!(psbt.unsigned_tx.output[1].value, 25_000 - fee.unwrap_or(0)); } #[test] @@ -558,11 +605,12 @@ fn test_create_tx_skip_change_dust() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 49_800); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); 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); + assert_eq!(fee.unwrap_or(0), 200); } #[test] @@ -588,13 +636,11 @@ fn test_create_tx_ordering_respected() { .add_recipient(addr.script_pubkey(), 30_000) .add_recipient(addr.script_pubkey(), 10_000) .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); 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[0].value, 10_000 - fee.unwrap_or(0)); assert_eq!(psbt.unsigned_tx.output[1].value, 10_000); assert_eq!(psbt.unsigned_tx.output[2].value, 30_000); } @@ -605,7 +651,7 @@ fn test_create_tx_default_sighash() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 30_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.inputs[0].sighash_type, None); } @@ -618,7 +664,7 @@ fn test_create_tx_custom_sighash() { builder .add_recipient(addr.script_pubkey(), 30_000) .sighash(EcdsaSighashType::Single.into()); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!( psbt.inputs[0].sighash_type, @@ -635,7 +681,7 @@ fn test_create_tx_input_hd_keypaths() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1); assert_eq!( @@ -657,7 +703,7 @@ fn test_create_tx_output_hd_keypaths() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1); let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index); @@ -679,7 +725,7 @@ fn test_create_tx_set_redeem_script_p2sh() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!( psbt.inputs[0].redeem_script, @@ -702,7 +748,7 @@ fn test_create_tx_set_witness_script_p2wsh() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.inputs[0].redeem_script, None); assert_eq!( @@ -723,7 +769,7 @@ fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let script = ScriptBuf::from_hex( "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", @@ -741,7 +787,7 @@ fn test_create_tx_non_witness_utxo() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].witness_utxo.is_none()); @@ -757,7 +803,7 @@ fn test_create_tx_only_witness_utxo() { .drain_to(addr.script_pubkey()) .only_witness_utxo() .drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_none()); assert!(psbt.inputs[0].witness_utxo.is_some()); @@ -770,7 +816,7 @@ fn test_create_tx_shwpkh_has_witness_utxo() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert!(psbt.inputs[0].witness_utxo.is_some()); } @@ -782,7 +828,7 @@ fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_some()); assert!(psbt.inputs[0].witness_utxo.is_some()); @@ -818,14 +864,18 @@ fn test_create_tx_add_utxo() { vout: 0, }) .unwrap(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); 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"); + assert_eq!( + sent_received.0, 75_000, + "total should be sum of both inputs" + ); } #[test] @@ -907,7 +957,7 @@ fn test_create_tx_policy_path_no_csv() { builder .add_recipient(addr.script_pubkey(), 30_000) .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); } @@ -928,7 +978,7 @@ fn test_create_tx_policy_path_use_csv() { builder .add_recipient(addr.script_pubkey(), 30_000) .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); } @@ -949,7 +999,7 @@ fn test_create_tx_policy_path_ignored_subtree_with_csv() { builder .add_recipient(addr.script_pubkey(), 30_000) .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); } @@ -965,7 +1015,7 @@ fn test_create_tx_global_xpubs_with_origin() { builder .add_recipient(addr.script_pubkey(), 25_000) .add_global_xpubs(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let key = bip32::ExtendedPubKey::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); @@ -1002,11 +1052,13 @@ fn test_add_foreign_utxo() { .only_witness_utxo() .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) .unwrap(); - let (mut psbt, details) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); + let sent_received = wallet1.sent_and_received(&psbt.clone().extract_tx()); assert_eq!( - details.sent - details.received, - 10_000 + details.fee.unwrap_or(0), + sent_received.0 - sent_received.1, + 10_000 + fee.unwrap_or(0), "we should have only net spent ~10_000" ); @@ -1069,8 +1121,8 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let utxo2 = wallet2.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); - let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); + let tx1 = wallet1.get_tx(txid1).unwrap().node.tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().node.tx.clone(); #[allow(deprecated)] let satisfaction_weight = wallet2 @@ -1159,9 +1211,9 @@ fn test_add_foreign_utxo_only_witness_utxo() { { let mut builder = builder.clone(); - let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); + let tx2 = wallet2.get_tx(txid2).unwrap().node.tx; let psbt_input = psbt::Input { - non_witness_utxo: Some(tx2), + non_witness_utxo: Some(tx2.clone()), ..Default::default() }; builder @@ -1209,7 +1261,7 @@ fn test_create_tx_global_xpubs_master_without_origin() { builder .add_recipient(addr.script_pubkey(), 25_000) .add_global_xpubs(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let key = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); @@ -1228,7 +1280,7 @@ fn test_bump_fee_irreplaceable_tx() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1245,7 +1297,7 @@ fn test_bump_fee_confirmed_tx() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1272,7 +1324,7 @@ fn test_bump_fee_low_fee_rate() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1295,7 +1347,7 @@ fn test_bump_fee_low_abs() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1318,7 +1370,7 @@ fn test_bump_fee_zero_abs() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1341,7 +1393,10 @@ fn test_bump_fee_reduce_change() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let original_fee = psbt.fee_amount(); + let tx = psbt.extract_tx(); let txid = tx.txid(); wallet @@ -1350,14 +1405,16 @@ fn test_bump_fee_reduce_change() { 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(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent); + assert_eq!(sent_received.0, original_sent_received.0); assert_eq!( - details.received + details.fee.unwrap_or(0), - original_details.received + original_details.fee.unwrap_or(0) + sent_received.1 + fee.unwrap_or(0), + original_sent_received.1 + original_fee.unwrap_or(0) ); - assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); + assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); let tx = &psbt.unsigned_tx; assert_eq!(tx.output.len(), 2); @@ -1375,26 +1432,28 @@ fn test_bump_fee_reduce_change() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - details.received + sent_received.1 ); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(200); builder.enable_rbf(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent); + assert_eq!(sent_received.0, original_sent_received.0); assert_eq!( - details.received + details.fee.unwrap_or(0), - original_details.received + original_details.fee.unwrap_or(0) + sent_received.1 + fee.unwrap_or(0), + original_sent_received.1 + original_fee.unwrap_or(0) ); assert!( - details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0), + fee.unwrap_or(0) > original_fee.unwrap_or(0), "{} > {}", - details.fee.unwrap_or(0), - original_details.fee.unwrap_or(0) + fee.unwrap_or(0), + original_fee.unwrap_or(0) ); let tx = &psbt.unsigned_tx; @@ -1413,10 +1472,10 @@ fn test_bump_fee_reduce_change() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - details.received + sent_received.1 ); - assert_eq!(details.fee.unwrap_or(0), 200); + assert_eq!(fee.unwrap_or(0), 200); } #[test] @@ -1430,8 +1489,10 @@ fn test_bump_fee_reduce_single_recipient() { .drain_to(addr.script_pubkey()) .drain_wallet() .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); - let tx = psbt.extract_tx(); + let psbt = builder.finish().unwrap(); + let tx = psbt.clone().extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); + let original_fee = psbt.fee_amount(); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) @@ -1442,16 +1503,18 @@ fn test_bump_fee_reduce_single_recipient() { .fee_rate(FeeRate::from_sat_per_vb(2.5)) .allow_shrinking(addr.script_pubkey()) .unwrap(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent); - assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); + assert_eq!(sent_received.0, original_sent_received.0); + assert!(fee.unwrap_or(0) > original_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!(tx.output[0].value + fee.unwrap_or(0), sent_received.0); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature); } #[test] @@ -1465,8 +1528,10 @@ fn test_bump_fee_absolute_reduce_single_recipient() { .drain_to(addr.script_pubkey()) .drain_wallet() .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let original_fee = psbt.fee_amount(); let tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) @@ -1477,16 +1542,18 @@ fn test_bump_fee_absolute_reduce_single_recipient() { .allow_shrinking(addr.script_pubkey()) .unwrap() .fee_absolute(300); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let tx = &psbt.unsigned_tx; + let sent_received = wallet.sent_and_received(tx); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent); - assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0)); + assert_eq!(sent_received.0, original_sent_received.0); + assert!(fee.unwrap_or(0) > original_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!(tx.output[0].value + fee.unwrap_or(0), sent_received.0); - assert_eq!(details.fee.unwrap_or(0), 300); + assert_eq!(fee.unwrap_or(0), 300); } #[test] @@ -1525,13 +1592,15 @@ fn test_bump_fee_drain_wallet() { .unwrap() .manually_selected_only() .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); + let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) .unwrap(); - assert_eq!(original_details.sent, 25_000); + assert_eq!(original_sent_received.0, 25_000); // for the new feerate, it should be enough to reduce the output, but since we specify // `drain_wallet` we expect to spend everything @@ -1541,9 +1610,10 @@ fn test_bump_fee_drain_wallet() { .allow_shrinking(addr.script_pubkey()) .unwrap() .fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (_, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.extract_tx()); - assert_eq!(details.sent, 75_000); + assert_eq!(sent_received.0, 75_000); } #[test] @@ -1590,13 +1660,14 @@ fn test_bump_fee_remove_output_manually_selected_only() { .unwrap() .manually_selected_only() .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) .unwrap(); - assert_eq!(original_details.sent, 25_000); + assert_eq!(original_sent_received.0, 25_000); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder @@ -1633,8 +1704,9 @@ fn test_bump_fee_add_input() { builder .add_recipient(addr.script_pubkey(), 45_000) .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); + let original_details = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) @@ -1642,10 +1714,11 @@ fn test_bump_fee_add_input() { 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 psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); + assert_eq!(sent_received.0, original_details.0 + 25_000); + assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); let tx = &psbt.unsigned_tx; assert_eq!(tx.input.len(), 2); @@ -1664,10 +1737,10 @@ fn test_bump_fee_add_input() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - details.received + sent_received.1 ); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); } #[test] @@ -1681,8 +1754,9 @@ fn test_bump_fee_absolute_add_input() { builder .add_recipient(addr.script_pubkey(), 45_000) .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) @@ -1690,10 +1764,12 @@ fn test_bump_fee_absolute_add_input() { let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(6_000); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + assert_eq!(sent_received.0, original_sent_received.0 + 25_000); + assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); let tx = &psbt.unsigned_tx; assert_eq!(tx.input.len(), 2); @@ -1712,10 +1788,10 @@ fn test_bump_fee_absolute_add_input() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - details.received + sent_received.1 ); - assert_eq!(details.fee.unwrap_or(0), 6_000); + assert_eq!(fee.unwrap_or(0), 6_000); } #[test] @@ -1734,7 +1810,9 @@ fn test_bump_fee_no_change_add_input_and_change() { .unwrap() .manually_selected_only() .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let original_fee = psbt.fee_amount(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1746,13 +1824,15 @@ fn test_bump_fee_no_change_add_input_and_change() { // extra input and a change output, and leave the original output untouched 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 psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - let original_send_all_amount = original_details.sent - original_details.fee.unwrap_or(0); - assert_eq!(details.sent, original_details.sent + 50_000); + let original_send_all_amount = original_sent_received.0 - original_fee.unwrap_or(0); + assert_eq!(sent_received.0, original_sent_received.0 + 50_000); assert_eq!( - details.received, - 75_000 - original_send_all_amount - details.fee.unwrap_or(0) + sent_received.1, + 75_000 - original_send_all_amount - fee.unwrap_or(0) ); let tx = &psbt.unsigned_tx; @@ -1772,10 +1852,10 @@ fn test_bump_fee_no_change_add_input_and_change() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - 75_000 - original_send_all_amount - details.fee.unwrap_or(0) + 75_000 - original_send_all_amount - fee.unwrap_or(0) ); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature); } #[test] @@ -1789,7 +1869,10 @@ fn test_bump_fee_add_input_change_dust() { builder .add_recipient(addr.script_pubkey(), 45_000) .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let original_fee = psbt.fee_amount(); + let mut tx = psbt.extract_tx(); for txin in &mut tx.input { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realisitc weight @@ -1818,16 +1901,15 @@ fn test_bump_fee_add_input_change_dust() { // We use epsilon here to avoid asking for a slightly too high feerate 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(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!( - original_details.received, - 5_000 - original_details.fee.unwrap_or(0) - ); + assert_eq!(original_sent_received.1, 5_000 - original_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); + assert_eq!(sent_received.0, original_sent_received.0 + 25_000); + assert_eq!(fee.unwrap_or(0), 30_000); + assert_eq!(sent_received.1, 0); let tx = &psbt.unsigned_tx; assert_eq!(tx.input.len(), 2); @@ -1841,7 +1923,7 @@ fn test_bump_fee_add_input_change_dust() { 45_000 ); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature); } #[test] @@ -1856,8 +1938,9 @@ fn test_bump_fee_force_add_input() { builder .add_recipient(addr.script_pubkey(), 45_000) .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); for txin in &mut tx.input { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature @@ -1872,10 +1955,12 @@ fn test_bump_fee_force_add_input() { .add_utxo(incoming_op) .unwrap() .fee_rate(FeeRate::from_sat_per_vb(5.0)); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + assert_eq!(sent_received.0, original_sent_received.0 + 25_000); + assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); let tx = &psbt.unsigned_tx; assert_eq!(tx.input.len(), 2); @@ -1894,10 +1979,10 @@ fn test_bump_fee_force_add_input() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - details.received + sent_received.1 ); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); + assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); } #[test] @@ -1912,8 +1997,9 @@ fn test_bump_fee_absolute_force_add_input() { builder .add_recipient(addr.script_pubkey(), 45_000) .enable_rbf(); - let (psbt, original_details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); + let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); // skip saving the new utxos, we know they can't be used anyways for txin in &mut tx.input { @@ -1927,10 +2013,12 @@ fn test_bump_fee_absolute_force_add_input() { // the addition of an extra input with `add_utxo()` let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.add_utxo(incoming_op).unwrap().fee_absolute(250); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); - assert_eq!(details.sent, original_details.sent + 25_000); - assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000); + assert_eq!(sent_received.0, original_sent_received.0 + 25_000); + assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); let tx = &psbt.unsigned_tx; assert_eq!(tx.input.len(), 2); @@ -1949,10 +2037,10 @@ fn test_bump_fee_absolute_force_add_input() { .find(|txout| txout.script_pubkey != addr.script_pubkey()) .unwrap() .value, - details.received + sent_received.1 ); - assert_eq!(details.fee.unwrap_or(0), 250); + assert_eq!(fee.unwrap_or(0), 250); } #[test] @@ -1973,7 +2061,7 @@ fn test_bump_fee_unconfirmed_inputs_only() { .drain_wallet() .drain_to(addr.script_pubkey()) .enable_rbf(); - let (psbt, __details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); // Now we receive one transaction with 0 confirmations. We won't be able to use that for // fee bumping, as it's still unconfirmed! receive_output( @@ -2013,7 +2101,7 @@ fn test_bump_fee_unconfirmed_input() { .drain_wallet() .drain_to(addr.script_pubkey()) .enable_rbf(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); for txin in &mut tx.input { @@ -2055,10 +2143,11 @@ fn test_fee_amount_negative_drain_val() { .unwrap() .enable_rbf() .fee_rate(fee_rate); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); - assert!(psbt.inputs.len() == 1); - assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate, @add_signature); + assert_eq!(psbt.inputs.len(), 1); + assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate, @add_signature); } #[test] @@ -2067,7 +2156,7 @@ fn test_sign_single_xprv() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); assert!(finalized); @@ -2082,7 +2171,7 @@ fn test_sign_single_xprv_with_master_fingerprint_and_path() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); assert!(finalized); @@ -2097,7 +2186,7 @@ fn test_sign_single_xprv_bip44_path() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); assert!(finalized); @@ -2112,7 +2201,7 @@ fn test_sign_single_xprv_sh_wpkh() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); assert!(finalized); @@ -2128,7 +2217,7 @@ fn test_sign_single_wif() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); assert!(finalized); @@ -2143,7 +2232,7 @@ fn test_sign_single_xprv_no_hd_keypaths() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); psbt.inputs[0].bip32_derivation.clear(); assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); @@ -2165,7 +2254,7 @@ fn test_include_output_redeem_witness_script() { builder .add_recipient(addr.script_pubkey(), 45_000) .include_output_redeem_witness_script(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); // p2sh-p2wsh transaction should contain both witness and redeem scripts assert!(psbt @@ -2184,7 +2273,7 @@ fn test_signing_only_one_of_multiple_inputs() { builder .add_recipient(addr.script_pubkey(), 45_000) .include_output_redeem_witness_script(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); // add another input to the psbt that is at least passable. let dud_input = bitcoin::psbt::Input { @@ -2228,7 +2317,7 @@ fn test_remove_partial_sigs_after_finalize_sign_option() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap().0; + let mut psbt = builder.finish().unwrap(); assert!(wallet .sign( @@ -2258,7 +2347,7 @@ fn test_try_finalize_sign_option() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let mut psbt = builder.finish().unwrap().0; + let mut psbt = builder.finish().unwrap(); let finalized = wallet .sign( @@ -2295,7 +2384,7 @@ fn test_sign_nonstandard_sighash() { .drain_to(addr.script_pubkey()) .sighash(sighash.into()) .drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let result = wallet.sign(&mut psbt, Default::default()); assert!( @@ -2566,7 +2655,7 @@ fn test_taproot_psbt_populate_tap_key_origins() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!( psbt.inputs[0] @@ -2607,7 +2696,7 @@ fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { builder .add_recipient(addr.script_pubkey(), 25_000) .policy_path(path, KeychainKind::External); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); let mut input_key_origins = psbt.inputs[0] .tap_key_origins @@ -2667,7 +2756,7 @@ fn test_taproot_psbt_input_tap_tree() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); assert_eq!( psbt.inputs[0].tap_merkle_root, @@ -2709,7 +2798,7 @@ fn test_taproot_sign_missing_witness_utxo() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let witness_utxo = psbt.inputs[0].witness_utxo.take(); let result = wallet.sign( @@ -2749,10 +2838,10 @@ fn test_taproot_sign_using_non_witness_utxo() { let addr = wallet.get_address(New); let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); psbt.inputs[0].witness_utxo = None; - psbt.inputs[0].non_witness_utxo = wallet.get_tx(prev_txid, true).unwrap().transaction; + psbt.inputs[0].non_witness_utxo = Some(wallet.get_tx(prev_txid).unwrap().node.tx.clone()); assert!( psbt.inputs[0].non_witness_utxo.is_some(), "Previous tx should be present in the database" @@ -2792,11 +2881,13 @@ fn test_taproot_foreign_utxo() { .add_recipient(addr.script_pubkey(), 60_000) .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) .unwrap(); - let (psbt, details) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); + let sent_received = wallet1.sent_and_received(&psbt.clone().extract_tx()); + let fee = psbt.fee_amount(); assert_eq!( - details.sent - details.received, - 10_000 + details.fee.unwrap_or(0), + sent_received.0 - sent_received.1, + 10_000 + fee.unwrap_or(0), "we should have only net spent ~10_000" ); @@ -2814,7 +2905,7 @@ fn test_spend_from_wallet(mut wallet: Wallet) { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); assert!( wallet.sign(&mut psbt, Default::default()).unwrap(), @@ -2838,7 +2929,7 @@ fn test_taproot_no_key_spend() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); assert!( wallet @@ -2873,7 +2964,7 @@ fn test_taproot_script_spend_sign_all_leaves() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); assert!( wallet @@ -2904,7 +2995,7 @@ fn test_taproot_script_spend_sign_include_some_leaves() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let mut script_leaves: Vec<_> = psbt.inputs[0] .tap_scripts .clone() @@ -2944,7 +3035,7 @@ fn test_taproot_script_spend_sign_exclude_some_leaves() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let mut script_leaves: Vec<_> = psbt.inputs[0] .tap_scripts .clone() @@ -2982,7 +3073,7 @@ fn test_taproot_script_spend_sign_no_leaves() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); wallet .sign( @@ -3005,7 +3096,7 @@ fn test_taproot_sign_derive_index_from_psbt() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); // re-create the wallet with an empty db let wallet_empty = @@ -3028,7 +3119,7 @@ fn test_taproot_sign_explicit_sighash_all() { .drain_to(addr.script_pubkey()) .sighash(TapSighashType::All.into()) .drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let result = wallet.sign(&mut psbt, Default::default()); assert!( @@ -3048,7 +3139,7 @@ fn test_taproot_sign_non_default_sighash() { .drain_to(addr.script_pubkey()) .sighash(sighash.into()) .drain_wallet(); - let (mut psbt, _) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); let witness_utxo = psbt.inputs[0].witness_utxo.take(); @@ -3243,7 +3334,8 @@ fn test_fee_rate_sign_no_grinding_high_r() { .drain_wallet() .fee_rate(fee_rate) .add_data(&data); - let (mut psbt, details) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); let (op_return_vout, _) = psbt .unsigned_tx .output @@ -3289,7 +3381,7 @@ fn test_fee_rate_sign_no_grinding_high_r() { ) .unwrap(); // ...and checking that everything is fine - assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate); + assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate); } #[test] @@ -3306,7 +3398,8 @@ fn test_fee_rate_sign_grinding_low_r() { .drain_to(addr.script_pubkey()) .drain_wallet() .fee_rate(fee_rate); - let (mut psbt, details) = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); + let fee = psbt.fee_amount(); wallet .sign( @@ -3322,7 +3415,7 @@ fn test_fee_rate_sign_grinding_low_r() { 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); + assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate); } // #[cfg(feature = "test-hardware-signer")] @@ -3386,7 +3479,7 @@ fn test_tx_cancellation() { let mut builder = $wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 10_000); - let (psbt, _) = builder.finish().unwrap(); + let psbt = builder.finish().unwrap(); psbt }}; diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index d53317f8c..52def58eb 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -81,7 +81,7 @@ fn main() -> Result<(), Box> { .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) .enable_rbf(); - let (mut psbt, _) = tx_builder.finish()?; + let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 144e1edf5..343a09763 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -87,7 +87,7 @@ async fn main() -> Result<(), Box> { .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) .enable_rbf(); - let (mut psbt, _) = tx_builder.finish()?; + let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 02d060430..d108742a5 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -87,7 +87,7 @@ fn main() -> Result<(), Box> { .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) .enable_rbf(); - let (mut psbt, _) = tx_builder.finish()?; + let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); From d443fe7f6613776a3dce00def0b655c4b2b36728 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 1 Aug 2023 12:42:37 -0500 Subject: [PATCH 2/6] feat(tx_graph)!: change TxGraph::calculate_fee to return Result added - tx_graph::CalculateFeeError enum BREAKING CHANGES: changed - TxGraph::calculate_fee function to return Result instead of Option --- crates/bdk/src/error.rs | 14 -------- crates/bdk/src/lib.rs | 2 +- crates/bdk/src/wallet/mod.rs | 24 ++++++------- crates/bdk/tests/wallet.rs | 9 ++--- crates/chain/src/tx_graph.rs | 54 ++++++++++++++++++++--------- crates/chain/tests/test_tx_graph.rs | 28 +++++++++------ 6 files changed, 72 insertions(+), 59 deletions(-) diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs index f0e33fea6..fcb5a6f7b 100644 --- a/crates/bdk/src/error.rs +++ b/crates/bdk/src/error.rs @@ -9,10 +9,6 @@ // You may not use this file except in accordance with one or both of these // licenses. -//! Errors -//! -//! This module defines the errors that can be thrown by [`crate`] functions. - use crate::bitcoin::Network; use crate::{descriptor, wallet}; use alloc::{string::String, vec::Vec}; @@ -93,17 +89,7 @@ pub enum Error { Psbt(bitcoin::psbt::Error), } -/// Errors returned by `Wallet::calculate_fee`. -#[derive(Debug)] -pub enum CalculateFeeError { - /// Missing `TxOut` for one of the inputs of the tx - MissingTxOut, - /// When the transaction is invalid according to the graph it has a negative fee - NegativeFee(i64), -} - /// Errors returned by miniscript when updating inconsistent PSBTs -#[allow(missing_docs)] // TODO add docs #[derive(Debug, Clone)] pub enum MiniscriptPsbtError { Conversion(miniscript::descriptor::ConversionError), diff --git a/crates/bdk/src/lib.rs b/crates/bdk/src/lib.rs index 93ed400b1..012a868a6 100644 --- a/crates/bdk/src/lib.rs +++ b/crates/bdk/src/lib.rs @@ -29,7 +29,7 @@ extern crate bip39; #[allow(unused_imports)] #[macro_use] -pub mod error; +pub(crate) mod error; pub mod descriptor; pub mod keys; pub mod psbt; diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index c939db1c8..e5095a1b4 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -40,6 +40,7 @@ use core::fmt; use core::ops::Deref; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; +use bdk_chain::tx_graph::CalculateFeeError; #[allow(unused_imports)] use log::{debug, error, info, trace}; @@ -66,7 +67,7 @@ use crate::descriptor::{ calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, }; -use crate::error::{CalculateFeeError, Error, MiniscriptPsbtError}; +use crate::error::{Error, MiniscriptPsbtError}; use crate::psbt::PsbtUtils; use crate::signer::SignerError; use crate::types::*; @@ -434,11 +435,7 @@ impl Wallet { /// /// Note `tx` does not have to be in the graph for this to work. pub fn calculate_fee(&self, tx: &Transaction) -> Result { - match self.indexed_graph.graph().calculate_fee(tx) { - None => Err(CalculateFeeError::MissingTxOut), - Some(fee) if fee < 0 => Err(CalculateFeeError::NegativeFee(fee)), - Some(fee) => Ok(u64::try_from(fee).unwrap()), - } + self.indexed_graph.graph().calculate_fee(tx) } /// Calculate the `FeeRate` for a given transaction. @@ -1072,13 +1069,12 @@ impl Wallet { return Err(Error::IrreplaceableTransaction); } - let fee = graph.calculate_fee(&tx).ok_or(Error::FeeRateUnavailable)?; - if fee < 0 { - // It's available but it's wrong so let's say it's unavailable - return Err(Error::FeeRateUnavailable)?; - } - let fee = fee as u64; - let feerate = FeeRate::from_wu(fee, tx.weight()); + let fee = self + .calculate_fee(&tx) + .map_err(|_| Error::FeeRateUnavailable)?; + let fee_rate = self + .calculate_fee_rate(&tx) + .map_err(|_| Error::FeeRateUnavailable)?; // remove the inputs from the tx and process them let original_txin = tx.input.drain(..).collect::>(); @@ -1162,7 +1158,7 @@ impl Wallet { utxos: original_utxos, bumping_fee: Some(tx_builder::PreviousFee { absolute: fee, - rate: feerate.as_sat_per_vb(), + rate: fee_rate.as_sat_per_vb(), }), ..Default::default() }; diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 8ef921e12..906900e46 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -6,6 +6,7 @@ use bdk::wallet::coin_selection::LargestFirstCoinSelection; use bdk::wallet::AddressIndex::*; use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; use bdk::{Error, FeeRate, KeychainKind}; +use bdk_chain::tx_graph::CalculateFeeError; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; @@ -104,7 +105,7 @@ fn test_get_funded_wallet_sent_and_received() { fn test_get_funded_wallet_tx_fees() { let (wallet, _) = get_funded_wallet(get_test_wpkh()); assert_eq!(wallet.get_balance().confirmed, 50000); - let mut tx_fee_amounts: Vec<(Txid, Result)> = wallet + let mut tx_fee_amounts: Vec<(Txid, Result)> = wallet .transactions() .map(|ct| { let fee = wallet.calculate_fee(ct.node.tx); @@ -116,7 +117,7 @@ fn test_get_funded_wallet_tx_fees() { assert_eq!(tx_fee_amounts.len(), 2); assert_matches!( tx_fee_amounts.get(1), - Some((_, Err(bdk::error::CalculateFeeError::MissingTxOut))) + Some((_, Err(CalculateFeeError::MissingTxOut(_)))) ); assert_matches!(tx_fee_amounts.get(0), Some((_, Ok(1000)))) } @@ -125,7 +126,7 @@ fn test_get_funded_wallet_tx_fees() { fn test_get_funded_wallet_tx_fee_rate() { let (wallet, _) = get_funded_wallet(get_test_wpkh()); assert_eq!(wallet.get_balance().confirmed, 50000); - let mut tx_fee_rates: Vec<(Txid, Result)> = wallet + let mut tx_fee_rates: Vec<(Txid, Result)> = wallet .transactions() .map(|ct| { let fee_rate = wallet.calculate_fee_rate(ct.node.tx); @@ -137,7 +138,7 @@ fn test_get_funded_wallet_tx_fee_rate() { assert_eq!(tx_fee_rates.len(), 2); assert_matches!( tx_fee_rates.get(1), - Some((_, Err(bdk::error::CalculateFeeError::MissingTxOut))) + Some((_, Err(CalculateFeeError::MissingTxOut(_)))) ); assert_matches!(tx_fee_rates.get(0), Some((_, Ok(_)))) } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index adb84ca22..1572cd9a5 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -135,6 +135,15 @@ pub struct CanonicalTx<'a, T, A> { pub tx_node: TxNode<'a, T, A>, } +/// Errors returned by `TxGraph::calculate_fee`. +#[derive(Debug, PartialEq, Eq)] +pub enum CalculateFeeError { + /// Missing `TxOut` for one or more of the inputs of the tx + MissingTxOut(Vec), + /// When the transaction is invalid according to the graph it has a negative fee + NegativeFee(i64), +} + impl TxGraph { /// Iterate over all tx outputs known by [`TxGraph`]. /// @@ -236,25 +245,33 @@ impl TxGraph { } /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. - /// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as - /// the full transactions or individual txouts). If the returned value is negative, then the - /// transaction is invalid according to the graph. - /// - /// Returns `None` if we're missing an input for the tx in the graph. + /// Returns `OK(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as + /// the full transactions or individual txouts). /// /// Note `tx` does not have to be in the graph for this to work. - pub fn calculate_fee(&self, tx: &Transaction) -> Option { + pub fn calculate_fee(&self, tx: &Transaction) -> Result { if tx.is_coin_base() { - return Some(0); + return Ok(0); } - let inputs_sum = tx - .input - .iter() - .map(|txin| { - self.get_txout(txin.previous_output) - .map(|txout| txout.value as i64) - }) - .sum::>()?; + let inputs_sum = tx.input.iter().fold( + (0_u64, Vec::new()), + |(mut sum, mut missing_outpoints), txin| match self.get_txout(txin.previous_output) { + None => { + missing_outpoints.push(txin.previous_output); + (sum, missing_outpoints) + } + Some(txout) => { + sum += txout.value; + (sum, missing_outpoints) + } + }, + ); + + let inputs_sum = if inputs_sum.1.is_empty() { + Ok(inputs_sum.0 as i64) + } else { + Err(CalculateFeeError::MissingTxOut(inputs_sum.1)) + }?; let outputs_sum = tx .output @@ -262,7 +279,12 @@ impl TxGraph { .map(|txout| txout.value as i64) .sum::(); - Some(inputs_sum - outputs_sum) + let fee = inputs_sum - outputs_sum; + if fee < 0 { + Err(CalculateFeeError::NegativeFee(fee)) + } else { + Ok(fee as u64) + } } /// The transactions spending from this output. diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 26475f762..4c68f5108 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1,5 +1,6 @@ #[macro_use] mod common; +use bdk_chain::tx_graph::CalculateFeeError; use bdk_chain::{ collections::*, local_chain::LocalChain, @@ -453,22 +454,29 @@ fn test_calculate_fee() { }], }; - assert_eq!(graph.calculate_fee(&tx), Some(100)); + assert_eq!(graph.calculate_fee(&tx), Ok(100)); tx.input.remove(2); - // fee would be negative - assert_eq!(graph.calculate_fee(&tx), Some(-200)); + // fee would be negative, should return CalculateFeeError::NegativeFee + assert_eq!( + graph.calculate_fee(&tx), + Err(CalculateFeeError::NegativeFee(-200)) + ); - // If we have an unknown outpoint, fee should return None. + // If we have an unknown outpoint, fee should return CalculateFeeError::MissingTxOut. + let outpoint = OutPoint { + txid: h!("unknown_txid"), + vout: 0, + }; tx.input.push(TxIn { - previous_output: OutPoint { - txid: h!("unknown_txid"), - vout: 0, - }, + previous_output: outpoint, ..Default::default() }); - assert_eq!(graph.calculate_fee(&tx), None); + assert_eq!( + graph.calculate_fee(&tx), + Err(CalculateFeeError::MissingTxOut(vec!(outpoint))) + ); } #[test] @@ -485,7 +493,7 @@ fn test_calculate_fee_on_coinbase() { let graph = TxGraph::<()>::default(); - assert_eq!(graph.calculate_fee(&tx), Some(0)); + assert_eq!(graph.calculate_fee(&tx), Ok(0)); } #[test] From 036299803f9f94c7c8218a75f3c206fa8c4297ab Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 6 Aug 2023 23:05:41 -0500 Subject: [PATCH 3/6] feat(wallet): add Wallet::insert_txout function and updated docs for fee functions added - Wallet::insert_txout function to allow inserting foreign TxOuts - test to verify error when trying to calculate fee with missing foreign utxo - test to calculate fee with inserted foreign utxo updated - docs for Wallet::calculate_fee, Wallet::calculate_fee_rate, and TxGraph::calculate_fee with note about missing foreign utxos --- crates/bdk/src/wallet/mod.rs | 75 +++++++++++++- crates/bdk/tests/common.rs | 11 +- crates/bdk/tests/wallet.rs | 153 ++++++++++++++++++++-------- crates/chain/src/spk_txout_index.rs | 4 +- crates/chain/src/tx_graph.rs | 24 +++-- 5 files changed, 206 insertions(+), 61 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index e5095a1b4..1f6700b64 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -431,16 +431,40 @@ impl Wallet { .next() } + /// Inserts the given foreign `TxOut` at `OutPoint` into the wallet's transaction graph. Any + /// inserted foreign TxOuts are not persisted until [`Self::commit`] is called. + /// + /// Only insert TxOuts you trust the values for! + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) + where + D: PersistBackend, + { + let additions = self.indexed_graph.insert_txout(outpoint, &txout); + self.persist.stage(ChangeSet::from(additions)); + } + /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. /// + /// To calculate the fee for a [`Transaction`] that depends on foreign [`TxOut`] values you must + /// first manually insert the foreign TxOuts into the tx graph using the [`insert_txout`] function. + /// Only insert TxOuts you trust the values for! + /// /// Note `tx` does not have to be in the graph for this to work. + /// + /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee(&self, tx: &Transaction) -> Result { self.indexed_graph.graph().calculate_fee(tx) } - /// Calculate the `FeeRate` for a given transaction. + /// Calculate the [`FeeRate`] for a given transaction. + /// + /// To calculate the fee rate for a [`Transaction`] that depends on foreign [`TxOut`] values you + /// must first manually insert the foreign TxOuts into the tx graph using the [`insert_txout`] function. + /// Only insert TxOuts you trust the values for! /// /// Note `tx` does not have to be in the graph for this to work. + /// + /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result { self.calculate_fee(tx).map(|fee| { let weight = tx.weight(); @@ -451,14 +475,55 @@ impl Wallet { /// Computes total input value going from script pubkeys in the index (sent) and the total output /// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed /// correctly, the output being spent must have already been scanned by the index. Calculating - /// received just uses the transaction outputs directly, so it will be correct even if it has not - /// been scanned. + /// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has + /// not been scanned. pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) { self.indexed_graph.index.sent_and_received(tx) } - /// Return a single `CanonicalTx` made and received by the wallet or `None` if it doesn't - /// exist in the wallet + /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists). + /// + /// `CanonicalTx` contains the full transaction alongside meta-data such as: + /// * Blocks that the transaction is [`Anchor`]ed in. These may or may not be blocks that exist + /// in the best chain. + /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is + /// confirmed or unconfirmed. If the transaction is confirmed, the anchor which proves the + /// confirmation is provided. If the transaction is unconfirmed, the unix timestamp of when + /// the transaction was last seen in the mempool is provided. + /// + /// ```rust, no_run + /// use bdk::{chain::ChainPosition, Wallet}; + /// use bdk_chain::Anchor; + /// # let wallet: Wallet<()> = todo!(); + /// # let my_txid: bitcoin::Txid = todo!(); + /// + /// let canonical_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); + /// + /// // get reference to full transaction + /// println!("my tx: {:#?}", canonical_tx.tx_node.tx); + /// + /// // list all transaction anchors + /// for anchor in canonical_tx.tx_node.anchors { + /// println!( + /// "tx is anchored by block of hash {}", + /// anchor.anchor_block().hash + /// ); + /// } + /// + /// // get confirmation status of transaction + /// match canonical_tx.chain_position { + /// ChainPosition::Confirmed(anchor) => println!( + /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", + /// anchor.confirmation_height, anchor.anchor_block.height, anchor.anchor_block.hash, + /// ), + /// ChainPosition::Unconfirmed(last_seen) => println!( + /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain", + /// last_seen, + /// ), + /// } + /// ``` + /// + /// [`Anchor`]: bdk_chain::Anchor pub fn get_tx( &self, txid: Txid, diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index 65e5bc3e7..ee8ed74e1 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -7,7 +7,11 @@ use bitcoin::hashes::Hash; use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid}; use std::str::FromStr; -/// Return a fake wallet that appears to be funded for testing. +// Return a fake wallet that appears to be funded for testing. +// +// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000 +// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 +// sats are the transaction fee. pub fn get_funded_wallet_with_change( descriptor: &str, change: Option<&str>, @@ -95,6 +99,11 @@ pub fn get_funded_wallet_with_change( (wallet, tx1.txid()) } +// Return a fake wallet that appears to be funded for testing. +// +// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000 +// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 +// sats are the transaction fee. pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) { get_funded_wallet_with_change(descriptor, None) } diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 906900e46..7ea364785 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -6,7 +6,6 @@ use bdk::wallet::coin_selection::LargestFirstCoinSelection; use bdk::wallet::AddressIndex::*; use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; use bdk::{Error, FeeRate, KeychainKind}; -use bdk_chain::tx_graph::CalculateFeeError; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; @@ -84,63 +83,60 @@ fn test_descriptor_checksum() { #[test] fn test_get_funded_wallet_balance() { let (wallet, _) = get_funded_wallet(get_test_wpkh()); - assert_eq!(wallet.get_balance().confirmed, 50000); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + assert_eq!(wallet.get_balance().confirmed, 50_000); } #[test] fn test_get_funded_wallet_sent_and_received() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - assert_eq!(wallet.get_balance().confirmed, 50000); + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); + let mut tx_amounts: Vec<(Txid, (u64, u64))> = wallet .transactions() - .map(|ct| (ct.node.txid, wallet.sent_and_received(ct.node.tx))) + .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(ct.tx_node.tx))) .collect(); tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); - assert_eq!(tx_amounts.len(), 2); - assert_matches!(tx_amounts.get(0), Some((_, (76_000, 50_000)))) + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let (sent, received) = wallet.sent_and_received(tx); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + assert_eq!(sent, 76_000); + assert_eq!(received, 50_000); } #[test] fn test_get_funded_wallet_tx_fees() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - assert_eq!(wallet.get_balance().confirmed, 50000); - let mut tx_fee_amounts: Vec<(Txid, Result)> = wallet - .transactions() - .map(|ct| { - let fee = wallet.calculate_fee(ct.node.tx); - (ct.node.txid, fee) - }) - .collect(); - tx_fee_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); - assert_eq!(tx_fee_amounts.len(), 2); - assert_matches!( - tx_fee_amounts.get(1), - Some((_, Err(CalculateFeeError::MissingTxOut(_)))) - ); - assert_matches!(tx_fee_amounts.get(0), Some((_, Ok(1000)))) + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx_fee = wallet.calculate_fee(tx).expect("transaction fee"); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + assert_eq!(tx_fee, 1000) } #[test] fn test_get_funded_wallet_tx_fee_rate() { - let (wallet, _) = get_funded_wallet(get_test_wpkh()); - assert_eq!(wallet.get_balance().confirmed, 50000); - let mut tx_fee_rates: Vec<(Txid, Result)> = wallet - .transactions() - .map(|ct| { - let fee_rate = wallet.calculate_fee_rate(ct.node.tx); - (ct.node.txid, fee_rate) - }) - .collect(); - tx_fee_rates.sort_by(|a1, a2| a1.0.cmp(&a2.0)); + let (wallet, txid) = get_funded_wallet(get_test_wpkh()); - assert_eq!(tx_fee_rates.len(), 2); - assert_matches!( - tx_fee_rates.get(1), - Some((_, Err(CalculateFeeError::MissingTxOut(_)))) - ); - assert_matches!(tx_fee_rates.get(0), Some((_, Ok(_)))) + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx_fee_rate = wallet.calculate_fee_rate(tx).expect("transaction fee rate"); + + // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 + // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 + // sats are the transaction fee. + + // tx weight = 452 bytes, as vbytes = (452+3)/4 = 113 + // fee rate (sats per vbyte) = fee / vbytes = 1000 / 113 = 8.8495575221 rounded to 8.849558 + assert_eq!(tx_fee_rate.as_sat_per_vb(), 8.849558); } macro_rules! assert_fee_rate { @@ -1098,6 +1094,77 @@ fn test_add_foreign_utxo() { assert!(finished, "all the inputs should have been signed now"); } +#[test] +#[should_panic( + expected = "MissingTxOut([OutPoint { txid: 0x21d7fb1bceda00ab4069fc52d06baa13470803e9050edd16f5736e5d8c4925fd, vout: 0 }])" +)] +fn test_calculate_fee_with_missing_foreign_utxo() { + let (mut 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().next().expect("must take!"); + #[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 psbt = builder.finish().unwrap(); + let tx = psbt.extract_tx(); + wallet1.calculate_fee(&tx).unwrap(); +} + +#[test] +fn test_calculate_fee_with_inserted_foreign_utxo() { + let (mut 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().next().expect("must take!"); + #[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 psbt = builder.finish().unwrap(); + let psbt_fee = psbt.fee_amount().expect("psbt fee"); + let tx = psbt.extract_tx(); + + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let wallet1_fee = wallet1.calculate_fee(&tx).expect("wallet fee"); + assert_eq!(psbt_fee, wallet1_fee); +} + #[test] #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] fn test_add_foreign_utxo_invalid_psbt_input() { @@ -1122,8 +1189,8 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let utxo2 = wallet2.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1).unwrap().node.tx.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().node.tx.clone(); + let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); #[allow(deprecated)] let satisfaction_weight = wallet2 @@ -1212,7 +1279,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { { let mut builder = builder.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().node.tx; + let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx; let psbt_input = psbt::Input { non_witness_utxo: Some(tx2.clone()), ..Default::default() @@ -2842,7 +2909,7 @@ fn test_taproot_sign_using_non_witness_utxo() { let mut psbt = builder.finish().unwrap(); psbt.inputs[0].witness_utxo = None; - psbt.inputs[0].non_witness_utxo = Some(wallet.get_tx(prev_txid).unwrap().node.tx.clone()); + psbt.inputs[0].non_witness_utxo = Some(wallet.get_tx(prev_txid).unwrap().tx_node.tx.clone()); assert!( psbt.inputs[0].non_witness_utxo.is_some(), "Previous tx should be present in the database" diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index db749f44c..5547f37c6 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -288,8 +288,8 @@ impl SpkTxOutIndex { /// Computes total input value going from script pubkeys in the index (sent) and the total output /// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed /// correctly, the output being spent must have already been scanned by the index. Calculating - /// received just uses the transaction outputs directly, so it will be correct even if it has not - /// been scanned. + /// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has + /// not been scanned. pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) { let mut sent = 0; let mut received = 0; diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 1572cd9a5..404068752 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -245,33 +245,37 @@ impl TxGraph { } /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. - /// Returns `OK(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as + /// Returns `OK(_)` if we have all the [`TxOut`]s being spent by `tx` in the graph (either as /// the full transactions or individual txouts). /// + /// To calculate the fee for a [`Transaction`] that depends on foreign [`TxOut`] values you must + /// first manually insert the foreign TxOuts into the tx graph using the [`insert_txout`] function. + /// Only insert TxOuts you trust the values for! + /// /// Note `tx` does not have to be in the graph for this to work. + /// + /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee(&self, tx: &Transaction) -> Result { if tx.is_coin_base() { return Ok(0); } - let inputs_sum = tx.input.iter().fold( - (0_u64, Vec::new()), + + let (inputs_sum, missing_outputs) = tx.input.iter().fold( + (0_i64, Vec::new()), |(mut sum, mut missing_outpoints), txin| match self.get_txout(txin.previous_output) { None => { missing_outpoints.push(txin.previous_output); (sum, missing_outpoints) } Some(txout) => { - sum += txout.value; + sum += txout.value as i64; (sum, missing_outpoints) } }, ); - - let inputs_sum = if inputs_sum.1.is_empty() { - Ok(inputs_sum.0 as i64) - } else { - Err(CalculateFeeError::MissingTxOut(inputs_sum.1)) - }?; + if !missing_outputs.is_empty() { + return Err(CalculateFeeError::MissingTxOut(missing_outputs)); + } let outputs_sum = tx .output From 465d53cc88b32bf8098dc61e9afdbb28d22b213d Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 23 Aug 2023 21:41:38 -0500 Subject: [PATCH 4/6] docs(wallet): update docs for calculate_fee/fee_rate and add_foreign_utxo --- crates/bdk/src/wallet/mod.rs | 88 +++++++++++++++++++++++++---- crates/bdk/src/wallet/tx_builder.rs | 4 ++ 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 1f6700b64..7a72ce4ab 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -431,10 +431,17 @@ impl Wallet { .next() } - /// Inserts the given foreign `TxOut` at `OutPoint` into the wallet's transaction graph. Any - /// inserted foreign TxOuts are not persisted until [`Self::commit`] is called. + /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph. + /// Any inserted TxOuts are not persisted until [`commit`] is called. + /// + /// This can be used to add a `TxOut` that the wallet doesn't own but is used as an input to + /// a [`Transaction`] passed to the [`calculate_fee`] or [`calculate_fee_rate`] functions. /// /// Only insert TxOuts you trust the values for! + /// + /// [`calculate_fee`]: Self::calculate_fee + /// [`calculate_fee_rate`]: Self::calculate_fee_rate + /// [`commit`]: Self::commit pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) where D: PersistBackend, @@ -445,12 +452,30 @@ impl Wallet { /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. /// - /// To calculate the fee for a [`Transaction`] that depends on foreign [`TxOut`] values you must - /// first manually insert the foreign TxOuts into the tx graph using the [`insert_txout`] function. - /// Only insert TxOuts you trust the values for! + /// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must + /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function. /// /// Note `tx` does not have to be in the graph for this to work. /// + /// # Examples + /// + /// ```rust, no_run + /// # use bitcoin::Txid; + /// # use bdk::Wallet; + /// # let mut wallet: Wallet<()> = todo!(); + /// # let txid:Txid = todo!(); + /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let fee = wallet.calculate_fee(tx).expect("fee"); + /// ``` + /// + /// ```rust, no_run + /// # use bitcoin::psbt::PartiallySignedTransaction; + /// # use bdk::Wallet; + /// # let mut wallet: Wallet<()> = todo!(); + /// # let mut psbt: PartiallySignedTransaction = todo!(); + /// let tx = &psbt.clone().extract_tx(); + /// let fee = wallet.calculate_fee(tx).expect("fee"); + /// ``` /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee(&self, tx: &Transaction) -> Result { self.indexed_graph.graph().calculate_fee(tx) @@ -458,12 +483,30 @@ impl Wallet { /// Calculate the [`FeeRate`] for a given transaction. /// - /// To calculate the fee rate for a [`Transaction`] that depends on foreign [`TxOut`] values you - /// must first manually insert the foreign TxOuts into the tx graph using the [`insert_txout`] function. - /// Only insert TxOuts you trust the values for! + /// To calculate the fee rate for a [`Transaction`] with inputs not owned by this wallet you must + /// manually insert the TxOut(s) into the tx graph using the [`insert_txout`] function. /// /// Note `tx` does not have to be in the graph for this to work. /// + /// # Examples + /// + /// ```rust, no_run + /// # use bitcoin::Txid; + /// # use bdk::Wallet; + /// # let mut wallet: Wallet<()> = todo!(); + /// # let txid:Txid = todo!(); + /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate"); + /// ``` + /// + /// ```rust, no_run + /// # use bitcoin::psbt::PartiallySignedTransaction; + /// # use bdk::Wallet; + /// # let mut wallet: Wallet<()> = todo!(); + /// # let mut psbt: PartiallySignedTransaction = todo!(); + /// let tx = &psbt.clone().extract_tx(); + /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate"); + /// ``` /// [`insert_txout`]: Self::insert_txout pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result { self.calculate_fee(tx).map(|fee| { @@ -473,10 +516,31 @@ impl Wallet { } /// Computes total input value going from script pubkeys in the index (sent) and the total output - /// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed - /// correctly, the output being spent must have already been scanned by the index. Calculating - /// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has - /// not been scanned. + /// value going to script pubkeys in the index (received) in `tx`. + /// + /// For the `sent` to be computed correctly, the outputs being spent must have already been + /// scanned by the index. Calculating received just uses the [`Transaction`] outputs directly, + /// so it will be correct even if it has not been scanned. + /// + /// # Examples + /// + /// ```rust, no_run + /// # use bitcoin::Txid; + /// # use bdk::Wallet; + /// # let mut wallet: Wallet<()> = todo!(); + /// # let txid:Txid = todo!(); + /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let (sent, received) = wallet.sent_and_received(tx); + /// ``` + /// + /// ```rust, no_run + /// # use bitcoin::psbt::PartiallySignedTransaction; + /// # use bdk::Wallet; + /// # let mut wallet: Wallet<()> = todo!(); + /// # let mut psbt: PartiallySignedTransaction = todo!(); + /// let tx = &psbt.clone().extract_tx(); + /// let (sent, received) = wallet.sent_and_received(tx); + /// ``` pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) { self.indexed_graph.index.sent_and_received(tx) } diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index 4ccb74d9f..67e4c0c7e 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -335,6 +335,10 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// /// This is an **EXPERIMENTAL** feature, API and other major changes are expected. /// + /// In order to use [`Wallet::calculate_fee`] or [`Wallet::calculate_fee_rate`] for a transaction + /// created with foreign UTXO(s) you must manually insert the corresponding TxOut(s) into the tx + /// graph using the [`Wallet::insert_txout`] function. + /// /// # Errors /// /// This method returns errors in the following circumstances: From dd5b8d759954f39baa4523fa10b4a64efe48a25b Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Thu, 24 Aug 2023 09:52:31 -0500 Subject: [PATCH 5/6] test(wallet): add check_fee!(wallet,psbt) macro and use it in place of psbt.fee_amount() - removed test_calculate_fee_with_inserted_foreign_utxo() since it now duplicates test in test_add_foreign_utxo() --- crates/bdk/tests/wallet.rs | 107 ++++++++++++++----------------------- 1 file changed, 41 insertions(+), 66 deletions(-) diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 7ea364785..aad8c2db2 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -436,6 +436,15 @@ fn test_create_tx_change_policy_no_internal() { builder.finish().unwrap(); } +macro_rules! check_fee { + ($wallet:expr, $psbt: expr) => {{ + let tx = $psbt.clone().extract_tx(); + let tx_fee = $wallet.calculate_fee(&tx).ok(); + assert_eq!(tx_fee, $psbt.fee_amount()); + tx_fee + }}; +} + #[test] fn test_create_tx_drain_wallet_and_drain_to() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); @@ -443,7 +452,7 @@ fn test_create_tx_drain_wallet_and_drain_to() { let mut builder = wallet.build_tx(); builder.drain_to(addr.script_pubkey()).drain_wallet(); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 1); assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); @@ -462,7 +471,7 @@ fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { .drain_to(drain_addr.script_pubkey()) .drain_wallet(); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); let outputs = psbt.unsigned_tx.output; assert_eq!(outputs.len(), 2); @@ -489,7 +498,7 @@ fn test_create_tx_drain_to_and_utxos() { .add_utxos(&utxos) .unwrap(); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 1); assert_eq!(psbt.unsigned_tx.output[0].value, 50_000 - fee.unwrap_or(0)); @@ -512,7 +521,7 @@ fn test_create_tx_default_fee_rate() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 25_000); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::default(), @add_signature); } @@ -526,7 +535,7 @@ fn test_create_tx_custom_fee_rate() { .add_recipient(addr.script_pubkey(), 25_000) .fee_rate(FeeRate::from_sat_per_vb(5.0)); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_fee_rate!(psbt, fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature); } @@ -541,7 +550,7 @@ fn test_create_tx_absolute_fee() { .drain_wallet() .fee_absolute(100); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(fee.unwrap_or(0), 100); assert_eq!(psbt.unsigned_tx.output.len(), 1); @@ -558,7 +567,7 @@ fn test_create_tx_absolute_zero_fee() { .drain_wallet() .fee_absolute(0); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(fee.unwrap_or(0), 0); assert_eq!(psbt.unsigned_tx.output.len(), 1); @@ -589,7 +598,7 @@ fn test_create_tx_add_change() { .add_recipient(addr.script_pubkey(), 25_000) .ordering(TxOrdering::Untouched); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 2); assert_eq!(psbt.unsigned_tx.output[0].value, 25_000); @@ -603,7 +612,7 @@ fn test_create_tx_skip_change_dust() { let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), 49_800); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 1); assert_eq!(psbt.unsigned_tx.output[0].value, 49_800); @@ -634,7 +643,7 @@ fn test_create_tx_ordering_respected() { .add_recipient(addr.script_pubkey(), 10_000) .ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 3); assert_eq!(psbt.unsigned_tx.output[0].value, 10_000 - fee.unwrap_or(0)); @@ -1050,7 +1059,8 @@ fn test_add_foreign_utxo() { .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) .unwrap(); let mut psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); let sent_received = wallet1.sent_and_received(&psbt.clone().extract_tx()); assert_eq!( @@ -1129,42 +1139,6 @@ fn test_calculate_fee_with_missing_foreign_utxo() { wallet1.calculate_fee(&tx).unwrap(); } -#[test] -fn test_calculate_fee_with_inserted_foreign_utxo() { - let (mut 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().next().expect("must take!"); - #[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 psbt = builder.finish().unwrap(); - let psbt_fee = psbt.fee_amount().expect("psbt fee"); - let tx = psbt.extract_tx(); - - wallet1.insert_txout(utxo.outpoint, utxo.txout); - let wallet1_fee = wallet1.calculate_fee(&tx).expect("wallet fee"); - assert_eq!(psbt_fee, wallet1_fee); -} - #[test] #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] fn test_add_foreign_utxo_invalid_psbt_input() { @@ -1463,7 +1437,7 @@ fn test_bump_fee_reduce_change() { .enable_rbf(); let psbt = builder.finish().unwrap(); let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let original_fee = psbt.fee_amount(); + let original_fee = check_fee!(wallet, psbt); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1475,7 +1449,7 @@ fn test_bump_fee_reduce_change() { builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf(); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0); assert_eq!( @@ -1510,7 +1484,7 @@ fn test_bump_fee_reduce_change() { builder.enable_rbf(); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0); assert_eq!( @@ -1560,7 +1534,7 @@ fn test_bump_fee_reduce_single_recipient() { let psbt = builder.finish().unwrap(); let tx = psbt.clone().extract_tx(); let original_sent_received = wallet.sent_and_received(&tx); - let original_fee = psbt.fee_amount(); + let original_fee = check_fee!(wallet, psbt); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) @@ -1573,7 +1547,7 @@ fn test_bump_fee_reduce_single_recipient() { .unwrap(); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0); assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); @@ -1597,7 +1571,7 @@ fn test_bump_fee_absolute_reduce_single_recipient() { .drain_wallet() .enable_rbf(); let psbt = builder.finish().unwrap(); - let original_fee = psbt.fee_amount(); + let original_fee = check_fee!(wallet, psbt); let tx = psbt.extract_tx(); let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); @@ -1613,7 +1587,7 @@ fn test_bump_fee_absolute_reduce_single_recipient() { let psbt = builder.finish().unwrap(); let tx = &psbt.unsigned_tx; let sent_received = wallet.sent_and_received(tx); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0); assert!(fee.unwrap_or(0) > original_fee.unwrap_or(0)); @@ -1784,7 +1758,7 @@ fn test_bump_fee_add_input() { builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_details.0 + 25_000); assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); @@ -1834,7 +1808,7 @@ fn test_bump_fee_absolute_add_input() { builder.fee_absolute(6_000); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0 + 25_000); assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); @@ -1880,7 +1854,7 @@ fn test_bump_fee_no_change_add_input_and_change() { .enable_rbf(); let psbt = builder.finish().unwrap(); let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let original_fee = psbt.fee_amount(); + let original_fee = check_fee!(wallet, psbt); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1894,7 +1868,7 @@ fn test_bump_fee_no_change_add_input_and_change() { builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); let original_send_all_amount = original_sent_received.0 - original_fee.unwrap_or(0); assert_eq!(sent_received.0, original_sent_received.0 + 50_000); @@ -1939,7 +1913,7 @@ fn test_bump_fee_add_input_change_dust() { .enable_rbf(); let psbt = builder.finish().unwrap(); let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let original_fee = psbt.fee_amount(); + let original_fee = check_fee!(wallet, psbt); let mut tx = psbt.extract_tx(); for txin in &mut tx.input { @@ -1971,7 +1945,7 @@ fn test_bump_fee_add_input_change_dust() { builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight)); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(original_sent_received.1, 5_000 - original_fee.unwrap_or(0)); @@ -2025,7 +1999,7 @@ fn test_bump_fee_force_add_input() { .fee_rate(FeeRate::from_sat_per_vb(5.0)); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0 + 25_000); assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); @@ -2083,7 +2057,7 @@ fn test_bump_fee_absolute_force_add_input() { builder.add_utxo(incoming_op).unwrap().fee_absolute(250); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(sent_received.0, original_sent_received.0 + 25_000); assert_eq!(fee.unwrap_or(0) + sent_received.1, 30_000); @@ -2212,7 +2186,7 @@ fn test_fee_amount_negative_drain_val() { .enable_rbf() .fee_rate(fee_rate); let psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); assert_eq!(psbt.inputs.len(), 1); assert_fee_rate!(psbt, fee.unwrap_or(0), fee_rate, @add_signature); @@ -2951,7 +2925,8 @@ fn test_taproot_foreign_utxo() { .unwrap(); let psbt = builder.finish().unwrap(); let sent_received = wallet1.sent_and_received(&psbt.clone().extract_tx()); - let fee = psbt.fee_amount(); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); assert_eq!( sent_received.0 - sent_received.1, @@ -3403,7 +3378,7 @@ fn test_fee_rate_sign_no_grinding_high_r() { .fee_rate(fee_rate) .add_data(&data); let mut psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); let (op_return_vout, _) = psbt .unsigned_tx .output @@ -3467,7 +3442,7 @@ fn test_fee_rate_sign_grinding_low_r() { .drain_wallet() .fee_rate(fee_rate); let mut psbt = builder.finish().unwrap(); - let fee = psbt.fee_amount(); + let fee = check_fee!(wallet, psbt); wallet .sign( From 5fb5061645ae92d37091a215e48e84423fea48a3 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 30 Aug 2023 12:12:33 -0500 Subject: [PATCH 6/6] ci: fix msrv dependency versions for rustls --- .github/workflows/cont_integration.yml | 3 ++- README.md | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index f02dcb834..63b9a0b73 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -32,7 +32,8 @@ jobs: run: | cargo update -p log --precise "0.4.18" cargo update -p tempfile --precise "3.6.0" - cargo update -p rustls:0.21.6 --precise "0.21.1" + cargo update -p rustls:0.21.7 --precise "0.21.1" + cargo update -p rustls:0.20.9 --precise "0.20.8" cargo update -p tokio:1.32.0 --precise "1.29.1" cargo update -p flate2:1.0.27 --precise "1.0.26" cargo update -p reqwest --precise "0.11.18" diff --git a/README.md b/README.md index ae230abbd..b54a89d00 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,15 @@ This library should compile with any combination of features with Rust 1.57.0. To build with the MSRV you will need to pin dependencies as follows: -``` +```shell # log 0.4.19 has MSRV 1.60.0+ cargo update -p log --precise "0.4.18" # tempfile 3.7.0 has MSRV 1.63.0+ cargo update -p tempfile --precise "3.6.0" # rustls 0.21.2 has MSRV 1.60.0+ -cargo update -p rustls:0.21.6 --precise "0.21.1" +cargo update -p rustls:0.21.7 --precise "0.21.1" +# rustls 0.20.9 has MSRV 1.60.0+ +cargo update -p rustls:0.20.9 --precise "0.20.8" # tokio 1.30 has MSRV 1.63.0+ cargo update -p tokio:1.32.0 --precise "1.29.1" # flate2 1.0.27 has MSRV 1.63.0+