From b95e3cfd3befe0a71dd00b721d93792ef7393f60 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Fri, 13 Mar 2026 18:02:40 -0300 Subject: [PATCH 1/4] fix(ci): update pinned dependencies for `1.63.0` --- ci/pin-msrv.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ci/pin-msrv.sh b/ci/pin-msrv.sh index c1319a402..67929d867 100755 --- a/ci/pin-msrv.sh +++ b/ci/pin-msrv.sh @@ -32,8 +32,6 @@ cargo update -p tracing-core --precise "0.1.33" cargo update -p "webpki-roots@1.0.6" --precise "1.0.1" cargo update -p rayon --precise "1.10.0" cargo update -p rayon-core --precise "1.12.1" -cargo update -p quote --precise "1.0.41" -cargo update -p syn --precise "2.0.106" cargo update -p openssl --precise "0.10.73" cargo update -p openssl-sys --precise "0.9.109" cargo update -p "getrandom@0.4.2" --precise "0.2.17" @@ -48,7 +46,6 @@ cargo update -p futures-core --precise "0.3.31" cargo update -p futures-io --precise "0.3.31" cargo update -p futures-sink --precise "0.3.31" cargo update -p futures-task --precise "0.3.31" -cargo update -p proc-macro2 --precise "1.0.92" cargo update -p log --precise "0.4.22" cargo update -p itoa --precise "1.0.11" cargo update -p anyhow --precise "1.0.86" @@ -57,3 +54,9 @@ cargo update -p hyper-util --precise "0.1.6" cargo update -p pin-project --precise "1.1.5" cargo update -p pin-project-internal --precise "1.1.5" cargo update -p "rustls@0.23.37" --precise "0.23.26" + +# all pinning required due to `clap` usage in `example_cli` +cargo update -p "clap@4.6.0" --precise "4.5.17" +cargo update -p proc-macro2 --precise "1.0.92" +cargo update -p quote --precise "1.0.41" +cargo update -p syn --precise "2.0.106" From 3992f7fe174a9b3438b42bb7ac96e27d5d2a90f0 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 10 Dec 2025 15:42:45 -0600 Subject: [PATCH 2/4] 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. --- crates/chain/src/indexer/keychain_txout.rs | 15 +++ crates/chain/src/indexer/spk_txout.rs | 70 +++++++++++++ crates/chain/tests/test_spk_txout_index.rs | 113 +++++++++++++++++++++ 3 files changed, 198 insertions(+) diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index c4668cbe9..77a5b6177 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -415,6 +415,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 6fb55d577..62b56e94e 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, 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 @@ -315,6 +317,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 1aa6ff4c7..537add2ae 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(); From f2dbdfce56af0b8675b5901d65c2538901dd32f1 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Thu, 11 Dec 2025 11:26:25 -0600 Subject: [PATCH 3/4] feat(chain): add input/output indices to sent_and_received_txouts Return tuple of (index, TxOut) in sent_and_received_txouts methods to identify which input/output positions the TxOuts correspond to in the original transaction. --- crates/chain/src/indexer.rs | 7 ++ crates/chain/src/indexer/keychain_txout.rs | 4 +- crates/chain/src/indexer/spk_txout.rs | 20 ++--- crates/chain/src/lib.rs | 2 +- crates/chain/tests/test_spk_txout_index.rs | 89 ++++++++++++++-------- 5 files changed, 76 insertions(+), 46 deletions(-) diff --git a/crates/chain/src/indexer.rs b/crates/chain/src/indexer.rs index 22e839815..75d5ad432 100644 --- a/crates/chain/src/indexer.rs +++ b/crates/chain/src/indexer.rs @@ -1,11 +1,18 @@ //! [`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`]. diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 77a5b6177..4316295a7 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -8,7 +8,7 @@ use crate::{ spk_client::{FullScanRequestBuilder, SyncRequestBuilder}, spk_iter::BIP32_MAX_INDEX, spk_txout::SpkTxOutIndex, - DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator, + DescriptorExt, DescriptorId, Indexed, IndexedTxOuts, Indexer, KeychainIndexed, SpkIterator, }; use alloc::{borrow::ToOwned, vec::Vec}; use bitcoin::{ @@ -425,7 +425,7 @@ impl KeychainTxOutIndex { &self, tx: &Transaction, range: impl RangeBounds, - ) -> (Vec, Vec) { + ) -> (IndexedTxOuts, IndexedTxOuts) { self.inner .sent_and_received_txouts(tx, self.map_to_inner_bounds(range)) } diff --git a/crates/chain/src/indexer/spk_txout.rs b/crates/chain/src/indexer/spk_txout.rs index 62b56e94e..447055c73 100644 --- a/crates/chain/src/indexer/spk_txout.rs +++ b/crates/chain/src/indexer/spk_txout.rs @@ -5,7 +5,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - Indexer, + IndexedTxOuts, Indexer, }; use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; @@ -345,15 +345,15 @@ impl SpkTxOutIndex { /// /// // Display addresses and amounts /// println!("Sent:"); - /// for txout in sent_txouts { + /// for (i, txout) in sent_txouts { /// let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?; - /// println!(" from {} - {} sats", address, txout.value.to_sat()); + /// println!("input {}: from {} - {} sats", i, address, txout.value.to_sat()); /// } /// /// println!("Received:"); - /// for txout in received_txouts { + /// for (i, txout) in received_txouts { /// let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?; - /// println!(" to {} - {} sats", address, txout.value.to_sat()); + /// println!("output {}: to {} + {} sats", i, address, txout.value.to_sat()); /// } /// # Ok(()) /// # } @@ -362,22 +362,22 @@ impl SpkTxOutIndex { &self, tx: &Transaction, range: impl RangeBounds, - ) -> (Vec, Vec) { + ) -> (IndexedTxOuts, IndexedTxOuts) { let mut sent = Vec::new(); let mut received = Vec::new(); - for txin in &tx.input { + for (i, txin) in tx.input.iter().enumerate() { if let Some((index, txout)) = self.txout(txin.previous_output) { if range.contains(index) { - sent.push(txout.clone()); + sent.push((i, txout.clone())); } } } - for txout in &tx.output { + 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(txout.clone()); + received.push((i, txout.clone())); } } } diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 0cb5b48d3..9aeb26894 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -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::Indexer; +pub use indexer::{IndexedTxOuts, Indexer}; pub mod local_chain; mod tx_data_traits; pub use tx_data_traits::*; diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs index 537add2ae..ea460bbbc 100644 --- a/crates/chain/tests/test_spk_txout_index.rs +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -98,24 +98,29 @@ fn spk_txout_sent_and_received_txouts() { 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(), - }] + 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()); assert_eq!( received_txouts, - vec![TxOut { - value: Amount::from_sat(42_000), - script_pubkey: spk1.clone(), - }] + 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() && received_txouts.is_empty()); @@ -147,49 +152,67 @@ fn spk_txout_sent_and_received_txouts() { 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(), - }] + vec![( + 0, + 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(), - } + ( + 0, + TxOut { + value: Amount::from_sat(20_000), + script_pubkey: spk2.clone(), + } + ), + ( + 1, + 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(), - }] + vec![( + 0, + 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(), - }] + vec![( + 1, + 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(), - }] + vec![( + 0, + TxOut { + value: Amount::from_sat(20_000), + script_pubkey: spk2.clone(), + } + )] ); } From a8fda13a38a2ea3ae9b9ae6663d7b5217f426522 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 28 Jan 2026 20:27:15 -0600 Subject: [PATCH 4/4] feat(chain): add spent_txouts and created_txouts methods to SPK and keychain indexes 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 | 7 - crates/chain/src/indexer/keychain_txout.rs | 35 +++-- crates/chain/src/indexer/spk_txout.rs | 161 +++++++++++++++------ crates/chain/src/lib.rs | 2 +- crates/chain/tests/test_spk_txout_index.rs | 156 +++++++++----------- 5 files changed, 204 insertions(+), 157 deletions(-) diff --git a/crates/chain/src/indexer.rs b/crates/chain/src/indexer.rs index 75d5ad432..22e839815 100644 --- a/crates/chain/src/indexer.rs +++ b/crates/chain/src/indexer.rs @@ -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`]. diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index 4316295a7..1a1c4c0a3 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -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`] @@ -415,19 +416,25 @@ 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, - ) -> (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> + '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> + 'a { + self.inner.created_txouts(tx) } /// Computes the net value that this transaction gives to the script pubkeys in the index and diff --git a/crates/chain/src/indexer/spk_txout.rs b/crates/chain/src/indexer/spk_txout.rs index 447055c73..588e1281c 100644 --- a/crates/chain/src/indexer/spk_txout.rs +++ b/crates/chain/src/indexer/spk_txout.rs @@ -5,11 +5,9 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - IndexedTxOuts, Indexer, + Indexer, }; -use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; - -use alloc::vec::Vec; +use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid}; /// An index storing [`TxOut`]s that have a script pubkey that matches those in a list. /// @@ -317,17 +315,14 @@ 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 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; @@ -340,49 +335,86 @@ impl SpkTxOutIndex { /// // ... 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> + '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`] + /// + /// 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> { + /// let mut index = SpkTxOutIndex::::default(); /// - /// 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()); + /// // ... 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, - ) -> (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> + '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 @@ -434,3 +466,38 @@ impl SpkTxOutIndex { 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 { + /// 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 SpentTxOut { + /// 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 { + /// 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, +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 9aeb26894..0cb5b48d3 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -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::*; diff --git a/crates/chain/tests/test_spk_txout_index.rs b/crates/chain/tests/test_spk_txout_index.rs index ea460bbbc..591a2b1c6 100644 --- a/crates/chain/tests/test_spk_txout_index.rs +++ b/crates/chain/tests/test_spk_txout_index.rs @@ -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::>(); + assert!(spent_txouts.is_empty()); + + let created_txouts = index.created_txouts(&tx1).collect::>(); + 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::>(); + 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::>(); + 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, + } ); }