]> Untitled Git - bdk/commitdiff
feat(chain)!: rm `get_chain_position` and associated methods
author志宇 <hello@evanlinjin.me>
Tue, 19 Nov 2024 06:26:50 +0000 (17:26 +1100)
committer志宇 <hello@evanlinjin.me>
Tue, 10 Dec 2024 10:39:23 +0000 (21:39 +1100)
crates/chain/src/tx_graph.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_tx_graph.rs
crates/wallet/src/wallet/mod.rs

index 0e7dd33fafacf87877c76f751c26ae1a2b446dc3..71fde71884add434e72b7a7e0cf98d2cca76c0e7 100644 (file)
@@ -20,8 +20,7 @@
 //! identifying and traversing conflicts and descendants of a given transaction. Some [`TxGraph`]
 //! methods only consider transactions that are "canonical" (i.e., in the best chain or in mempool).
 //! We decide which transactions are canonical based on the transaction's anchors and the
-//! `last_seen` (as unconfirmed) timestamp; see the [`try_get_chain_position`] documentation for
-//! more details.
+//! `last_seen` (as unconfirmed) timestamp.
 //!
 //! The [`ChangeSet`] reports changes made to a [`TxGraph`]; it can be used to either save to
 //! persistent storage, or to be applied to another [`TxGraph`].
@@ -89,7 +88,6 @@
 //! let changeset = graph.apply_update(update);
 //! assert!(changeset.is_empty());
 //! ```
-//! [`try_get_chain_position`]: TxGraph::try_get_chain_position
 //! [`insert_txout`]: TxGraph::insert_txout
 
 use crate::collections::*;
@@ -791,224 +789,6 @@ impl<A: Anchor> TxGraph<A> {
 }
 
 impl<A: Anchor> TxGraph<A> {
-    /// Get the position of the transaction in `chain` with tip `chain_tip`.
-    ///
-    /// Chain data is fetched from `chain`, a [`ChainOracle`] implementation.
-    ///
-    /// This method returns `Ok(None)` if the transaction is not found in the chain, and no longer
-    /// belongs in the mempool. The following factors are used to approximate whether an
-    /// unconfirmed transaction exists in the mempool (not evicted):
-    ///
-    /// 1. Unconfirmed transactions that conflict with confirmed transactions are evicted.
-    /// 2. Unconfirmed transactions that spend from transactions that are evicted, are also
-    ///    evicted.
-    /// 3. Given two conflicting unconfirmed transactions, the transaction with the lower
-    ///    `last_seen_unconfirmed` parameter is evicted. A transaction's `last_seen_unconfirmed`
-    ///    parameter is the max of all it's descendants' `last_seen_unconfirmed` parameters. If the
-    ///    final `last_seen_unconfirmed`s are the same, the transaction with the lower `txid` (by
-    ///    lexicographical order) is evicted.
-    ///
-    /// # Error
-    ///
-    /// An error will occur if the [`ChainOracle`] implementation (`chain`) fails. If the
-    /// [`ChainOracle`] is infallible, [`get_chain_position`] can be used instead.
-    ///
-    /// [`get_chain_position`]: Self::get_chain_position
-    pub fn try_get_chain_position<C: ChainOracle>(
-        &self,
-        chain: &C,
-        chain_tip: BlockId,
-        txid: Txid,
-    ) -> Result<Option<ChainPosition<&A>>, C::Error> {
-        let tx_node = match self.txs.get(&txid) {
-            Some(v) => v,
-            None => return Ok(None),
-        };
-
-        for anchor in self.anchors.get(&txid).unwrap_or(&self.empty_anchors) {
-            match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
-                Some(true) => {
-                    return Ok(Some(ChainPosition::Confirmed {
-                        anchor,
-                        transitively: None,
-                    }))
-                }
-                _ => continue,
-            }
-        }
-
-        // If no anchors are in best chain and we don't have a last_seen, we can return
-        // early because by definition the tx doesn't have a chain position.
-        let last_seen = match self.last_seen.get(&txid) {
-            Some(t) => *t,
-            None => return Ok(None),
-        };
-
-        // The tx is not anchored to a block in the best chain, which means that it
-        // might be in mempool, or it might have been dropped already.
-        // Let's check conflicts to find out!
-        let tx = match tx_node {
-            TxNodeInternal::Whole(tx) => {
-                // A coinbase tx that is not anchored in the best chain cannot be unconfirmed and
-                // should always be filtered out.
-                if tx.is_coinbase() {
-                    return Ok(None);
-                }
-                tx.clone()
-            }
-            TxNodeInternal::Partial(_) => {
-                // Partial transactions (outputs only) cannot have conflicts.
-                return Ok(None);
-            }
-        };
-
-        // We want to retrieve all the transactions that conflict with us, plus all the
-        // transactions that conflict with our unconfirmed ancestors, since they conflict with us
-        // as well.
-        // We only traverse unconfirmed ancestors since conflicts of confirmed transactions
-        // cannot be in the best chain.
-
-        // First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
-        // resulting array will also include `tx`
-        let unconfirmed_ancestor_txs =
-            TxAncestors::new_include_root(self, tx.clone(), |_, ancestor_tx: Arc<Transaction>| {
-                let tx_node = self.get_tx_node(ancestor_tx.as_ref().compute_txid())?;
-                // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
-                // the best chain)
-                for block in tx_node.anchors {
-                    match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
-                        Ok(Some(true)) => return None,
-                        Err(e) => return Some(Err(e)),
-                        _ => continue,
-                    }
-                }
-                Some(Ok(tx_node))
-            })
-            .collect::<Result<Vec<_>, C::Error>>()?;
-
-        // We determine our tx's last seen, which is the max between our last seen,
-        // and our unconf descendants' last seen.
-        let unconfirmed_descendants_txs = TxDescendants::new_include_root(
-            self,
-            tx.as_ref().compute_txid(),
-            |_, descendant_txid: Txid| {
-                let tx_node = self.get_tx_node(descendant_txid)?;
-                // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
-                // the best chain)
-                for block in tx_node.anchors {
-                    match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
-                        Ok(Some(true)) => return None,
-                        Err(e) => return Some(Err(e)),
-                        _ => continue,
-                    }
-                }
-                Some(Ok(tx_node))
-            },
-        )
-        .collect::<Result<Vec<_>, C::Error>>()?;
-
-        let tx_last_seen = unconfirmed_descendants_txs
-            .iter()
-            .max_by_key(|tx| tx.last_seen_unconfirmed)
-            .map(|tx| tx.last_seen_unconfirmed)
-            .expect("descendants always includes at least one transaction (the root tx");
-
-        // Now we traverse our ancestors and consider all their conflicts
-        for tx_node in unconfirmed_ancestor_txs {
-            // We retrieve all the transactions conflicting with this specific ancestor
-            let conflicting_txs =
-                self.walk_conflicts(tx_node.tx.as_ref(), |_, txid| self.get_tx_node(txid));
-
-            // If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
-            // this tx cannot exist in the best chain
-            for conflicting_tx in conflicting_txs {
-                for block in conflicting_tx.anchors {
-                    if chain.is_block_in_chain(block.anchor_block(), chain_tip)? == Some(true) {
-                        return Ok(None);
-                    }
-                }
-                if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
-                    return Ok(None);
-                }
-                if conflicting_tx.last_seen_unconfirmed == Some(last_seen)
-                    && conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
-                {
-                    // Conflicting tx has priority if txid of conflicting tx > txid of original tx
-                    return Ok(None);
-                }
-            }
-        }
-
-        Ok(Some(ChainPosition::Unconfirmed {
-            last_seen: Some(last_seen),
-        }))
-    }
-
-    /// Get the position of the transaction in `chain` with tip `chain_tip`.
-    ///
-    /// This is the infallible version of [`try_get_chain_position`].
-    ///
-    /// [`try_get_chain_position`]: Self::try_get_chain_position
-    pub fn get_chain_position<C: ChainOracle<Error = Infallible>>(
-        &self,
-        chain: &C,
-        chain_tip: BlockId,
-        txid: Txid,
-    ) -> Option<ChainPosition<&A>> {
-        self.try_get_chain_position(chain, chain_tip, txid)
-            .expect("error is infallible")
-    }
-
-    /// Get the txid of the spending transaction and where the spending transaction is observed in
-    /// the `chain` of `chain_tip`.
-    ///
-    /// If no in-chain transaction spends `outpoint`, `None` will be returned.
-    ///
-    /// # Error
-    ///
-    /// An error will occur only if the [`ChainOracle`] implementation (`chain`) fails.
-    ///
-    /// If the [`ChainOracle`] is infallible, [`get_chain_spend`] can be used instead.
-    ///
-    /// [`get_chain_spend`]: Self::get_chain_spend
-    pub fn try_get_chain_spend<C: ChainOracle>(
-        &self,
-        chain: &C,
-        chain_tip: BlockId,
-        outpoint: OutPoint,
-    ) -> Result<Option<(ChainPosition<&A>, Txid)>, C::Error> {
-        if self
-            .try_get_chain_position(chain, chain_tip, outpoint.txid)?
-            .is_none()
-        {
-            return Ok(None);
-        }
-        if let Some(spends) = self.spends.get(&outpoint) {
-            for &txid in spends {
-                if let Some(observed_at) = self.try_get_chain_position(chain, chain_tip, txid)? {
-                    return Ok(Some((observed_at, txid)));
-                }
-            }
-        }
-        Ok(None)
-    }
-
-    /// Get the txid of the spending transaction and where the spending transaction is observed in
-    /// the `chain` of `chain_tip`.
-    ///
-    /// This is the infallible version of [`try_get_chain_spend`]
-    ///
-    /// [`try_get_chain_spend`]: Self::try_get_chain_spend
-    pub fn get_chain_spend<C: ChainOracle<Error = Infallible>>(
-        &self,
-        chain: &C,
-        static_block: BlockId,
-        outpoint: OutPoint,
-    ) -> Option<(ChainPosition<&A>, Txid)> {
-        self.try_get_chain_spend(chain, static_block, outpoint)
-            .expect("error is infallible")
-    }
-
     /// List graph transactions that are in `chain` with `chain_tip`.
     ///
     /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
index e37a639c23eb7ae2a4f508725c0d2e643b0a2c23..882041fba4bb8d2a6cfef8601e1092b88444ade8 100644 (file)
@@ -588,14 +588,17 @@ fn test_get_chain_position() {
         }
 
         // check chain position
-        let res = graph
+        let chain_pos = graph
             .graph()
-            .get_chain_position(chain, chain.tip().block_id(), txid);
-        assert_eq!(
-            res.map(ChainPosition::cloned),
-            exp_pos,
-            "failed test case: {name}"
-        );
+            .list_canonical_txs(chain, chain.tip().block_id())
+            .find_map(|canon_tx| {
+                if canon_tx.tx_node.txid == txid {
+                    Some(canon_tx.chain_position)
+                } else {
+                    None
+                }
+            });
+        assert_eq!(chain_pos, exp_pos, "failed test case: {name}");
     }
 
     [
@@ -655,7 +658,7 @@ fn test_get_chain_position() {
             exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
         },
         TestCase {
-            name: "tx unknown anchor - no chain pos",
+            name: "tx unknown anchor - unconfirmed",
             tx: Transaction {
                 output: vec![TxOut {
                     value: Amount::ONE_BTC,
@@ -665,7 +668,7 @@ fn test_get_chain_position() {
             },
             anchor: Some(block_id!(2, "B'")),
             last_seen: None,
-            exp_pos: None,
+            exp_pos: Some(ChainPosition::Unconfirmed { last_seen: None }),
         },
     ]
     .into_iter()
index 8f1634ba12b30c0b9ed9c324eb85a08123d3f096..ef57ac15b65ed5fdc5d6af3eb541c391fb6abb0c 100644 (file)
@@ -877,63 +877,77 @@ fn test_chain_spends() {
         );
     }
 
-    // Assert that confirmed spends are returned correctly.
-    assert_eq!(
-        graph.get_chain_spend(
-            &local_chain,
-            tip.block_id(),
-            OutPoint::new(tx_0.compute_txid(), 0)
-        ),
-        Some((
-            ChainPosition::Confirmed {
-                anchor: &ConfirmationBlockTime {
-                    block_id: BlockId {
-                        hash: tip.get(98).unwrap().hash(),
-                        height: 98,
+    let build_canonical_spends =
+        |chain: &LocalChain, tx_graph: &TxGraph<ConfirmationBlockTime>| -> HashMap<OutPoint, _> {
+            tx_graph
+                .filter_chain_txouts(
+                    chain,
+                    tip.block_id(),
+                    tx_graph.all_txouts().map(|(op, _)| ((), op)),
+                )
+                .filter_map(|(_, full_txo)| Some((full_txo.outpoint, full_txo.spent_by?)))
+                .collect()
+        };
+    let build_canonical_positions = |chain: &LocalChain,
+                                     tx_graph: &TxGraph<ConfirmationBlockTime>|
+     -> HashMap<Txid, ChainPosition<ConfirmationBlockTime>> {
+        tx_graph
+            .list_canonical_txs(chain, tip.block_id())
+            .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position))
+            .collect()
+    };
+
+    {
+        let canonical_spends = build_canonical_spends(&local_chain, &graph);
+        let canonical_positions = build_canonical_positions(&local_chain, &graph);
+
+        // Assert that confirmed spends are returned correctly.
+        assert_eq!(
+            canonical_spends
+                .get(&OutPoint::new(tx_0.compute_txid(), 0))
+                .cloned(),
+            Some((
+                ChainPosition::Confirmed {
+                    anchor: ConfirmationBlockTime {
+                        block_id: tip.get(98).unwrap().block_id(),
+                        confirmation_time: 100
                     },
+                    transitively: None,
+                },
+                tx_1.compute_txid(),
+            )),
+        );
+        // Check if chain position is returned correctly.
+        assert_eq!(
+            canonical_positions.get(&tx_0.compute_txid()).cloned(),
+            Some(ChainPosition::Confirmed {
+                anchor: ConfirmationBlockTime {
+                    block_id: tip.get(95).unwrap().block_id(),
                     confirmation_time: 100
                 },
                 transitively: None
-            },
-            tx_1.compute_txid(),
-        )),
-    );
-
-    // Check if chain position is returned correctly.
-    assert_eq!(
-        graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
-        // Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
-        Some(ChainPosition::Confirmed {
-            anchor: &ConfirmationBlockTime {
-                block_id: BlockId {
-                    hash: tip.get(95).unwrap().hash(),
-                    height: 95,
-                },
-                confirmation_time: 100
-            },
-            transitively: None
-        })
-    );
+            })
+        );
+    }
 
     // Mark the unconfirmed as seen and check correct ObservedAs status is returned.
     let _ = graph.insert_seen_at(tx_2.compute_txid(), 1234567);
+    {
+        let canonical_spends = build_canonical_spends(&local_chain, &graph);
 
-    // Check chain spend returned correctly.
-    assert_eq!(
-        graph
-            .get_chain_spend(
-                &local_chain,
-                tip.block_id(),
-                OutPoint::new(tx_0.compute_txid(), 1)
-            )
-            .unwrap(),
-        (
-            ChainPosition::Unconfirmed {
-                last_seen: Some(1234567)
-            },
-            tx_2.compute_txid()
-        )
-    );
+        // Check chain spend returned correctly.
+        assert_eq!(
+            canonical_spends
+                .get(&OutPoint::new(tx_0.compute_txid(), 1))
+                .cloned(),
+            Some((
+                ChainPosition::Unconfirmed {
+                    last_seen: Some(1234567)
+                },
+                tx_2.compute_txid()
+            ))
+        );
+    }
 
     // A conflicting transaction that conflicts with tx_1.
     let tx_1_conflict = Transaction {
@@ -944,11 +958,14 @@ fn test_chain_spends() {
         ..new_tx(0)
     };
     let _ = graph.insert_tx(tx_1_conflict.clone());
+    {
+        let canonical_positions = build_canonical_positions(&local_chain, &graph);
 
-    // Because this tx conflicts with an already confirmed transaction, chain position should return none.
-    assert!(graph
-        .get_chain_position(&local_chain, tip.block_id(), tx_1_conflict.compute_txid())
-        .is_none());
+        // Because this tx conflicts with an already confirmed transaction, chain position should return none.
+        assert!(canonical_positions
+            .get(&tx_1_conflict.compute_txid())
+            .is_none());
+    }
 
     // Another conflicting tx that conflicts with tx_2.
     let tx_2_conflict = Transaction {
@@ -958,42 +975,39 @@ fn test_chain_spends() {
         }],
         ..new_tx(0)
     };
-
     // Insert in graph and mark it as seen.
     let _ = graph.insert_tx(tx_2_conflict.clone());
     let _ = graph.insert_seen_at(tx_2_conflict.compute_txid(), 1234568);
+    {
+        let canonical_spends = build_canonical_spends(&local_chain, &graph);
+        let canonical_positions = build_canonical_positions(&local_chain, &graph);
 
-    // This should return a valid observation with correct last seen.
-    assert_eq!(
-        graph
-            .get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid())
-            .expect("position expected"),
-        ChainPosition::Unconfirmed {
-            last_seen: Some(1234568)
-        }
-    );
-
-    // Chain_spend now catches the new transaction as the spend.
-    assert_eq!(
-        graph
-            .get_chain_spend(
-                &local_chain,
-                tip.block_id(),
-                OutPoint::new(tx_0.compute_txid(), 1)
-            )
-            .expect("expect observation"),
-        (
-            ChainPosition::Unconfirmed {
+        // This should return a valid observation with correct last seen.
+        assert_eq!(
+            canonical_positions
+                .get(&tx_2_conflict.compute_txid())
+                .cloned(),
+            Some(ChainPosition::Unconfirmed {
                 last_seen: Some(1234568)
-            },
-            tx_2_conflict.compute_txid()
-        )
-    );
+            })
+        );
 
-    // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
-    assert!(graph
-        .get_chain_position(&local_chain, tip.block_id(), tx_2.compute_txid())
-        .is_none());
+        // Chain_spend now catches the new transaction as the spend.
+        assert_eq!(
+            canonical_spends
+                .get(&OutPoint::new(tx_0.compute_txid(), 1))
+                .cloned(),
+            Some((
+                ChainPosition::Unconfirmed {
+                    last_seen: Some(1234568)
+                },
+                tx_2_conflict.compute_txid()
+            ))
+        );
+
+        // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
+        assert!(canonical_positions.get(&tx_2.compute_txid()).is_none());
+    }
 }
 
 /// Ensure that `last_seen` values only increase during [`Merge::merge`].
index 9174ebab21ef33d03effa0fe8757dd7a03644815..542db3b0664bcd4d2e16a1763792a0fd74f2d0a4 100644 (file)
@@ -59,7 +59,7 @@ pub mod signer;
 pub mod tx_builder;
 pub(crate) mod utils;
 
-use crate::collections::{BTreeMap, HashMap};
+use crate::collections::{BTreeMap, HashMap, HashSet};
 use crate::descriptor::{
     check_wallet_descriptor, error::Error as DescriptorError, policy::BuildSatisfaction,
     DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor,
@@ -1061,13 +1061,9 @@ impl Wallet {
     /// [`Anchor`]: bdk_chain::Anchor
     pub fn get_tx(&self, txid: Txid) -> Option<WalletTx> {
         let graph = self.indexed_graph.graph();
-
-        Some(WalletTx {
-            chain_position: graph
-                .get_chain_position(&self.chain, self.chain.tip().block_id(), txid)?
-                .cloned(),
-            tx_node: graph.get_tx_node(txid)?,
-        })
+        graph
+            .list_canonical_txs(&self.chain, self.chain.tip().block_id())
+            .find(|tx| tx.tx_node.txid == txid)
     }
 
     /// Iterate over the transactions in the wallet.
@@ -1588,6 +1584,10 @@ impl Wallet {
         let graph = self.indexed_graph.graph();
         let txout_index = &self.indexed_graph.index;
         let chain_tip = self.chain.tip().block_id();
+        let chain_positions = graph
+            .list_canonical_txs(&self.chain, chain_tip)
+            .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position))
+            .collect::<HashMap<Txid, _>>();
 
         let mut tx = graph
             .get_tx(txid)
@@ -1595,10 +1595,11 @@ impl Wallet {
             .as_ref()
             .clone();
 
-        let pos = graph
-            .get_chain_position(&self.chain, chain_tip, txid)
-            .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
-        if pos.is_confirmed() {
+        if chain_positions
+            .get(&txid)
+            .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
+            .is_confirmed()
+        {
             return Err(BuildFeeBumpError::TransactionConfirmed(txid));
         }
 
@@ -1629,10 +1630,10 @@ impl Wallet {
                     .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
                 let txout = &prev_tx.output[txin.previous_output.vout as usize];
 
-                let chain_position = graph
-                    .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
-                    .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?
-                    .cloned();
+                let chain_position = chain_positions
+                    .get(&txin.previous_output.txid)
+                    .cloned()
+                    .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
 
                 let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) {
                     Some(&(keychain, derivation_index)) => {
@@ -1831,9 +1832,28 @@ impl Wallet {
         psbt: &mut Psbt,
         sign_options: SignOptions,
     ) -> Result<bool, SignerError> {
+        let tx = &psbt.unsigned_tx;
         let chain_tip = self.chain.tip().block_id();
+        let prev_txids = tx
+            .input
+            .iter()
+            .map(|txin| txin.previous_output.txid)
+            .collect::<HashSet<Txid>>();
+        let confirmation_heights = self
+            .indexed_graph
+            .graph()
+            .list_canonical_txs(&self.chain, chain_tip)
+            .filter(|canon_tx| prev_txids.contains(&canon_tx.tx_node.txid))
+            .take(prev_txids.len())
+            .map(|canon_tx| {
+                let txid = canon_tx.tx_node.txid;
+                match canon_tx.chain_position {
+                    ChainPosition::Confirmed { anchor, .. } => (txid, anchor.block_id.height),
+                    ChainPosition::Unconfirmed { .. } => (txid, u32::MAX),
+                }
+            })
+            .collect::<HashMap<Txid, u32>>();
 
-        let tx = &psbt.unsigned_tx;
         let mut finished = true;
 
         for (n, input) in tx.input.iter().enumerate() {
@@ -1844,15 +1864,9 @@ impl Wallet {
             if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
                 continue;
             }
-            let confirmation_height = self
-                .indexed_graph
-                .graph()
-                .get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
-                .map(|chain_position| {
-                    chain_position
-                        .confirmation_height_upper_bound()
-                        .unwrap_or(u32::MAX)
-                });
+            let confirmation_height = confirmation_heights
+                .get(&input.previous_output.txid)
+                .copied();
             let current_height = sign_options
                 .assume_height
                 .unwrap_or_else(|| self.chain.tip().height());
@@ -2012,20 +2026,22 @@ impl Wallet {
             return (must_spend, vec![]);
         }
 
+        let canon_txs = self
+            .indexed_graph
+            .graph()
+            .list_canonical_txs(&self.chain, chain_tip)
+            .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx))
+            .collect::<HashMap<Txid, _>>();
+
         let satisfies_confirmed = may_spend
             .iter()
             .map(|u| -> bool {
                 let txid = u.0.outpoint.txid;
-                let tx = match self.indexed_graph.graph().get_tx(txid) {
-                    Some(tx) => tx,
-                    None => return false,
-                };
-                let chain_position = match self.indexed_graph.graph().get_chain_position(
-                    &self.chain,
-                    chain_tip,
-                    txid,
-                ) {
-                    Some(chain_position) => chain_position.cloned(),
+                let (chain_position, tx) = match canon_txs.get(&txid) {
+                    Some(CanonicalTx {
+                        chain_position,
+                        tx_node,
+                    }) => (chain_position, tx_node.tx.clone()),
                     None => return false,
                 };