--- /dev/null
+//! Canonical view of transactions and unspent outputs.
+//!
+//! This module provides [`CanonicalView`], a utility for obtaining a canonical (ordered and
+//! conflict-resolved) view of transactions from a [`TxGraph`].
+//!
+//! ## Example
+//!
+//! ```
+//! # use bdk_chain::{TxGraph, CanonicalParams, CanonicalTask, local_chain::LocalChain};
+//! # use bdk_core::BlockId;
+//! # use bitcoin::hashes::Hash;
+//! # let tx_graph = TxGraph::<BlockId>::default();
+//! # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
+//! let chain_tip = chain.tip().block_id();
+//! let params = CanonicalParams::default();
+//! let task = CanonicalTask::new(&tx_graph, chain_tip, params);
+//! let view = chain.canonicalize(task);
+//!
+//! // Iterate over canonical transactions
+//! for tx in view.txs() {
+//! println!("Transaction {}: {:?}", tx.txid, tx.pos);
+//! }
+//! ```
+
+use crate::collections::HashMap;
+use alloc::sync::Arc;
+use alloc::vec::Vec;
+use core::{fmt, ops::RangeBounds};
+
+use bdk_core::BlockId;
+use bitcoin::{
+ constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid,
+};
+
+use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalViewTask, ChainPosition, TxGraph};
+
+/// A single canonical transaction with its position.
+///
+/// This struct represents a transaction that has been determined to be canonical (not
+/// conflicted). It includes the transaction itself along with its position information.
+/// The position type `P` is generic - it can be [`ChainPosition`] for resolved views,
+/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization
+/// results.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct CanonicalTx<P> {
+ /// The position of this transaction.
+ ///
+ /// When `P` is [`ChainPosition`], this indicates whether the transaction is confirmed
+ /// (and at what height) or unconfirmed (most likely pending in the mempool).
+ pub pos: P,
+ /// The transaction ID (hash) of this transaction.
+ pub txid: Txid,
+ /// The full transaction.
+ pub tx: Arc<Transaction>,
+}
+
+impl<P: Ord> Ord for CanonicalTx<P> {
+ fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+ self.pos
+ .cmp(&other.pos)
+ // Txid tiebreaker for same position
+ .then_with(|| self.txid.cmp(&other.txid))
+ }
+}
+
+impl<P: Ord> PartialOrd for CanonicalTx<P> {
+ fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+/// A canonical transaction output with position and spend information.
+///
+/// The position type `P` is generic - it can be [`ChainPosition`] for resolved views,
+/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization
+/// results.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CanonicalTxOut<P> {
+ /// The position of the transaction in `outpoint` in the overall chain.
+ pub pos: P,
+ /// The location of the `TxOut`.
+ pub outpoint: OutPoint,
+ /// The `TxOut`.
+ pub txout: TxOut,
+ /// The txid and position of the transaction (if any) that has spent this output.
+ pub spent_by: Option<(P, Txid)>,
+ /// Whether this output is on a coinbase transaction.
+ pub is_on_coinbase: bool,
+}
+
+impl<P: Ord> Ord for CanonicalTxOut<P> {
+ fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+ self.pos
+ .cmp(&other.pos)
+ // Tie-break with `outpoint` and `spent_by`.
+ .then_with(|| self.outpoint.cmp(&other.outpoint))
+ .then_with(|| self.spent_by.cmp(&other.spent_by))
+ }
+}
+
+impl<P: Ord> PartialOrd for CanonicalTxOut<P> {
+ fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
+ /// Whether the `txout` is considered mature.
+ ///
+ /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
+ /// method may return false-negatives. In other words, interpreted confirmation count may be
+ /// less than the actual value.
+ ///
+ /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
+ pub fn is_mature(&self, tip: u32) -> bool {
+ if self.is_on_coinbase {
+ let conf_height = match self.pos.confirmation_height_upper_bound() {
+ Some(height) => height,
+ None => {
+ debug_assert!(false, "coinbase tx can never be unconfirmed");
+ return false;
+ }
+ };
+ let age = tip.saturating_sub(conf_height);
+ if age + 1 < COINBASE_MATURITY {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ /// Whether the utxo is/was/will be spendable with chain `tip`.
+ ///
+ /// This method does not take into account the lock time.
+ ///
+ /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
+ /// method may return false-negatives. In other words, interpreted confirmation count may be
+ /// less than the actual value.
+ ///
+ /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
+ pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
+ if !self.is_mature(tip) {
+ return false;
+ }
+
+ let conf_height = match self.pos.confirmation_height_upper_bound() {
+ Some(height) => height,
+ None => return false,
+ };
+ if conf_height > tip {
+ return false;
+ }
+
+ // if the spending tx is confirmed within tip height, the txout is no longer spendable
+ if let Some(spend_height) = self
+ .spent_by
+ .as_ref()
+ .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
+ {
+ if spend_height <= tip {
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+/// Canonical set of transactions from a [`TxGraph`].
+///
+/// `Canonical` provides a conflict-resolved list of transactions. It determines
+/// which transactions are canonical (non-conflicted) based on the current chain state and
+/// provides methods to query transaction data, unspent outputs, and balances.
+///
+/// The position type `P` is generic:
+/// - [`ChainPosition<A>`] for resolved views (aka [`CanonicalView`])
+/// - [`CanonicalReason<A>`](crate::canonical_task::CanonicalReason) for unresolved results (aka
+/// [`CanonicalTxs`])
+///
+/// The view maintains:
+/// - A list of canonical transactions
+/// - A mapping of outpoints to the transactions that spend them
+/// - The chain tip used for canonicalization
+///
+/// [`TxGraph`]: crate::TxGraph
+#[derive(Debug)]
+pub struct Canonical<A, P> {
+ /// List of canonical transaction IDs.
+ pub(crate) order: Vec<Txid>,
+ /// Map of transaction IDs to their transaction data and position.
+ pub(crate) txs: HashMap<Txid, (Arc<Transaction>, P)>,
+ /// Map of outpoints to the transaction ID that spends them.
+ pub(crate) spends: HashMap<OutPoint, Txid>,
+ /// The chain tip at the time this view was created.
+ pub(crate) tip: BlockId,
+ /// Marker for the anchor type.
+ pub(crate) _anchor: core::marker::PhantomData<A>,
+}
+
+/// Type alias for canonical transactions with resolved [`ChainPosition`]s.
+pub type CanonicalView<A> = Canonical<A, ChainPosition<A>>;
+
+/// Type alias for canonical transactions with unresolved
+/// [`CanonicalReason`](crate::canonical_task::CanonicalReason)s.
+pub type CanonicalTxs<A> = Canonical<A, crate::canonical_task::CanonicalReason<A>>;
+
+impl<A, P: Clone> Canonical<A, P> {
+ /// Creates a [`Canonical`] from its constituent parts.
+ ///
+ /// This internal constructor is used by [`CanonicalTask`] to build the canonical set
+ /// after completing the canonicalization process. It takes the processed transaction
+ /// data including the canonical ordering, transaction map with positions, and
+ /// spend information.
+ pub(crate) fn new(
+ tip: BlockId,
+ order: Vec<Txid>,
+ txs: HashMap<Txid, (Arc<Transaction>, P)>,
+ spends: HashMap<OutPoint, Txid>,
+ ) -> Self {
+ Self {
+ tip,
+ order,
+ txs,
+ spends,
+ _anchor: core::marker::PhantomData,
+ }
+ }
+
+ /// Get the chain tip used to construct this canonical set.
+ pub fn tip(&self) -> BlockId {
+ self.tip
+ }
+
+ /// Get a single canonical transaction by its transaction ID.
+ ///
+ /// Returns `Some(CanonicalTx)` if the transaction exists in the canonical set,
+ /// or `None` if the transaction doesn't exist or was excluded due to conflicts.
+ pub fn tx(&self, txid: Txid) -> Option<CanonicalTx<P>> {
+ self.txs
+ .get(&txid)
+ .cloned()
+ .map(|(tx, pos)| CanonicalTx { pos, txid, tx })
+ }
+
+ /// Get a single canonical transaction output.
+ ///
+ /// Returns detailed information about a transaction output, including whether it has been
+ /// spent and by which transaction.
+ ///
+ /// Returns `None` if:
+ /// - The transaction doesn't exist in the canonical set
+ /// - The output index is out of bounds
+ /// - The transaction was excluded due to conflicts
+ pub fn txout(&self, op: OutPoint) -> Option<CanonicalTxOut<P>> {
+ 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(CanonicalTxOut {
+ pos: pos.clone(),
+ outpoint: op,
+ txout: txout.clone(),
+ spent_by,
+ is_on_coinbase: tx.is_coinbase(),
+ })
+ }
+
+ /// Get an iterator over all canonical transactions in order.
+ ///
+ /// Transactions are returned in canonical order, with confirmed transactions ordered by
+ /// block height and position, followed by unconfirmed transactions.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain};
+ /// # use bdk_core::BlockId;
+ /// # use bitcoin::hashes::Hash;
+ /// # let tx_graph = TxGraph::<BlockId>::default();
+ /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
+ /// # let chain_tip = chain.tip().block_id();
+ /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
+ /// # let view = chain.canonicalize(task);
+ /// // Iterate over all canonical transactions
+ /// for tx in view.txs() {
+ /// println!("TX {}: {:?}", tx.txid, tx.pos);
+ /// }
+ ///
+ /// // Get the total number of canonical transactions
+ /// println!("Total canonical transactions: {}", view.txs().len());
+ /// ```
+ pub fn txs(&self) -> impl ExactSizeIterator<Item = CanonicalTx<P>> + DoubleEndedIterator + '_ {
+ self.order.iter().map(|&txid| {
+ let (tx, pos) = self.txs[&txid].clone();
+ CanonicalTx { pos, txid, tx }
+ })
+ }
+
+ /// Get a filtered list of outputs from the given outpoints.
+ ///
+ /// This method takes an iterator of `(identifier, outpoint)` pairs and returns an iterator
+ /// of `(identifier, canonical_txout)` pairs for outpoints that exist in the canonical set.
+ /// Non-existent outpoints are silently filtered out.
+ ///
+ /// The identifier type `O` is useful for tracking which outpoints correspond to which addresses
+ /// or keys.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
+ /// # use bdk_core::BlockId;
+ /// # use bitcoin::hashes::Hash;
+ /// # let tx_graph = TxGraph::<BlockId>::default();
+ /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
+ /// # let chain_tip = chain.tip().block_id();
+ /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
+ /// # let view = chain.canonicalize(task);
+ /// # let indexer = KeychainTxOutIndex::<&str>::default();
+ /// // Get all outputs from an indexer
+ /// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) {
+ /// println!("{}: {} sats", keychain.0, txout.txout.value);
+ /// }
+ /// ```
+ pub fn filter_outpoints<'v, O: Clone + 'v>(
+ &'v self,
+ outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
+ ) -> impl Iterator<Item = (O, CanonicalTxOut<P>)> + '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.
+ ///
+ /// Similar to [`filter_outpoints`](Self::filter_outpoints), but only returns outputs that
+ /// have not been spent. This is useful for finding available UTXOs for spending.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
+ /// # use bdk_core::BlockId;
+ /// # use bitcoin::hashes::Hash;
+ /// # let tx_graph = TxGraph::<BlockId>::default();
+ /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
+ /// # let chain_tip = chain.tip().block_id();
+ /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
+ /// # let view = chain.canonicalize(task);
+ /// # let indexer = KeychainTxOutIndex::<&str>::default();
+ /// // Get unspent outputs (UTXOs) from an indexer
+ /// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) {
+ /// println!("{} UTXO: {} sats", keychain.0, utxo.txout.value);
+ /// }
+ /// ```
+ pub fn filter_unspent_outpoints<'v, O: Clone + 'v>(
+ &'v self,
+ outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
+ ) -> impl Iterator<Item = (O, CanonicalTxOut<P>)> + 'v {
+ self.filter_outpoints(outpoints)
+ .filter(|(_, txo)| txo.spent_by.is_none())
+ }
+
+ /// List transaction IDs that are expected to exist for the given script pubkeys.
+ ///
+ /// This method is primarily used for synchronization with external sources, helping to
+ /// identify which transactions are expected to exist for a set of script pubkeys. It's
+ /// commonly used with
+ /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids)
+ /// to inform sync operations about known transactions.
+ 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()
+ })
+ }
+}
+
+impl<A: Anchor> CanonicalView<A> {
+ /// Calculate the total balance of the given outpoints.
+ ///
+ /// This method computes a detailed balance breakdown for a set of outpoints, categorizing
+ /// outputs as confirmed, pending (trusted/untrusted), or immature based on their chain
+ /// position and the provided trust predicate.
+ ///
+ /// # Arguments
+ ///
+ /// * `outpoints` - Iterator of `(identifier, outpoint)` pairs to calculate balance for
+ /// * `trust_predicate` - Function that returns `true` for trusted scripts. Trusted outputs
+ /// count toward `trusted_pending` balance, while untrusted ones count toward
+ /// `untrusted_pending`
+ /// * `min_confirmations` - Minimum confirmations required for an output to be considered
+ /// confirmed. Outputs with fewer confirmations are treated as pending.
+ ///
+ /// # Minimum Confirmations
+ ///
+ /// The `min_confirmations` parameter controls when outputs are considered confirmed. A
+ /// `min_confirmations` value of `0` is equivalent to `1` (require at least 1 confirmation).
+ ///
+ /// Outputs with fewer than `min_confirmations` are categorized as pending (trusted or
+ /// untrusted based on the trust predicate).
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use bdk_chain::{CanonicalParams, TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
+ /// # use bdk_core::BlockId;
+ /// # use bitcoin::hashes::Hash;
+ /// # let tx_graph = TxGraph::<BlockId>::default();
+ /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
+ /// # let chain_tip = chain.tip().block_id();
+ /// # let view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default());
+ /// # let indexer = KeychainTxOutIndex::<&str>::default();
+ /// // Calculate balance with 6 confirmations, trusting all outputs
+ /// let balance = view.balance(
+ /// indexer.outpoints().into_iter().map(|(k, op)| (k.clone(), *op)),
+ /// |_keychain, _script| true, // Trust all outputs
+ /// 6, // Require 6 confirmations
+ /// );
+ /// ```
+ pub fn balance<'v, O: Clone + 'v>(
+ &'v self,
+ outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
+ mut trust_predicate: impl FnMut(&O, &CanonicalTxOut<ChainPosition<A>>) -> bool,
+ min_confirmations: u32,
+ ) -> 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.pos {
+ ChainPosition::Confirmed { anchor, .. } => {
+ let confirmation_height = anchor.confirmation_height_upper_bound();
+ let confirmations = self
+ .tip
+ .height
+ .saturating_sub(confirmation_height)
+ .saturating_add(1);
+ let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically
+
+ if confirmations < min_confirmations {
+ // Not enough confirmations, treat as trusted/untrusted pending
+ if trust_predicate(&spk_i, &txout) {
+ trusted_pending += txout.txout.value;
+ } else {
+ untrusted_pending += txout.txout.value;
+ }
+ } else 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) {
+ trusted_pending += txout.txout.value;
+ } else {
+ untrusted_pending += txout.txout.value;
+ }
+ }
+ }
+ }
+
+ Balance {
+ immature,
+ trusted_pending,
+ untrusted_pending,
+ confirmed,
+ }
+ }
+}
+
+impl<A: Anchor> CanonicalTxs<A> {
+ /// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`]s into [`ChainPosition`]s.
+ ///
+ /// This is the second phase of the canonicalization pipeline. The resulting task
+ /// queries the chain to verify anchors for transitively anchored transactions and
+ /// produces a [`CanonicalView`] with resolved chain positions.
+ pub fn view_task<'g>(self, tx_graph: &'g TxGraph<A>) -> CanonicalViewTask<'g, A> {
+ CanonicalViewTask::new(tx_graph, self.tip, self.order, self.txs, self.spends)
+ }
+}
use crate::collections::{HashMap, HashSet, VecDeque};
use crate::tx_graph::{TxAncestors, TxDescendants};
-use crate::{Anchor, CanonicalTxs, CanonicalView, ChainPosition, TxGraph};
+use crate::{Anchor, CanonicalTxs, TxGraph};
use alloc::boxed::Box;
use alloc::collections::BTreeSet;
use alloc::sync::Arc;
}
}
-/// Represents the current stage of view task processing.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-enum ViewStage {
- /// Processing transactions to resolve their chain positions.
- #[default]
- ResolvingPositions,
- /// All processing is complete.
- Finished,
-}
-
-/// Resolves [`CanonicalReason`]s into [`ChainPosition`]s.
-///
-/// This task implements the second phase of canonicalization: given a set of canonical
-/// transactions with their reasons (from [`CanonicalTask`]), it resolves each reason
-/// into a concrete [`ChainPosition`] (confirmed or unconfirmed). For transitively
-/// anchored transactions, it queries the chain to check if they have their own direct
-/// anchors.
-pub struct CanonicalViewTask<'g, A> {
- tx_graph: &'g TxGraph<A>,
- tip: BlockId,
-
- /// Transactions in canonical order with their reasons.
- canonical_order: Vec<Txid>,
- canonical_txs: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>,
- spends: HashMap<bitcoin::OutPoint, Txid>,
-
- /// Transactions that need anchor verification (transitively anchored).
- unprocessed_anchor_checks: VecDeque<(Txid, &'g BTreeSet<A>)>,
-
- /// Resolved direct anchors for transitively anchored transactions.
- direct_anchors: HashMap<Txid, A>,
-
- current_stage: ViewStage,
-}
-
-impl<'g, A: Anchor> ChainQuery for CanonicalViewTask<'g, A> {
- type Output = CanonicalView<A>;
-
- fn tip(&self) -> BlockId {
- self.tip
- }
-
- fn next_query(&mut self) -> Option<ChainRequest> {
- loop {
- match self.current_stage {
- ViewStage::ResolvingPositions => {
- if let Some((_txid, anchors)) = self.unprocessed_anchor_checks.front() {
- let block_ids =
- anchors.iter().map(|anchor| anchor.anchor_block()).collect();
- return Some(block_ids);
- }
- }
- ViewStage::Finished => return None,
- }
-
- self.current_stage = ViewStage::Finished;
- }
- }
-
- fn resolve_query(&mut self, response: ChainResponse) {
- match self.current_stage {
- ViewStage::ResolvingPositions => {
- if let Some((txid, anchors)) = self.unprocessed_anchor_checks.pop_front() {
- let best_anchor = response.and_then(|block_id| {
- anchors
- .iter()
- .find(|anchor| anchor.anchor_block() == block_id)
- .cloned()
- });
-
- if let Some(best_anchor) = best_anchor {
- self.direct_anchors.insert(txid, best_anchor);
- }
- }
- }
- ViewStage::Finished => {
- debug_assert!(false, "resolve_query called in Finished stage");
- }
- }
- }
-
- fn finish(self) -> Self::Output {
- let mut view_order = Vec::new();
- let mut view_txs = HashMap::new();
-
- for txid in &self.canonical_order {
- if let Some((tx, reason)) = self.canonical_txs.get(txid) {
- view_order.push(*txid);
-
- // Get transaction node for first_seen/last_seen info
- let tx_node = match self.tx_graph.get_tx_node(*txid) {
- Some(tx_node) => tx_node,
- None => {
- debug_assert!(false, "tx node must exist!");
- continue;
- }
- };
-
- // Determine chain position based on reason
- let chain_position = match reason {
- CanonicalReason::Assumed { descendant } => match descendant {
- Some(_) => match self.direct_anchors.get(txid) {
- 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 self.direct_anchors.get(txid) {
- 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.clone(), chain_position.cloned()));
- }
- }
-
- CanonicalView::new(self.tip, view_order, view_txs, self.spends)
- }
-}
-
-impl<A: Anchor> CanonicalTxs<A> {
- /// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`]s into [`ChainPosition`]s.
- ///
- /// This is the second phase of the canonicalization pipeline. The resulting task
- /// queries the chain to verify anchors for transitively anchored transactions and
- /// produces a [`CanonicalView`] with resolved chain positions.
- pub fn view_task<'g>(self, tx_graph: &'g TxGraph<A>) -> CanonicalViewTask<'g, A> {
- let all_anchors = tx_graph.all_anchors();
-
- // Find transactions that need anchor verification
- let mut unprocessed_anchor_checks = VecDeque::new();
- for txid in &self.order {
- if let Some((_, reason)) = self.txs.get(txid) {
- // Skip ObservedIn transactions - they don't have anchors to verify
- if matches!(reason, CanonicalReason::ObservedIn { .. }) {
- continue;
- }
- // Transitively anchored transactions need their own anchor checked
- if reason.is_transitive() {
- if let Some(anchors) = all_anchors.get(txid) {
- unprocessed_anchor_checks.push_back((*txid, anchors));
- }
- }
- }
- }
-
- CanonicalViewTask {
- tx_graph,
- tip: self.tip,
- canonical_order: self.order,
- canonical_txs: self.txs,
- spends: self.spends,
- unprocessed_anchor_checks,
- direct_anchors: HashMap::new(),
- current_stage: ViewStage::default(),
- }
- }
-}
-
/// Represents when and where a transaction was last observed in.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ObservedIn {
pub fn is_transitive(&self) -> bool {
self.descendant().is_some()
}
+
+ /// Returns true if this reason is [`CanonicalReason::Assumed`].
+ pub fn is_assumed(&self) -> bool {
+ matches!(self, CanonicalReason::Assumed { .. })
+ }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::local_chain::LocalChain;
+ use crate::ChainPosition;
use bitcoin::{hashes::Hash, BlockHash, TxIn, TxOut};
#[test]
+++ /dev/null
-//! Canonical view of transactions and unspent outputs.
-//!
-//! This module provides [`CanonicalView`], a utility for obtaining a canonical (ordered and
-//! conflict-resolved) view of transactions from a [`TxGraph`].
-//!
-//! ## Example
-//!
-//! ```
-//! # use bdk_chain::{TxGraph, CanonicalParams, CanonicalTask, local_chain::LocalChain};
-//! # use bdk_core::BlockId;
-//! # use bitcoin::hashes::Hash;
-//! # let tx_graph = TxGraph::<BlockId>::default();
-//! # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
-//! let chain_tip = chain.tip().block_id();
-//! let params = CanonicalParams::default();
-//! let task = CanonicalTask::new(&tx_graph, chain_tip, params);
-//! let view = chain.canonicalize(task);
-//!
-//! // Iterate over canonical transactions
-//! for tx in view.txs() {
-//! println!("Transaction {}: {:?}", tx.txid, tx.pos);
-//! }
-//! ```
-
-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, Anchor, Balance, ChainPosition, FullTxOut};
-
-/// A single canonical transaction with its position.
-///
-/// This struct represents a transaction that has been determined to be canonical (not
-/// conflicted). It includes the transaction itself along with its position information.
-/// The position type `P` is generic - it can be [`ChainPosition`] for resolved views,
-/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization
-/// results.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct CanonicalTx<P> {
- /// The position of this transaction.
- ///
- /// When `P` is [`ChainPosition`], this indicates whether the transaction is confirmed
- /// (and at what height) or unconfirmed (most likely pending in the mempool).
- pub pos: P,
- /// The transaction ID (hash) of this transaction.
- pub txid: Txid,
- /// The full transaction.
- pub tx: Arc<Transaction>,
-}
-
-impl<P: Ord> Ord for CanonicalTx<P> {
- fn cmp(&self, other: &Self) -> core::cmp::Ordering {
- self.pos
- .cmp(&other.pos)
- // Txid tiebreaker for same position
- .then_with(|| self.txid.cmp(&other.txid))
- }
-}
-
-impl<P: Ord> PartialOrd for CanonicalTx<P> {
- fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-
-/// Canonical set of transactions from a [`TxGraph`].
-///
-/// `Canonical` provides a conflict-resolved list of transactions. It determines
-/// which transactions are canonical (non-conflicted) based on the current chain state and
-/// provides methods to query transaction data, unspent outputs, and balances.
-///
-/// The position type `P` is generic:
-/// - [`ChainPosition<A>`] for resolved views (aka [`CanonicalView`])
-/// - [`CanonicalReason<A>`](crate::canonical_task::CanonicalReason) for unresolved results (aka
-/// [`CanonicalTxs`])
-///
-/// The view maintains:
-/// - A list of canonical transactions
-/// - A mapping of outpoints to the transactions that spend them
-/// - The chain tip used for canonicalization
-///
-/// [`TxGraph`]: crate::TxGraph
-#[derive(Debug)]
-pub struct Canonical<A, P> {
- /// List of canonical transaction IDs.
- pub(crate) order: Vec<Txid>,
- /// Map of transaction IDs to their transaction data and position.
- pub(crate) txs: HashMap<Txid, (Arc<Transaction>, P)>,
- /// Map of outpoints to the transaction ID that spends them.
- pub(crate) spends: HashMap<OutPoint, Txid>,
- /// The chain tip at the time this view was created.
- pub(crate) tip: BlockId,
- /// Marker for the anchor type.
- pub(crate) _anchor: core::marker::PhantomData<A>,
-}
-
-/// Type alias for canonical transactions with resolved [`ChainPosition`]s.
-pub type CanonicalView<A> = Canonical<A, ChainPosition<A>>;
-
-/// Type alias for canonical transactions with unresolved
-/// [`CanonicalReason`](crate::canonical_task::CanonicalReason)s.
-pub type CanonicalTxs<A> = Canonical<A, crate::canonical_task::CanonicalReason<A>>;
-
-impl<A, P: Clone> Canonical<A, P> {
- /// Creates a [`Canonical`] from its constituent parts.
- ///
- /// This internal constructor is used by [`CanonicalTask`] to build the canonical set
- /// after completing the canonicalization process. It takes the processed transaction
- /// data including the canonical ordering, transaction map with positions, and
- /// spend information.
- pub(crate) fn new(
- tip: BlockId,
- order: Vec<Txid>,
- txs: HashMap<Txid, (Arc<Transaction>, P)>,
- spends: HashMap<OutPoint, Txid>,
- ) -> Self {
- Self {
- tip,
- order,
- txs,
- spends,
- _anchor: core::marker::PhantomData,
- }
- }
-
- /// Get the chain tip used to construct this canonical set.
- pub fn tip(&self) -> BlockId {
- self.tip
- }
-
- /// Get a single canonical transaction by its transaction ID.
- ///
- /// Returns `Some(CanonicalTx)` if the transaction exists in the canonical set,
- /// or `None` if the transaction doesn't exist or was excluded due to conflicts.
- pub fn tx(&self, txid: Txid) -> Option<CanonicalTx<P>> {
- self.txs
- .get(&txid)
- .cloned()
- .map(|(tx, pos)| CanonicalTx { pos, txid, tx })
- }
-
- /// Get a single canonical transaction output.
- ///
- /// Returns detailed information about a transaction output, including whether it has been
- /// spent and by which transaction.
- ///
- /// Returns `None` if:
- /// - The transaction doesn't exist in the canonical set
- /// - The output index is out of bounds
- /// - The transaction was excluded due to conflicts
- pub fn txout(&self, op: OutPoint) -> Option<FullTxOut<P>> {
- 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 {
- pos: pos.clone(),
- outpoint: op,
- txout: txout.clone(),
- spent_by,
- is_on_coinbase: tx.is_coinbase(),
- })
- }
-
- /// Get an iterator over all canonical transactions in order.
- ///
- /// Transactions are returned in canonical order, with confirmed transactions ordered by
- /// block height and position, followed by unconfirmed transactions.
- ///
- /// # Example
- ///
- /// ```
- /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain};
- /// # use bdk_core::BlockId;
- /// # use bitcoin::hashes::Hash;
- /// # let tx_graph = TxGraph::<BlockId>::default();
- /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
- /// # let chain_tip = chain.tip().block_id();
- /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
- /// # let view = chain.canonicalize(task);
- /// // Iterate over all canonical transactions
- /// for tx in view.txs() {
- /// println!("TX {}: {:?}", tx.txid, tx.pos);
- /// }
- ///
- /// // Get the total number of canonical transactions
- /// println!("Total canonical transactions: {}", view.txs().len());
- /// ```
- pub fn txs(&self) -> impl ExactSizeIterator<Item = CanonicalTx<P>> + DoubleEndedIterator + '_ {
- self.order.iter().map(|&txid| {
- let (tx, pos) = self.txs[&txid].clone();
- CanonicalTx { pos, txid, tx }
- })
- }
-
- /// Get a filtered list of outputs from the given outpoints.
- ///
- /// This method takes an iterator of `(identifier, outpoint)` pairs and returns an iterator
- /// of `(identifier, full_txout)` pairs for outpoints that exist in the canonical set.
- /// Non-existent outpoints are silently filtered out.
- ///
- /// The identifier type `O` is useful for tracking which outpoints correspond to which addresses
- /// or keys.
- ///
- /// # Example
- ///
- /// ```
- /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
- /// # use bdk_core::BlockId;
- /// # use bitcoin::hashes::Hash;
- /// # let tx_graph = TxGraph::<BlockId>::default();
- /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
- /// # let chain_tip = chain.tip().block_id();
- /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
- /// # let view = chain.canonicalize(task);
- /// # let indexer = KeychainTxOutIndex::<&str>::default();
- /// // Get all outputs from an indexer
- /// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) {
- /// println!("{}: {} sats", keychain.0, txout.txout.value);
- /// }
- /// ```
- pub fn filter_outpoints<'v, O: Clone + 'v>(
- &'v self,
- outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- ) -> impl Iterator<Item = (O, FullTxOut<P>)> + '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.
- ///
- /// Similar to [`filter_outpoints`](Self::filter_outpoints), but only returns outputs that
- /// have not been spent. This is useful for finding available UTXOs for spending.
- ///
- /// # Example
- ///
- /// ```
- /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
- /// # use bdk_core::BlockId;
- /// # use bitcoin::hashes::Hash;
- /// # let tx_graph = TxGraph::<BlockId>::default();
- /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
- /// # let chain_tip = chain.tip().block_id();
- /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
- /// # let view = chain.canonicalize(task);
- /// # let indexer = KeychainTxOutIndex::<&str>::default();
- /// // Get unspent outputs (UTXOs) from an indexer
- /// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) {
- /// println!("{} UTXO: {} sats", keychain.0, utxo.txout.value);
- /// }
- /// ```
- pub fn filter_unspent_outpoints<'v, O: Clone + 'v>(
- &'v self,
- outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- ) -> impl Iterator<Item = (O, FullTxOut<P>)> + 'v {
- self.filter_outpoints(outpoints)
- .filter(|(_, txo)| txo.spent_by.is_none())
- }
-
- /// List transaction IDs that are expected to exist for the given script pubkeys.
- ///
- /// This method is primarily used for synchronization with external sources, helping to
- /// identify which transactions are expected to exist for a set of script pubkeys. It's
- /// commonly used with
- /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids)
- /// to inform sync operations about known transactions.
- 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()
- })
- }
-}
-
-impl<A: Anchor> CanonicalView<A> {
- /// Calculate the total balance of the given outpoints.
- ///
- /// This method computes a detailed balance breakdown for a set of outpoints, categorizing
- /// outputs as confirmed, pending (trusted/untrusted), or immature based on their chain
- /// position and the provided trust predicate.
- ///
- /// # Arguments
- ///
- /// * `outpoints` - Iterator of `(identifier, outpoint)` pairs to calculate balance for
- /// * `trust_predicate` - Function that returns `true` for trusted scripts. Trusted outputs
- /// count toward `trusted_pending` balance, while untrusted ones count toward
- /// `untrusted_pending`
- /// * `min_confirmations` - Minimum confirmations required for an output to be considered
- /// confirmed. Outputs with fewer confirmations are treated as pending.
- ///
- /// # Minimum Confirmations
- ///
- /// The `min_confirmations` parameter controls when outputs are considered confirmed. A
- /// `min_confirmations` value of `0` is equivalent to `1` (require at least 1 confirmation).
- ///
- /// Outputs with fewer than `min_confirmations` are categorized as pending (trusted or
- /// untrusted based on the trust predicate).
- ///
- /// # Example
- ///
- /// ```
- /// # use bdk_chain::{CanonicalParams, TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
- /// # use bdk_core::BlockId;
- /// # use bitcoin::hashes::Hash;
- /// # let tx_graph = TxGraph::<BlockId>::default();
- /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
- /// # let chain_tip = chain.tip().block_id();
- /// # let view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default());
- /// # let indexer = KeychainTxOutIndex::<&str>::default();
- /// // Calculate balance with 6 confirmations, trusting all outputs
- /// let balance = view.balance(
- /// indexer.outpoints().into_iter().map(|(k, op)| (k.clone(), *op)),
- /// |_keychain, _script| true, // Trust all outputs
- /// 6, // Require 6 confirmations
- /// );
- /// ```
- pub fn balance<'v, O: Clone + 'v>(
- &'v self,
- outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- mut trust_predicate: impl FnMut(&O, &FullTxOut<ChainPosition<A>>) -> bool,
- min_confirmations: u32,
- ) -> 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.pos {
- ChainPosition::Confirmed { anchor, .. } => {
- let confirmation_height = anchor.confirmation_height_upper_bound();
- let confirmations = self
- .tip
- .height
- .saturating_sub(confirmation_height)
- .saturating_add(1);
- let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically
-
- if confirmations < min_confirmations {
- // Not enough confirmations, treat as trusted/untrusted pending
- if trust_predicate(&spk_i, &txout) {
- trusted_pending += txout.txout.value;
- } else {
- untrusted_pending += txout.txout.value;
- }
- } else 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) {
- trusted_pending += txout.txout.value;
- } else {
- untrusted_pending += txout.txout.value;
- }
- }
- }
- }
-
- Balance {
- immature,
- trusted_pending,
- untrusted_pending,
- confirmed,
- }
- }
-}
--- /dev/null
+//! Phase 2 task: resolves canonical reasons into chain positions.
+
+use crate::canonical_task::{CanonicalReason, ObservedIn};
+use crate::collections::{HashMap, VecDeque};
+use alloc::collections::BTreeSet;
+use alloc::sync::Arc;
+use alloc::vec::Vec;
+
+use bdk_core::{BlockId, ChainQuery, ChainRequest, ChainResponse};
+use bitcoin::{OutPoint, Transaction, Txid};
+
+use crate::{Anchor, CanonicalView, ChainPosition, TxGraph};
+
+/// Represents the current stage of view task processing.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+enum ViewStage {
+ /// Processing transactions to resolve their chain positions.
+ #[default]
+ ResolvingPositions,
+ /// All processing is complete.
+ Finished,
+}
+
+/// Resolves [`CanonicalReason`]s into [`ChainPosition`]s.
+///
+/// This task implements the second phase of canonicalization: given a set of canonical
+/// transactions with their reasons (from [`CanonicalTask`](crate::CanonicalTask)), it resolves each
+/// reason into a concrete [`ChainPosition`] (confirmed or unconfirmed). For transitively
+/// anchored transactions, it queries the chain to check if they have their own direct
+/// anchors.
+pub struct CanonicalViewTask<'g, A> {
+ tx_graph: &'g TxGraph<A>,
+ tip: BlockId,
+
+ /// Transactions in canonical order with their reasons.
+ canonical_order: Vec<Txid>,
+ canonical_txs: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>,
+ spends: HashMap<OutPoint, Txid>,
+
+ /// Transactions that need anchor verification (transitively anchored).
+ unprocessed_anchor_checks: VecDeque<(Txid, &'g BTreeSet<A>)>,
+
+ /// Resolved direct anchors for transitively anchored transactions.
+ direct_anchors: HashMap<Txid, A>,
+
+ current_stage: ViewStage,
+}
+
+impl<'g, A: Anchor> CanonicalViewTask<'g, A> {
+ /// Creates a new [`CanonicalViewTask`].
+ ///
+ /// Accepts canonical transaction data and a reference to the [`TxGraph`].
+ /// Scans transactions to find those needing anchor verification.
+ pub fn new(
+ tx_graph: &'g TxGraph<A>,
+ tip: BlockId,
+ order: Vec<Txid>,
+ txs: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>,
+ spends: HashMap<OutPoint, Txid>,
+ ) -> Self {
+ let all_anchors = tx_graph.all_anchors();
+
+ let mut unprocessed_anchor_checks = VecDeque::new();
+ for txid in &order {
+ if let Some((_, reason)) = txs.get(txid) {
+ if matches!(reason, CanonicalReason::ObservedIn { .. }) {
+ continue;
+ }
+ if reason.is_transitive() || reason.is_assumed() {
+ if let Some(anchors) = all_anchors.get(txid) {
+ unprocessed_anchor_checks.push_back((*txid, anchors));
+ }
+ }
+ }
+ }
+
+ Self {
+ tx_graph,
+ tip,
+ canonical_order: order,
+ canonical_txs: txs,
+ spends,
+ unprocessed_anchor_checks,
+ direct_anchors: HashMap::new(),
+ current_stage: ViewStage::default(),
+ }
+ }
+}
+
+impl<'g, A: Anchor> ChainQuery for CanonicalViewTask<'g, A> {
+ type Output = CanonicalView<A>;
+
+ fn tip(&self) -> BlockId {
+ self.tip
+ }
+
+ fn next_query(&mut self) -> Option<ChainRequest> {
+ loop {
+ match self.current_stage {
+ ViewStage::ResolvingPositions => {
+ if let Some((_txid, anchors)) = self.unprocessed_anchor_checks.front() {
+ let block_ids =
+ anchors.iter().map(|anchor| anchor.anchor_block()).collect();
+ return Some(block_ids);
+ }
+ }
+ ViewStage::Finished => return None,
+ }
+
+ self.current_stage = ViewStage::Finished;
+ }
+ }
+
+ fn resolve_query(&mut self, response: ChainResponse) {
+ match self.current_stage {
+ ViewStage::ResolvingPositions => {
+ if let Some((txid, anchors)) = self.unprocessed_anchor_checks.pop_front() {
+ let best_anchor = response.and_then(|block_id| {
+ anchors
+ .iter()
+ .find(|anchor| anchor.anchor_block() == block_id)
+ .cloned()
+ });
+
+ if let Some(best_anchor) = best_anchor {
+ self.direct_anchors.insert(txid, best_anchor);
+ }
+ }
+ }
+ ViewStage::Finished => {
+ debug_assert!(false, "resolve_query called in Finished stage");
+ }
+ }
+ }
+
+ fn finish(self) -> Self::Output {
+ let mut view_order = Vec::new();
+ let mut view_txs = HashMap::new();
+
+ for txid in &self.canonical_order {
+ if let Some((tx, reason)) = self.canonical_txs.get(txid) {
+ view_order.push(*txid);
+
+ // Get transaction node for first_seen/last_seen info
+ let tx_node = match self.tx_graph.get_tx_node(*txid) {
+ Some(tx_node) => tx_node,
+ None => {
+ debug_assert!(false, "tx node must exist!");
+ continue;
+ }
+ };
+
+ // Determine chain position based on reason
+ let chain_position = match reason {
+ CanonicalReason::Assumed { .. } => match self.direct_anchors.get(txid) {
+ Some(anchor) => ChainPosition::Confirmed {
+ anchor,
+ transitively: None,
+ },
+ None => ChainPosition::Unconfirmed {
+ first_seen: tx_node.first_seen,
+ last_seen: tx_node.last_seen,
+ },
+ },
+ CanonicalReason::Anchor { anchor, descendant } => match descendant {
+ Some(_) => match self.direct_anchors.get(txid) {
+ 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.clone(), chain_position.cloned()));
+ }
+ }
+
+ CanonicalView::new(self.tip, view_order, view_txs, self.spends)
+ }
+}
-use bitcoin::{constants::COINBASE_MATURITY, OutPoint, TxOut, Txid};
+use bitcoin::Txid;
use crate::Anchor;
}
}
-/// A `TxOut` with as much data as we can retrieve about it.
-///
-/// The position type `P` is generic — it can be [`ChainPosition`] for resolved views,
-/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization
-/// results.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct FullTxOut<P> {
- /// The position of the transaction in `outpoint` in the overall chain.
- pub pos: P,
- /// The location of the `TxOut`.
- pub outpoint: OutPoint,
- /// The `TxOut`.
- pub txout: TxOut,
- /// The txid and position of the transaction (if any) that has spent this output.
- pub spent_by: Option<(P, Txid)>,
- /// Whether this output is on a coinbase transaction.
- pub is_on_coinbase: bool,
-}
-
-impl<P: Ord> Ord for FullTxOut<P> {
- fn cmp(&self, other: &Self) -> core::cmp::Ordering {
- self.pos
- .cmp(&other.pos)
- // Tie-break with `outpoint` and `spent_by`.
- .then_with(|| self.outpoint.cmp(&other.outpoint))
- .then_with(|| self.spent_by.cmp(&other.spent_by))
- }
-}
-
-impl<P: Ord> PartialOrd for FullTxOut<P> {
- fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl<A: Anchor> FullTxOut<ChainPosition<A>> {
- /// Whether the `txout` is considered mature.
- ///
- /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
- /// method may return false-negatives. In other words, interpreted confirmation count may be
- /// less than the actual value.
- ///
- /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
- pub fn is_mature(&self, tip: u32) -> bool {
- if self.is_on_coinbase {
- let conf_height = match self.pos.confirmation_height_upper_bound() {
- Some(height) => height,
- None => {
- debug_assert!(false, "coinbase tx can never be unconfirmed");
- return false;
- }
- };
- let age = tip.saturating_sub(conf_height);
- if age + 1 < COINBASE_MATURITY {
- return false;
- }
- }
-
- true
- }
-
- /// Whether the utxo is/was/will be spendable with chain `tip`.
- ///
- /// This method does not take into account the lock time.
- ///
- /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
- /// method may return false-negatives. In other words, interpreted confirmation count may be
- /// less than the actual value.
- ///
- /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
- pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
- if !self.is_mature(tip) {
- return false;
- }
-
- let conf_height = match self.pos.confirmation_height_upper_bound() {
- Some(height) => height,
- None => return false,
- };
- if conf_height > tip {
- return false;
- }
-
- // if the spending tx is confirmed within tip height, the txout is no longer spendable
- if let Some(spend_height) = self
- .spent_by
- .as_ref()
- .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
- {
- if spend_height <= tip {
- return false;
- }
- }
-
- true
- }
-}
-
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
pub use chain_oracle::*;
mod canonical_task;
pub use canonical_task::*;
-mod canonical_view;
-pub use canonical_view::*;
+mod canonical;
+pub use canonical::*;
+mod canonical_view_task;
+pub use canonical_view_task::*;
#[doc(hidden)]
pub mod example_utils;