diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs
index 92a08a9ef..73ae458ff 100644
--- a/crates/chain/src/indexed_tx_graph.rs
+++ b/crates/chain/src/indexed_tx_graph.rs
@@ -90,14 +90,36 @@ where
/// Apply an `update` directly.
///
- /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`].
- pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet {
- let graph = self.graph.apply_update(update);
- let indexer = self.index_tx_graph_changeset(&graph);
- ChangeSet {
- tx_graph: graph,
- indexer,
- }
+ /// `update` is a [`tx_graph::Update`] and the resultant changes is returned as [`ChangeSet`].
+ #[cfg(feature = "std")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+ pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet {
+ let tx_graph = self.graph.apply_update(update);
+ let indexer = self.index_tx_graph_changeset(&tx_graph);
+ ChangeSet { tx_graph, indexer }
+ }
+
+ /// Apply the given `update` with an optional `seen_at` timestamp.
+ ///
+ /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the
+ /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The
+ /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed
+ /// transactions (where the transaction with the lower `last_seen` value is omitted from the
+ /// canonical history).
+ ///
+ /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will
+ /// not be part of the canonical history of transactions.
+ ///
+ /// Use [`apply_update`](IndexedTxGraph::apply_update) to have the `seen_at` value automatically
+ /// set to the current time.
+ pub fn apply_update_at(
+ &mut self,
+ update: tx_graph::Update,
+ seen_at: Option,
+ ) -> ChangeSet {
+ let tx_graph = self.graph.apply_update_at(update, seen_at);
+ let indexer = self.index_tx_graph_changeset(&tx_graph);
+ ChangeSet { tx_graph, indexer }
}
/// Insert a floating `txout` of given `outpoint`.
diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs
index 3fb8c0eda..029eedc28 100644
--- a/crates/chain/src/lib.rs
+++ b/crates/chain/src/lib.rs
@@ -17,6 +17,12 @@
//!
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
+// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
+#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(
+ docsrs,
+ doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")
+)]
#![no_std]
#![warn(missing_docs)]
diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs
index 567a8f0a9..e31b431dd 100644
--- a/crates/chain/src/spk_client.rs
+++ b/crates/chain/src/spk_client.rs
@@ -3,7 +3,7 @@ use crate::{
alloc::{boxed::Box, collections::VecDeque, vec::Vec},
collections::BTreeMap,
local_chain::CheckPoint,
- ConfirmationBlockTime, Indexed, TxGraph,
+ ConfirmationBlockTime, Indexed,
};
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
@@ -345,8 +345,8 @@ impl SyncRequest {
#[must_use]
#[derive(Debug)]
pub struct SyncResult {
- /// The update to apply to the receiving [`TxGraph`].
- pub graph_update: TxGraph,
+ /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph).
+ pub graph_update: crate::tx_graph::Update,
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
pub chain_update: Option,
}
@@ -497,8 +497,8 @@ impl FullScanRequest {
#[derive(Debug)]
pub struct FullScanResult {
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
- pub graph_update: TxGraph,
- /// The update to apply to the receiving [`TxGraph`].
+ pub graph_update: crate::tx_graph::Update,
+ /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph).
pub chain_update: Option,
/// Last active indices for the corresponding keychains (`K`).
pub last_active_indices: BTreeMap,
diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs
index 0eab93867..e953580ce 100644
--- a/crates/chain/src/tx_graph.rs
+++ b/crates/chain/src/tx_graph.rs
@@ -70,13 +70,17 @@
//!
//! ```
//! # use bdk_chain::{Merge, BlockId};
-//! # use bdk_chain::tx_graph::TxGraph;
+//! # use bdk_chain::tx_graph::{self, TxGraph};
//! # use bdk_chain::example_utils::*;
//! # use bitcoin::Transaction;
+//! # use std::sync::Arc;
//! # let tx_a = tx_from_hex(RAW_TX_1);
//! # let tx_b = tx_from_hex(RAW_TX_2);
//! let mut graph: TxGraph = TxGraph::default();
-//! let update = TxGraph::new(vec![tx_a, tx_b]);
+//!
+//! let mut update = tx_graph::Update::default();
+//! update.txs.push(Arc::new(tx_a));
+//! update.txs.push(Arc::new(tx_b));
//!
//! // apply the update graph
//! let changeset = graph.apply_update(update.clone());
@@ -101,6 +105,62 @@ use core::{
ops::{Deref, RangeInclusive},
};
+/// Data object used to update the [`TxGraph`] with.
+#[derive(Debug, Clone)]
+pub struct Update {
+ /// Full transactions.
+ pub txs: Vec>,
+ /// Floating txouts.
+ pub txouts: BTreeMap,
+ /// Transaction anchors.
+ pub anchors: BTreeSet<(A, Txid)>,
+ /// Seen at times for transactions.
+ pub seen_ats: HashMap,
+}
+
+impl Default for Update {
+ fn default() -> Self {
+ Self {
+ txs: Default::default(),
+ txouts: Default::default(),
+ anchors: Default::default(),
+ seen_ats: Default::default(),
+ }
+ }
+}
+
+impl From> for Update {
+ fn from(graph: TxGraph) -> Self {
+ Self {
+ txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(),
+ txouts: graph
+ .floating_txouts()
+ .map(|(op, txo)| (op, txo.clone()))
+ .collect(),
+ anchors: graph.anchors,
+ seen_ats: graph.last_seen.into_iter().collect(),
+ }
+ }
+}
+
+impl From> for TxGraph {
+ fn from(update: Update) -> Self {
+ let mut graph = TxGraph::::default();
+ let _ = graph.apply_update_at(update, None);
+ graph
+ }
+}
+
+impl Update {
+ /// Extend this update with `other`.
+ pub fn extend(&mut self, other: Update) {
+ self.txs.extend(other.txs);
+ self.txouts.extend(other.txouts);
+ self.anchors.extend(other.anchors);
+ self.seen_ats.extend(other.seen_ats);
+ }
+}
+
/// A graph of transactions and spends.
///
/// See the [module-level documentation] for more.
@@ -512,28 +572,66 @@ impl TxGraph {
///
/// [`apply_changeset`]: Self::apply_changeset
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet {
- let mut update = Self::default();
- update.txs.insert(
- outpoint.txid,
- (
- TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
- BTreeSet::new(),
- ),
- );
- self.apply_update(update)
+ let mut changeset = ChangeSet::::default();
+ let (tx_node, _) = self.txs.entry(outpoint.txid).or_default();
+ match tx_node {
+ TxNodeInternal::Whole(_) => {
+ // ignore this txout we have the full one already.
+ // NOTE: You might think putting a debug_assert! here to check the output being
+ // replaced was actually correct is a good idea but the tests have already been
+ // written assuming this never panics.
+ }
+ TxNodeInternal::Partial(partial_tx) => {
+ match partial_tx.insert(outpoint.vout, txout.clone()) {
+ Some(old_txout) => {
+ debug_assert_eq!(
+ txout, old_txout,
+ "txout of the same outpoint should never change"
+ );
+ }
+ None => {
+ changeset.txouts.insert(outpoint, txout);
+ }
+ }
+ }
+ }
+ changeset
}
/// Inserts the given transaction into [`TxGraph`].
///
/// The [`ChangeSet`] returned will be empty if `tx` already exists.
pub fn insert_tx>>(&mut self, tx: T) -> ChangeSet {
- let tx = tx.into();
- let mut update = Self::default();
- update.txs.insert(
- tx.compute_txid(),
- (TxNodeInternal::Whole(tx), BTreeSet::new()),
- );
- self.apply_update(update)
+ let tx: Arc = tx.into();
+ let txid = tx.compute_txid();
+ let mut changeset = ChangeSet::::default();
+
+ let (tx_node, _) = self.txs.entry(txid).or_default();
+ match tx_node {
+ TxNodeInternal::Whole(existing_tx) => {
+ debug_assert_eq!(
+ existing_tx.as_ref(),
+ tx.as_ref(),
+ "tx of same txid should never change"
+ );
+ }
+ partial_tx => {
+ for txin in &tx.input {
+ // this means the tx is coinbase so there is no previous output
+ if txin.previous_output.is_null() {
+ continue;
+ }
+ self.spends
+ .entry(txin.previous_output)
+ .or_default()
+ .insert(txid);
+ }
+ *partial_tx = TxNodeInternal::Whole(tx.clone());
+ changeset.txs.insert(tx);
+ }
+ }
+
+ changeset
}
/// Batch insert unconfirmed transactions.
@@ -558,206 +656,112 @@ impl TxGraph {
/// The [`ChangeSet`] returned will be empty if graph already knows that `txid` exists in
/// `anchor`.
pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet {
- let mut update = Self::default();
- update.anchors.insert((anchor, txid));
- self.apply_update(update)
+ let mut changeset = ChangeSet::::default();
+ if self.anchors.insert((anchor.clone(), txid)) {
+ let (_tx_node, anchors) = self.txs.entry(txid).or_default();
+ let _inserted = anchors.insert(anchor.clone());
+ debug_assert!(
+ _inserted,
+ "anchors in `.anchors` and `.txs` should be consistent"
+ );
+ changeset.anchors.insert((anchor, txid));
+ }
+ changeset
}
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
///
- /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch
- /// update all unconfirmed transactions with the latest `seen_at`, see
- /// [`update_last_seen_unconfirmed`].
- ///
- /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
+ /// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet {
- let mut update = Self::default();
- update.last_seen.insert(txid, seen_at);
- self.apply_update(update)
- }
-
- /// Update the last seen time for all unconfirmed transactions.
- ///
- /// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting
- /// the given `seen_at` for every transaction not yet anchored to a confirmed block,
- /// and returns the [`ChangeSet`] after applying all updates to `self`.
- ///
- /// This is useful for keeping track of the latest time a transaction was seen
- /// unconfirmed, which is important for evaluating transaction conflicts in the same
- /// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for
- /// [`try_get_chain_position`].
- ///
- /// A normal use of this method is to call it with the current system time. Although
- /// block headers contain a timestamp, using the header time would be less effective
- /// at tracking mempool transactions, because it can drift from actual clock time, plus
- /// we may want to update a transaction's last seen time repeatedly between blocks.
- ///
- /// # Example
- ///
- /// ```rust
- /// # use bdk_chain::example_utils::*;
- /// # use std::time::UNIX_EPOCH;
- /// # let tx = tx_from_hex(RAW_TX_1);
- /// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]);
- /// let now = std::time::SystemTime::now()
- /// .duration_since(UNIX_EPOCH)
- /// .expect("valid duration")
- /// .as_secs();
- /// let changeset = tx_graph.update_last_seen_unconfirmed(now);
- /// assert!(!changeset.last_seen.is_empty());
- /// ```
- ///
- /// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must
- /// by strictly greater than what is currently stored for a transaction to have an effect.
- /// To insert a last seen time for a single txid, see [`insert_seen_at`].
- ///
- /// [`insert_seen_at`]: Self::insert_seen_at
- /// [`try_get_chain_position`]: Self::try_get_chain_position
- pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet {
- let mut changeset = ChangeSet::default();
- let unanchored_txs: Vec = self
- .txs
- .iter()
- .filter_map(
- |(&txid, (_, anchors))| {
- if anchors.is_empty() {
- Some(txid)
- } else {
- None
- }
- },
- )
- .collect();
-
- for txid in unanchored_txs {
- changeset.merge(self.insert_seen_at(txid, seen_at));
+ let mut changeset = ChangeSet::::default();
+ let last_seen = self.last_seen.entry(txid).or_default();
+ if seen_at > *last_seen {
+ *last_seen = seen_at;
+ changeset.last_seen.insert(txid, seen_at);
}
changeset
}
- /// Extends this graph with another so that `self` becomes the union of the two sets of
- /// transactions.
+ /// Extends this graph with the given `update`.
///
/// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that
/// exist in `update` but not in `self`).
- pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet {
- let changeset = self.determine_changeset(update);
- self.apply_changeset(changeset.clone());
+ #[cfg(feature = "std")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
+ pub fn apply_update(&mut self, update: Update) -> ChangeSet {
+ use std::time::*;
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("current time must be greater than epoch anchor");
+ self.apply_update_at(update, Some(now.as_secs()))
+ }
+
+ /// Extends this graph with the given `update` alongside an optional `seen_at` timestamp.
+ ///
+ /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the
+ /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The
+ /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed
+ /// transactions (where the transaction with the lower `last_seen` value is omitted from the
+ /// canonical history).
+ ///
+ /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will
+ /// not be part of the canonical history of transactions.
+ ///
+ /// Use [`apply_update`](TxGraph::apply_update) to have the `seen_at` value automatically set
+ /// to the current time.
+ pub fn apply_update_at(&mut self, update: Update, seen_at: Option) -> ChangeSet {
+ let mut changeset = ChangeSet::::default();
+ let mut unanchored_txs = HashSet::::new();
+ for tx in update.txs {
+ if unanchored_txs.insert(tx.compute_txid()) {
+ changeset.merge(self.insert_tx(tx));
+ }
+ }
+ for (outpoint, txout) in update.txouts {
+ changeset.merge(self.insert_txout(outpoint, txout));
+ }
+ for (anchor, txid) in update.anchors {
+ unanchored_txs.remove(&txid);
+ changeset.merge(self.insert_anchor(txid, anchor));
+ }
+ for (txid, seen_at) in update.seen_ats {
+ changeset.merge(self.insert_seen_at(txid, seen_at));
+ }
+ if let Some(seen_at) = seen_at {
+ for txid in unanchored_txs {
+ changeset.merge(self.insert_seen_at(txid, seen_at));
+ }
+ }
changeset
}
/// Determines the [`ChangeSet`] between `self` and an empty [`TxGraph`].
pub fn initial_changeset(&self) -> ChangeSet {
- Self::default().determine_changeset(self.clone())
+ ChangeSet {
+ txs: self.full_txs().map(|tx_node| tx_node.tx).collect(),
+ txouts: self
+ .floating_txouts()
+ .map(|(op, txout)| (op, txout.clone()))
+ .collect(),
+ anchors: self.anchors.clone(),
+ last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(),
+ }
}
/// Applies [`ChangeSet`] to [`TxGraph`].
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
- for wrapped_tx in changeset.txs {
- let tx = wrapped_tx.as_ref();
- let txid = tx.compute_txid();
-
- tx.input
- .iter()
- .map(|txin| txin.previous_output)
- // coinbase spends are not to be counted
- .filter(|outpoint| !outpoint.is_null())
- // record spend as this tx has spent this outpoint
- .for_each(|outpoint| {
- self.spends.entry(outpoint).or_default().insert(txid);
- });
-
- match self.txs.get_mut(&txid) {
- Some((tx_node @ TxNodeInternal::Partial(_), _)) => {
- *tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
- }
- Some((TxNodeInternal::Whole(tx), _)) => {
- debug_assert_eq!(
- tx.as_ref().compute_txid(),
- txid,
- "tx should produce txid that is same as key"
- );
- }
- None => {
- self.txs
- .insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new()));
- }
- }
+ for tx in changeset.txs {
+ let _ = self.insert_tx(tx);
}
-
for (outpoint, txout) in changeset.txouts {
- let tx_entry = self.txs.entry(outpoint.txid).or_default();
-
- match tx_entry {
- (TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ }
- (TxNodeInternal::Partial(txouts), _) => {
- txouts.insert(outpoint.vout, txout);
- }
- }
+ let _ = self.insert_txout(outpoint, txout);
}
-
for (anchor, txid) in changeset.anchors {
- if self.anchors.insert((anchor.clone(), txid)) {
- let (_, anchors) = self.txs.entry(txid).or_default();
- anchors.insert(anchor);
- }
- }
-
- for (txid, new_last_seen) in changeset.last_seen {
- let last_seen = self.last_seen.entry(txid).or_default();
- if new_last_seen > *last_seen {
- *last_seen = new_last_seen;
- }
- }
- }
-
- /// Previews the resultant [`ChangeSet`] when [`Self`] is updated against the `update` graph.
- ///
- /// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that
- /// exist in `update` but not in `self`).
- pub(crate) fn determine_changeset(&self, update: TxGraph) -> ChangeSet {
- let mut changeset = ChangeSet::::default();
-
- for (&txid, (update_tx_node, _)) in &update.txs {
- match (self.txs.get(&txid), update_tx_node) {
- (None, TxNodeInternal::Whole(update_tx)) => {
- changeset.txs.insert(update_tx.clone());
- }
- (None, TxNodeInternal::Partial(update_txos)) => {
- changeset.txouts.extend(
- update_txos
- .iter()
- .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
- );
- }
- (Some((TxNodeInternal::Whole(_), _)), _) => {}
- (Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => {
- changeset.txs.insert(update_tx.clone());
- }
- (
- Some((TxNodeInternal::Partial(txos), _)),
- TxNodeInternal::Partial(update_txos),
- ) => {
- changeset.txouts.extend(
- update_txos
- .iter()
- .filter(|(vout, _)| !txos.contains_key(*vout))
- .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
- );
- }
- }
+ let _ = self.insert_anchor(txid, anchor);
}
-
- for (txid, update_last_seen) in update.last_seen {
- let prev_last_seen = self.last_seen.get(&txid).copied();
- if Some(update_last_seen) > prev_last_seen {
- changeset.last_seen.insert(txid, update_last_seen);
- }
+ for (txid, seen_at) in changeset.last_seen {
+ let _ = self.insert_seen_at(txid, seen_at);
}
-
- changeset.anchors = update.anchors.difference(&self.anchors).cloned().collect();
-
- changeset
}
}
diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs
index 8ddf7f30a..df5e6a621 100644
--- a/crates/chain/tests/test_tx_graph.rs
+++ b/crates/chain/tests/test_tx_graph.rs
@@ -2,7 +2,7 @@
#[macro_use]
mod common;
-use bdk_chain::tx_graph::CalculateFeeError;
+use bdk_chain::tx_graph::{self, CalculateFeeError};
use bdk_chain::{
collections::*,
local_chain::LocalChain,
@@ -49,7 +49,7 @@ fn insert_txouts() {
)];
// One full transaction to be included in the update
- let update_txs = Transaction {
+ let update_tx = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn {
@@ -63,17 +63,17 @@ fn insert_txouts() {
};
// Conf anchor used to mark the full transaction as confirmed.
- let conf_anchor = ChainPosition::Confirmed(BlockId {
+ let conf_anchor = BlockId {
height: 100,
hash: h!("random blockhash"),
- });
+ };
- // Unconfirmed anchor to mark the partial transactions as unconfirmed
- let unconf_anchor = ChainPosition::::Unconfirmed(1000000);
+ // Unconfirmed seen_at timestamp to mark the partial transactions as unconfirmed.
+ let unconf_seen_at = 1000000_u64;
// Make the original graph
let mut graph = {
- let mut graph = TxGraph::>::default();
+ let mut graph = TxGraph::::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
@@ -88,57 +88,21 @@ fn insert_txouts() {
// Make the update graph
let update = {
- let mut graph = TxGraph::default();
+ let mut update = tx_graph::Update::default();
for (outpoint, txout) in &update_ops {
- // Insert partials transactions
- assert_eq!(
- graph.insert_txout(*outpoint, txout.clone()),
- ChangeSet {
- txouts: [(*outpoint, txout.clone())].into(),
- ..Default::default()
- }
- );
+ // Insert partials transactions.
+ update.txouts.insert(*outpoint, txout.clone());
// Mark them unconfirmed.
- assert_eq!(
- graph.insert_anchor(outpoint.txid, unconf_anchor),
- ChangeSet {
- txs: [].into(),
- txouts: [].into(),
- anchors: [(unconf_anchor, outpoint.txid)].into(),
- last_seen: [].into()
- }
- );
- // Mark them last seen at.
- assert_eq!(
- graph.insert_seen_at(outpoint.txid, 1000000),
- ChangeSet {
- txs: [].into(),
- txouts: [].into(),
- anchors: [].into(),
- last_seen: [(outpoint.txid, 1000000)].into()
- }
- );
+ update.seen_ats.insert(outpoint.txid, unconf_seen_at);
}
- // Insert the full transaction
- assert_eq!(
- graph.insert_tx(update_txs.clone()),
- ChangeSet {
- txs: [Arc::new(update_txs.clone())].into(),
- ..Default::default()
- }
- );
+ // Insert the full transaction.
+ update.txs.push(update_tx.clone().into());
// Mark it as confirmed.
- assert_eq!(
- graph.insert_anchor(update_txs.compute_txid(), conf_anchor),
- ChangeSet {
- txs: [].into(),
- txouts: [].into(),
- anchors: [(conf_anchor, update_txs.compute_txid())].into(),
- last_seen: [].into()
- }
- );
- graph
+ update
+ .anchors
+ .insert((conf_anchor, update_tx.compute_txid()));
+ update
};
// Check the resulting addition.
@@ -147,13 +111,9 @@ fn insert_txouts() {
assert_eq!(
changeset,
ChangeSet {
- txs: [Arc::new(update_txs.clone())].into(),
+ txs: [Arc::new(update_tx.clone())].into(),
txouts: update_ops.clone().into(),
- anchors: [
- (conf_anchor, update_txs.compute_txid()),
- (unconf_anchor, h!("tx2"))
- ]
- .into(),
+ anchors: [(conf_anchor, update_tx.compute_txid()),].into(),
last_seen: [(h!("tx2"), 1000000)].into()
}
);
@@ -188,7 +148,7 @@ fn insert_txouts() {
assert_eq!(
graph
- .tx_outputs(update_txs.compute_txid())
+ .tx_outputs(update_tx.compute_txid())
.expect("should exists"),
[(
0u32,
@@ -204,13 +164,9 @@ fn insert_txouts() {
assert_eq!(
graph.initial_changeset(),
ChangeSet {
- txs: [Arc::new(update_txs.clone())].into(),
+ txs: [Arc::new(update_tx.clone())].into(),
txouts: update_ops.into_iter().chain(original_ops).collect(),
- anchors: [
- (conf_anchor, update_txs.compute_txid()),
- (unconf_anchor, h!("tx2"))
- ]
- .into(),
+ anchors: [(conf_anchor, update_tx.compute_txid()),].into(),
last_seen: [(h!("tx2"), 1000000)].into()
}
);
@@ -301,59 +257,28 @@ fn insert_tx_can_retrieve_full_tx_from_graph() {
#[test]
fn insert_tx_displaces_txouts() {
let mut tx_graph = TxGraph::<()>::default();
+
let tx = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::from_sat(42_000),
- script_pubkey: ScriptBuf::new(),
+ script_pubkey: ScriptBuf::default(),
}],
};
+ let txid = tx.compute_txid();
+ let outpoint = OutPoint::new(txid, 0);
+ let txout = tx.output.first().unwrap();
- let changeset = tx_graph.insert_txout(
- OutPoint {
- txid: tx.compute_txid(),
- vout: 0,
- },
- TxOut {
- value: Amount::from_sat(1_337_000),
- script_pubkey: ScriptBuf::default(),
- },
- );
-
+ let changeset = tx_graph.insert_txout(outpoint, txout.clone());
assert!(!changeset.is_empty());
- let _ = tx_graph.insert_txout(
- OutPoint {
- txid: tx.compute_txid(),
- vout: 0,
- },
- TxOut {
- value: Amount::from_sat(1_000_000_000),
- script_pubkey: ScriptBuf::new(),
- },
- );
-
- let _changeset = tx_graph.insert_tx(tx.clone());
-
- assert_eq!(
- tx_graph
- .get_txout(OutPoint {
- txid: tx.compute_txid(),
- vout: 0
- })
- .unwrap()
- .value,
- Amount::from_sat(42_000)
- );
- assert_eq!(
- tx_graph.get_txout(OutPoint {
- txid: tx.compute_txid(),
- vout: 1
- }),
- None
- );
+ let changeset = tx_graph.insert_tx(tx.clone());
+ assert_eq!(changeset.txs.len(), 1);
+ assert!(changeset.txouts.is_empty());
+ assert!(tx_graph.get_tx(txid).is_some());
+ assert_eq!(tx_graph.get_txout(outpoint), Some(txout));
}
#[test]
@@ -385,7 +310,7 @@ fn insert_txout_does_not_displace_tx() {
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.compute_txid(),
- vout: 0,
+ vout: 1,
},
TxOut {
value: Amount::from_sat(1_000_000_000),
@@ -1088,42 +1013,6 @@ fn test_changeset_last_seen_merge() {
}
}
-#[test]
-fn update_last_seen_unconfirmed() {
- let mut graph = TxGraph::<()>::default();
- let tx = new_tx(0);
- let txid = tx.compute_txid();
-
- // insert a new tx
- // initially we have a last_seen of None and no anchors
- let _ = graph.insert_tx(tx);
- let tx = graph.full_txs().next().unwrap();
- assert_eq!(tx.last_seen_unconfirmed, None);
- assert!(tx.anchors.is_empty());
-
- // higher timestamp should update last seen
- let changeset = graph.update_last_seen_unconfirmed(2);
- assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2);
-
- // lower timestamp has no effect
- let changeset = graph.update_last_seen_unconfirmed(1);
- assert!(changeset.last_seen.is_empty());
-
- // once anchored, last seen is not updated
- let _ = graph.insert_anchor(txid, ());
- let changeset = graph.update_last_seen_unconfirmed(4);
- assert!(changeset.is_empty());
- assert_eq!(
- graph
- .full_txs()
- .next()
- .unwrap()
- .last_seen_unconfirmed
- .unwrap(),
- 2
- );
-}
-
#[test]
fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() {
let txs = vec![new_tx(0), new_tx(1)];
@@ -1247,3 +1136,130 @@ fn call_map_anchors_with_non_deterministic_anchor() {
]
);
}
+
+/// Tests `From` impls for conversion between [`TxGraph`] and [`tx_graph::Update`].
+#[test]
+fn tx_graph_update_conversion() {
+ use tx_graph::Update;
+
+ type TestCase = (&'static str, Update);
+
+ fn make_tx(v: i32) -> Transaction {
+ Transaction {
+ version: transaction::Version(v),
+ lock_time: absolute::LockTime::ZERO,
+ input: vec![],
+ output: vec![],
+ }
+ }
+
+ fn make_txout(a: u64) -> TxOut {
+ TxOut {
+ value: Amount::from_sat(a),
+ script_pubkey: ScriptBuf::default(),
+ }
+ }
+
+ let test_cases: &[TestCase] = &[
+ ("empty_update", Update::default()),
+ (
+ "single_tx",
+ Update {
+ txs: vec![make_tx(0).into()],
+ ..Default::default()
+ },
+ ),
+ (
+ "two_txs",
+ Update {
+ txs: vec![make_tx(0).into(), make_tx(1).into()],
+ ..Default::default()
+ },
+ ),
+ (
+ "with_floating_txouts",
+ Update {
+ txs: vec![make_tx(0).into(), make_tx(1).into()],
+ txouts: [
+ (OutPoint::new(h!("a"), 0), make_txout(0)),
+ (OutPoint::new(h!("a"), 1), make_txout(1)),
+ (OutPoint::new(h!("b"), 0), make_txout(2)),
+ ]
+ .into(),
+ ..Default::default()
+ },
+ ),
+ (
+ "with_anchors",
+ Update {
+ txs: vec![make_tx(0).into(), make_tx(1).into()],
+ txouts: [
+ (OutPoint::new(h!("a"), 0), make_txout(0)),
+ (OutPoint::new(h!("a"), 1), make_txout(1)),
+ (OutPoint::new(h!("b"), 0), make_txout(2)),
+ ]
+ .into(),
+ anchors: [
+ (ConfirmationBlockTime::default(), h!("a")),
+ (ConfirmationBlockTime::default(), h!("b")),
+ ]
+ .into(),
+ ..Default::default()
+ },
+ ),
+ (
+ "with_seen_ats",
+ Update {
+ txs: vec![make_tx(0).into(), make_tx(1).into()],
+ txouts: [
+ (OutPoint::new(h!("a"), 0), make_txout(0)),
+ (OutPoint::new(h!("a"), 1), make_txout(1)),
+ (OutPoint::new(h!("d"), 0), make_txout(2)),
+ ]
+ .into(),
+ anchors: [
+ (ConfirmationBlockTime::default(), h!("a")),
+ (ConfirmationBlockTime::default(), h!("b")),
+ ]
+ .into(),
+ seen_ats: [(h!("c"), 12346)].into_iter().collect(),
+ },
+ ),
+ ];
+
+ for (test_name, update) in test_cases {
+ let mut tx_graph = TxGraph::::default();
+ let _ = tx_graph.apply_update_at(update.clone(), None);
+ let update_from_tx_graph: Update = tx_graph.into();
+
+ assert_eq!(
+ update
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
+ .collect::>(),
+ update_from_tx_graph
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
+ .collect::>(),
+ "{}: txs do not match",
+ test_name
+ );
+ assert_eq!(
+ update.txouts, update_from_tx_graph.txouts,
+ "{}: txouts do not match",
+ test_name
+ );
+ assert_eq!(
+ update.anchors, update_from_tx_graph.anchors,
+ "{}: anchors do not match",
+ test_name
+ );
+ assert_eq!(
+ update.seen_ats, update_from_tx_graph.seen_ats,
+ "{}: seen_ats do not match",
+ test_name
+ );
+ }
+}
diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs
index 1458e2bd9..571660756 100644
--- a/crates/electrum/src/bdk_electrum_client.rs
+++ b/crates/electrum/src/bdk_electrum_client.rs
@@ -3,12 +3,12 @@ use bdk_chain::{
collections::{BTreeMap, HashMap},
local_chain::CheckPoint,
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
- tx_graph::TxGraph,
+ tx_graph::{self, TxGraph},
Anchor, BlockId, ConfirmationBlockTime,
};
use electrum_client::{ElectrumApi, Error, HeaderNotification};
use std::{
- collections::BTreeSet,
+ collections::HashSet,
sync::{Arc, Mutex},
};
@@ -138,7 +138,7 @@ impl BdkElectrumClient {
None => None,
};
- let mut graph_update = TxGraph::::default();
+ let mut graph_update = tx_graph::Update::::default();
let mut last_active_indices = BTreeMap::::default();
for keychain in request.keychains() {
let spks = request.iter_spks(keychain.clone());
@@ -158,7 +158,7 @@ impl BdkElectrumClient {
Some((chain_tip, latest_blocks)) => Some(chain_update(
chain_tip,
&latest_blocks,
- graph_update.all_anchors(),
+ graph_update.anchors.iter().cloned(),
)?),
_ => None,
};
@@ -205,7 +205,7 @@ impl BdkElectrumClient {
None => None,
};
- let mut graph_update = TxGraph::::default();
+ let mut graph_update = tx_graph::Update::::default();
self.populate_with_spks(
&mut graph_update,
request
@@ -227,7 +227,7 @@ impl BdkElectrumClient {
Some((chain_tip, latest_blocks)) => Some(chain_update(
chain_tip,
&latest_blocks,
- graph_update.all_anchors(),
+ graph_update.anchors.iter().cloned(),
)?),
None => None,
};
@@ -245,7 +245,7 @@ impl BdkElectrumClient {
/// also included.
fn populate_with_spks(
&self,
- graph_update: &mut TxGraph,
+ graph_update: &mut tx_graph::Update,
mut spks: impl Iterator- ,
stop_gap: usize,
batch_size: usize,
@@ -278,7 +278,7 @@ impl BdkElectrumClient {
}
for tx_res in spk_history {
- let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
+ graph_update.txs.push(self.fetch_tx(tx_res.tx_hash)?);
self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?;
}
}
@@ -291,7 +291,7 @@ impl BdkElectrumClient {
/// included. Anchors of the aforementioned transactions are included.
fn populate_with_outpoints(
&self,
- graph_update: &mut TxGraph,
+ graph_update: &mut tx_graph::Update,
outpoints: impl IntoIterator
- ,
) -> Result<(), Error> {
for outpoint in outpoints {
@@ -314,7 +314,7 @@ impl BdkElectrumClient {
if !has_residing && res.tx_hash == op_txid {
has_residing = true;
- let _ = graph_update.insert_tx(Arc::clone(&op_tx));
+ graph_update.txs.push(Arc::clone(&op_tx));
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
}
@@ -328,7 +328,7 @@ impl BdkElectrumClient {
if !has_spending {
continue;
}
- let _ = graph_update.insert_tx(Arc::clone(&res_tx));
+ graph_update.txs.push(Arc::clone(&res_tx));
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
}
}
@@ -339,7 +339,7 @@ impl BdkElectrumClient {
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
fn populate_with_txids(
&self,
- graph_update: &mut TxGraph,
+ graph_update: &mut tx_graph::Update,
txids: impl IntoIterator
- ,
) -> Result<(), Error> {
for txid in txids {
@@ -366,7 +366,7 @@ impl BdkElectrumClient {
self.validate_merkle_for_anchor(graph_update, txid, r.height)?;
}
- let _ = graph_update.insert_tx(tx);
+ graph_update.txs.push(tx);
}
Ok(())
}
@@ -375,7 +375,7 @@ impl BdkElectrumClient {
// An anchor is inserted if the transaction is validated to be in a confirmed block.
fn validate_merkle_for_anchor(
&self,
- graph_update: &mut TxGraph,
+ graph_update: &mut tx_graph::Update,
txid: Txid,
confirmation_height: i32,
) -> Result<(), Error> {
@@ -402,8 +402,7 @@ impl BdkElectrumClient {
}
if is_confirmed_tx {
- let _ = graph_update.insert_anchor(
- txid,
+ graph_update.anchors.insert((
ConfirmationBlockTime {
confirmation_time: header.time as u64,
block_id: BlockId {
@@ -411,7 +410,8 @@ impl BdkElectrumClient {
hash: header.block_hash(),
},
},
- );
+ txid,
+ ));
}
}
Ok(())
@@ -421,17 +421,18 @@ impl BdkElectrumClient {
// which we do not have by default. This data is needed to calculate the transaction fee.
fn fetch_prev_txout(
&self,
- graph_update: &mut TxGraph,
+ graph_update: &mut tx_graph::Update,
) -> Result<(), Error> {
- let full_txs: Vec> =
- graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
- for tx in full_txs {
- for vin in &tx.input {
- let outpoint = vin.previous_output;
- let vout = outpoint.vout;
- let prev_tx = self.fetch_tx(outpoint.txid)?;
- let txout = prev_tx.output[vout as usize].clone();
- let _ = graph_update.insert_txout(outpoint, txout);
+ let mut no_dup = HashSet::::new();
+ for tx in &graph_update.txs {
+ if no_dup.insert(tx.compute_txid()) {
+ for vin in &tx.input {
+ let outpoint = vin.previous_output;
+ let vout = outpoint.vout;
+ let prev_tx = self.fetch_tx(outpoint.txid)?;
+ let txout = prev_tx.output[vout as usize].clone();
+ let _ = graph_update.txouts.insert(outpoint, txout);
+ }
}
}
Ok(())
@@ -516,7 +517,7 @@ fn fetch_tip_and_latest_blocks(
fn chain_update(
mut tip: CheckPoint,
latest_blocks: &BTreeMap,
- anchors: &BTreeSet<(A, Txid)>,
+ anchors: impl Iterator
- ,
) -> Result {
for anchor in anchors {
let height = anchor.0.anchor_block().height;
diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs
index 63e91081b..d5e4a1596 100644
--- a/crates/electrum/tests/test_electrum.rs
+++ b/crates/electrum/tests/test_electrum.rs
@@ -1,9 +1,9 @@
use bdk_chain::{
- bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
+ bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
local_chain::LocalChain,
spk_client::{FullScanRequest, SyncRequest, SyncResult},
spk_txout::SpkTxOutIndex,
- Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge,
+ Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph,
};
use bdk_electrum::BdkElectrumClient;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
@@ -38,19 +38,12 @@ where
Spks: IntoIterator
- ,
Spks::IntoIter: ExactSizeIterator + Send + 'static,
{
- let mut update = client.sync(
+ let update = client.sync(
SyncRequest::builder().chain_tip(chain.tip()).spks(spks),
BATCH_SIZE,
true,
)?;
- // Update `last_seen` to be able to calculate balance for unconfirmed transactions.
- let now = std::time::UNIX_EPOCH
- .elapsed()
- .expect("must get time")
- .as_secs();
- let _ = update.graph_update.update_last_seen_unconfirmed(now);
-
if let Some(chain_update) = update.chain_update.clone() {
let _ = chain
.apply_update(chain_update)
@@ -128,18 +121,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
);
let graph_update = sync_update.graph_update;
+ let updated_graph = {
+ let mut graph = TxGraph::::default();
+ let _ = graph.apply_update(graph_update.clone());
+ graph
+ };
// Check to see if we have the floating txouts available from our two created transactions'
// previous outputs in order to calculate transaction fees.
- for tx in graph_update.full_txs() {
+ for tx in &graph_update.txs {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transactions' previous outputs.
- let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
+ let fee = updated_graph.calculate_fee(tx).expect("Fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
- .get_transaction(&tx.txid, None)
+ .get_transaction(&tx.compute_txid(), None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
@@ -151,12 +149,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
assert_eq!(fee, tx_fee);
}
- let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect();
- graph_update_txids.sort();
- let mut expected_txids = vec![txid1, txid2];
- expected_txids.sort();
- assert_eq!(graph_update_txids, expected_txids);
-
+ assert_eq!(
+ graph_update
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
+ .collect::>(),
+ [txid1, txid2].into(),
+ "update must include all expected transactions",
+ );
Ok(())
}
@@ -216,7 +217,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
.spks_for_keychain(0, spks.clone());
client.full_scan(request, 3, 1, false)?
};
- assert!(full_scan_update.graph_update.full_txs().next().is_none());
+ assert!(full_scan_update.graph_update.txs.is_empty());
assert!(full_scan_update.last_active_indices.is_empty());
let full_scan_update = {
let request = FullScanRequest::builder()
@@ -227,10 +228,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
assert_eq!(
full_scan_update
.graph_update
- .full_txs()
- .next()
+ .txs
+ .first()
.unwrap()
- .txid,
+ .compute_txid(),
txid_4th_addr
);
assert_eq!(full_scan_update.last_active_indices[&0], 3);
@@ -259,8 +260,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
};
let txs: HashSet<_> = full_scan_update
.graph_update
- .full_txs()
- .map(|tx| tx.txid)
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
.collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
@@ -273,8 +275,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
};
let txs: HashSet<_> = full_scan_update
.graph_update
- .full_txs()
- .map(|tx| tx.txid)
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
.collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
@@ -475,13 +478,12 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
)?;
// Retain a snapshot of all anchors before reorg process.
- let initial_anchors = update.graph_update.all_anchors();
- let anchors: Vec<_> = initial_anchors.iter().cloned().collect();
- assert_eq!(anchors.len(), REORG_COUNT);
+ let initial_anchors = update.graph_update.anchors.clone();
+ assert_eq!(initial_anchors.len(), REORG_COUNT);
for i in 0..REORG_COUNT {
- let (anchor, txid) = anchors[i];
+ let (anchor, txid) = initial_anchors.iter().nth(i).unwrap();
assert_eq!(anchor.block_id.hash, hashes[i]);
- assert_eq!(txid, txids[i]);
+ assert_eq!(*txid, txids[i]);
}
// Check if initial balance is correct.
@@ -507,7 +509,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
)?;
// Check that no new anchors are added during current reorg.
- assert!(initial_anchors.is_superset(update.graph_update.all_anchors()));
+ assert!(initial_anchors.is_superset(&update.graph_update.anchors));
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs
index 066b91e17..f3c8e966a 100644
--- a/crates/esplora/src/async_ext.rs
+++ b/crates/esplora/src/async_ext.rs
@@ -1,4 +1,4 @@
-use std::collections::BTreeSet;
+use std::collections::{BTreeSet, HashSet};
use async_trait::async_trait;
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};
@@ -6,10 +6,9 @@ use bdk_chain::{
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
collections::BTreeMap,
local_chain::CheckPoint,
- BlockId, ConfirmationBlockTime, TxGraph,
+ BlockId, ConfirmationBlockTime,
};
-use bdk_chain::{Anchor, Indexed};
-use esplora_client::{Tx, TxStatus};
+use bdk_chain::{tx_graph, Anchor, Indexed};
use futures::{stream::FuturesOrdered, TryStreamExt};
use crate::{insert_anchor_from_status, insert_prevouts};
@@ -72,23 +71,29 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
None
};
- let mut graph_update = TxGraph::default();
+ let mut graph_update = tx_graph::Update::::default();
+ let mut inserted_txs = HashSet::::new();
let mut last_active_indices = BTreeMap::::new();
for keychain in keychains {
let keychain_spks = request.iter_spks(keychain.clone());
- let (tx_graph, last_active_index) =
- fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests)
- .await?;
- let _ = graph_update.apply_update(tx_graph);
+ let (update, last_active_index) = fetch_txs_with_keychain_spks(
+ self,
+ &mut inserted_txs,
+ keychain_spks,
+ stop_gap,
+ parallel_requests,
+ )
+ .await?;
+ graph_update.extend(update);
if let Some(last_active_index) = last_active_index {
last_active_indices.insert(keychain, last_active_index);
}
}
let chain_update = match (chain_tip, latest_blocks) {
- (Some(chain_tip), Some(latest_blocks)) => Some(
- chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?,
- ),
+ (Some(chain_tip), Some(latest_blocks)) => {
+ Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?)
+ }
_ => None,
};
@@ -113,20 +118,40 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
None
};
- let mut graph_update = TxGraph::::default();
- let _ = graph_update
- .apply_update(fetch_txs_with_spks(self, request.iter_spks(), parallel_requests).await?);
- let _ = graph_update.apply_update(
- fetch_txs_with_txids(self, request.iter_txids(), parallel_requests).await?,
+ let mut graph_update = tx_graph::Update::::default();
+ let mut inserted_txs = HashSet::::new();
+ graph_update.extend(
+ fetch_txs_with_spks(
+ self,
+ &mut inserted_txs,
+ request.iter_spks(),
+ parallel_requests,
+ )
+ .await?,
+ );
+ graph_update.extend(
+ fetch_txs_with_txids(
+ self,
+ &mut inserted_txs,
+ request.iter_txids(),
+ parallel_requests,
+ )
+ .await?,
);
- let _ = graph_update.apply_update(
- fetch_txs_with_outpoints(self, request.iter_outpoints(), parallel_requests).await?,
+ graph_update.extend(
+ fetch_txs_with_outpoints(
+ self,
+ &mut inserted_txs,
+ request.iter_outpoints(),
+ parallel_requests,
+ )
+ .await?,
);
let chain_update = match (chain_tip, latest_blocks) {
- (Some(chain_tip), Some(latest_blocks)) => Some(
- chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?,
- ),
+ (Some(chain_tip), Some(latest_blocks)) => {
+ Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?)
+ }
_ => None,
};
@@ -252,13 +277,14 @@ async fn chain_update(
/// Refer to [crate-level docs](crate) for more.
async fn fetch_txs_with_keychain_spks> + Send>(
client: &esplora_client::AsyncClient,
+ inserted_txs: &mut HashSet,
mut keychain_spks: I,
stop_gap: usize,
parallel_requests: usize,
-) -> Result<(TxGraph, Option), Error> {
+) -> Result<(tx_graph::Update, Option), Error> {
type TxsOfSpkIndex = (u32, Vec);
- let mut tx_graph = TxGraph::default();
+ let mut update = tx_graph::Update::::default();
let mut last_index = Option::::None;
let mut last_active_index = Option::::None;
@@ -294,9 +320,11 @@ async fn fetch_txs_with_keychain_spks> + S
last_active_index = Some(index);
}
for tx in txs {
- let _ = tx_graph.insert_tx(tx.to_tx());
- insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status);
- insert_prevouts(&mut tx_graph, tx.vin);
+ if inserted_txs.insert(tx.txid) {
+ update.txs.push(tx.to_tx().into());
+ }
+ insert_anchor_from_status(&mut update, tx.txid, tx.status);
+ insert_prevouts(&mut update, tx.vin);
}
}
@@ -311,7 +339,7 @@ async fn fetch_txs_with_keychain_spks> + S
}
}
- Ok((tx_graph, last_active_index))
+ Ok((update, last_active_index))
}
/// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks`
@@ -324,20 +352,22 @@ async fn fetch_txs_with_keychain_spks> + S
/// Refer to [crate-level docs](crate) for more.
async fn fetch_txs_with_spks + Send>(
client: &esplora_client::AsyncClient,
+ inserted_txs: &mut HashSet,
spks: I,
parallel_requests: usize,
-) -> Result, Error>
+) -> Result, Error>
where
I::IntoIter: Send,
{
fetch_txs_with_keychain_spks(
client,
+ inserted_txs,
spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)),
usize::MAX,
parallel_requests,
)
.await
- .map(|(tx_graph, _)| tx_graph)
+ .map(|(update, _)| update)
}
/// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids`
@@ -348,39 +378,27 @@ where
/// Refer to [crate-level docs](crate) for more.
async fn fetch_txs_with_txids + Send>(
client: &esplora_client::AsyncClient,
+ inserted_txs: &mut HashSet,
txids: I,
parallel_requests: usize,
-) -> Result, Error>
+) -> Result, Error>
where
I::IntoIter: Send,
{
- enum EsploraResp {
- TxStatus(TxStatus),
- Tx(Option),
- }
-
- let mut tx_graph = TxGraph::default();
- let mut txids = txids.into_iter();
+ let mut update = tx_graph::Update::::default();
+ // Only fetch for non-inserted txs.
+ let mut txids = txids
+ .into_iter()
+ .filter(|txid| !inserted_txs.contains(txid))
+ .collect::>()
+ .into_iter();
loop {
let handles = txids
.by_ref()
.take(parallel_requests)
.map(|txid| {
let client = client.clone();
- let tx_already_exists = tx_graph.get_tx(txid).is_some();
- async move {
- if tx_already_exists {
- client
- .get_tx_status(&txid)
- .await
- .map(|s| (txid, EsploraResp::TxStatus(s)))
- } else {
- client
- .get_tx_info(&txid)
- .await
- .map(|t| (txid, EsploraResp::Tx(t)))
- }
- }
+ async move { client.get_tx_info(&txid).await.map(|t| (txid, t)) }
})
.collect::>();
@@ -388,21 +406,17 @@ where
break;
}
- for (txid, resp) in handles.try_collect::>().await? {
- match resp {
- EsploraResp::TxStatus(status) => {
- insert_anchor_from_status(&mut tx_graph, txid, status);
- }
- EsploraResp::Tx(Some(tx_info)) => {
- let _ = tx_graph.insert_tx(tx_info.to_tx());
- insert_anchor_from_status(&mut tx_graph, txid, tx_info.status);
- insert_prevouts(&mut tx_graph, tx_info.vin);
+ for (txid, tx_info) in handles.try_collect::>().await? {
+ if let Some(tx_info) = tx_info {
+ if inserted_txs.insert(txid) {
+ update.txs.push(tx_info.to_tx().into());
}
- _ => continue,
+ insert_anchor_from_status(&mut update, txid, tx_info.status);
+ insert_prevouts(&mut update, tx_info.vin);
}
}
}
- Ok(tx_graph)
+ Ok(update)
}
/// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided
@@ -413,22 +427,27 @@ where
/// Refer to [crate-level docs](crate) for more.
async fn fetch_txs_with_outpoints + Send>(
client: &esplora_client::AsyncClient,
+ inserted_txs: &mut HashSet,
outpoints: I,
parallel_requests: usize,
-) -> Result, Error>
+) -> Result, Error>
where
I::IntoIter: Send,
{
let outpoints = outpoints.into_iter().collect::>();
+ let mut update = tx_graph::Update::::default();
// make sure txs exists in graph and tx statuses are updated
// TODO: We should maintain a tx cache (like we do with Electrum).
- let mut tx_graph = fetch_txs_with_txids(
- client,
- outpoints.iter().copied().map(|op| op.txid),
- parallel_requests,
- )
- .await?;
+ update.extend(
+ fetch_txs_with_txids(
+ client,
+ inserted_txs,
+ outpoints.iter().copied().map(|op| op.txid),
+ parallel_requests,
+ )
+ .await?,
+ );
// get outpoint spend-statuses
let mut outpoints = outpoints.into_iter();
@@ -452,18 +471,18 @@ where
Some(txid) => txid,
None => continue,
};
- if tx_graph.get_tx(spend_txid).is_none() {
+ if !inserted_txs.contains(&spend_txid) {
missing_txs.push(spend_txid);
}
if let Some(spend_status) = op_status.status {
- insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status);
+ insert_anchor_from_status(&mut update, spend_txid, spend_status);
}
}
}
- let _ =
- tx_graph.apply_update(fetch_txs_with_txids(client, missing_txs, parallel_requests).await?);
- Ok(tx_graph)
+ update
+ .extend(fetch_txs_with_txids(client, inserted_txs, missing_txs, parallel_requests).await?);
+ Ok(update)
}
#[cfg(test)]
diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs
index 6e3e25afe..62f0d351e 100644
--- a/crates/esplora/src/blocking_ext.rs
+++ b/crates/esplora/src/blocking_ext.rs
@@ -1,4 +1,4 @@
-use std::collections::BTreeSet;
+use std::collections::{BTreeSet, HashSet};
use std::thread::JoinHandle;
use bdk_chain::collections::BTreeMap;
@@ -6,10 +6,10 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe
use bdk_chain::{
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
local_chain::CheckPoint,
- BlockId, ConfirmationBlockTime, TxGraph,
+ BlockId, ConfirmationBlockTime,
};
-use bdk_chain::{Anchor, Indexed};
-use esplora_client::{OutputStatus, Tx, TxStatus};
+use bdk_chain::{tx_graph, Anchor, Indexed};
+use esplora_client::{OutputStatus, Tx};
use crate::{insert_anchor_from_status, insert_prevouts};
@@ -66,13 +66,19 @@ impl EsploraExt for esplora_client::BlockingClient {
None
};
- let mut graph_update = TxGraph::default();
+ let mut graph_update = tx_graph::Update::default();
+ let mut inserted_txs = HashSet::::new();
let mut last_active_indices = BTreeMap::::new();
for keychain in request.keychains() {
let keychain_spks = request.iter_spks(keychain.clone());
- let (tx_graph, last_active_index) =
- fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests)?;
- let _ = graph_update.apply_update(tx_graph);
+ let (update, last_active_index) = fetch_txs_with_keychain_spks(
+ self,
+ &mut inserted_txs,
+ keychain_spks,
+ stop_gap,
+ parallel_requests,
+ )?;
+ graph_update.extend(update);
if let Some(last_active_index) = last_active_index {
last_active_indices.insert(keychain, last_active_index);
}
@@ -83,7 +89,7 @@ impl EsploraExt for esplora_client::BlockingClient {
self,
&latest_blocks,
&chain_tip,
- graph_update.all_anchors(),
+ &graph_update.anchors,
)?),
_ => None,
};
@@ -109,19 +115,23 @@ impl EsploraExt for esplora_client::BlockingClient {
None
};
- let mut graph_update = TxGraph::default();
- let _ = graph_update.apply_update(fetch_txs_with_spks(
+ let mut graph_update = tx_graph::Update::::default();
+ let mut inserted_txs = HashSet::::new();
+ graph_update.extend(fetch_txs_with_spks(
self,
+ &mut inserted_txs,
request.iter_spks(),
parallel_requests,
)?);
- let _ = graph_update.apply_update(fetch_txs_with_txids(
+ graph_update.extend(fetch_txs_with_txids(
self,
+ &mut inserted_txs,
request.iter_txids(),
parallel_requests,
)?);
- let _ = graph_update.apply_update(fetch_txs_with_outpoints(
+ graph_update.extend(fetch_txs_with_outpoints(
self,
+ &mut inserted_txs,
request.iter_outpoints(),
parallel_requests,
)?);
@@ -131,7 +141,7 @@ impl EsploraExt for esplora_client::BlockingClient {
self,
&latest_blocks,
&chain_tip,
- graph_update.all_anchors(),
+ &graph_update.anchors,
)?),
_ => None,
};
@@ -244,13 +254,14 @@ fn chain_update(
fn fetch_txs_with_keychain_spks>>(
client: &esplora_client::BlockingClient,
+ inserted_txs: &mut HashSet,
mut keychain_spks: I,
stop_gap: usize,
parallel_requests: usize,
-) -> Result<(TxGraph, Option), Error> {
+) -> Result<(tx_graph::Update, Option), Error> {
type TxsOfSpkIndex = (u32, Vec);
- let mut tx_graph = TxGraph::default();
+ let mut update = tx_graph::Update::::default();
let mut last_index = Option::::None;
let mut last_active_index = Option::::None;
@@ -289,9 +300,11 @@ fn fetch_txs_with_keychain_spks>>(
last_active_index = Some(index);
}
for tx in txs {
- let _ = tx_graph.insert_tx(tx.to_tx());
- insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status);
- insert_prevouts(&mut tx_graph, tx.vin);
+ if inserted_txs.insert(tx.txid) {
+ update.txs.push(tx.to_tx().into());
+ }
+ insert_anchor_from_status(&mut update, tx.txid, tx.status);
+ insert_prevouts(&mut update, tx.vin);
}
}
@@ -306,7 +319,7 @@ fn fetch_txs_with_keychain_spks>>(
}
}
- Ok((tx_graph, last_active_index))
+ Ok((update, last_active_index))
}
/// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks`
@@ -319,16 +332,18 @@ fn fetch_txs_with_keychain_spks>>(
/// Refer to [crate-level docs](crate) for more.
fn fetch_txs_with_spks>(
client: &esplora_client::BlockingClient,
+ inserted_txs: &mut HashSet,
spks: I,
parallel_requests: usize,
-) -> Result, Error> {
+) -> Result, Error> {
fetch_txs_with_keychain_spks(
client,
+ inserted_txs,
spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)),
usize::MAX,
parallel_requests,
)
- .map(|(tx_graph, _)| tx_graph)
+ .map(|(update, _)| update)
}
/// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids`
@@ -339,59 +354,48 @@ fn fetch_txs_with_spks>(
/// Refer to [crate-level docs](crate) for more.
fn fetch_txs_with_txids>(
client: &esplora_client::BlockingClient,
+ inserted_txs: &mut HashSet,
txids: I,
parallel_requests: usize,
-) -> Result, Error> {
- enum EsploraResp {
- TxStatus(TxStatus),
- Tx(Option),
- }
-
- let mut tx_graph = TxGraph::default();
- let mut txids = txids.into_iter();
+) -> Result, Error> {
+ let mut update = tx_graph::Update::::default();
+ // Only fetch for non-inserted txs.
+ let mut txids = txids
+ .into_iter()
+ .filter(|txid| !inserted_txs.contains(txid))
+ .collect::>()
+ .into_iter();
loop {
let handles = txids
.by_ref()
.take(parallel_requests)
.map(|txid| {
let client = client.clone();
- let tx_already_exists = tx_graph.get_tx(txid).is_some();
std::thread::spawn(move || {
- if tx_already_exists {
- client
- .get_tx_status(&txid)
- .map_err(Box::new)
- .map(|s| (txid, EsploraResp::TxStatus(s)))
- } else {
- client
- .get_tx_info(&txid)
- .map_err(Box::new)
- .map(|t| (txid, EsploraResp::Tx(t)))
- }
+ client
+ .get_tx_info(&txid)
+ .map_err(Box::new)
+ .map(|t| (txid, t))
})
})
- .collect::>>>();
+ .collect::), Error>>>>();
if handles.is_empty() {
break;
}
for handle in handles {
- let (txid, resp) = handle.join().expect("thread must not panic")?;
- match resp {
- EsploraResp::TxStatus(status) => {
- insert_anchor_from_status(&mut tx_graph, txid, status);
+ let (txid, tx_info) = handle.join().expect("thread must not panic")?;
+ if let Some(tx_info) = tx_info {
+ if inserted_txs.insert(txid) {
+ update.txs.push(tx_info.to_tx().into());
}
- EsploraResp::Tx(Some(tx_info)) => {
- let _ = tx_graph.insert_tx(tx_info.to_tx());
- insert_anchor_from_status(&mut tx_graph, txid, tx_info.status);
- insert_prevouts(&mut tx_graph, tx_info.vin);
- }
- _ => continue,
+ insert_anchor_from_status(&mut update, txid, tx_info.status);
+ insert_prevouts(&mut update, tx_info.vin);
}
}
}
- Ok(tx_graph)
+ Ok(update)
}
/// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided
@@ -402,18 +406,21 @@ fn fetch_txs_with_txids>(
/// Refer to [crate-level docs](crate) for more.
fn fetch_txs_with_outpoints>(
client: &esplora_client::BlockingClient,
+ inserted_txs: &mut HashSet,
outpoints: I,
parallel_requests: usize,
-) -> Result, Error> {
+) -> Result, Error> {
let outpoints = outpoints.into_iter().collect::>();
+ let mut update = tx_graph::Update::::default();
// make sure txs exists in graph and tx statuses are updated
// TODO: We should maintain a tx cache (like we do with Electrum).
- let mut tx_graph = fetch_txs_with_txids(
+ update.extend(fetch_txs_with_txids(
client,
+ inserted_txs,
outpoints.iter().map(|op| op.txid),
parallel_requests,
- )?;
+ )?);
// get outpoint spend-statuses
let mut outpoints = outpoints.into_iter();
@@ -442,22 +449,23 @@ fn fetch_txs_with_outpoints>(
Some(txid) => txid,
None => continue,
};
- if tx_graph.get_tx(spend_txid).is_none() {
+ if !inserted_txs.contains(&spend_txid) {
missing_txs.push(spend_txid);
}
if let Some(spend_status) = op_status.status {
- insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status);
+ insert_anchor_from_status(&mut update, spend_txid, spend_status);
}
}
}
}
- let _ = tx_graph.apply_update(fetch_txs_with_txids(
+ update.extend(fetch_txs_with_txids(
client,
+ inserted_txs,
missing_txs,
parallel_requests,
)?);
- Ok(tx_graph)
+ Ok(update)
}
#[cfg(test)]
diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs
index 7db6967b6..9a6e8f1df 100644
--- a/crates/esplora/src/lib.rs
+++ b/crates/esplora/src/lib.rs
@@ -26,7 +26,7 @@
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
use bdk_chain::bitcoin::{Amount, OutPoint, TxOut, Txid};
-use bdk_chain::{BlockId, ConfirmationBlockTime, TxGraph};
+use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime};
use esplora_client::TxStatus;
pub use esplora_client;
@@ -42,7 +42,7 @@ mod async_ext;
pub use async_ext::*;
fn insert_anchor_from_status(
- tx_graph: &mut TxGraph,
+ update: &mut tx_graph::Update,
txid: Txid,
status: TxStatus,
) {
@@ -57,21 +57,21 @@ fn insert_anchor_from_status(
block_id: BlockId { height, hash },
confirmation_time: time,
};
- let _ = tx_graph.insert_anchor(txid, anchor);
+ update.anchors.insert((anchor, txid));
}
}
/// Inserts floating txouts into `tx_graph` using [`Vin`](esplora_client::api::Vin)s returned by
/// Esplora.
fn insert_prevouts(
- tx_graph: &mut TxGraph,
+ update: &mut tx_graph::Update,
esplora_inputs: impl IntoIterator
- ,
) {
let prevouts = esplora_inputs
.into_iter()
.filter_map(|vin| Some((vin.txid, vin.vout, vin.prevout?)));
for (prev_txid, prev_vout, prev_txout) in prevouts {
- let _ = tx_graph.insert_txout(
+ update.txouts.insert(
OutPoint::new(prev_txid, prev_vout),
TxOut {
script_pubkey: prev_txout.scriptpubkey,
diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs
index 70d464194..7b0ef7fa6 100644
--- a/crates/esplora/tests/async_ext.rs
+++ b/crates/esplora/tests/async_ext.rs
@@ -1,4 +1,5 @@
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
+use bdk_chain::{ConfirmationBlockTime, TxGraph};
use bdk_esplora::EsploraAsyncExt;
use esplora_client::{self, Builder};
use std::collections::{BTreeSet, HashSet};
@@ -6,7 +7,7 @@ use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
-use bdk_chain::bitcoin::{Address, Amount, Txid};
+use bdk_chain::bitcoin::{Address, Amount};
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
#[tokio::test]
@@ -78,18 +79,23 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
);
let graph_update = sync_update.graph_update;
+ let updated_graph = {
+ let mut graph = TxGraph::::default();
+ let _ = graph.apply_update(graph_update.clone());
+ graph
+ };
// Check to see if we have the floating txouts available from our two created transactions'
// previous outputs in order to calculate transaction fees.
- for tx in graph_update.full_txs() {
+ for tx in &graph_update.txs {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transactions' previous outputs.
- let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
+ let fee = updated_graph.calculate_fee(tx).expect("Fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
- .get_transaction(&tx.txid, None)
+ .get_transaction(&tx.compute_txid(), None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
@@ -101,11 +107,15 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
assert_eq!(fee, tx_fee);
}
- let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect();
- graph_update_txids.sort();
- let mut expected_txids = vec![txid1, txid2];
- expected_txids.sort();
- assert_eq!(graph_update_txids, expected_txids);
+ assert_eq!(
+ graph_update
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
+ .collect::>(),
+ [txid1, txid2].into(),
+ "update must include all expected transactions"
+ );
Ok(())
}
@@ -167,7 +177,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
.spks_for_keychain(0, spks.clone());
client.full_scan(request, 3, 1).await?
};
- assert!(full_scan_update.graph_update.full_txs().next().is_none());
+ assert!(full_scan_update.graph_update.txs.is_empty());
assert!(full_scan_update.last_active_indices.is_empty());
let full_scan_update = {
let request = FullScanRequest::builder()
@@ -178,10 +188,10 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
assert_eq!(
full_scan_update
.graph_update
- .full_txs()
- .next()
+ .txs
+ .first()
.unwrap()
- .txid,
+ .compute_txid(),
txid_4th_addr
);
assert_eq!(full_scan_update.last_active_indices[&0], 3);
@@ -212,8 +222,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
};
let txs: HashSet<_> = full_scan_update
.graph_update
- .full_txs()
- .map(|tx| tx.txid)
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
.collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
@@ -226,8 +237,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
};
let txs: HashSet<_> = full_scan_update
.graph_update
- .full_txs()
- .map(|tx| tx.txid)
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
.collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs
index 818f1f5fb..b3833b899 100644
--- a/crates/esplora/tests/blocking_ext.rs
+++ b/crates/esplora/tests/blocking_ext.rs
@@ -1,4 +1,5 @@
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
+use bdk_chain::{ConfirmationBlockTime, TxGraph};
use bdk_esplora::EsploraExt;
use esplora_client::{self, Builder};
use std::collections::{BTreeSet, HashSet};
@@ -6,7 +7,7 @@ use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
-use bdk_chain::bitcoin::{Address, Amount, Txid};
+use bdk_chain::bitcoin::{Address, Amount};
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
#[test]
@@ -78,18 +79,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
);
let graph_update = sync_update.graph_update;
+ let updated_graph = {
+ let mut graph = TxGraph::::default();
+ let _ = graph.apply_update(graph_update.clone());
+ graph
+ };
// Check to see if we have the floating txouts available from our two created transactions'
// previous outputs in order to calculate transaction fees.
- for tx in graph_update.full_txs() {
+ for tx in &graph_update.txs {
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
// floating txouts available from the transactions' previous outputs.
- let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist");
+ let fee = updated_graph.calculate_fee(tx).expect("Fee must exist");
// Retrieve the fee in the transaction data from `bitcoind`.
let tx_fee = env
.bitcoind
.client
- .get_transaction(&tx.txid, None)
+ .get_transaction(&tx.compute_txid(), None)
.expect("Tx must exist")
.fee
.expect("Fee must exist")
@@ -101,12 +107,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
assert_eq!(fee, tx_fee);
}
- let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect();
- graph_update_txids.sort();
- let mut expected_txids = vec![txid1, txid2];
- expected_txids.sort();
- assert_eq!(graph_update_txids, expected_txids);
-
+ assert_eq!(
+ graph_update
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
+ .collect::>(),
+ [txid1, txid2].into(),
+ "update must include all expected transactions"
+ );
Ok(())
}
@@ -168,7 +177,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
.spks_for_keychain(0, spks.clone());
client.full_scan(request, 3, 1)?
};
- assert!(full_scan_update.graph_update.full_txs().next().is_none());
+ assert!(full_scan_update.graph_update.txs.is_empty());
assert!(full_scan_update.last_active_indices.is_empty());
let full_scan_update = {
let request = FullScanRequest::builder()
@@ -179,10 +188,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
assert_eq!(
full_scan_update
.graph_update
- .full_txs()
- .next()
+ .txs
+ .first()
.unwrap()
- .txid,
+ .compute_txid(),
txid_4th_addr
);
assert_eq!(full_scan_update.last_active_indices[&0], 3);
@@ -213,8 +222,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
};
let txs: HashSet<_> = full_scan_update
.graph_update
- .full_txs()
- .map(|tx| tx.txid)
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
.collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
@@ -227,8 +237,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
};
let txs: HashSet<_> = full_scan_update
.graph_update
- .full_txs()
- .map(|tx| tx.txid)
+ .txs
+ .iter()
+ .map(|tx| tx.compute_txid())
.collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs
index 6dce7503b..386d9d4e3 100644
--- a/crates/wallet/src/wallet/export.rs
+++ b/crates/wallet/src/wallet/export.rs
@@ -219,13 +219,13 @@ mod test {
use bdk_chain::{BlockId, ConfirmationBlockTime};
use bitcoin::hashes::Hash;
use bitcoin::{transaction, BlockHash, Network, Transaction};
+ use chain::tx_graph;
use super::*;
use crate::Wallet;
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
use crate::wallet::Update;
- use bdk_chain::TxGraph;
let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string())
.network(network)
.create_wallet_no_persist()
@@ -253,11 +253,12 @@ mod test {
confirmation_time: 0,
block_id,
};
- let mut graph = TxGraph::default();
- let _ = graph.insert_anchor(txid, anchor);
wallet
.apply_update(Update {
- graph,
+ graph: tx_graph::Update {
+ anchors: [(anchor, txid)].into_iter().collect(),
+ ..Default::default()
+ },
..Default::default()
})
.unwrap();
diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs
index 47e440c71..638bb5757 100644
--- a/crates/wallet/src/wallet/mod.rs
+++ b/crates/wallet/src/wallet/mod.rs
@@ -132,7 +132,7 @@ pub struct Update {
pub last_active_indices: BTreeMap,
/// Update for the wallet's internal [`TxGraph`].
- pub graph: TxGraph,
+ pub graph: chain::tx_graph::Update,
/// Update for the wallet's internal [`LocalChain`].
///
@@ -2277,7 +2277,34 @@ impl Wallet {
/// to persist staged wallet changes see [`Wallet::reveal_next_address`]. `
///
/// [`commit`]: Self::commit
+ #[cfg(feature = "std")]
+ #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> {
+ use std::time::*;
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("time now must surpass epoch anchor");
+ self.apply_update_at(update, Some(now.as_secs()))
+ }
+
+ /// Applies an `update` alongside an optional `seen_at` timestamp and stages the changes.
+ ///
+ /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the
+ /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The
+ /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed
+ /// transactions (where the transaction with the lower `last_seen` value is omitted from the
+ /// canonical history).
+ ///
+ /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will
+ /// not be part of the canonical history of transactions.
+ ///
+ /// Use [`apply_update`](Wallet::apply_update) to have the `seen_at` value automatically set to
+ /// the current time.
+ pub fn apply_update_at(
+ &mut self,
+ update: impl Into,
+ seen_at: Option,
+ ) -> Result<(), CannotConnectError> {
let update = update.into();
let mut changeset = match update.chain {
Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?),
@@ -2289,7 +2316,11 @@ impl Wallet {
.index
.reveal_to_target_multi(&update.last_active_indices);
changeset.merge(index_changeset.into());
- changeset.merge(self.indexed_graph.apply_update(update.graph).into());
+ changeset.merge(
+ self.indexed_graph
+ .apply_update_at(update.graph, seen_at)
+ .into(),
+ );
self.stage.merge(changeset);
Ok(())
}
@@ -2562,7 +2593,7 @@ macro_rules! floating_rate {
macro_rules! doctest_wallet {
() => {{
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
- use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph};
+ use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph};
use $crate::{Update, KeychainKind, Wallet};
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
@@ -2590,9 +2621,13 @@ macro_rules! doctest_wallet {
confirmation_time: 50_000,
block_id,
};
- let mut graph = TxGraph::default();
- let _ = graph.insert_anchor(txid, anchor);
- let update = Update { graph, ..Default::default() };
+ let update = Update {
+ graph: tx_graph::Update {
+ anchors: [(anchor, txid)].into_iter().collect(),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
wallet.apply_update(update).unwrap();
wallet
}}
diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs
index 288560b0a..561a9a5fb 100644
--- a/crates/wallet/tests/common.rs
+++ b/crates/wallet/tests/common.rs
@@ -1,5 +1,5 @@
#![allow(unused)]
-use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
+use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet};
use bitcoin::{
hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction,
@@ -218,11 +218,12 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir
})
.expect("confirmation height cannot be greater than tip");
- let mut graph = TxGraph::default();
- let _ = graph.insert_anchor(txid, anchor);
wallet
.apply_update(Update {
- graph,
+ graph: tx_graph::Update {
+ anchors: [(anchor, txid)].into(),
+ ..Default::default()
+ },
..Default::default()
})
.unwrap();
diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs
index c530e779c..243161658 100644
--- a/crates/wallet/tests/wallet.rs
+++ b/crates/wallet/tests/wallet.rs
@@ -5,7 +5,7 @@ use std::str::FromStr;
use anyhow::Context;
use assert_matches::assert_matches;
-use bdk_chain::COINBASE_MATURITY;
+use bdk_chain::{tx_graph, COINBASE_MATURITY};
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
@@ -81,11 +81,12 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) {
use bdk_wallet::Update;
- let mut graph = bdk_chain::TxGraph::default();
- let _ = graph.insert_seen_at(txid, seen_at);
wallet
.apply_update(Update {
- graph,
+ graph: tx_graph::Update {
+ seen_ats: [(txid, seen_at)].into_iter().collect(),
+ ..Default::default()
+ },
..Default::default()
})
.unwrap();
diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs
index 49608fbf1..662bc4237 100644
--- a/example-crates/example_electrum/src/main.rs
+++ b/example-crates/example_electrum/src/main.rs
@@ -129,7 +129,7 @@ fn main() -> anyhow::Result<()> {
// Tell the electrum client about the txs we've already got locally so it doesn't re-download them
client.populate_tx_cache(&*graph.lock().unwrap());
- let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
+ let (chain_update, graph_update, keychain_update) = match electrum_cmd.clone() {
ElectrumCommands::Scan {
stop_gap,
scan_options,
@@ -248,12 +248,6 @@ fn main() -> anyhow::Result<()> {
}
};
- let now = std::time::UNIX_EPOCH
- .elapsed()
- .expect("must get time")
- .as_secs();
- let _ = graph_update.update_last_seen_unconfirmed(now);
-
let db_changeset = {
let mut chain = chain.lock().unwrap();
let mut graph = graph.lock().unwrap();
diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs
index b07a6697d..d4692e35c 100644
--- a/example-crates/example_esplora/src/main.rs
+++ b/example-crates/example_esplora/src/main.rs
@@ -166,14 +166,10 @@ fn main() -> anyhow::Result<()> {
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
// represents the last active spk derivation indices of keychains
// (`keychain_indices_update`).
- let mut update = client
+ let update = client
.full_scan(request, *stop_gap, scan_options.parallel_requests)
.context("scanning for transactions")?;
- // We want to keep track of the latest time a transaction was seen unconfirmed.
- let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
- let _ = update.graph_update.update_last_seen_unconfirmed(now);
-
let mut graph = graph.lock().expect("mutex must not be poisoned");
let mut chain = chain.lock().expect("mutex must not be poisoned");
// Because we did a stop gap based scan we are likely to have some updates to our
@@ -265,11 +261,7 @@ fn main() -> anyhow::Result<()> {
}
}
- let mut update = client.sync(request, scan_options.parallel_requests)?;
-
- // Update last seen unconfirmed
- let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
- let _ = update.graph_update.update_last_seen_unconfirmed(now);
+ let update = client.sync(request, scan_options.parallel_requests)?;
(
chain
diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs
index f4596ce18..c05184052 100644
--- a/example-crates/wallet_electrum/src/main.rs
+++ b/example-crates/wallet_electrum/src/main.rs
@@ -64,10 +64,7 @@ fn main() -> Result<(), anyhow::Error> {
}
});
- let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?;
-
- let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
- let _ = update.graph_update.update_last_seen_unconfirmed(now);
+ let update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?;
println!();
diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs
index f81f8101c..6fd215dff 100644
--- a/example-crates/wallet_esplora_async/src/main.rs
+++ b/example-crates/wallet_esplora_async/src/main.rs
@@ -57,11 +57,9 @@ async fn main() -> Result<(), anyhow::Error> {
}
});
- let mut update = client
+ let update = client
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
.await?;
- let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
- let _ = update.graph_update.update_last_seen_unconfirmed(now);
wallet.apply_update(update)?;
wallet.persist(&mut conn)?;
diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs
index bec395611..45e4685b7 100644
--- a/example-crates/wallet_esplora_blocking/src/main.rs
+++ b/example-crates/wallet_esplora_blocking/src/main.rs
@@ -59,9 +59,7 @@ fn main() -> Result<(), anyhow::Error> {
}
});
- let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?;
- let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
- let _ = update.graph_update.update_last_seen_unconfirmed(now);
+ let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?;
wallet.apply_update(update)?;
if let Some(changeset) = wallet.take_staged() {