From: Steve Myers Date: Wed, 10 Dec 2025 21:42:45 +0000 (-0600) Subject: feat(chain): add sent_and_received_txouts method to SPK and keychain indexes X-Git-Url: http://internal-gitweb-vhost/address/enum.FromScriptError.html?a=commitdiff_plain;h=7777dfc08cd5141b50dc239abeca7316684f95d8;p=bdk feat(chain): add sent_and_received_txouts method to SPK and keychain indexes 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. --- diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 99931cf5..b243460e 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -418,6 +418,21 @@ impl KeychainTxOutIndex { .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, + ) -> (Vec, Vec) { + 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. diff --git a/crates/chain/src/indexer/spk_txout.rs b/crates/chain/src/indexer/spk_txout.rs index 32ad6f0d..a66ffef9 100644 --- a/crates/chain/src/indexer/spk_txout.rs +++ b/crates/chain/src/indexer/spk_txout.rs @@ -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 SpkTxOutIndex { (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> { + /// let mut index = SpkTxOutIndex::::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, + ) -> (Vec, Vec) { + 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. /// diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs index 1aa6ff4c..537add2a 100644 --- a/crates/chain/tests/test_spk_txout_index.rs +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -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();