]> Untitled Git - bdk/commitdiff
feat(chain): add sent_and_received_txouts method to SPK and keychain indexes
authorSteve Myers <steve@notmandatory.org>
Wed, 10 Dec 2025 21:42:45 +0000 (15:42 -0600)
committerSteve Myers <steve@notmandatory.org>
Wed, 28 Jan 2026 21:52:12 +0000 (15:52 -0600)
Implement sent_and_received_txouts methods on SpkTxOutIndex and
KeychainTxOutIndex. These methods return actual TxOut structs allowing callers
to access complete transaction output information including script pubkeys
and values.

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

index 99931cf5ed5f7a5e3bed6e5783ab88da961200f4..b243460e7d600b999072024f6cd8f8f364cac423 100644 (file)
@@ -418,6 +418,21 @@ 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>,
+    ) -> (Vec<TxOut>, Vec<TxOut>) {
+        self.inner
+            .sent_and_received_txouts(tx, self.map_to_inner_bounds(range))
+    }
+
     /// Computes the net value that this transaction gives to the script pubkeys in the index and
     /// *takes* from the transaction outputs in the index. Shorthand for calling
     /// [`sent_and_received`] and subtracting sent from received.
index 32ad6f0d9ec17e404fd30a9409ce8b90cc22c7bb..a66ffef94bd828a34fa0d517efd58a24e8953a9e 100644 (file)
@@ -9,6 +9,8 @@ use crate::{
 };
 use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
 
+use alloc::vec::Vec;
+
 /// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
 ///
 /// The basic idea is that you insert script pubkeys you care about into the index with
@@ -318,6 +320,74 @@ 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 a tuple of (sent_txouts, received_txouts).
+    ///
+    /// # Example
+    /// Shows the addresses of the TxOut sent from or received by a Transaction relevant to all 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 sent and received txouts for a transaction across all tracked addresses
+    /// let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx, ..);
+    ///
+    /// // Display addresses and amounts
+    /// println!("Sent:");
+    /// for txout in sent_txouts {
+    ///     let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?;
+    ///     println!(" from {} - {} sats", address, txout.value.to_sat());
+    /// }
+    ///
+    /// println!("Received:");
+    /// for txout in received_txouts {
+    ///     let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?;
+    ///     println!(" to {} - {} sats", address, txout.value.to_sat());
+    /// }
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn sent_and_received_txouts(
+        &self,
+        tx: &Transaction,
+        range: impl RangeBounds<I>,
+    ) -> (Vec<TxOut>, Vec<TxOut>) {
+        let mut sent = Vec::new();
+        let mut received = Vec::new();
+
+        for txin in &tx.input {
+            if let Some((index, txout)) = self.txout(txin.previous_output) {
+                if range.contains(index) {
+                    sent.push(txout.clone());
+                }
+            }
+        }
+
+        for txout in &tx.output {
+            if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
+                if range.contains(index) {
+                    received.push(txout.clone());
+                }
+            }
+        }
+
+        (sent, received)
+    }
+
     /// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
     /// for calling [`sent_and_received`] and subtracting sent from received.
     ///
index 1aa6ff4c75c9a441921fe5a86987fde54d441105..537add2aee216056fad994524ca2263ca78ff398 100644 (file)
@@ -80,6 +80,119 @@ fn spk_txout_sent_and_received() {
     assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
 }
 
+#[test]
+fn spk_txout_sent_and_received_txouts() {
+    let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
+    let spk2 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
+
+    let mut index = SpkTxOutIndex::default();
+    index.insert_spk(0, spk1.clone());
+    index.insert_spk(1, spk2.clone());
+
+    let tx1 = Transaction {
+        version: transaction::Version::TWO,
+        lock_time: absolute::LockTime::ZERO,
+        input: vec![],
+        output: vec![TxOut {
+            value: Amount::from_sat(42_000),
+            script_pubkey: spk1.clone(),
+        }],
+    };
+
+    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, ..);
+    assert!(sent_txouts.is_empty());
+    assert_eq!(
+        received_txouts,
+        vec![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());
+    assert_eq!(
+        received_txouts,
+        vec![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() && received_txouts.is_empty());
+
+    index.index_tx(&tx1);
+
+    let tx2 = Transaction {
+        version: transaction::Version::ONE,
+        lock_time: absolute::LockTime::ZERO,
+        input: vec![TxIn {
+            previous_output: OutPoint {
+                txid: tx1.compute_txid(),
+                vout: 0,
+            },
+            ..Default::default()
+        }],
+        output: vec![
+            TxOut {
+                value: Amount::from_sat(20_000),
+                script_pubkey: spk2.clone(),
+            },
+            TxOut {
+                script_pubkey: spk1.clone(),
+                value: Amount::from_sat(30_000),
+            },
+        ],
+    };
+
+    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, ..);
+    assert_eq!(
+        sent_txouts,
+        vec![TxOut {
+            value: Amount::from_sat(42_000),
+            script_pubkey: spk1.clone(),
+        }]
+    );
+    assert_eq!(
+        received_txouts,
+        vec![
+            TxOut {
+                value: Amount::from_sat(20_000),
+                script_pubkey: spk2.clone(),
+            },
+            TxOut {
+                value: Amount::from_sat(30_000),
+                script_pubkey: spk1.clone(),
+            }
+        ]
+    );
+
+    let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, ..1);
+    assert_eq!(
+        sent_txouts,
+        vec![TxOut {
+            value: Amount::from_sat(42_000),
+            script_pubkey: spk1.clone(),
+        }]
+    );
+    assert_eq!(
+        received_txouts,
+        vec![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![TxOut {
+            value: Amount::from_sat(20_000),
+            script_pubkey: spk2.clone(),
+        }]
+    );
+}
+
 #[test]
 fn mark_used() {
     let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();