From: 志宇 Date: Thu, 11 Sep 2025 03:48:27 +0000 (+0000) Subject: feat(chain)!: Introduce `CanonicalView` and migrate API X-Git-Url: http://internal-gitweb-vhost/script/%22https:/database/struct.CommandStringError.html?a=commitdiff_plain;h=0a55710ea41d4122a7b15481502cfd8a6e24f068;p=bdk feat(chain)!: Introduce `CanonicalView` and migrate API - 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 --- diff --git a/crates/bitcoind_rpc/examples/filter_iter.rs b/crates/bitcoind_rpc/examples/filter_iter.rs index c0e755f9..5a3dc297 100644 --- a/crates/bitcoind_rpc/examples/filter_iter.rs +++ b/crates/bitcoind_rpc/examples/filter_iter.rs @@ -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); } } diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 079551bf..79b44b00 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -310,13 +310,9 @@ fn get_balance( ) -> anyhow::Result { 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::>(); 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::>(); // tx1 should no longer be canonical. assert!(!canonical_txids.contains(&txid_1)); diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index bf11e1eb..3d8d8b29 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -95,31 +95,32 @@ fn setup(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); } diff --git a/crates/chain/benches/indexer.rs b/crates/chain/benches/indexer.rs index c1786a62..df4e3f36 100644 --- a/crates/chain/benches/indexer.rs +++ b/crates/chain/benches/indexer.rs @@ -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 index 00000000..2685344e --- /dev/null +++ b/crates/chain/src/canonical_view.rs @@ -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 { + /// Chain position. + pub pos: ChainPosition, + /// Transaction ID. + pub txid: Txid, + /// The actual transaction. + pub tx: Arc, +} + +/// A view of canonical transactions. +#[derive(Debug)] +pub struct CanonicalView { + order: Vec, + txs: HashMap, ChainPosition)>, + spends: HashMap, + tip: BlockId, +} + +impl CanonicalView { + /// Create a canonical view. + pub fn new<'g, C>( + tx_graph: &'g TxGraph, + chain: &'g C, + chain_tip: BlockId, + params: CanonicalizationParams, + ) -> Result + where + C: ChainOracle, + { + fn find_direct_anchor<'g, A: Anchor, C: ChainOracle>( + tx_node: &TxNode<'g, Arc, A>, + chain: &C, + chain_tip: BlockId, + ) -> Result, C::Error> { + tx_node + .anchors + .iter() + .find_map(|a| -> Option> { + 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> { + self.txs + .get(&txid) + .cloned() + .map(|(tx, pos)| CanonicalViewTx { pos, txid, tx }) + } + + /// Get a single canonical txout. + pub fn txout(&self, op: OutPoint) -> Option> { + 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> + 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 + 'v, + ) -> impl Iterator)> + '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 + 'v, + ) -> impl Iterator)> + '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 + '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>, + spk_index_range: impl RangeBounds + 'v, + ) -> impl Iterator + '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() + }) + } +} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index f0c1d121..9adf7ed9 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -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`] paired with an indexer `I`, enforcing that every insertion into the graph is @@ -431,53 +427,26 @@ impl IndexedTxGraph 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 + 'a, - ) -> impl Iterator> + 'a - where - C: ChainOracle, - X: AsRef> + '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, 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>( &'a self, chain: &'a C, chain_tip: BlockId, - spk_index_range: impl RangeBounds + 'a, - ) -> impl Iterator + 'a - where - C: ChainOracle, - X: AsRef> + '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 { + self.graph.canonical_view(chain, chain_tip, params) } } diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 0cb5b48d..be9170b1 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -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; diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 6b9a4cf9..3416f0df 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -120,21 +120,18 @@ //! [`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 TxGraph { } impl TxGraph { - /// 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, A>, C::Error>> { - fn find_direct_anchor( - tx_node: &TxNode<'_, Arc, A>, - chain: &C, - chain_tip: BlockId, - ) -> Result, C::Error> { - tx_node - .anchors - .iter() - .find_map(|a| -> Option> { - 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 + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> impl Iterator, 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 + 'a, - ) -> Result)> + 'a, C::Error> { - let mut canon_txs = HashMap::, A>>::new(); - let mut canon_spends = HashMap::::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 TxGraph { 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 + 'a, OI: Clone + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - params: CanonicalizationParams, - outpoints: impl IntoIterator + 'a, - ) -> impl Iterator)> + '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 + 'a, - ) -> Result)> + 'a, C::Error> { - Ok(self - .try_filter_chain_txouts(chain, chain_tip, params, outpoints)? - .filter(|(_, full_txo)| full_txo.spent_by.is_none())) + ) -> Result, 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 + 'a, OI: Clone + 'a>( + /// Returns a [`CanonicalView`]. + pub fn canonical_view<'a, C: ChainOracle>( &'a self, chain: &'a C, chain_tip: BlockId, params: CanonicalizationParams, - txouts: impl IntoIterator + 'a, - ) -> impl Iterator)> + '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( - &self, - chain: &C, - chain_tip: BlockId, - params: CanonicalizationParams, - outpoints: impl IntoIterator, - mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool, - ) -> Result { - 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::::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, OI: Clone>( - &self, - chain: &C, - chain_tip: BlockId, - params: CanonicalizationParams, - outpoints: impl IntoIterator, - 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>, - spk_index_range: impl RangeBounds + 'a, - ) -> impl Iterator> + '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> { - 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>, - spk_index_range: impl RangeBounds + 'a, - ) -> impl Iterator + 'a - where - C: ChainOracle, - 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 { + CanonicalView::new(self, chain, chain_tip, params).expect("infallible") } /// Construct a `TxGraph` from a `changeset`. diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index db91a34b..13a3ab0b 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -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::>(); 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::>(); - 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 } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 685b62c6..b2a35960 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1015,12 +1015,8 @@ fn test_chain_spends() { let build_canonical_spends = |chain: &LocalChain, tx_graph: &TxGraph| -> HashMap { 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| -> HashMap> { 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()); diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index cbd5d541..1c413e4e 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -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::>(); 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::>(); 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::>(); 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", diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 7fcf5d80..8c7580f9 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -40,13 +40,9 @@ fn get_balance( ) -> anyhow::Result { 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 diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index c90c3311..3c628c20 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -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 diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index a09b3ccc..4d5683e8 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -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 diff --git a/examples/example_bitcoind_rpc_polling/src/main.rs b/examples/example_bitcoind_rpc_polling/src/main.rs index cb710151..8c5483bf 100644 --- a/examples/example_bitcoind_rpc_polling/src/main.rs +++ b/examples/example_bitcoind_rpc_polling/src/main.rs @@ -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: {}", diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs index 96a41802..5f642581 100644 --- a/examples/example_cli/src/lib.rs +++ b/examples/example_cli/src/lib.rs @@ -432,13 +432,8 @@ pub fn planned_utxos( 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> { let desc = graph .index @@ -529,13 +524,15 @@ pub fn handle_commands( } } - 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( 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(), diff --git a/examples/example_electrum/src/main.rs b/examples/example_electrum/src/main.rs index bc76776a..aa89f07e 100644 --- a/examples/example_electrum/src/main.rs +++ b/examples/example_electrum/src/main.rs @@ -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), ); } diff --git a/examples/example_esplora/src/main.rs b/examples/example_esplora/src/main.rs index f41d2536..99f72391 100644 --- a/examples/example_esplora/src/main.rs +++ b/examples/example_esplora/src/main.rs @@ -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), ); } }