Skip to content

Commit b927934

Browse files
committed
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.
1 parent 8149e1b commit b927934

File tree

3 files changed

+198
-0
lines changed

3 files changed

+198
-0
lines changed

crates/chain/src/indexer/keychain_txout.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,21 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
418418
.sent_and_received(tx, self.map_to_inner_bounds(range))
419419
}
420420

421+
/// Returns the sent and received [`TxOut`]s for this `tx` relative to the script pubkeys
422+
/// belonging to the keychains in `range`. A TxOut is *sent* when a script pubkey in the
423+
/// `range` is on an input and *received* when it is on an output. For `sent` to be computed
424+
/// correctly, the index must have already scanned the output being spent. Calculating
425+
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if
426+
/// it has not been scanned.
427+
pub fn sent_and_received_txouts(
428+
&self,
429+
tx: &Transaction,
430+
range: impl RangeBounds<K>,
431+
) -> (Vec<TxOut>, Vec<TxOut>) {
432+
self.inner
433+
.sent_and_received_txouts(tx, self.map_to_inner_bounds(range))
434+
}
435+
421436
/// Computes the net value that this transaction gives to the script pubkeys in the index and
422437
/// *takes* from the transaction outputs in the index. Shorthand for calling
423438
/// [`sent_and_received`] and subtracting sent from received.

crates/chain/src/indexer/spk_txout.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use crate::{
99
};
1010
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
1111

12+
use alloc::vec::Vec;
13+
1214
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
1315
///
1416
/// 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> {
318320
(sent, received)
319321
}
320322

323+
/// Collects the sent and received [`TxOut`]s for `tx` on the script pubkeys in `range`.
324+
/// TxOuts are *sent* when a script pubkey in the `range` is on an input and *received* when
325+
/// it is on an output. For `sent` to be computed correctly, the index must have already
326+
/// scanned the output being spent. Calculating received just uses the [`Transaction`]
327+
/// outputs directly, so it will be correct even if it has not been scanned.
328+
///
329+
/// Returns a tuple of (sent_txouts, received_txouts).
330+
///
331+
/// # Example
332+
/// Shows the addresses of the TxOut sent from or received by a Transaction relevant to all spks
333+
/// in this index.
334+
///
335+
/// ```rust
336+
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
337+
/// # use bitcoin::{Address, Network, Transaction};
338+
/// # use std::str::FromStr;
339+
/// #
340+
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
341+
/// let mut index = SpkTxOutIndex::<u32>::default();
342+
///
343+
/// // ... scan transactions to populate the index ...
344+
/// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
345+
///
346+
/// // Get sent and received txouts for a transaction across all tracked addresses
347+
/// let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx, ..);
348+
///
349+
/// // Display addresses and amounts
350+
/// println!("Sent:");
351+
/// for txout in sent_txouts {
352+
/// let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?;
353+
/// println!(" from {} - {} sats", address, txout.value.to_sat());
354+
/// }
355+
///
356+
/// println!("Received:");
357+
/// for txout in received_txouts {
358+
/// let address = Address::from_script(&txout.script_pubkey, Network::Bitcoin)?;
359+
/// println!(" to {} - {} sats", address, txout.value.to_sat());
360+
/// }
361+
/// # Ok(())
362+
/// # }
363+
/// ```
364+
pub fn sent_and_received_txouts(
365+
&self,
366+
tx: &Transaction,
367+
range: impl RangeBounds<I>,
368+
) -> (Vec<TxOut>, Vec<TxOut>) {
369+
let mut sent = Vec::new();
370+
let mut received = Vec::new();
371+
372+
for txin in &tx.input {
373+
if let Some((index, txout)) = self.txout(txin.previous_output) {
374+
if range.contains(index) {
375+
sent.push(txout.clone());
376+
}
377+
}
378+
}
379+
380+
for txout in &tx.output {
381+
if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
382+
if range.contains(index) {
383+
received.push(txout.clone());
384+
}
385+
}
386+
}
387+
388+
(sent, received)
389+
}
390+
321391
/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
322392
/// for calling [`sent_and_received`] and subtracting sent from received.
323393
///

crates/chain/tests/test_spk_txout_index.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,119 @@ fn spk_txout_sent_and_received() {
8080
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
8181
}
8282

83+
#[test]
84+
fn spk_txout_sent_and_received_txouts() {
85+
let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
86+
let spk2 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
87+
88+
let mut index = SpkTxOutIndex::default();
89+
index.insert_spk(0, spk1.clone());
90+
index.insert_spk(1, spk2.clone());
91+
92+
let tx1 = Transaction {
93+
version: transaction::Version::TWO,
94+
lock_time: absolute::LockTime::ZERO,
95+
input: vec![],
96+
output: vec![TxOut {
97+
value: Amount::from_sat(42_000),
98+
script_pubkey: spk1.clone(),
99+
}],
100+
};
101+
102+
let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, ..);
103+
assert!(sent_txouts.is_empty());
104+
assert_eq!(
105+
received_txouts,
106+
vec![TxOut {
107+
value: Amount::from_sat(42_000),
108+
script_pubkey: spk1.clone(),
109+
}]
110+
);
111+
let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, ..1);
112+
assert!(sent_txouts.is_empty());
113+
assert_eq!(
114+
received_txouts,
115+
vec![TxOut {
116+
value: Amount::from_sat(42_000),
117+
script_pubkey: spk1.clone(),
118+
}]
119+
);
120+
let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx1, 1..);
121+
assert!(sent_txouts.is_empty() && received_txouts.is_empty());
122+
123+
index.index_tx(&tx1);
124+
125+
let tx2 = Transaction {
126+
version: transaction::Version::ONE,
127+
lock_time: absolute::LockTime::ZERO,
128+
input: vec![TxIn {
129+
previous_output: OutPoint {
130+
txid: tx1.compute_txid(),
131+
vout: 0,
132+
},
133+
..Default::default()
134+
}],
135+
output: vec![
136+
TxOut {
137+
value: Amount::from_sat(20_000),
138+
script_pubkey: spk2.clone(),
139+
},
140+
TxOut {
141+
script_pubkey: spk1.clone(),
142+
value: Amount::from_sat(30_000),
143+
},
144+
],
145+
};
146+
147+
let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, ..);
148+
assert_eq!(
149+
sent_txouts,
150+
vec![TxOut {
151+
value: Amount::from_sat(42_000),
152+
script_pubkey: spk1.clone(),
153+
}]
154+
);
155+
assert_eq!(
156+
received_txouts,
157+
vec![
158+
TxOut {
159+
value: Amount::from_sat(20_000),
160+
script_pubkey: spk2.clone(),
161+
},
162+
TxOut {
163+
value: Amount::from_sat(30_000),
164+
script_pubkey: spk1.clone(),
165+
}
166+
]
167+
);
168+
169+
let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, ..1);
170+
assert_eq!(
171+
sent_txouts,
172+
vec![TxOut {
173+
value: Amount::from_sat(42_000),
174+
script_pubkey: spk1.clone(),
175+
}]
176+
);
177+
assert_eq!(
178+
received_txouts,
179+
vec![TxOut {
180+
value: Amount::from_sat(30_000),
181+
script_pubkey: spk1.clone(),
182+
}]
183+
);
184+
185+
let (sent_txouts, received_txouts) = index.sent_and_received_txouts(&tx2, 1..);
186+
assert!(sent_txouts.is_empty());
187+
assert_eq!(
188+
received_txouts,
189+
vec![TxOut {
190+
value: Amount::from_sat(20_000),
191+
script_pubkey: spk2.clone(),
192+
}]
193+
);
194+
}
195+
83196
#[test]
84197
fn mark_used() {
85198
let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();

0 commit comments

Comments
 (0)