Skip to content

Commit

Permalink
feat(bdk)!: remove TransactionDetails, change Wallet::get_tx, TxBuild…
Browse files Browse the repository at this point in the history
…er::finish return types

BREAKING CHANGES:

Removed
- TransactionDetails struct

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

Added
- TransactionAmount struct with tx sent and received amounts
- Wallet::sent_and_received function returns TransactionAmount
- Wallet::fee_amount and Wallet::fee_rate functions for tx fees
- Wallet::get_txout function for all known wallet txouts

Fixed
- impacted tests
  • Loading branch information
notmandatory committed Jul 28, 2023
1 parent 8f38e96 commit d147f66
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 337 deletions.
2 changes: 1 addition & 1 deletion crates/bdk/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 7 additions & 32 deletions crates/bdk/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, util::psbt};
use bitcoin::blockdata::transaction::{OutPoint, TxOut};
use bitcoin::util::psbt;

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -234,38 +234,13 @@ impl Utxo {
}
}

/// A wallet transaction
/// A `Transaction` sent and received amounts.
#[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 struct TransactionAmounts {
/// amount sent, in sats
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))
}
/// amount received, in sats
pub received: u64,
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/bdk/src/wallet/coin_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
//! // create wallet, sync, ...
//!
//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").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()?
Expand Down
182 changes: 95 additions & 87 deletions crates/bdk/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use core::fmt;
use core::ops::Deref;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};

use bdk_chain::indexed_tx_graph::Indexer;
#[allow(unused_imports)]
use log::{debug, error, info, trace};

Expand Down Expand Up @@ -427,27 +428,97 @@ impl<D> Wallet<D> {
.next()
}

/// Return a single transactions made and received by the wallet
///
/// 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> {
/// Returns a `TxOut` known by this wallet corresponding to `outpoint` if it exists in the
/// wallet's database whether spent or not.
pub fn get_txout(&self, outpoint: &OutPoint) -> Option<TxOut> {
self.indexed_graph
.graph()
.all_txouts()
.filter_map(|(op, txo)| {
if op.eq(outpoint) {
Some(txo.clone())
} else {
None
}
})
.next()
}

/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
/// If the `Wallet` is missing a TxOut for an input returns None.
pub fn fee_amount(&self, tx: &Transaction) -> Option<u64> {
let txouts: Option<Vec<TxOut>> = tx
.input
.iter()
.map(|txin| self.get_txout(&txin.previous_output))
.collect();

txouts.map(|inputs| {
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
let output_amount: u64 = tx.output.iter().map(|o| o.value).sum();
input_amount
.checked_sub(output_amount)
.expect("input amount must be greater than output amount")
})
}

/// The transaction's `FeeRate`. If the `Wallet` is missing a `TxOut` for an input returns None.
pub fn fee_rate(&self, tx: &Transaction) -> Option<FeeRate> {
let fee_amount = self.fee_amount(tx);
fee_amount.map(|fee| {
let weight = tx.weight();
FeeRate::from_wu(fee, weight)
})
}

/// Return `TransactionAmounts` for a `Transaction` in relation to the `Wallet` and it's
/// descriptors.
pub fn sent_and_received(&self, tx: &Transaction) -> TransactionAmounts {
let index = &self.indexed_graph.index;

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

TransactionAmounts { sent, received }
}

/// Return a single `CanonicalTx` made and received by the wallet or `None` if it doesn't
/// exist in the wallet
pub fn get_tx(
&self,
txid: Txid,
) -> Option<CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> {
let graph = self.indexed_graph.graph();

let canonical_tx = CanonicalTx {
Some(CanonicalTx {
observed_as: graph.get_chain_position(
&self.chain,
self.chain.tip().unwrap_or_default(),
txid,
)?,
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.
Expand Down Expand Up @@ -524,6 +595,10 @@ impl<D> Wallet<D> {
let changeset: ChangeSet = self.indexed_graph.insert_tx(&tx, anchor, last_seen).into();
let changed = !changeset.is_empty();
self.persist.stage(changeset);

let additions = self.indexed_graph.index.index_tx(&tx);
self.indexed_graph.index.apply_additions(additions);

Ok(changed)
}

Expand Down Expand Up @@ -599,7 +674,7 @@ impl<D> Wallet<D> {
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let (psbt, details) = {
/// let psbt = {
/// let mut builder = wallet.build_tx();
/// builder
/// .add_recipient(to_address.script_pubkey(), 50_000);
Expand All @@ -624,7 +699,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>,
{
Expand Down Expand Up @@ -969,20 +1044,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.
Expand All @@ -1001,7 +1064,7 @@ impl<D> Wallet<D> {
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let (mut psbt, _) = {
/// let mut psbt = {
/// let mut builder = wallet.build_tx();
/// builder
/// .add_recipient(to_address.script_pubkey(), 50_000)
Expand All @@ -1011,7 +1074,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(FeeRate::from_sat_per_vb(5.0));
Expand Down Expand Up @@ -1171,7 +1234,7 @@ impl<D> Wallet<D> {
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// let (mut psbt, _) = {
/// let mut psbt = {
/// let mut builder = wallet.build_tx();
/// builder.add_recipient(to_address.script_pubkey(), 50_000);
/// builder.finish()?
Expand Down Expand Up @@ -1721,7 +1784,7 @@ impl<D> Wallet<D> {
Ok(changed)
}

/// 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.
Expand Down Expand Up @@ -1812,61 +1875,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.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.node.txid,
received,
sent,
fee,
confirmation_time: canonical_tx.observed_as.cloned().into(),
}
}

#[macro_export]
#[doc(hidden)]
/// Macro for getting a wallet for use in a doctest
Expand Down
15 changes: 6 additions & 9 deletions crates/bdk/src/wallet/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>(())
//! ```
Expand All @@ -48,10 +48,7 @@ use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};

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 {}
Expand Down Expand Up @@ -85,7 +82,7 @@ impl TxBuilderContext for BumpFee {}
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
/// # let addr2 = addr1.clone();
/// // chaining
/// let (psbt1, details) = {
/// let psbt1 = {
/// let mut builder = wallet.build_tx();
/// builder
/// .ordering(TxOrdering::Untouched)
Expand All @@ -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] {
Expand Down Expand Up @@ -527,7 +524,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>,
{
Expand Down Expand Up @@ -637,7 +634,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// .drain_to(to_address.script_pubkey())
/// .fee_rate(FeeRate::from_sat_per_vb(5.0))
/// .enable_rbf();
/// let (psbt, tx_details) = tx_builder.finish()?;
/// let psbt = tx_builder.finish()?;
/// # Ok::<(), bdk::Error>(())
/// ```
///
Expand Down
Loading

0 comments on commit d147f66

Please sign in to comment.