]> Untitled Git - bdk/commitdiff
feat!: introduce `tx_graph::Update`
author志宇 <hello@evanlinjin.me>
Fri, 23 Aug 2024 06:13:42 +0000 (06:13 +0000)
committer志宇 <hello@evanlinjin.me>
Fri, 23 Aug 2024 13:42:21 +0000 (13:42 +0000)
Instead of updating a `TxGraph` with a `TxGraph`, we introduce a
dedicated data object (`tx_graph::Update`). This brings us closer to
completing #1543.

Co-authored-by: Wei Chen <wzc110@gmail.com>
20 files changed:
crates/chain/src/indexed_tx_graph.rs
crates/chain/src/spk_client.rs
crates/chain/src/tx_graph.rs
crates/chain/tests/test_tx_graph.rs
crates/electrum/src/bdk_electrum_client.rs
crates/electrum/tests/test_electrum.rs
crates/esplora/src/async_ext.rs
crates/esplora/src/blocking_ext.rs
crates/esplora/src/lib.rs
crates/esplora/tests/async_ext.rs
crates/esplora/tests/blocking_ext.rs
crates/wallet/src/wallet/export.rs
crates/wallet/src/wallet/mod.rs
crates/wallet/tests/common.rs
crates/wallet/tests/wallet.rs
example-crates/example_electrum/src/main.rs
example-crates/example_esplora/src/main.rs
example-crates/wallet_electrum/src/main.rs
example-crates/wallet_esplora_async/src/main.rs
example-crates/wallet_esplora_blocking/src/main.rs

index 92a08a9ef5a1c11f085624ded48f6490766ba643..d24b1b307b77355ccdc56d0dd7197c24f9ad892c 100644 (file)
@@ -91,13 +91,10 @@ where
     /// Apply an `update` directly.
     ///
     /// `update` is a [`TxGraph<A>`] and the resultant changes is returned as [`ChangeSet`].
-    pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
-        let graph = self.graph.apply_update(update);
-        let indexer = self.index_tx_graph_changeset(&graph);
-        ChangeSet {
-            tx_graph: graph,
-            indexer,
-        }
+    pub fn apply_update(&mut self, update: tx_graph::Update<A>) -> ChangeSet<A, I::ChangeSet> {
+        let tx_graph = self.graph.apply_update(update);
+        let indexer = self.index_tx_graph_changeset(&tx_graph);
+        ChangeSet { tx_graph, indexer }
     }
 
     /// Insert a floating `txout` of given `outpoint`.
index 567a8f0a956ed4ed72ad5c337092aaeb548c66d7..e31b431dd8be97a9d45d15d1c585d2ea5e8b3843 100644 (file)
@@ -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<I> SyncRequest<I> {
 #[must_use]
 #[derive(Debug)]
 pub struct SyncResult<A = ConfirmationBlockTime> {
-    /// The update to apply to the receiving [`TxGraph`].
-    pub graph_update: TxGraph<A>,
+    /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph).
+    pub graph_update: crate::tx_graph::Update<A>,
     /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
     pub chain_update: Option<CheckPoint>,
 }
@@ -497,8 +497,8 @@ impl<K: Ord + Clone> FullScanRequest<K> {
 #[derive(Debug)]
 pub struct FullScanResult<K, A = ConfirmationBlockTime> {
     /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
-    pub graph_update: TxGraph<A>,
-    /// The update to apply to the receiving [`TxGraph`].
+    pub graph_update: crate::tx_graph::Update<A>,
+    /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph).
     pub chain_update: Option<CheckPoint>,
     /// Last active indices for the corresponding keychains (`K`).
     pub last_active_indices: BTreeMap<K, u32>,
index 9ab1268b376a3d457cde11853272dc79d5f67bc5..ba894fa938da538dd8d9c03fe08d04ef980c3432 100644 (file)
 //!
 //! ```
 //! # 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,79 @@ use core::{
     ops::{Deref, RangeInclusive},
 };
 
+/// Data object used to update the [`TxGraph`] with.
+#[derive(Debug, Clone)]
+pub struct Update<A = ()> {
+    /// Full transactions.
+    pub txs: Vec<Arc<Transaction>>,
+    /// Floating txouts.
+    pub txouts: BTreeMap<OutPoint, TxOut>,
+    /// Transaction anchors.
+    pub anchors: BTreeSet<(A, Txid)>,
+    /// Seen at times for transactions.
+    pub seen_ats: HashMap<Txid, u64>,
+}
+
+impl<A> Default for Update<A> {
+    fn default() -> Self {
+        Self {
+            txs: Default::default(),
+            txouts: Default::default(),
+            anchors: Default::default(),
+            seen_ats: Default::default(),
+        }
+    }
+}
+
+impl<A> From<TxGraph<A>> for Update<A> {
+    fn from(graph: TxGraph<A>) -> 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<A: Ord + Clone> From<Update<A>> for TxGraph<A> {
+    fn from(update: Update<A>) -> Self {
+        let mut graph = TxGraph::<A>::default();
+        let _ = graph.apply_update(update);
+        graph
+    }
+}
+
+impl<A: Ord> Update<A> {
+    /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions.
+    pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) {
+        let seen_ats = &mut self.seen_ats;
+        let anchors = &self.anchors;
+        let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| {
+            for (_, anchor_txid) in anchors {
+                if txid == anchor_txid {
+                    return false;
+                }
+            }
+            true
+        });
+        for txid in unanchored_txids {
+            seen_ats.insert(txid, seen_at);
+        }
+    }
+
+    /// Extend this update with `other`.
+    pub fn extend(&mut self, other: Update<A>) {
+        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.
@@ -690,19 +767,19 @@ impl<A: Clone + Ord> TxGraph<A> {
     ///
     /// 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<A>) -> ChangeSet<A> {
+    pub fn apply_update(&mut self, update: Update<A>) -> ChangeSet<A> {
         let mut changeset = ChangeSet::<A>::default();
-        for tx_node in update.full_txs() {
-            changeset.merge(self.insert_tx(tx_node.tx));
+        for tx in update.txs {
+            changeset.merge(self.insert_tx(tx));
         }
-        for (outpoint, txout) in update.floating_txouts() {
-            changeset.merge(self.insert_txout(outpoint, txout.clone()));
+        for (outpoint, txout) in update.txouts {
+            changeset.merge(self.insert_txout(outpoint, txout));
         }
-        for (anchor, txid) in &update.anchors {
-            changeset.merge(self.insert_anchor(*txid, anchor.clone()));
+        for (anchor, txid) in update.anchors {
+            changeset.merge(self.insert_anchor(txid, anchor));
         }
-        for (&txid, &last_seen) in &update.last_seen {
-            changeset.merge(self.insert_seen_at(txid, last_seen));
+        for (txid, seen_at) in update.seen_ats {
+            changeset.merge(self.insert_seen_at(txid, seen_at));
         }
         changeset
     }
index 3ffa82439aaebb0417dfd43c37c805e951823e20..c6399f53b0f7fd0db0776345cdc4e00df3a96bd4 100644 (file)
@@ -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::<BlockId>::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::<ChainPosition<BlockId>>::default();
+        let mut graph = TxGraph::<BlockId>::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()
         }
     );
index 1458e2bd9661e2636920a0c02897c859619681ec..57166075669fc04cfcf7c700d999dc4f674c6079 100644 (file)
@@ -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<E: ElectrumApi> BdkElectrumClient<E> {
             None => None,
         };
 
-        let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
+        let mut graph_update = tx_graph::Update::<ConfirmationBlockTime>::default();
         let mut last_active_indices = BTreeMap::<K, u32>::default();
         for keychain in request.keychains() {
             let spks = request.iter_spks(keychain.clone());
@@ -158,7 +158,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
             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<E: ElectrumApi> BdkElectrumClient<E> {
             None => None,
         };
 
-        let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
+        let mut graph_update = tx_graph::Update::<ConfirmationBlockTime>::default();
         self.populate_with_spks(
             &mut graph_update,
             request
@@ -227,7 +227,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
             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<E: ElectrumApi> BdkElectrumClient<E> {
     /// also included.
     fn populate_with_spks(
         &self,
-        graph_update: &mut TxGraph<ConfirmationBlockTime>,
+        graph_update: &mut tx_graph::Update<ConfirmationBlockTime>,
         mut spks: impl Iterator<Item = (u32, ScriptBuf)>,
         stop_gap: usize,
         batch_size: usize,
@@ -278,7 +278,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
                 }
 
                 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<E: ElectrumApi> BdkElectrumClient<E> {
     /// included. Anchors of the aforementioned transactions are included.
     fn populate_with_outpoints(
         &self,
-        graph_update: &mut TxGraph<ConfirmationBlockTime>,
+        graph_update: &mut tx_graph::Update<ConfirmationBlockTime>,
         outpoints: impl IntoIterator<Item = OutPoint>,
     ) -> Result<(), Error> {
         for outpoint in outpoints {
@@ -314,7 +314,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
 
                 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<E: ElectrumApi> BdkElectrumClient<E> {
                     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<E: ElectrumApi> BdkElectrumClient<E> {
     /// Populate the `graph_update` with transactions/anchors of the provided `txids`.
     fn populate_with_txids(
         &self,
-        graph_update: &mut TxGraph<ConfirmationBlockTime>,
+        graph_update: &mut tx_graph::Update<ConfirmationBlockTime>,
         txids: impl IntoIterator<Item = Txid>,
     ) -> Result<(), Error> {
         for txid in txids {
@@ -366,7 +366,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
                 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<E: ElectrumApi> BdkElectrumClient<E> {
     // 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<ConfirmationBlockTime>,
+        graph_update: &mut tx_graph::Update<ConfirmationBlockTime>,
         txid: Txid,
         confirmation_height: i32,
     ) -> Result<(), Error> {
@@ -402,8 +402,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
             }
 
             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<E: ElectrumApi> BdkElectrumClient<E> {
                             hash: header.block_hash(),
                         },
                     },
-                );
+                    txid,
+                ));
             }
         }
         Ok(())
@@ -421,17 +421,18 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
     // 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<ConfirmationBlockTime>,
+        graph_update: &mut tx_graph::Update<ConfirmationBlockTime>,
     ) -> Result<(), Error> {
-        let full_txs: Vec<Arc<Transaction>> =
-            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::<Txid>::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<A: Anchor>(
     mut tip: CheckPoint,
     latest_blocks: &BTreeMap<u32, BlockHash>,
-    anchors: &BTreeSet<(A, Txid)>,
+    anchors: impl Iterator<Item = (A, Txid)>,
 ) -> Result<CheckPoint, Error> {
     for anchor in anchors {
         let height = anchor.0.anchor_block().height;
index 63e91081b61c1e711b25d7578d9f15e0e2d7d1e4..e8b054d339703da5e7f0ae3ab119c3ee9ec7cca5 100644 (file)
@@ -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};
@@ -49,7 +49,7 @@ where
         .elapsed()
         .expect("must get time")
         .as_secs();
-    let _ = update.graph_update.update_last_seen_unconfirmed(now);
+    update.graph_update.update_last_seen_unconfirmed(now);
 
     if let Some(chain_update) = update.chain_update.clone() {
         let _ = chain
@@ -128,18 +128,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::<ConfirmationBlockTime>::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 +156,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
         assert_eq!(fee, tx_fee);
     }
 
-    let mut graph_update_txids: Vec<Txid> = 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::<BTreeSet<_>>(),
+        [txid1, txid2].into(),
+        "update must include all expected transactions",
+    );
     Ok(())
 }
 
@@ -216,7 +224,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 +235,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 +267,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 +282,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 +485,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 +516,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)?,
index 066b91e170466496b1c3900286f2f2bb0a77c5fe..f3c8e966aad782e1790ca0a0a55aff4cbd66a77b 100644 (file)
@@ -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::<ConfirmationBlockTime>::default();
+        let mut inserted_txs = HashSet::<Txid>::new();
         let mut last_active_indices = BTreeMap::<K, u32>::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::<ConfirmationBlockTime>::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::<ConfirmationBlockTime>::default();
+        let mut inserted_txs = HashSet::<Txid>::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<A: Anchor>(
 /// Refer to [crate-level docs](crate) for more.
 async fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>> + Send>(
     client: &esplora_client::AsyncClient,
+    inserted_txs: &mut HashSet<Txid>,
     mut keychain_spks: I,
     stop_gap: usize,
     parallel_requests: usize,
-) -> Result<(TxGraph<ConfirmationBlockTime>, Option<u32>), Error> {
+) -> Result<(tx_graph::Update<ConfirmationBlockTime>, Option<u32>), Error> {
     type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
 
-    let mut tx_graph = TxGraph::default();
+    let mut update = tx_graph::Update::<ConfirmationBlockTime>::default();
     let mut last_index = Option::<u32>::None;
     let mut last_active_index = Option::<u32>::None;
 
@@ -294,9 +320,11 @@ async fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>> + 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<I: Iterator<Item = Indexed<ScriptBuf>> + 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<I: Iterator<Item = Indexed<ScriptBuf>> + S
 /// Refer to [crate-level docs](crate) for more.
 async fn fetch_txs_with_spks<I: IntoIterator<Item = ScriptBuf> + Send>(
     client: &esplora_client::AsyncClient,
+    inserted_txs: &mut HashSet<Txid>,
     spks: I,
     parallel_requests: usize,
-) -> Result<TxGraph<ConfirmationBlockTime>, Error>
+) -> Result<tx_graph::Update<ConfirmationBlockTime>, 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<I: IntoIterator<Item = Txid> + Send>(
     client: &esplora_client::AsyncClient,
+    inserted_txs: &mut HashSet<Txid>,
     txids: I,
     parallel_requests: usize,
-) -> Result<TxGraph<ConfirmationBlockTime>, Error>
+) -> Result<tx_graph::Update<ConfirmationBlockTime>, Error>
 where
     I::IntoIter: Send,
 {
-    enum EsploraResp {
-        TxStatus(TxStatus),
-        Tx(Option<Tx>),
-    }
-
-    let mut tx_graph = TxGraph::default();
-    let mut txids = txids.into_iter();
+    let mut update = tx_graph::Update::<ConfirmationBlockTime>::default();
+    // Only fetch for non-inserted txs.
+    let mut txids = txids
+        .into_iter()
+        .filter(|txid| !inserted_txs.contains(txid))
+        .collect::<Vec<Txid>>()
+        .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::<FuturesOrdered<_>>();
 
@@ -388,21 +406,17 @@ where
             break;
         }
 
-        for (txid, resp) in handles.try_collect::<Vec<_>>().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::<Vec<_>>().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<I: IntoIterator<Item = OutPoint> + Send>(
     client: &esplora_client::AsyncClient,
+    inserted_txs: &mut HashSet<Txid>,
     outpoints: I,
     parallel_requests: usize,
-) -> Result<TxGraph<ConfirmationBlockTime>, Error>
+) -> Result<tx_graph::Update<ConfirmationBlockTime>, Error>
 where
     I::IntoIter: Send,
 {
     let outpoints = outpoints.into_iter().collect::<Vec<_>>();
+    let mut update = tx_graph::Update::<ConfirmationBlockTime>::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)]
index 6e3e25afe70e613dcc1b4fd337757f339db7c2e0..62f0d351eaf782677ec8ffd69b2d1af2098e84ba 100644 (file)
@@ -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::<Txid>::new();
         let mut last_active_indices = BTreeMap::<K, u32>::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::<ConfirmationBlockTime>::default();
+        let mut inserted_txs = HashSet::<Txid>::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<A: Anchor>(
 
 fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
     client: &esplora_client::BlockingClient,
+    inserted_txs: &mut HashSet<Txid>,
     mut keychain_spks: I,
     stop_gap: usize,
     parallel_requests: usize,
-) -> Result<(TxGraph<ConfirmationBlockTime>, Option<u32>), Error> {
+) -> Result<(tx_graph::Update<ConfirmationBlockTime>, Option<u32>), Error> {
     type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
 
-    let mut tx_graph = TxGraph::default();
+    let mut update = tx_graph::Update::<ConfirmationBlockTime>::default();
     let mut last_index = Option::<u32>::None;
     let mut last_active_index = Option::<u32>::None;
 
@@ -289,9 +300,11 @@ fn fetch_txs_with_keychain_spks<I: Iterator<Item = Indexed<ScriptBuf>>>(
                 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<I: Iterator<Item = Indexed<ScriptBuf>>>(
         }
     }
 
-    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<I: Iterator<Item = Indexed<ScriptBuf>>>(
 /// Refer to [crate-level docs](crate) for more.
 fn fetch_txs_with_spks<I: IntoIterator<Item = ScriptBuf>>(
     client: &esplora_client::BlockingClient,
+    inserted_txs: &mut HashSet<Txid>,
     spks: I,
     parallel_requests: usize,
-) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
+) -> Result<tx_graph::Update<ConfirmationBlockTime>, 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<I: IntoIterator<Item = ScriptBuf>>(
 /// Refer to [crate-level docs](crate) for more.
 fn fetch_txs_with_txids<I: IntoIterator<Item = Txid>>(
     client: &esplora_client::BlockingClient,
+    inserted_txs: &mut HashSet<Txid>,
     txids: I,
     parallel_requests: usize,
-) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
-    enum EsploraResp {
-        TxStatus(TxStatus),
-        Tx(Option<Tx>),
-    }
-
-    let mut tx_graph = TxGraph::default();
-    let mut txids = txids.into_iter();
+) -> Result<tx_graph::Update<ConfirmationBlockTime>, Error> {
+    let mut update = tx_graph::Update::<ConfirmationBlockTime>::default();
+    // Only fetch for non-inserted txs.
+    let mut txids = txids
+        .into_iter()
+        .filter(|txid| !inserted_txs.contains(txid))
+        .collect::<Vec<Txid>>()
+        .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::<Vec<JoinHandle<Result<(Txid, EsploraResp), Error>>>>();
+            .collect::<Vec<JoinHandle<Result<(Txid, Option<Tx>), 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<I: IntoIterator<Item = Txid>>(
 /// Refer to [crate-level docs](crate) for more.
 fn fetch_txs_with_outpoints<I: IntoIterator<Item = OutPoint>>(
     client: &esplora_client::BlockingClient,
+    inserted_txs: &mut HashSet<Txid>,
     outpoints: I,
     parallel_requests: usize,
-) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
+) -> Result<tx_graph::Update<ConfirmationBlockTime>, Error> {
     let outpoints = outpoints.into_iter().collect::<Vec<_>>();
+    let mut update = tx_graph::Update::<ConfirmationBlockTime>::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<I: IntoIterator<Item = OutPoint>>(
                     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)]
index 7db6967b65cb17d6c346f47f4acd91fd10af4968..9a6e8f1df2c0d3e5168a09c915969bc3b31ee385 100644 (file)
@@ -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<ConfirmationBlockTime>,
+    update: &mut tx_graph::Update<ConfirmationBlockTime>,
     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<ConfirmationBlockTime>,
+    update: &mut tx_graph::Update<ConfirmationBlockTime>,
     esplora_inputs: impl IntoIterator<Item = esplora_client::api::Vin>,
 ) {
     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,
index 70d4641941b195a9d0eefca629fd47b71a846059..7b0ef7fa646790a610b2ac4cd0039c689c1e4895 100644 (file)
@@ -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::<ConfirmationBlockTime>::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<Txid> = 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::<BTreeSet<_>>(),
+        [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));
index 818f1f5fb62f6315173261b15453e59c8246be87..b3833b89957199bb8fa86c0d74de7954c25222f3 100644 (file)
@@ -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::<ConfirmationBlockTime>::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<Txid> = 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::<BTreeSet<_>>(),
+        [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));
index 6dce7503b25b8864f426e8a301e82e481f847c8f..386d9d4e35314a4ab338965421a33e424f619b09 100644 (file)
@@ -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();
index 47e440c710196e487a68e9fbf91a037ac991d15a..0d6cdf184400acd8cc026848e88c20d7341f890d 100644 (file)
@@ -132,7 +132,7 @@ pub struct Update {
     pub last_active_indices: BTreeMap<KeychainKind, u32>,
 
     /// Update for the wallet's internal [`TxGraph`].
-    pub graph: TxGraph<ConfirmationBlockTime>,
+    pub graph: chain::tx_graph::Update<ConfirmationBlockTime>,
 
     /// Update for the wallet's internal [`LocalChain`].
     ///
@@ -2562,7 +2562,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 +2590,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
     }}
index 288560b0ac341681be28885a5998ebcdbc82668e..561a9a5fb6b758e5cd4c913f656c6cbfeb46b6ef 100644 (file)
@@ -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();
index c530e779ceb1c2675970a21a573f7c7a851481ef..2431616584e336d16f7ad9a1c7e754ebfe15f432 100644 (file)
@@ -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();
index 49608fbf152c01178a8c60422f308bf2db60e3d4..7212547d6e6a58eef823f69d2dc431266794212d 100644 (file)
@@ -252,7 +252,7 @@ fn main() -> anyhow::Result<()> {
         .elapsed()
         .expect("must get time")
         .as_secs();
-    let _ = graph_update.update_last_seen_unconfirmed(now);
+    graph_update.update_last_seen_unconfirmed(now);
 
     let db_changeset = {
         let mut chain = chain.lock().unwrap();
index b07a6697d9d5a98c8fe6487b33adb4ff74d3e2e2..d188eab76153347203bdf89f3f1a41c9d909012c 100644 (file)
@@ -172,7 +172,7 @@ fn main() -> anyhow::Result<()> {
 
             // 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);
+            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");
@@ -269,7 +269,7 @@ fn main() -> anyhow::Result<()> {
 
             // Update last seen unconfirmed
             let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
-            let _ = update.graph_update.update_last_seen_unconfirmed(now);
+            update.graph_update.update_last_seen_unconfirmed(now);
 
             (
                 chain
index f4596ce18cf0cb03d8660c57efc833568c99feb3..4cc698a008514d349539198b182ecd169d61e6a4 100644 (file)
@@ -67,7 +67,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);
+    update.graph_update.update_last_seen_unconfirmed(now);
 
     println!();
 
index f81f8101ca38963cfaabca56548f05e057690dd5..d4dae1f3f5d270a89a33d11370fa0dca2d8d7771 100644 (file)
@@ -61,7 +61,7 @@ async fn main() -> Result<(), anyhow::Error> {
         .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);
+    update.graph_update.update_last_seen_unconfirmed(now);
 
     wallet.apply_update(update)?;
     wallet.persist(&mut conn)?;
index bec3956114ad842f30f22a893628ae3a73bd109f..9f79d6bf6f1c4609027ea1bbd9c9c5a85bd77e2f 100644 (file)
@@ -61,7 +61,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);
+    update.graph_update.update_last_seen_unconfirmed(now);
 
     wallet.apply_update(update)?;
     if let Some(changeset) = wallet.take_staged() {