Merge bitcoindevkit/bdk#1048: Remove TransactionDetails from Wallet API

5fb5061645ae92d37091a215e48e84423fea48a3 ci: fix msrv dependency versions for rustls (Steve Myers)
dd5b8d759954f39baa4523fa10b4a64efe48a25b test(wallet): add check_fee!(wallet,psbt) macro and use it in place of psbt.fee_amount() (Steve Myers)
465d53cc88b32bf8098dc61e9afdbb28d22b213d docs(wallet): update docs for calculate_fee/fee_rate and add_foreign_utxo (Steve Myers)
036299803f9f94c7c8218a75f3c206fa8c4297ab feat(wallet): add Wallet::insert_txout function and updated docs for fee functions (Steve Myers)
d443fe7f6613776a3dce00def0b655c4b2b36728 feat(tx_graph)!: change TxGraph::calculate_fee to return Result<u64,CalculateFeeError> (Steve Myers)
b4c31cd5bad4fea18044aab2cffd657b16ec185b feat(wallet)!: remove TransactionDetails from bdk::Wallet API (Steve Myers)

Pull request description:

  ### Description

  Removed `TransactionDetails` and changed `Wallet::get_tx` to return a `CanonicalTx`, and `TxBuilder::finish` to return only a `PartiallySignedTransaction`. This should fix #922 and fix #1015.

  I also added `Wallet` functions to get a `Transaction` send and receive amounts, fee, and `FeeRate`.

  see: https://github.com/bitcoindevkit/bdk/issues/922#issuecomment-1652814975

  ### Notes to the reviewers

  Alot of wallet tests had to change since `TxBuilder::finish` only returns a PSBT now.

  I added a new `CalculateFeeError` which follows changes coming in #1028.

  ### Changelog notice

  Added
  - Wallet::sent_and_received function
  - Wallet::calculate_fee and Wallet::calculate_fee_rate functions
  - Wallet::error::CalculateFeeError
  - Wallet::insert_txout function to allow inserting foreign TxOuts

  BREAKING CHANGES:

  Removed
  - TransactionDetails struct

  Changed
  - Wallet::get_tx now returns CanonicalTx instead of TransactionDetails
  - TxBuilder::finish now returns only a PartiallySignedTransaction

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [x] I've added tests for the new feature
  * [x] I've added docs for the new feature

ACKs for top commit:
  evanlinjin:
    ACK 5fb5061645ae92d37091a215e48e84423fea48a3

Tree-SHA512: 1a0be1c229b8871e5ee23ba6874ff40370170477a0a8bb104c0197e7fd97765d84854285f863dd1b38a34c3b71815e75e4db5b25288c81caea99a14ddaa78254
This commit is contained in:
志宇 2023-08-31 13:29:01 +08:00
commit 93e8eaf7ee
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
16 changed files with 690 additions and 404 deletions

View File

@ -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"

View File

@ -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+

View File

@ -754,7 +754,7 @@ fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, 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)

View File

@ -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>,
/// 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<u64>,
/// 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<core::cmp::Ordering> {
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::*;

View File

@ -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()?

View File

@ -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<D> Wallet<D> {
.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<TransactionDetails> {
/// 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<ChangeSet>,
{
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<u64, CalculateFeeError> {
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<FeeRate, CalculateFeeError> {
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<CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> {
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<D> Wallet<D> {
/// # 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<D> Wallet<D> {
&mut self,
coin_selection: Cs,
params: TxParams,
) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error>
) -> Result<psbt::PartiallySignedTransaction, Error>
where
D: PersistBackend<ChangeSet>,
{
@ -976,20 +1127,8 @@ impl<D> Wallet<D> {
// 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<D> Wallet<D> {
/// # 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<D> Wallet<D> {
/// 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<D> Wallet<D> {
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::<Vec<_>>();
@ -1149,7 +1287,7 @@ impl<D> Wallet<D> {
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<D> Wallet<D> {
/// # 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<D> Wallet<D> {
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<ConfirmationTimeAnchor, KeychainTxOutIndex<KeychainKind>>,
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::<Option<u64>>();
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

View File

@ -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<Psbt, Error>
where
D: PersistBackend<ChangeSet>,
{
@ -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>(())
/// ```
///

View File

@ -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};
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>,
) -> (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![],
output: vec![TxOut {
value: 50_000,
script_pubkey: address.script_pubkey(),
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: 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)
}

View File

@ -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());

File diff suppressed because it is too large Load Diff

View File

@ -288,8 +288,8 @@ impl<I: Clone + Ord> SpkTxOutIndex<I> {
/// 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;

View File

@ -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<OutPoint>),
/// When the transaction is invalid according to the graph it has a negative fee
NegativeFee(i64),
}
impl<A> TxGraph<A> {
/// Iterate over all tx outputs known by [`TxGraph`].
///
@ -236,25 +245,37 @@ impl<A> TxGraph<A> {
}
/// 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<i64> {
///
/// [`insert_txout`]: Self::insert_txout
pub fn calculate_fee(&self, tx: &Transaction) -> Result<u64, CalculateFeeError> {
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::<Option<i64>>()?;
let outputs_sum = tx
.output
@ -262,7 +283,12 @@ impl<A> TxGraph<A> {
.map(|txout| txout.value as i64)
.sum::<i64>();
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.

View File

@ -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]

View File

@ -81,7 +81,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.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);

View File

@ -87,7 +87,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.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);

View File

@ -87,7 +87,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.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);