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+ 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/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..7a72ce4ab 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}; @@ -430,27 +431,177 @@ impl Wallet { .next() } - /// Return a single transactions made and received by the wallet + /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph. + /// Any inserted TxOuts are not persisted until [`commit`] is called. /// - /// 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 { + /// 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, + { + 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`] 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) + } + + /// Calculate the [`FeeRate`] for a given transaction. + /// + /// 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| { + 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 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) + } + + /// 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, + ) -> 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 +754,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 +779,7 @@ impl Wallet { &mut self, coin_selection: Cs, params: TxParams, - ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> + ) -> Result where D: PersistBackend, { @@ -976,20 +1127,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 +1147,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 +1157,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)); @@ -1059,13 +1198,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::>(); @@ -1149,7 +1287,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() }; @@ -1179,7 +1317,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 +1873,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 +1964,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..67e4c0c7e 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] { @@ -338,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: @@ -531,7 +532,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 +646,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..ee8ed74e1 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -1,25 +1,68 @@ #![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}; - -/// Return a fake wallet that appears to be funded for testing. +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. +// +// 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>, ) -> (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,19 +71,39 @@ 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()) } +// 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/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..aad8c2db2 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; @@ -85,7 +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, txid) = get_funded_wallet(get_test_wpkh()); + + let mut tx_amounts: Vec<(Txid, (u64, u64))> = wallet + .transactions() + .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)); + + 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, txid) = get_funded_wallet(get_test_wpkh()); + + 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, txid) = get_funded_wallet(get_test_wpkh()); + + 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 { @@ -195,7 +246,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 +258,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 +272,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 +287,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 +301,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 +317,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 +342,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 +355,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 +381,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 +406,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 +417,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)); } @@ -385,19 +436,26 @@ 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()); 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 = check_fee!(wallet, psbt); 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 +470,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 = check_fee!(wallet, psbt); let outputs = psbt.unsigned_tx.output; assert_eq!(outputs.len(), 2); @@ -425,7 +484,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 +497,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 = check_fee!(wallet, psbt); 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 +520,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 = check_fee!(wallet, psbt); - 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 +534,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 = check_fee!(wallet, psbt); - 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 +549,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 = check_fee!(wallet, psbt); - 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 +566,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 = check_fee!(wallet, psbt); - 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 +584,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 +597,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 = check_fee!(wallet, psbt); 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 +611,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 = check_fee!(wallet, psbt); 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 +642,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 = check_fee!(wallet, psbt); 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 +657,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 +670,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 +687,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 +709,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 +731,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 +754,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 +775,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 +793,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 +809,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 +822,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 +834,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 +870,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 +963,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 +984,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 +1005,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 +1021,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 +1058,14 @@ 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(); + 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!( - 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" ); @@ -1045,6 +1104,41 @@ 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] #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] fn test_add_foreign_utxo_invalid_psbt_input() { @@ -1069,8 +1163,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().tx_node.tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); #[allow(deprecated)] let satisfaction_weight = wallet2 @@ -1159,9 +1253,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().tx_node.tx; let psbt_input = psbt::Input { - non_witness_utxo: Some(tx2), + non_witness_utxo: Some(tx2.clone()), ..Default::default() }; builder @@ -1209,7 +1303,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 +1322,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 +1339,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 +1366,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 +1389,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 +1412,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 +1435,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 = check_fee!(wallet, psbt); + let tx = psbt.extract_tx(); let txid = tx.txid(); wallet @@ -1350,14 +1447,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 = check_fee!(wallet, psbt); - 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 +1474,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 = check_fee!(wallet, psbt); - 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 +1514,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 +1531,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 = check_fee!(wallet, psbt); let txid = tx.txid(); wallet .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) @@ -1442,16 +1545,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 = check_fee!(wallet, psbt); - 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 +1570,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 = check_fee!(wallet, psbt); 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 +1584,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 = check_fee!(wallet, psbt); - 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 +1634,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 +1652,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 +1702,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 +1746,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 +1756,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 = 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); let tx = &psbt.unsigned_tx; assert_eq!(tx.input.len(), 2); @@ -1664,10 +1779,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 +1796,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 +1806,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 = check_fee!(wallet, psbt); - 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 +1830,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 +1852,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 = check_fee!(wallet, psbt); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1746,13 +1866,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 = check_fee!(wallet, psbt); - 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 +1894,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 +1911,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 = check_fee!(wallet, psbt); + 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 +1943,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 = check_fee!(wallet, psbt); - 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 +1965,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 +1980,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 +1997,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 = check_fee!(wallet, psbt); - 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 +2021,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 +2039,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 +2055,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 = check_fee!(wallet, psbt); - 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 +2079,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 +2103,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 +2143,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 +2185,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 = check_fee!(wallet, psbt); - 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 +2198,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 +2213,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 +2228,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 +2243,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 +2259,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 +2274,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 +2296,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 +2315,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 +2359,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 +2389,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 +2426,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 +2697,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 +2738,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 +2798,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 +2840,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 +2880,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().tx_node.tx.clone()); assert!( psbt.inputs[0].non_witness_utxo.is_some(), "Previous tx should be present in the database" @@ -2792,11 +2923,14 @@ 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()); + wallet1.insert_txout(utxo.outpoint, utxo.txout); + let fee = check_fee!(wallet1, psbt); 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 +2948,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 +2972,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 +3007,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 +3038,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 +3078,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 +3116,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 +3139,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 +3162,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 +3182,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 +3377,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 = check_fee!(wallet, psbt); let (op_return_vout, _) = psbt .unsigned_tx .output @@ -3289,7 +3424,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 +3441,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 = check_fee!(wallet, psbt); wallet .sign( @@ -3322,7 +3458,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 +3522,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/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 adb84ca22..404068752 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,37 @@ 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 `OK(_)` if we have all the [`TxOut`]s being spent by `tx` in the graph (either as + /// the full transactions or individual txouts). /// - /// Returns `None` if we're missing an input for the tx in the graph. + /// 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. - pub fn calculate_fee(&self, tx: &Transaction) -> Option { + /// + /// [`insert_txout`]: Self::insert_txout + pub fn calculate_fee(&self, tx: &Transaction) -> Result { if tx.is_coin_base() { - return Some(0); + return Ok(0); + } + + 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 as i64; + (sum, missing_outpoints) + } + }, + ); + if !missing_outputs.is_empty() { + return Err(CalculateFeeError::MissingTxOut(missing_outputs)); } - let inputs_sum = tx - .input - .iter() - .map(|txin| { - self.get_txout(txin.previous_output) - .map(|txout| txout.value as i64) - }) - .sum::>()?; let outputs_sum = tx .output @@ -262,7 +283,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] 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);