]> Untitled Git - bdk/commitdiff
feat(chain): add spent_txouts and created_txouts methods to SPK and keychain indexes
authorSteve Myers <steve@notmandatory.org>
Thu, 29 Jan 2026 02:27:15 +0000 (20:27 -0600)
committerSteve Myers <steve@notmandatory.org>
Thu, 29 Jan 2026 02:40:33 +0000 (20:40 -0600)
Implement sent_txouts and created_txouts methods on SpkTxOutIndex and
KeychainTxOutIndex. These methods return new SpentTxOut and CreatedTxOut
structs allowing callers to access complete transaction information
including script pubkeys, values, and spk index.

crates/chain/src/indexer.rs
crates/chain/src/indexer/keychain_txout.rs
crates/chain/src/indexer/spk_txout.rs
crates/chain/src/lib.rs
crates/chain/tests/test_spk_txout_index.rs

index 75d5ad4324588ba7cedb89a511e69501da03c055..22e8398152be2b3e74be4474d8c071388291f426 100644 (file)
@@ -1,18 +1,11 @@
 //! [`Indexer`] provides utilities for indexing transaction data.
 
-use alloc::vec::Vec;
 use bitcoin::{OutPoint, Transaction, TxOut};
 
 #[cfg(feature = "miniscript")]
 pub mod keychain_txout;
 pub mod spk_txout;
 
-/// Type alias for a list of indexed transaction outputs.
-///
-/// Each element is a tuple of `(index, TxOut)` where index is the index of the input or output in
-/// the original [`Transaction`].
-pub type IndexedTxOuts = Vec<(usize, TxOut)>;
-
 /// Utilities for indexing transaction data.
 ///
 /// Types which implement this trait can be used to construct an [`IndexedTxGraph`].
index df84dc2c6ea76a4e4e0a7f91601294eb9d5b447f..4a3495a848c4b140a13f6b2ae995dc7a1a48770d 100644 (file)
@@ -8,7 +8,7 @@ use crate::{
     spk_client::{FullScanRequestBuilder, SyncRequestBuilder},
     spk_iter::BIP32_MAX_INDEX,
     spk_txout::SpkTxOutIndex,
-    DescriptorExt, DescriptorId, Indexed, IndexedTxOuts, Indexer, KeychainIndexed, SpkIterator,
+    DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator,
 };
 use alloc::{borrow::ToOwned, vec::Vec};
 use bitcoin::{
@@ -19,6 +19,7 @@ use core::{
     ops::{Bound, RangeBounds},
 };
 
+use crate::spk_txout::{CreatedTxOut, SpentTxOut};
 use crate::Merge;
 
 /// The default lookahead for a [`KeychainTxOutIndex`]
@@ -418,19 +419,25 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
             .sent_and_received(tx, self.map_to_inner_bounds(range))
     }
 
-    /// Returns the sent and received [`TxOut`]s for this `tx` relative to the script pubkeys
-    /// belonging to the keychains in `range`. A TxOut is *sent* when a script pubkey in the
-    /// `range` is on an input and *received* when it is on an output. For `sent` to be computed
-    /// correctly, the index must have already scanned the output being spent. Calculating
-    /// received just uses the [`Transaction`] outputs directly, so it will be correct even if
-    /// it has not been scanned.
-    pub fn sent_and_received_txouts(
-        &self,
-        tx: &Transaction,
-        range: impl RangeBounds<K>,
-    ) -> (IndexedTxOuts, IndexedTxOuts) {
-        self.inner
-            .sent_and_received_txouts(tx, self.map_to_inner_bounds(range))
+    /// Returns the [`SpentTxOut`]s for the `tx` relative to the script pubkeys belonging to the
+    /// keychain. A TxOut is *spent* when a keychain script pubkey is in any input. For
+    /// `spent_txouts` to be computed correctly, the index must have already scanned the output
+    /// being spent.
+    pub fn spent_txouts<'a>(
+        &'a self,
+        tx: &'a Transaction,
+    ) -> impl Iterator<Item = SpentTxOut<(K, u32)>> + 'a {
+        self.inner.spent_txouts(tx)
+    }
+
+    /// Returns the [`CreatedTxOut`]s for the `tx` relative to the script pubkeys
+    /// belonging to the keychain. A TxOut is *created* when it is on an output.
+    /// These are computed directly from the transaction outputs.
+    pub fn created_txouts<'a>(
+        &'a self,
+        tx: &'a Transaction,
+    ) -> impl Iterator<Item = CreatedTxOut<(K, u32)>> + 'a {
+        self.inner.created_txouts(tx)
     }
 
     /// Computes the net value that this transaction gives to the script pubkeys in the index and
index 73aee0c8b50390427ceb427a9dd283dc42d30fee..2bf78ec2d77b647d1757804065abf166d1103462 100644 (file)
@@ -5,11 +5,9 @@ use core::ops::RangeBounds;
 
 use crate::{
     collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
-    IndexedTxOuts, Indexer,
+    Indexer,
 };
-use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
-
-use alloc::vec::Vec;
+use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid};
 
 /// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
 ///
@@ -320,17 +318,14 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
         (sent, received)
     }
 
-    /// Collects the sent and received [`TxOut`]s for `tx` on the script pubkeys in `range`.
-    /// TxOuts are *sent* when a script pubkey in the `range` is on an input and *received* when
-    /// it is on an output. For `sent` to be computed correctly, the index must have already
-    /// scanned the output being spent. Calculating received just uses the [`Transaction`]
-    /// outputs directly, so it will be correct even if it has not been scanned.
+    /// Returns the relevant [`SpentTxOut`]s for a [`Transaction`]
     ///
-    /// Returns a tuple of (sent_txouts, received_txouts).
+    /// TxOuts are *spent* when an indexed script pubkey is found in one of the transaction's
+    /// inputs. For these to be computed correctly, the index must have already scanned the
+    /// output being spent.
     ///
     /// # Example
-    /// Shows the addresses of the TxOut sent from or received by a Transaction relevant to all spks
-    /// in this index.
+    /// Shows the addresses of the TxOut spent from a Transaction relevant to spks in this index.
     ///
     /// ```rust
     /// # use bdk_chain::spk_txout::SpkTxOutIndex;
@@ -343,49 +338,86 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
     /// // ... scan transactions to populate the index ...
     /// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
     ///
-    /// // Get sent and received txouts for a transaction across all tracked addresses
-    /// let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx, ..);
+    /// // Get spent txouts for a transaction for all indexed spks
+    /// let spent_txouts = index.spent_txouts(&tx);
     ///
     /// // Display addresses and amounts
-    /// println!("Sent:");
-    /// for (i, txout) in sent_txouts {
-    ///     let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?;
-    ///     println!("input {}: from {} - {} sats", i, address, txout.value.to_sat());
+    /// println!("Spent:");
+    /// for spent in spent_txouts {
+    ///     let address = Address::from_script(&spent.txout.script_pubkey, Network::Bitcoin)?;
+    ///     println!("input {}: from {} - {}", spent.outpoint().vout, address, &spent.txout.value.to_sat());
     /// }
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn spent_txouts<'a>(
+        &'a self,
+        tx: &'a Transaction,
+    ) -> impl Iterator<Item = SpentTxOut<I>> + 'a {
+        tx.input
+            .iter()
+            .enumerate()
+            .filter_map(|(input_index, txin)| {
+                self.txout(txin.previous_output)
+                    .map(|(index, txout)| SpentTxOut {
+                        txout: txout.clone(),
+                        spending_input: txin.clone(),
+                        spending_input_index: u32::try_from(input_index)
+                            .expect("invalid input index"),
+                        spk_index: index.clone(),
+                    })
+            })
+    }
+
+    /// Returns the relevant [`CreatedTxOut`]s for a [`Transaction`]
     ///
-    /// println!("Received:");
-    /// for (i, txout) in received_txouts {
-    ///     let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?;
-    ///     println!("output {}: to {} + {} sats", i, address, txout.value.to_sat());
+    /// TxOuts are *created* when an indexed script pubkey is found in one of the transaction's
+    /// outputs. These are computed directly from the transaction outputs.
+    ///
+    /// # Example
+    /// Shows the addresses of the TxOut created by a Transaction relevant to spks in this index.
+    ///
+    /// ```rust
+    /// # use bdk_chain::spk_txout::SpkTxOutIndex;
+    /// # use bitcoin::{Address, Network, Transaction};
+    /// # use std::str::FromStr;
+    /// #
+    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
+    /// let mut index = SpkTxOutIndex::<u32>::default();
+    ///
+    /// // ... scan transactions to populate the index ...
+    /// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
+    ///
+    /// // Get created txouts for a transaction for all indexed spks
+    /// let created_txouts = index.created_txouts(&tx);
+    ///
+    /// // Display addresses and amounts
+    /// println!("Created:");
+    /// for created in created_txouts {
+    ///     let address = Address::from_script(&created.txout.script_pubkey, Network::Bitcoin)?;
+    ///     println!("output {}: to {} + {}", &created.outpoint.vout, address, &created.txout.value.display_dynamic());
     /// }
     /// # Ok(())
     /// # }
     /// ```
-    pub fn sent_and_received_txouts(
-        &self,
-        tx: &Transaction,
-        range: impl RangeBounds<I>,
-    ) -> (IndexedTxOuts, IndexedTxOuts) {
-        let mut sent = Vec::new();
-        let mut received = Vec::new();
-
-        for (i, txin) in tx.input.iter().enumerate() {
-            if let Some((index, txout)) = self.txout(txin.previous_output) {
-                if range.contains(index) {
-                    sent.push((i, txout.clone()));
-                }
-            }
-        }
-
-        for (i, txout) in tx.output.iter().enumerate() {
-            if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
-                if range.contains(index) {
-                    received.push((i, txout.clone()));
-                }
-            }
-        }
-
-        (sent, received)
+    pub fn created_txouts<'a>(
+        &'a self,
+        tx: &'a Transaction,
+    ) -> impl Iterator<Item = CreatedTxOut<I>> + 'a {
+        tx.output
+            .iter()
+            .enumerate()
+            .filter_map(|(output_index, txout)| {
+                self.index_of_spk(txout.script_pubkey.clone())
+                    .map(|index| CreatedTxOut {
+                        outpoint: OutPoint {
+                            txid: tx.compute_txid(),
+                            vout: u32::try_from(output_index).expect("invalid output index"),
+                        },
+                        txout: txout.clone(),
+                        spk_index: index.clone(),
+                    })
+            })
     }
 
     /// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
@@ -437,3 +469,38 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
         spks_from_inputs.chain(spks_from_outputs).collect()
     }
 }
+
+/// A transaction output that was spent by a transaction input.
+///
+/// Contains information about the spent output and the input that spent it.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+pub struct SpentTxOut<I> {
+    /// The transaction output that was spent.
+    pub txout: TxOut,
+    /// The transaction input that spent the output.
+    pub spending_input: TxIn,
+    /// The index of the spending input in the transaction.
+    pub spending_input_index: u32,
+    /// The script pubkey index associated with the spent output.
+    pub spk_index: I,
+}
+
+impl<I> SpentTxOut<I> {
+    /// Returns the outpoint of the spent transaction output.
+    pub fn outpoint(&self) -> OutPoint {
+        self.spending_input.previous_output
+    }
+}
+
+/// A transaction output that was created by a transaction.
+///
+/// Contains information about the created output and its location.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+pub struct CreatedTxOut<I> {
+    /// The outpoint identifying the created output.
+    pub outpoint: OutPoint,
+    /// The transaction output that was created.
+    pub txout: TxOut,
+    /// The script pubkey index associated with the created output.
+    pub spk_index: I,
+}
index e81fef78e0644e630095fca819dfd42eeab8e82d..be9170b1a5b310161ec41aa5e392a0cd768c743a 100644 (file)
@@ -36,7 +36,7 @@ pub mod indexed_tx_graph;
 pub use indexed_tx_graph::IndexedTxGraph;
 pub mod indexer;
 pub use indexer::spk_txout;
-pub use indexer::{IndexedTxOuts, Indexer};
+pub use indexer::Indexer;
 pub mod local_chain;
 mod tx_data_traits;
 pub use tx_data_traits::*;
index ea460bbbc10b06ca1f4a19deb94fda6b7f6a5dca..591a2b1c6389a97b4bc72624c07d034ba4457512 100644 (file)
@@ -1,3 +1,4 @@
+use bdk_chain::spk_txout::{CreatedTxOut, SpentTxOut};
 use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
 use bitcoin::{
     absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
@@ -81,13 +82,13 @@ fn spk_txout_sent_and_received() {
 }
 
 #[test]
-fn spk_txout_sent_and_received_txouts() {
-    let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
-    let spk2 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
+fn spk_txout_spent_created_txouts() {
+    let spk0 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
+    let spk1 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
 
     let mut index = SpkTxOutIndex::default();
-    index.insert_spk(0, spk1.clone());
-    index.insert_spk(1, spk2.clone());
+    index.insert_spk(0, spk0.clone());
+    index.insert_spk(1, spk1.clone());
 
     let tx1 = Transaction {
         version: transaction::Version::TWO,
@@ -95,37 +96,29 @@ fn spk_txout_sent_and_received_txouts() {
         input: vec![],
         output: vec![TxOut {
             value: Amount::from_sat(42_000),
-            script_pubkey: spk1.clone(),
+            script_pubkey: spk0.clone(),
         }],
     };
-    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, ..);
-    assert!(sent_txouts.is_empty());
-    assert_eq!(
-        received_txouts,
-        vec![(
-            0,
-            TxOut {
-                value: Amount::from_sat(42_000),
-                script_pubkey: spk1.clone(),
-            }
-        )]
-    );
-    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, ..1);
-    assert!(sent_txouts.is_empty());
+    index.scan(&tx1);
+    let spent_txouts = index.spent_txouts(&tx1).collect::<Vec<_>>();
+    assert!(spent_txouts.is_empty());
+
+    let created_txouts = index.created_txouts(&tx1).collect::<Vec<_>>();
+    assert_eq!(created_txouts.len(), 1);
     assert_eq!(
-        received_txouts,
-        vec![(
-            0,
-            TxOut {
+        created_txouts[0],
+        CreatedTxOut {
+            outpoint: OutPoint {
+                txid: tx1.compute_txid(),
+                vout: 0,
+            },
+            txout: TxOut {
                 value: Amount::from_sat(42_000),
-                script_pubkey: spk1.clone(),
-            }
-        )]
+                script_pubkey: spk0.clone(),
+            },
+            spk_index: 0,
+        }
     );
-    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, 1..);
-    assert!(sent_txouts.is_empty() && received_txouts.is_empty());
-
-    index.index_tx(&tx1);
 
     let tx2 = Transaction {
         version: transaction::Version::ONE,
@@ -140,79 +133,66 @@ fn spk_txout_sent_and_received_txouts() {
         output: vec![
             TxOut {
                 value: Amount::from_sat(20_000),
-                script_pubkey: spk2.clone(),
+                script_pubkey: spk1.clone(),
             },
             TxOut {
-                script_pubkey: spk1.clone(),
+                script_pubkey: spk0.clone(),
                 value: Amount::from_sat(30_000),
             },
         ],
     };
+    index.scan(&tx2);
 
-    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, ..);
+    let spent_txouts = index.spent_txouts(&tx2).collect::<Vec<_>>();
+    assert_eq!(spent_txouts.len(), 1);
     assert_eq!(
-        sent_txouts,
-        vec![(
-            0,
-            TxOut {
+        spent_txouts[0],
+        SpentTxOut {
+            txout: TxOut {
                 value: Amount::from_sat(42_000),
-                script_pubkey: spk1.clone(),
-            }
-        )]
-    );
-    assert_eq!(
-        received_txouts,
-        vec![
-            (
-                0,
-                TxOut {
-                    value: Amount::from_sat(20_000),
-                    script_pubkey: spk2.clone(),
-                }
-            ),
-            (
-                1,
-                TxOut {
-                    value: Amount::from_sat(30_000),
-                    script_pubkey: spk1.clone(),
-                }
-            )
-        ]
+                script_pubkey: spk0.clone(),
+            },
+            spending_input: TxIn {
+                previous_output: OutPoint {
+                    txid: tx1.compute_txid(),
+                    vout: 0,
+                },
+                ..Default::default()
+            },
+            spending_input_index: 0,
+            spk_index: 0,
+        }
     );
 
-    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, ..1);
+    let created_txouts = index.created_txouts(&tx2).collect::<Vec<_>>();
+    assert_eq!(created_txouts.len(), 2);
     assert_eq!(
-        sent_txouts,
-        vec![(
-            0,
-            TxOut {
-                value: Amount::from_sat(42_000),
+        created_txouts[0],
+        CreatedTxOut {
+            outpoint: OutPoint {
+                txid: tx2.compute_txid(),
+                vout: 0,
+            },
+            txout: TxOut {
+                value: Amount::from_sat(20_000),
                 script_pubkey: spk1.clone(),
-            }
-        )]
+            },
+            spk_index: 1,
+        }
     );
     assert_eq!(
-        received_txouts,
-        vec![(
-            1,
-            TxOut {
+        created_txouts[1],
+        CreatedTxOut {
+            outpoint: OutPoint {
+                txid: tx2.compute_txid(),
+                vout: 1,
+            },
+            txout: TxOut {
                 value: Amount::from_sat(30_000),
-                script_pubkey: spk1.clone(),
-            }
-        )]
-    );
-
-    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, 1..);
-    assert!(sent_txouts.is_empty());
-    assert_eq!(
-        received_txouts,
-        vec![(
-            0,
-            TxOut {
-                value: Amount::from_sat(20_000),
-                script_pubkey: spk2.clone(),
-            }
-        )]
+                script_pubkey: spk0.clone(),
+            },
+            spk_index: 0,
+        }
     );
 }