]> Untitled Git - bdk/commitdiff
feat(chain)!: Introduce `CanonicalView` and migrate API
author志宇 <hello@evanlinjin.me>
Thu, 11 Sep 2025 03:48:27 +0000 (03:48 +0000)
committer志宇 <hello@evanlinjin.me>
Wed, 17 Sep 2025 23:46:29 +0000 (23:46 +0000)
- Add `CanonicalView` structure with canonical transaction methods
- Move methods from `TxGraph` to `CanonicalView` (txs, filter_outpoints, balance, etc.)
- Add canonical view methods to `IndexedTxGraph`
- Update all tests and examples to use new API
- Optimize examples to reuse canonical view instances

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
18 files changed:
crates/bitcoind_rpc/examples/filter_iter.rs
crates/bitcoind_rpc/tests/test_emitter.rs
crates/chain/benches/canonicalization.rs
crates/chain/benches/indexer.rs
crates/chain/src/canonical_view.rs [new file with mode: 0644]
crates/chain/src/indexed_tx_graph.rs
crates/chain/src/lib.rs
crates/chain/src/tx_graph.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_tx_graph.rs
crates/chain/tests/test_tx_graph_conflicts.rs
crates/electrum/tests/test_electrum.rs
crates/esplora/tests/async_ext.rs
crates/esplora/tests/blocking_ext.rs
examples/example_bitcoind_rpc_polling/src/main.rs
examples/example_cli/src/lib.rs
examples/example_electrum/src/main.rs
examples/example_esplora/src/main.rs

index c0e755f9ce44fa477cb9148f27eba3bab4baf3b8..5a3dc29746293fd83ab7d75f1f35ca693692eb49 100644 (file)
@@ -69,13 +69,8 @@ fn main() -> anyhow::Result<()> {
     println!("\ntook: {}s", start.elapsed().as_secs());
     println!("Local tip: {}", chain.tip().height());
     let unspent: Vec<_> = graph
-        .graph()
-        .filter_chain_unspents(
-            &chain,
-            chain.tip().block_id(),
-            Default::default(),
-            graph.index.outpoints().clone(),
-        )
+        .canonical_view(&chain, chain.tip().block_id(), Default::default())
+        .filter_unspent_outpoints(graph.index.outpoints().clone())
         .collect();
     if !unspent.is_empty() {
         println!("\nUnspent");
@@ -85,16 +80,16 @@ fn main() -> anyhow::Result<()> {
         }
     }
 
-    for canon_tx in graph.graph().list_canonical_txs(
-        &chain,
-        chain.tip().block_id(),
-        bdk_chain::CanonicalizationParams::default(),
-    ) {
-        if !canon_tx.chain_position.is_confirmed() {
-            eprintln!(
-                "ERROR: canonical tx should be confirmed {}",
-                canon_tx.tx_node.txid
-            );
+    for canon_tx in graph
+        .canonical_view(
+            &chain,
+            chain.tip().block_id(),
+            bdk_chain::CanonicalizationParams::default(),
+        )
+        .txs()
+    {
+        if !canon_tx.pos.is_confirmed() {
+            eprintln!("ERROR: canonical tx should be confirmed {}", canon_tx.txid);
         }
     }
 
index 079551bf01ce834edc21e05c18c65f0ca22d778f..79b44b00a7ff4da396dcaaf462e290925530619f 100644 (file)
@@ -310,13 +310,9 @@ fn get_balance(
 ) -> anyhow::Result<Balance> {
     let chain_tip = recv_chain.tip().block_id();
     let outpoints = recv_graph.index.outpoints().clone();
-    let balance = recv_graph.graph().balance(
-        recv_chain,
-        chain_tip,
-        CanonicalizationParams::default(),
-        outpoints,
-        |_, _| true,
-    );
+    let balance = recv_graph
+        .canonical_view(recv_chain, chain_tip, CanonicalizationParams::default())
+        .balance(outpoints, |_, _| true);
     Ok(balance)
 }
 
@@ -621,7 +617,8 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
 
     // Retrieve the expected unconfirmed txids and spks from the graph.
     let exp_spk_txids = graph
-        .list_expected_spk_txids(&chain, chain_tip, ..)
+        .canonical_view(&chain, chain_tip, Default::default())
+        .list_expected_spk_txids(&graph.index, ..)
         .collect::<Vec<_>>();
     assert_eq!(exp_spk_txids, vec![(spk, txid_1)]);
 
@@ -636,9 +633,9 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
     let _ = graph.batch_insert_relevant_evicted_at(mempool_event.evicted);
 
     let canonical_txids = graph
-        .graph()
-        .list_canonical_txs(&chain, chain_tip, CanonicalizationParams::default())
-        .map(|tx| tx.tx_node.compute_txid())
+        .canonical_view(&chain, chain_tip, CanonicalizationParams::default())
+        .txs()
+        .map(|tx| tx.txid)
         .collect::<Vec<_>>();
     // tx1 should no longer be canonical.
     assert!(!canonical_txids.contains(&txid_1));
index bf11e1ebc33ef95fff0e99759d9437361965e951..3d8d8b2954a72a261aafcf0cb110609a4024d901 100644 (file)
@@ -95,31 +95,32 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
 }
 
 fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
-    let txs = tx_graph.graph().list_canonical_txs(
+    let view = tx_graph.canonical_view(
         chain,
         chain.tip().block_id(),
         CanonicalizationParams::default(),
     );
+    let txs = view.txs();
     assert_eq!(txs.count(), exp_txs);
 }
 
 fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
-    let utxos = tx_graph.graph().filter_chain_txouts(
+    let view = tx_graph.canonical_view(
         chain,
         chain.tip().block_id(),
         CanonicalizationParams::default(),
-        tx_graph.index.outpoints().clone(),
     );
+    let utxos = view.filter_outpoints(tx_graph.index.outpoints().clone());
     assert_eq!(utxos.count(), exp_txos);
 }
 
 fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_utxos: usize) {
-    let utxos = tx_graph.graph().filter_chain_unspents(
+    let view = tx_graph.canonical_view(
         chain,
         chain.tip().block_id(),
         CanonicalizationParams::default(),
-        tx_graph.index.outpoints().clone(),
     );
+    let utxos = view.filter_unspent_outpoints(tx_graph.index.outpoints().clone());
     assert_eq!(utxos.count(), exp_utxos);
 }
 
index c1786a62723b917ee6828597bd3047c310199659..df4e3f361aa4487127ffc30666761baacd922b0d 100644 (file)
@@ -84,13 +84,9 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
     // Check balance
     let chain_tip = chain.tip().block_id();
     let op = graph.index.outpoints().clone();
-    let bal = graph.graph().balance(
-        chain,
-        chain_tip,
-        CanonicalizationParams::default(),
-        op,
-        |_, _| false,
-    );
+    let bal = graph
+        .canonical_view(chain, chain_tip, CanonicalizationParams::default())
+        .balance(op, |_, _| false);
     assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
 }
 
diff --git a/crates/chain/src/canonical_view.rs b/crates/chain/src/canonical_view.rs
new file mode 100644 (file)
index 0000000..2685344
--- /dev/null
@@ -0,0 +1,275 @@
+//! Canonical view.
+
+use crate::collections::HashMap;
+use alloc::sync::Arc;
+use core::{fmt, ops::RangeBounds};
+
+use alloc::vec::Vec;
+
+use bdk_core::BlockId;
+use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid};
+
+use crate::{
+    spk_txout::SpkTxOutIndex, tx_graph::TxNode, Anchor, Balance, CanonicalIter, CanonicalReason,
+    CanonicalizationParams, ChainOracle, ChainPosition, FullTxOut, ObservedIn, TxGraph,
+};
+
+/// A single canonical transaction.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct CanonicalViewTx<A> {
+    /// Chain position.
+    pub pos: ChainPosition<A>,
+    /// Transaction ID.
+    pub txid: Txid,
+    /// The actual transaction.
+    pub tx: Arc<Transaction>,
+}
+
+/// A view of canonical transactions.
+#[derive(Debug)]
+pub struct CanonicalView<A> {
+    order: Vec<Txid>,
+    txs: HashMap<Txid, (Arc<Transaction>, ChainPosition<A>)>,
+    spends: HashMap<OutPoint, Txid>,
+    tip: BlockId,
+}
+
+impl<A: Anchor> CanonicalView<A> {
+    /// Create a canonical view.
+    pub fn new<'g, C>(
+        tx_graph: &'g TxGraph<A>,
+        chain: &'g C,
+        chain_tip: BlockId,
+        params: CanonicalizationParams,
+    ) -> Result<Self, C::Error>
+    where
+        C: ChainOracle,
+    {
+        fn find_direct_anchor<'g, A: Anchor, C: ChainOracle>(
+            tx_node: &TxNode<'g, Arc<Transaction>, A>,
+            chain: &C,
+            chain_tip: BlockId,
+        ) -> Result<Option<A>, C::Error> {
+            tx_node
+                .anchors
+                .iter()
+                .find_map(|a| -> Option<Result<A, C::Error>> {
+                    match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
+                        Ok(Some(true)) => Some(Ok(a.clone())),
+                        Ok(Some(false)) | Ok(None) => None,
+                        Err(err) => Some(Err(err)),
+                    }
+                })
+                .transpose()
+        }
+
+        let mut view = Self {
+            tip: chain_tip,
+            order: vec![],
+            txs: HashMap::new(),
+            spends: HashMap::new(),
+        };
+
+        for r in CanonicalIter::new(tx_graph, chain, chain_tip, params) {
+            let (txid, tx, why) = r?;
+
+            let tx_node = match tx_graph.get_tx_node(txid) {
+                Some(tx_node) => tx_node,
+                None => {
+                    // TODO: Have the `CanonicalIter` return `TxNode`s.
+                    debug_assert!(false, "tx node must exist!");
+                    continue;
+                }
+            };
+
+            view.order.push(txid);
+
+            if !tx.is_coinbase() {
+                view.spends
+                    .extend(tx.input.iter().map(|txin| (txin.previous_output, txid)));
+            }
+
+            let pos = match why {
+                CanonicalReason::Assumed { descendant } => match descendant {
+                    Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
+                        Some(anchor) => ChainPosition::Confirmed {
+                            anchor,
+                            transitively: None,
+                        },
+                        None => ChainPosition::Unconfirmed {
+                            first_seen: tx_node.first_seen,
+                            last_seen: tx_node.last_seen,
+                        },
+                    },
+                    None => ChainPosition::Unconfirmed {
+                        first_seen: tx_node.first_seen,
+                        last_seen: tx_node.last_seen,
+                    },
+                },
+                CanonicalReason::Anchor { anchor, descendant } => match descendant {
+                    Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
+                        Some(anchor) => ChainPosition::Confirmed {
+                            anchor,
+                            transitively: None,
+                        },
+                        None => ChainPosition::Confirmed {
+                            anchor,
+                            transitively: descendant,
+                        },
+                    },
+                    None => ChainPosition::Confirmed {
+                        anchor,
+                        transitively: None,
+                    },
+                },
+                CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
+                    ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
+                        first_seen: tx_node.first_seen,
+                        last_seen: Some(last_seen),
+                    },
+                    ObservedIn::Block(_) => ChainPosition::Unconfirmed {
+                        first_seen: tx_node.first_seen,
+                        last_seen: None,
+                    },
+                },
+            };
+            view.txs.insert(txid, (tx_node.tx, pos));
+        }
+
+        Ok(view)
+    }
+
+    /// Get a single canonical transaction.
+    pub fn tx(&self, txid: Txid) -> Option<CanonicalViewTx<A>> {
+        self.txs
+            .get(&txid)
+            .cloned()
+            .map(|(tx, pos)| CanonicalViewTx { pos, txid, tx })
+    }
+
+    /// Get a single canonical txout.
+    pub fn txout(&self, op: OutPoint) -> Option<FullTxOut<A>> {
+        let (tx, pos) = self.txs.get(&op.txid)?;
+        let vout: usize = op.vout.try_into().ok()?;
+        let txout = tx.output.get(vout)?;
+        let spent_by = self.spends.get(&op).map(|spent_by_txid| {
+            let (_, spent_by_pos) = &self.txs[spent_by_txid];
+            (spent_by_pos.clone(), *spent_by_txid)
+        });
+        Some(FullTxOut {
+            chain_position: pos.clone(),
+            outpoint: op,
+            txout: txout.clone(),
+            spent_by,
+            is_on_coinbase: tx.is_coinbase(),
+        })
+    }
+
+    /// Ordered transactions.
+    pub fn txs(
+        &self,
+    ) -> impl ExactSizeIterator<Item = CanonicalViewTx<A>> + DoubleEndedIterator + '_ {
+        self.order.iter().map(|&txid| {
+            let (tx, pos) = self.txs[&txid].clone();
+            CanonicalViewTx { pos, txid, tx }
+        })
+    }
+
+    /// Get a filtered list of outputs from the given `outpoints`.
+    ///
+    /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
+    /// (`O`) for convenience. If `O` is not necessary, the caller can use `()`, or
+    /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
+    pub fn filter_outpoints<'v, O: Clone + 'v>(
+        &'v self,
+        outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
+    ) -> impl Iterator<Item = (O, FullTxOut<A>)> + 'v {
+        outpoints
+            .into_iter()
+            .filter_map(|(op_i, op)| Some((op_i, self.txout(op)?)))
+    }
+
+    /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints`
+    ///
+    /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
+    /// (`O`) for convenience. If `O` is not necessary, the caller can use `()`, or
+    /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
+    pub fn filter_unspent_outpoints<'v, O: Clone + 'v>(
+        &'v self,
+        outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
+    ) -> impl Iterator<Item = (O, FullTxOut<A>)> + 'v {
+        self.filter_outpoints(outpoints)
+            .filter(|(_, txo)| txo.spent_by.is_none())
+    }
+
+    /// Get the total balance of `outpoints`.
+    ///
+    /// The output of `trust_predicate` should return `true` for scripts that we trust.
+    ///
+    /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
+    /// (`O`) for convenience. If `O` is not necessary, the caller can use `()`, or
+    /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
+    pub fn balance<'v, O: Clone + 'v>(
+        &'v self,
+        outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
+        mut trust_predicate: impl FnMut(&O, ScriptBuf) -> bool,
+    ) -> Balance {
+        let mut immature = Amount::ZERO;
+        let mut trusted_pending = Amount::ZERO;
+        let mut untrusted_pending = Amount::ZERO;
+        let mut confirmed = Amount::ZERO;
+
+        for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) {
+            match &txout.chain_position {
+                ChainPosition::Confirmed { .. } => {
+                    if txout.is_confirmed_and_spendable(self.tip.height) {
+                        confirmed += txout.txout.value;
+                    } else if !txout.is_mature(self.tip.height) {
+                        immature += txout.txout.value;
+                    }
+                }
+                ChainPosition::Unconfirmed { .. } => {
+                    if trust_predicate(&spk_i, txout.txout.script_pubkey) {
+                        trusted_pending += txout.txout.value;
+                    } else {
+                        untrusted_pending += txout.txout.value;
+                    }
+                }
+            }
+        }
+
+        Balance {
+            immature,
+            trusted_pending,
+            untrusted_pending,
+            confirmed,
+        }
+    }
+
+    /// List txids that are expected to exist under the given spks.
+    ///
+    /// This is used to fill
+    /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids).
+    ///
+    ///
+    /// The spk index range can be constrained with `range`.
+    pub fn list_expected_spk_txids<'v, I>(
+        &'v self,
+        indexer: &'v impl AsRef<SpkTxOutIndex<I>>,
+        spk_index_range: impl RangeBounds<I> + 'v,
+    ) -> impl Iterator<Item = (ScriptBuf, Txid)> + 'v
+    where
+        I: fmt::Debug + Clone + Ord + 'v,
+    {
+        let indexer = indexer.as_ref();
+        self.txs().flat_map(move |c_tx| -> Vec<_> {
+            let range = &spk_index_range;
+            let relevant_spks = indexer.relevant_spks_of_tx(&c_tx.tx);
+            relevant_spks
+                .into_iter()
+                .filter(|(i, _)| range.contains(i))
+                .map(|(_, spk)| (spk, c_tx.txid))
+                .collect()
+        })
+    }
+}
index f0c1d121d02ef9e4b966af65f5f78f67b7e8e956..9adf7ed936cc11359a9ca6e551b5c1a27119008a 100644 (file)
@@ -1,18 +1,14 @@
 //! Contains the [`IndexedTxGraph`] and associated types. Refer to the
 //! [`IndexedTxGraph`] documentation for more.
-use core::{
-    convert::Infallible,
-    fmt::{self, Debug},
-    ops::RangeBounds,
-};
+use core::{convert::Infallible, fmt::Debug};
 
 use alloc::{sync::Arc, vec::Vec};
-use bitcoin::{Block, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
+use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
 
 use crate::{
-    spk_txout::SpkTxOutIndex,
     tx_graph::{self, TxGraph},
-    Anchor, BlockId, ChainOracle, Indexer, Merge, TxPosInBlock,
+    Anchor, BlockId, CanonicalView, CanonicalizationParams, ChainOracle, Indexer, Merge,
+    TxPosInBlock,
 };
 
 /// A [`TxGraph<A>`] paired with an indexer `I`, enforcing that every insertion into the graph is
@@ -431,53 +427,26 @@ impl<A, X> IndexedTxGraph<A, X>
 where
     A: Anchor,
 {
-    /// List txids that are expected to exist under the given spks.
-    ///
-    /// This is used to fill
-    /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids).
-    ///
-    ///
-    /// The spk index range can be contrained with `range`.
-    ///
-    /// # Error
-    ///
-    /// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
-    /// returned item.
-    ///
-    /// If the [`ChainOracle`] is infallible,
-    /// [`list_expected_spk_txids`](Self::list_expected_spk_txids) can be used instead.
-    pub fn try_list_expected_spk_txids<'a, C, I>(
+    /// Returns a [`CanonicalView`].
+    pub fn try_canonical_view<'a, C: ChainOracle>(
         &'a self,
         chain: &'a C,
         chain_tip: BlockId,
-        spk_index_range: impl RangeBounds<I> + 'a,
-    ) -> impl Iterator<Item = Result<(ScriptBuf, Txid), C::Error>> + 'a
-    where
-        C: ChainOracle,
-        X: AsRef<SpkTxOutIndex<I>> + 'a,
-        I: fmt::Debug + Clone + Ord + 'a,
-    {
-        self.graph
-            .try_list_expected_spk_txids(chain, chain_tip, &self.index, spk_index_range)
+        params: CanonicalizationParams,
+    ) -> Result<CanonicalView<A>, C::Error> {
+        self.graph.try_canonical_view(chain, chain_tip, params)
     }
 
-    /// List txids that are expected to exist under the given spks.
+    /// Returns a [`CanonicalView`].
     ///
-    /// This is the infallible version of
-    /// [`try_list_expected_spk_txids`](Self::try_list_expected_spk_txids).
-    pub fn list_expected_spk_txids<'a, C, I>(
+    /// This is the infallible version of [`try_canonical_view`](Self::try_canonical_view).
+    pub fn canonical_view<'a, C: ChainOracle<Error = Infallible>>(
         &'a self,
         chain: &'a C,
         chain_tip: BlockId,
-        spk_index_range: impl RangeBounds<I> + 'a,
-    ) -> impl Iterator<Item = (ScriptBuf, Txid)> + 'a
-    where
-        C: ChainOracle<Error = Infallible>,
-        X: AsRef<SpkTxOutIndex<I>> + 'a,
-        I: fmt::Debug + Clone + Ord + 'a,
-    {
-        self.try_list_expected_spk_txids(chain, chain_tip, spk_index_range)
-            .map(|r| r.expect("infallible"))
+        params: CanonicalizationParams,
+    ) -> CanonicalView<A> {
+        self.graph.canonical_view(chain, chain_tip, params)
     }
 }
 
index 0cb5b48d39d9a42143b5f05630850e1fa0d94ef5..be9170b1a5b310161ec41aa5e392a0cd768c743a 100644 (file)
@@ -46,6 +46,8 @@ mod chain_oracle;
 pub use chain_oracle::*;
 mod canonical_iter;
 pub use canonical_iter::*;
+mod canonical_view;
+pub use canonical_view::*;
 
 #[doc(hidden)]
 pub mod example_utils;
index 6b9a4cf96f448768bcaa8dfd0f9059206ed70b5e..3416f0df97d8372844f43abd1f3e47943245f51f 100644 (file)
 //! [`insert_txout`]: TxGraph::insert_txout
 
 use crate::collections::*;
-use crate::spk_txout::SpkTxOutIndex;
 use crate::BlockId;
 use crate::CanonicalIter;
-use crate::CanonicalReason;
+use crate::CanonicalView;
 use crate::CanonicalizationParams;
-use crate::ObservedIn;
-use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge};
+use crate::{Anchor, ChainOracle, ChainPosition, Merge};
 use alloc::collections::vec_deque::VecDeque;
 use alloc::sync::Arc;
 use alloc::vec::Vec;
 use bdk_core::ConfirmationBlockTime;
 pub use bdk_core::TxUpdate;
-use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
+use bitcoin::{Amount, OutPoint, SignedAmount, Transaction, TxOut, Txid};
 use core::fmt::{self, Formatter};
-use core::ops::RangeBounds;
 use core::{
     convert::Infallible,
     ops::{Deref, RangeInclusive},
@@ -980,183 +977,6 @@ impl<A: Anchor> TxGraph<A> {
 }
 
 impl<A: Anchor> TxGraph<A> {
-    /// List graph transactions that are in `chain` with `chain_tip`.
-    ///
-    /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
-    /// observed in-chain, and the [`TxNode`].
-    ///
-    /// # Error
-    ///
-    /// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
-    /// returned item.
-    ///
-    /// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
-    ///
-    /// [`list_canonical_txs`]: Self::list_canonical_txs
-    pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
-        &'a self,
-        chain: &'a C,
-        chain_tip: BlockId,
-        params: CanonicalizationParams,
-    ) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
-        fn find_direct_anchor<A: Anchor, C: ChainOracle>(
-            tx_node: &TxNode<'_, Arc<Transaction>, A>,
-            chain: &C,
-            chain_tip: BlockId,
-        ) -> Result<Option<A>, C::Error> {
-            tx_node
-                .anchors
-                .iter()
-                .find_map(|a| -> Option<Result<A, C::Error>> {
-                    match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
-                        Ok(Some(true)) => Some(Ok(a.clone())),
-                        Ok(Some(false)) | Ok(None) => None,
-                        Err(err) => Some(Err(err)),
-                    }
-                })
-                .transpose()
-        }
-        self.canonical_iter(chain, chain_tip, params)
-            .flat_map(move |res| {
-                res.map(|(txid, _, canonical_reason)| {
-                    let tx_node = self.get_tx_node(txid).expect("must contain tx");
-                    let chain_position = match canonical_reason {
-                        CanonicalReason::Assumed { descendant } => match descendant {
-                            Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
-                                Some(anchor) => ChainPosition::Confirmed {
-                                    anchor,
-                                    transitively: None,
-                                },
-                                None => ChainPosition::Unconfirmed {
-                                    first_seen: tx_node.first_seen,
-                                    last_seen: tx_node.last_seen,
-                                },
-                            },
-                            None => ChainPosition::Unconfirmed {
-                                first_seen: tx_node.first_seen,
-                                last_seen: tx_node.last_seen,
-                            },
-                        },
-                        CanonicalReason::Anchor { anchor, descendant } => match descendant {
-                            Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
-                                Some(anchor) => ChainPosition::Confirmed {
-                                    anchor,
-                                    transitively: None,
-                                },
-                                None => ChainPosition::Confirmed {
-                                    anchor,
-                                    transitively: descendant,
-                                },
-                            },
-                            None => ChainPosition::Confirmed {
-                                anchor,
-                                transitively: None,
-                            },
-                        },
-                        CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
-                            ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
-                                first_seen: tx_node.first_seen,
-                                last_seen: Some(last_seen),
-                            },
-                            ObservedIn::Block(_) => ChainPosition::Unconfirmed {
-                                first_seen: tx_node.first_seen,
-                                last_seen: None,
-                            },
-                        },
-                    };
-                    Ok(CanonicalTx {
-                        chain_position,
-                        tx_node,
-                    })
-                })
-            })
-    }
-
-    /// List graph transactions that are in `chain` with `chain_tip`.
-    ///
-    /// This is the infallible version of [`try_list_canonical_txs`].
-    ///
-    /// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
-    pub fn list_canonical_txs<'a, C: ChainOracle<Error = Infallible> + 'a>(
-        &'a self,
-        chain: &'a C,
-        chain_tip: BlockId,
-        params: CanonicalizationParams,
-    ) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
-        self.try_list_canonical_txs(chain, chain_tip, params)
-            .map(|res| res.expect("infallible"))
-    }
-
-    /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
-    /// `chain_tip`.
-    ///
-    /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
-    /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
-    /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
-    ///
-    /// Floating outputs (i.e., outputs for which we don't have the full transaction in the graph)
-    /// are ignored.
-    ///
-    /// # Error
-    ///
-    /// An [`Iterator::Item`] can be an [`Err`] if the [`ChainOracle`] implementation (`chain`)
-    /// fails.
-    ///
-    /// If the [`ChainOracle`] implementation is infallible, [`filter_chain_txouts`] can be used
-    /// instead.
-    ///
-    /// [`filter_chain_txouts`]: Self::filter_chain_txouts
-    pub fn try_filter_chain_txouts<'a, C: ChainOracle + 'a, OI: Clone + 'a>(
-        &'a self,
-        chain: &'a C,
-        chain_tip: BlockId,
-        params: CanonicalizationParams,
-        outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
-    ) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
-        let mut canon_txs = HashMap::<Txid, CanonicalTx<Arc<Transaction>, A>>::new();
-        let mut canon_spends = HashMap::<OutPoint, Txid>::new();
-        for r in self.try_list_canonical_txs(chain, chain_tip, params) {
-            let canonical_tx = r?;
-            let txid = canonical_tx.tx_node.txid;
-
-            if !canonical_tx.tx_node.tx.is_coinbase() {
-                for txin in &canonical_tx.tx_node.tx.input {
-                    let _res = canon_spends.insert(txin.previous_output, txid);
-                    assert!(_res.is_none(), "tried to replace {_res:?} with {txid:?}",);
-                }
-            }
-            canon_txs.insert(txid, canonical_tx);
-        }
-        Ok(outpoints.into_iter().filter_map(move |(spk_i, outpoint)| {
-            let canon_tx = canon_txs.get(&outpoint.txid)?;
-            let txout = canon_tx
-                .tx_node
-                .tx
-                .output
-                .get(outpoint.vout as usize)
-                .cloned()?;
-            let chain_position = canon_tx.chain_position.clone();
-            let spent_by = canon_spends.get(&outpoint).map(|spend_txid| {
-                let spend_tx = canon_txs
-                    .get(spend_txid)
-                    .cloned()
-                    .expect("must be canonical");
-                (spend_tx.chain_position, *spend_txid)
-            });
-            let is_on_coinbase = canon_tx.tx_node.is_coinbase();
-            Some((
-                spk_i,
-                FullTxOut {
-                    outpoint,
-                    txout,
-                    chain_position,
-                    spent_by,
-                    is_on_coinbase,
-                },
-            ))
-        }))
-    }
-
     /// List txids by descending anchor height order.
     ///
     /// If multiple anchors exist for a txid, the highest anchor height will be used. Transactions
@@ -1192,262 +1012,24 @@ impl<A: Anchor> TxGraph<A> {
         CanonicalIter::new(self, chain, chain_tip, params)
     }
 
-    /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
-    /// `chain_tip`.
-    ///
-    /// This is the infallible version of [`try_filter_chain_txouts`].
-    ///
-    /// [`try_filter_chain_txouts`]: Self::try_filter_chain_txouts
-    pub fn filter_chain_txouts<'a, C: ChainOracle<Error = Infallible> + 'a, OI: Clone + 'a>(
-        &'a self,
-        chain: &'a C,
-        chain_tip: BlockId,
-        params: CanonicalizationParams,
-        outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
-    ) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
-        self.try_filter_chain_txouts(chain, chain_tip, params, outpoints)
-            .expect("oracle is infallible")
-    }
-
-    /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints` that are in
-    /// `chain` with `chain_tip`.
-    ///
-    /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
-    /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
-    /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
-    ///
-    /// Floating outputs are ignored.
-    ///
-    /// # Error
-    ///
-    /// An [`Iterator::Item`] can be an [`Err`] if the [`ChainOracle`] implementation (`chain`)
-    /// fails.
-    ///
-    /// If the [`ChainOracle`] implementation is infallible, [`filter_chain_unspents`] can be used
-    /// instead.
-    ///
-    /// [`filter_chain_unspents`]: Self::filter_chain_unspents
-    pub fn try_filter_chain_unspents<'a, C: ChainOracle + 'a, OI: Clone + 'a>(
+    /// Returns a [`CanonicalView`].
+    pub fn try_canonical_view<'a, C: ChainOracle>(
         &'a self,
         chain: &'a C,
         chain_tip: BlockId,
         params: CanonicalizationParams,
-        outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
-    ) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
-        Ok(self
-            .try_filter_chain_txouts(chain, chain_tip, params, outpoints)?
-            .filter(|(_, full_txo)| full_txo.spent_by.is_none()))
+    ) -> Result<CanonicalView<A>, C::Error> {
+        CanonicalView::new(self, chain, chain_tip, params)
     }
 
-    /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints` that are in
-    /// `chain` with `chain_tip`.
-    ///
-    /// This is the infallible version of [`try_filter_chain_unspents`].
-    ///
-    /// [`try_filter_chain_unspents`]: Self::try_filter_chain_unspents
-    pub fn filter_chain_unspents<'a, C: ChainOracle<Error = Infallible> + 'a, OI: Clone + 'a>(
+    /// Returns a [`CanonicalView`].
+    pub fn canonical_view<'a, C: ChainOracle<Error = Infallible>>(
         &'a self,
         chain: &'a C,
         chain_tip: BlockId,
         params: CanonicalizationParams,
-        txouts: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
-    ) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
-        self.try_filter_chain_unspents(chain, chain_tip, params, txouts)
-            .expect("oracle is infallible")
-    }
-
-    /// Get the total balance of `outpoints` that are in `chain` of `chain_tip`.
-    ///
-    /// The output of `trust_predicate` should return `true` for scripts that we trust.
-    ///
-    /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
-    /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or
-    /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
-    ///
-    /// If the provided [`ChainOracle`] implementation (`chain`) is infallible, [`balance`] can be
-    /// used instead.
-    ///
-    /// [`balance`]: Self::balance
-    pub fn try_balance<C: ChainOracle, OI: Clone>(
-        &self,
-        chain: &C,
-        chain_tip: BlockId,
-        params: CanonicalizationParams,
-        outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
-        mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
-    ) -> Result<Balance, C::Error> {
-        let mut immature = Amount::ZERO;
-        let mut trusted_pending = Amount::ZERO;
-        let mut untrusted_pending = Amount::ZERO;
-        let mut confirmed = Amount::ZERO;
-
-        for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, params, outpoints)? {
-            match &txout.chain_position {
-                ChainPosition::Confirmed { .. } => {
-                    if txout.is_confirmed_and_spendable(chain_tip.height) {
-                        confirmed += txout.txout.value;
-                    } else if !txout.is_mature(chain_tip.height) {
-                        immature += txout.txout.value;
-                    }
-                }
-                ChainPosition::Unconfirmed { .. } => {
-                    if trust_predicate(&spk_i, txout.txout.script_pubkey) {
-                        trusted_pending += txout.txout.value;
-                    } else {
-                        untrusted_pending += txout.txout.value;
-                    }
-                }
-            }
-        }
-
-        Ok(Balance {
-            immature,
-            trusted_pending,
-            untrusted_pending,
-            confirmed,
-        })
-    }
-
-    /// Get the total balance of `outpoints` that are in `chain` of `chain_tip`.
-    ///
-    /// This is the infallible version of [`try_balance`].
-    ///
-    /// ### Minimum confirmations
-    ///
-    /// To filter for transactions with at least `N` confirmations, pass a `chain_tip` that is
-    /// `N - 1` blocks below the actual tip. This ensures that only transactions with at least `N`
-    /// confirmations are counted as confirmed in the returned [`Balance`].
-    ///
-    /// ```
-    /// # use bdk_chain::tx_graph::TxGraph;
-    /// # use bdk_chain::{local_chain::LocalChain, CanonicalizationParams, ConfirmationBlockTime};
-    /// # use bdk_testenv::{hash, utils::new_tx};
-    /// # use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
-    ///
-    /// # let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap();
-    /// # let chain =
-    /// #     LocalChain::<BlockHash>::from_blocks((0..=15).map(|i| (i as u32, hash!("h"))).collect()).unwrap();
-    /// # let mut graph: TxGraph = TxGraph::default();
-    /// # let coinbase_tx = Transaction {
-    /// #     input: vec![TxIn {
-    /// #         previous_output: OutPoint::null(),
-    /// #         ..Default::default()
-    /// #     }],
-    /// #     output: vec![TxOut {
-    /// #         value: Amount::from_sat(70000),
-    /// #         script_pubkey: spk.clone(),
-    /// #     }],
-    /// #     ..new_tx(0)
-    /// # };
-    /// # let tx = Transaction {
-    /// #     input: vec![TxIn {
-    /// #         previous_output: OutPoint::new(coinbase_tx.compute_txid(), 0),
-    /// #         ..Default::default()
-    /// #     }],
-    /// #     output: vec![TxOut {
-    /// #         value: Amount::from_sat(42_000),
-    /// #         script_pubkey: spk.clone(),
-    /// #     }],
-    /// #     ..new_tx(1)
-    /// # };
-    /// # let txid = tx.compute_txid();
-    /// # let _ = graph.insert_tx(tx.clone());
-    /// # let _ = graph.insert_anchor(
-    /// #     txid,
-    /// #     ConfirmationBlockTime {
-    /// #         block_id: chain.get(10).unwrap().block_id(),
-    /// #         confirmation_time: 123456,
-    /// #     },
-    /// # );
-    ///
-    /// let minimum_confirmations = 6;
-    /// let target_tip = chain
-    ///     .tip()
-    ///     .floor_below(minimum_confirmations - 1)
-    ///     .expect("checkpoint from local chain must have genesis");
-    /// let balance = graph.balance(
-    ///     &chain,
-    ///     target_tip.block_id(),
-    ///     CanonicalizationParams::default(),
-    ///     std::iter::once(((), OutPoint::new(txid, 0))),
-    ///     |_: &(), _| true,
-    /// );
-    /// assert_eq!(balance.confirmed, Amount::from_sat(42_000));
-    /// ```
-    ///
-    /// [`try_balance`]: Self::try_balance
-    pub fn balance<C: ChainOracle<Error = Infallible>, OI: Clone>(
-        &self,
-        chain: &C,
-        chain_tip: BlockId,
-        params: CanonicalizationParams,
-        outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
-        trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
-    ) -> Balance {
-        self.try_balance(chain, chain_tip, params, outpoints, trust_predicate)
-            .expect("oracle is infallible")
-    }
-
-    /// List txids that are expected to exist under the given spks.
-    ///
-    /// This is used to fill
-    /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids).
-    ///
-    ///
-    /// The spk index range can be constrained with `range`.
-    ///
-    /// # Error
-    ///
-    /// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
-    /// returned item.
-    ///
-    /// If the [`ChainOracle`] is infallible,
-    /// [`list_expected_spk_txids`](Self::list_expected_spk_txids) can be used instead.
-    pub fn try_list_expected_spk_txids<'a, C, I>(
-        &'a self,
-        chain: &'a C,
-        chain_tip: BlockId,
-        indexer: &'a impl AsRef<SpkTxOutIndex<I>>,
-        spk_index_range: impl RangeBounds<I> + 'a,
-    ) -> impl Iterator<Item = Result<(ScriptBuf, Txid), C::Error>> + 'a
-    where
-        C: ChainOracle,
-        I: fmt::Debug + Clone + Ord + 'a,
-    {
-        let indexer = indexer.as_ref();
-        self.try_list_canonical_txs(chain, chain_tip, CanonicalizationParams::default())
-            .flat_map(move |res| -> Vec<Result<(ScriptBuf, Txid), C::Error>> {
-                let range = &spk_index_range;
-                let c_tx = match res {
-                    Ok(c_tx) => c_tx,
-                    Err(err) => return vec![Err(err)],
-                };
-                let relevant_spks = indexer.relevant_spks_of_tx(&c_tx.tx_node);
-                relevant_spks
-                    .into_iter()
-                    .filter(|(i, _)| range.contains(i))
-                    .map(|(_, spk)| Ok((spk, c_tx.tx_node.txid)))
-                    .collect()
-            })
-    }
-
-    /// List txids that are expected to exist under the given spks.
-    ///
-    /// This is the infallible version of
-    /// [`try_list_expected_spk_txids`](Self::try_list_expected_spk_txids).
-    pub fn list_expected_spk_txids<'a, C, I>(
-        &'a self,
-        chain: &'a C,
-        chain_tip: BlockId,
-        indexer: &'a impl AsRef<SpkTxOutIndex<I>>,
-        spk_index_range: impl RangeBounds<I> + 'a,
-    ) -> impl Iterator<Item = (ScriptBuf, Txid)> + 'a
-    where
-        C: ChainOracle<Error = Infallible>,
-        I: fmt::Debug + Clone + Ord + 'a,
-    {
-        self.try_list_expected_spk_txids(chain, chain_tip, indexer, spk_index_range)
-            .map(|r| r.expect("infallible"))
+    ) -> CanonicalView<A> {
+        CanonicalView::new(self, chain, chain_tip, params).expect("infallible")
     }
 
     /// Construct a `TxGraph` from a `changeset`.
index db91a34b3c3032ccd82f455742bd52741040c62d..13a3ab0ba831333e9eba27275c86a16a5476419f 100644 (file)
@@ -460,32 +460,21 @@ fn test_list_owned_txouts() {
                 .map(|cp| cp.block_id())
                 .unwrap_or_else(|| panic!("block must exist at {height}"));
             let txouts = graph
-                .graph()
-                .filter_chain_txouts(
-                    &local_chain,
-                    chain_tip,
-                    CanonicalizationParams::default(),
-                    graph.index.outpoints().iter().cloned(),
-                )
+                .canonical_view(&local_chain, chain_tip, CanonicalizationParams::default())
+                .filter_outpoints(graph.index.outpoints().iter().cloned())
                 .collect::<Vec<_>>();
 
             let utxos = graph
-                .graph()
-                .filter_chain_unspents(
-                    &local_chain,
-                    chain_tip,
-                    CanonicalizationParams::default(),
-                    graph.index.outpoints().iter().cloned(),
-                )
+                .canonical_view(&local_chain, chain_tip, CanonicalizationParams::default())
+                .filter_unspent_outpoints(graph.index.outpoints().iter().cloned())
                 .collect::<Vec<_>>();
 
-            let balance = graph.graph().balance(
-                &local_chain,
-                chain_tip,
-                CanonicalizationParams::default(),
-                graph.index.outpoints().iter().cloned(),
-                |_, spk: ScriptBuf| trusted_spks.contains(&spk),
-            );
+            let balance = graph
+                .canonical_view(&local_chain, chain_tip, CanonicalizationParams::default())
+                .balance(
+                    graph.index.outpoints().iter().cloned(),
+                    |_, spk: ScriptBuf| trusted_spks.contains(&spk),
+                );
 
             let confirmed_txouts_txid = txouts
                 .iter()
@@ -789,15 +778,15 @@ fn test_get_chain_position() {
 
         // check chain position
         let chain_pos = graph
-            .graph()
-            .list_canonical_txs(
+            .canonical_view(
                 chain,
                 chain.tip().block_id(),
                 CanonicalizationParams::default(),
             )
+            .txs()
             .find_map(|canon_tx| {
-                if canon_tx.tx_node.txid == txid {
-                    Some(canon_tx.chain_position)
+                if canon_tx.txid == txid {
+                    Some(canon_tx.pos)
                 } else {
                     None
                 }
index 685b62c6e79c6d3c7b1da6fa84b252139a744d37..b2a3596085e904cd70ecc6d23c72970cc622099b 100644 (file)
@@ -1015,12 +1015,8 @@ fn test_chain_spends() {
     let build_canonical_spends =
         |chain: &LocalChain, tx_graph: &TxGraph<ConfirmationBlockTime>| -> HashMap<OutPoint, _> {
             tx_graph
-                .filter_chain_txouts(
-                    chain,
-                    tip.block_id(),
-                    CanonicalizationParams::default(),
-                    tx_graph.all_txouts().map(|(op, _)| ((), op)),
-                )
+                .canonical_view(chain, tip.block_id(), CanonicalizationParams::default())
+                .filter_outpoints(tx_graph.all_txouts().map(|(op, _)| ((), op)))
                 .filter_map(|(_, full_txo)| Some((full_txo.outpoint, full_txo.spent_by?)))
                 .collect()
         };
@@ -1028,8 +1024,9 @@ fn test_chain_spends() {
                                      tx_graph: &TxGraph<ConfirmationBlockTime>|
      -> HashMap<Txid, ChainPosition<ConfirmationBlockTime>> {
         tx_graph
-            .list_canonical_txs(chain, tip.block_id(), CanonicalizationParams::default())
-            .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position))
+            .canonical_view(chain, tip.block_id(), CanonicalizationParams::default())
+            .txs()
+            .map(|canon_tx| (canon_tx.txid, canon_tx.pos))
             .collect()
     };
 
@@ -1201,36 +1198,36 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch
         .collect();
     let chain = LocalChain::from_blocks(blocks).unwrap();
     let canonical_txs: Vec<_> = graph
-        .list_canonical_txs(
+        .canonical_view(
             &chain,
             chain.tip().block_id(),
             CanonicalizationParams::default(),
         )
+        .txs()
         .collect();
     assert!(canonical_txs.is_empty());
 
     // tx0 with seen_at should be returned by canonical txs
     let _ = graph.insert_seen_at(txids[0], 2);
-    let mut canonical_txs = graph.list_canonical_txs(
+    let canonical_view = graph.canonical_view(
         &chain,
         chain.tip().block_id(),
         CanonicalizationParams::default(),
     );
-    assert_eq!(
-        canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(),
-        txids[0]
-    );
+    let mut canonical_txs = canonical_view.txs();
+    assert_eq!(canonical_txs.next().map(|tx| tx.txid).unwrap(), txids[0]);
     drop(canonical_txs);
 
     // tx1 with anchor is also canonical
     let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
     let canonical_txids: Vec<_> = graph
-        .list_canonical_txs(
+        .canonical_view(
             &chain,
             chain.tip().block_id(),
             CanonicalizationParams::default(),
         )
-        .map(|tx| tx.tx_node.txid)
+        .txs()
+        .map(|tx| tx.txid)
         .collect();
     assert!(canonical_txids.contains(&txids[1]));
     assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none());
index cbd5d54177f79e4d1062b9997ac3ee7b46966cc3..1c413e4e94c38868215098103b501eb2cbe915ac 100644 (file)
@@ -972,8 +972,9 @@ fn test_tx_conflict_handling() {
 
         let txs = env
             .tx_graph
-            .list_canonical_txs(&local_chain, chain_tip, env.canonicalization_params.clone())
-            .map(|tx| tx.tx_node.txid)
+            .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone())
+            .txs()
+            .map(|tx| tx.txid)
             .collect::<BTreeSet<_>>();
         let exp_txs = scenario
             .exp_chain_txs
@@ -988,12 +989,8 @@ fn test_tx_conflict_handling() {
 
         let txouts = env
             .tx_graph
-            .filter_chain_txouts(
-                &local_chain,
-                chain_tip,
-                env.canonicalization_params.clone(),
-                env.indexer.outpoints().iter().cloned(),
-            )
+            .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone())
+            .filter_outpoints(env.indexer.outpoints().iter().cloned())
             .map(|(_, full_txout)| full_txout.outpoint)
             .collect::<BTreeSet<_>>();
         let exp_txouts = scenario
@@ -1012,12 +1009,8 @@ fn test_tx_conflict_handling() {
 
         let utxos = env
             .tx_graph
-            .filter_chain_unspents(
-                &local_chain,
-                chain_tip,
-                env.canonicalization_params.clone(),
-                env.indexer.outpoints().iter().cloned(),
-            )
+            .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone())
+            .filter_unspent_outpoints(env.indexer.outpoints().iter().cloned())
             .map(|(_, full_txout)| full_txout.outpoint)
             .collect::<BTreeSet<_>>();
         let exp_utxos = scenario
@@ -1034,13 +1027,13 @@ fn test_tx_conflict_handling() {
             scenario.name
         );
 
-        let balance = env.tx_graph.balance(
-            &local_chain,
-            chain_tip,
-            env.canonicalization_params.clone(),
-            env.indexer.outpoints().iter().cloned(),
-            |_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(),
-        );
+        let balance = env
+            .tx_graph
+            .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone())
+            .balance(
+                env.indexer.outpoints().iter().cloned(),
+                |_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(),
+            );
         assert_eq!(
             balance, scenario.exp_balance,
             "\n[{}] 'balance' failed",
index 7fcf5d801176a2cd845961c20284ccfb25665d2d..8c7580f972603456ae640c71fecfe8563f7bda59 100644 (file)
@@ -40,13 +40,9 @@ fn get_balance(
 ) -> anyhow::Result<Balance> {
     let chain_tip = recv_chain.tip().block_id();
     let outpoints = recv_graph.index.outpoints().clone();
-    let balance = recv_graph.graph().balance(
-        recv_chain,
-        chain_tip,
-        CanonicalizationParams::default(),
-        outpoints,
-        |_, _| true,
-    );
+    let balance = recv_graph
+        .canonical_view(recv_chain, chain_tip, CanonicalizationParams::default())
+        .balance(outpoints, |_, _| true);
     Ok(balance)
 }
 
@@ -150,7 +146,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let sync_request = SyncRequest::builder()
         .chain_tip(chain.tip())
         .spks_with_indexes(graph.index.all_spks().clone())
-        .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..));
+        .expected_spk_txids(
+            graph
+                .canonical_view(&chain, chain.tip().block_id(), Default::default())
+                .list_expected_spk_txids(&graph.index, ..),
+        );
     let sync_response = client.sync(sync_request, BATCH_SIZE, true)?;
     assert!(
         sync_response
@@ -175,7 +175,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let sync_request = SyncRequest::builder()
         .chain_tip(chain.tip())
         .spks_with_indexes(graph.index.all_spks().clone())
-        .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..));
+        .expected_spk_txids(
+            graph
+                .canonical_view(&chain, chain.tip().block_id(), Default::default())
+                .list_expected_spk_txids(&graph.index, ..),
+        );
     let sync_response = client.sync(sync_request, BATCH_SIZE, true)?;
     assert!(
         sync_response
index c90c3311202075286e871d2d7779ef27781360f7..3c628c20df58955116789e46e4eed36aca372b8d 100644 (file)
@@ -87,7 +87,11 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let sync_request = SyncRequest::builder()
         .chain_tip(chain.tip())
         .spks_with_indexes(graph.index.all_spks().clone())
-        .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..));
+        .expected_spk_txids(
+            graph
+                .canonical_view(&chain, chain.tip().block_id(), Default::default())
+                .list_expected_spk_txids(&graph.index, ..),
+        );
     let sync_response = client.sync(sync_request, 1).await?;
     assert!(
         sync_response
@@ -112,7 +116,11 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let sync_request = SyncRequest::builder()
         .chain_tip(chain.tip())
         .spks_with_indexes(graph.index.all_spks().clone())
-        .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..));
+        .expected_spk_txids(
+            graph
+                .canonical_view(&chain, chain.tip().block_id(), Default::default())
+                .list_expected_spk_txids(&graph.index, ..),
+        );
     let sync_response = client.sync(sync_request, 1).await?;
     assert!(
         sync_response
index a09b3ccce718dc275b0648340abcd4403b9bb511..4d5683e8b2a1f1fe7e21e89d6e34a9088dba5a45 100644 (file)
@@ -87,7 +87,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let sync_request = SyncRequest::builder()
         .chain_tip(chain.tip())
         .spks_with_indexes(graph.index.all_spks().clone())
-        .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..));
+        .expected_spk_txids(
+            graph
+                .canonical_view(&chain, chain.tip().block_id(), Default::default())
+                .list_expected_spk_txids(&graph.index, ..),
+        );
     let sync_response = client.sync(sync_request, 1)?;
     assert!(
         sync_response
@@ -112,7 +116,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
     let sync_request = SyncRequest::builder()
         .chain_tip(chain.tip())
         .spks_with_indexes(graph.index.all_spks().clone())
-        .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..));
+        .expected_spk_txids(
+            graph
+                .canonical_view(&chain, chain.tip().block_id(), Default::default())
+                .list_expected_spk_txids(&graph.index, ..),
+        );
     let sync_response = client.sync(sync_request, 1)?;
     assert!(
         sync_response
index cb710151a858422e494d4691d8a6e7adece8795b..8c5483bf4d298011dff483b9d82723e46b4f53f3 100644 (file)
@@ -145,13 +145,14 @@ fn main() -> anyhow::Result<()> {
                     chain.tip(),
                     fallback_height,
                     graph
-                        .graph()
-                        .list_canonical_txs(
+                        .canonical_view(
                             &*chain,
                             chain.tip().block_id(),
                             CanonicalizationParams::default(),
                         )
-                        .filter(|tx| tx.chain_position.is_unconfirmed()),
+                        .txs()
+                        .filter(|tx| tx.pos.is_unconfirmed())
+                        .map(|tx| tx.tx),
                 )
             };
             let mut db_stage = ChangeSet::default();
@@ -195,13 +196,15 @@ fn main() -> anyhow::Result<()> {
                     last_print = Instant::now();
                     let synced_to = chain.tip();
                     let balance = {
-                        graph.graph().balance(
-                            &*chain,
-                            synced_to.block_id(),
-                            CanonicalizationParams::default(),
-                            graph.index.outpoints().iter().cloned(),
-                            |(k, _), _| k == &Keychain::Internal,
-                        )
+                        graph
+                            .canonical_view(
+                                &*chain,
+                                synced_to.block_id(),
+                                CanonicalizationParams::default(),
+                            )
+                            .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
+                                k == &Keychain::Internal
+                            })
                     };
                     println!(
                         "[{:>10}s] synced to {} @ {} | total: {}",
@@ -245,13 +248,14 @@ fn main() -> anyhow::Result<()> {
                     chain.tip(),
                     fallback_height,
                     graph
-                        .graph()
-                        .list_canonical_txs(
+                        .canonical_view(
                             &*chain,
                             chain.tip().block_id(),
                             CanonicalizationParams::default(),
                         )
-                        .filter(|tx| tx.chain_position.is_unconfirmed()),
+                        .txs()
+                        .filter(|tx| tx.pos.is_unconfirmed())
+                        .map(|tx| tx.tx),
                 )
             };
 
@@ -350,13 +354,15 @@ fn main() -> anyhow::Result<()> {
                     last_print = Some(Instant::now());
                     let synced_to = chain.tip();
                     let balance = {
-                        graph.graph().balance(
-                            &*chain,
-                            synced_to.block_id(),
-                            CanonicalizationParams::default(),
-                            graph.index.outpoints().iter().cloned(),
-                            |(k, _), _| k == &Keychain::Internal,
-                        )
+                        graph
+                            .canonical_view(
+                                &*chain,
+                                synced_to.block_id(),
+                                CanonicalizationParams::default(),
+                            )
+                            .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
+                                k == &Keychain::Internal
+                            })
                     };
                     println!(
                         "[{:>10}s] synced to {} @ {} / {} | total: {}",
index 96a41802f56d4b15353344b1aa75f569d353ef3a..5f642581a9175d07fbe50ea7b4d618493d5e6225 100644 (file)
@@ -432,13 +432,8 @@ pub fn planned_utxos<O: ChainOracle>(
     let chain_tip = chain.get_chain_tip()?;
     let outpoints = graph.index.outpoints();
     graph
-        .graph()
-        .try_filter_chain_unspents(
-            chain,
-            chain_tip,
-            CanonicalizationParams::default(),
-            outpoints.iter().cloned(),
-        )?
+        .try_canonical_view(chain, chain_tip, CanonicalizationParams::default())?
+        .filter_unspent_outpoints(outpoints.iter().cloned())
         .filter_map(|((k, i), full_txo)| -> Option<Result<PlanUtxo, _>> {
             let desc = graph
                 .index
@@ -529,13 +524,15 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
                 }
             }
 
-            let balance = graph.graph().try_balance(
-                chain,
-                chain.get_chain_tip()?,
-                CanonicalizationParams::default(),
-                graph.index.outpoints().iter().cloned(),
-                |(k, _), _| k == &Keychain::Internal,
-            )?;
+            let balance = graph
+                .try_canonical_view(
+                    chain,
+                    chain.get_chain_tip()?,
+                    CanonicalizationParams::default(),
+                )?
+                .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
+                    k == &Keychain::Internal
+                });
 
             let confirmed_total = balance.confirmed + balance.immature;
             let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
@@ -573,13 +570,8 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
                     unconfirmed,
                 } => {
                     let txouts = graph
-                        .graph()
-                        .try_filter_chain_txouts(
-                            chain,
-                            chain_tip,
-                            CanonicalizationParams::default(),
-                            outpoints.iter().cloned(),
-                        )?
+                        .try_canonical_view(chain, chain_tip, CanonicalizationParams::default())?
+                        .filter_outpoints(outpoints.iter().cloned())
                         .filter(|(_, full_txo)| match (spent, unspent) {
                             (true, false) => full_txo.spent_by.is_some(),
                             (false, true) => full_txo.spent_by.is_none(),
index bc76776a6e87a8842d14102d36a970ba33bc62be..aa89f07e18340fdb61e8309e9e0d3546ac3a5f87 100644 (file)
@@ -213,11 +213,14 @@ fn main() -> anyhow::Result<()> {
                         eprintln!("[ SCANNING {pc:03.0}% ] {item}");
                     });
 
-            request = request.expected_spk_txids(graph.list_expected_spk_txids(
+            let canonical_view = graph.canonical_view(
                 &*chain,
                 chain_tip.block_id(),
-                ..,
-            ));
+                CanonicalizationParams::default(),
+            );
+
+            request = request
+                .expected_spk_txids(canonical_view.list_expected_spk_txids(&graph.index, ..));
             if all_spks {
                 request = request.spks_with_indexes(graph.index.revealed_spks(..));
             }
@@ -227,28 +230,17 @@ fn main() -> anyhow::Result<()> {
             if utxos {
                 let init_outpoints = graph.index.outpoints();
                 request = request.outpoints(
-                    graph
-                        .graph()
-                        .filter_chain_unspents(
-                            &*chain,
-                            chain_tip.block_id(),
-                            CanonicalizationParams::default(),
-                            init_outpoints.iter().cloned(),
-                        )
+                    canonical_view
+                        .filter_unspent_outpoints(init_outpoints.iter().cloned())
                         .map(|(_, utxo)| utxo.outpoint),
                 );
             };
             if unconfirmed {
                 request = request.txids(
-                    graph
-                        .graph()
-                        .list_canonical_txs(
-                            &*chain,
-                            chain_tip.block_id(),
-                            CanonicalizationParams::default(),
-                        )
-                        .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
-                        .map(|canonical_tx| canonical_tx.tx_node.txid),
+                    canonical_view
+                        .txs()
+                        .filter(|canonical_tx| !canonical_tx.pos.is_confirmed())
+                        .map(|canonical_tx| canonical_tx.txid),
                 );
             }
 
index f41d2536e3d262ce9f5ef310c91e5bae088d9513..99f72391cda6c1748a5f157c9d15ff55a1c3faa0 100644 (file)
@@ -225,11 +225,14 @@ fn main() -> anyhow::Result<()> {
             {
                 let graph = graph.lock().unwrap();
                 let chain = chain.lock().unwrap();
-                request = request.expected_spk_txids(graph.list_expected_spk_txids(
+                let canonical_view = graph.canonical_view(
                     &*chain,
                     local_tip.block_id(),
-                    ..,
-                ));
+                    CanonicalizationParams::default(),
+                );
+
+                request = request
+                    .expected_spk_txids(canonical_view.list_expected_spk_txids(&graph.index, ..));
                 if *all_spks {
                     request = request.spks_with_indexes(graph.index.revealed_spks(..));
                 }
@@ -242,14 +245,8 @@ fn main() -> anyhow::Result<()> {
                     // `EsploraExt::update_tx_graph_without_keychain`.
                     let init_outpoints = graph.index.outpoints();
                     request = request.outpoints(
-                        graph
-                            .graph()
-                            .filter_chain_unspents(
-                                &*chain,
-                                local_tip.block_id(),
-                                CanonicalizationParams::default(),
-                                init_outpoints.iter().cloned(),
-                            )
+                        canonical_view
+                            .filter_unspent_outpoints(init_outpoints.iter().cloned())
                             .map(|(_, utxo)| utxo.outpoint),
                     );
                 };
@@ -258,15 +255,10 @@ fn main() -> anyhow::Result<()> {
                     // We provide the unconfirmed txids to
                     // `EsploraExt::update_tx_graph_without_keychain`.
                     request = request.txids(
-                        graph
-                            .graph()
-                            .list_canonical_txs(
-                                &*chain,
-                                local_tip.block_id(),
-                                CanonicalizationParams::default(),
-                            )
-                            .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
-                            .map(|canonical_tx| canonical_tx.tx_node.txid),
+                        canonical_view
+                            .txs()
+                            .filter(|canonical_tx| !canonical_tx.pos.is_confirmed())
+                            .map(|canonical_tx| canonical_tx.txid),
                     );
                 }
             }