Skip to content

Commit 81d72ef

Browse files
committed
Merge #2081: Add spent/created_txouts to SPK and Keychain TxOut indexes
d17d85f feat(chain): add spent_txouts and created_txouts methods to SPK and keychain indexes (Steve Myers) b347b03 feat(chain): add input/output indices to sent_and_received_txouts (Steve Myers) 7777dfc feat(chain): add sent_and_received_txouts method to SPK and keychain indexes (Steve Myers) Pull request description: ### Description Implement spent_txouts and created_txouts methods on SpkTxOutIndex and KeychainTxOutIndex. These methods return SpentTxOut and CreatedTxOut structs allowing callers to access complete transaction output information including script pubkeys and values. ### Notes to the reviewers This info is useful to users who use the new WalletEvent data in bitcoindevkit/bdk_wallet#319. In particular to determine the addresses and amounts for TxOuts that are sent/spent or have been received in a newly seen or confirmed transaction. See: [bdk_wallet#319 comment](bitcoindevkit/bdk_wallet#319 (comment)) If/when this PR is merged I'll make a corresponding one to `bdk_wallet::Wallet`. ### Changelog notice Added * New spent_txouts and created_txouts methods on SpkTxOutIndex and KeychainTxOutIndex. * New SpentTxOut and CreatedTxOut structs returned by spent_txouts and created_txouts functions. ### Checklists #### All Submissions: * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: evanlinjin: ACK d17d85f Tree-SHA512: 29c5e97b2f75ca026086a1a32549fd1ac7081614bf344daef05c232779e3e80e350e4aeaea35626e82591cdbb73e4bbd451516a82380e27834d0f8fd0a89cb63
2 parents abe121d + d17d85f commit 81d72ef

File tree

3 files changed

+276
-1
lines changed

3 files changed

+276
-1
lines changed

crates/chain/src/indexer/keychain_txout.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use core::{
1919
ops::{Bound, RangeBounds},
2020
};
2121

22+
use crate::spk_txout::{CreatedTxOut, SpentTxOut};
2223
use crate::Merge;
2324

2425
/// The default lookahead for a [`KeychainTxOutIndex`]
@@ -418,6 +419,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
418419
.sent_and_received(tx, self.map_to_inner_bounds(range))
419420
}
420421

422+
/// Returns the [`SpentTxOut`]s for the `tx` relative to the script pubkeys belonging to the
423+
/// keychain. A TxOut is *spent* when a keychain script pubkey is in any input. For
424+
/// `spent_txouts` to be computed correctly, the index must have already scanned the output
425+
/// being spent.
426+
pub fn spent_txouts<'a>(
427+
&'a self,
428+
tx: &'a Transaction,
429+
) -> impl Iterator<Item = SpentTxOut<(K, u32)>> + 'a {
430+
self.inner.spent_txouts(tx)
431+
}
432+
433+
/// Returns the [`CreatedTxOut`]s for the `tx` relative to the script pubkeys
434+
/// belonging to the keychain. A TxOut is *created* when it is on an output.
435+
/// These are computed directly from the transaction outputs.
436+
pub fn created_txouts<'a>(
437+
&'a self,
438+
tx: &'a Transaction,
439+
) -> impl Iterator<Item = CreatedTxOut<(K, u32)>> + 'a {
440+
self.inner.created_txouts(tx)
441+
}
442+
421443
/// Computes the net value that this transaction gives to the script pubkeys in the index and
422444
/// *takes* from the transaction outputs in the index. Shorthand for calling
423445
/// [`sent_and_received`] and subtracting sent from received.

crates/chain/src/indexer/spk_txout.rs

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
88
Indexer,
99
};
10-
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
10+
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid};
1111

1212
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
1313
///
@@ -318,6 +318,108 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
318318
(sent, received)
319319
}
320320

321+
/// Returns the relevant [`SpentTxOut`]s for a [`Transaction`]
322+
///
323+
/// TxOuts are *spent* when an indexed script pubkey is found in one of the transaction's
324+
/// inputs. For these to be computed correctly, the index must have already scanned the
325+
/// output being spent.
326+
///
327+
/// # Example
328+
/// Shows the addresses of the TxOut spent from a Transaction relevant to spks in this index.
329+
///
330+
/// ```rust
331+
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
332+
/// # use bitcoin::{Address, Network, Transaction};
333+
/// # use std::str::FromStr;
334+
/// #
335+
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
336+
/// let mut index = SpkTxOutIndex::<u32>::default();
337+
///
338+
/// // ... scan transactions to populate the index ...
339+
/// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
340+
///
341+
/// // Get spent txouts for a transaction for all indexed spks
342+
/// let spent_txouts = index.spent_txouts(&tx);
343+
///
344+
/// // Display addresses and amounts
345+
/// println!("Spent:");
346+
/// for spent in spent_txouts {
347+
/// let address = Address::from_script(&spent.txout.script_pubkey, Network::Bitcoin)?;
348+
/// println!("input {}: from {} - {}", spent.outpoint().vout, address, &spent.txout.value.to_sat());
349+
/// }
350+
/// # Ok(())
351+
/// # }
352+
/// ```
353+
pub fn spent_txouts<'a>(
354+
&'a self,
355+
tx: &'a Transaction,
356+
) -> impl Iterator<Item = SpentTxOut<I>> + 'a {
357+
tx.input
358+
.iter()
359+
.enumerate()
360+
.filter_map(|(input_index, txin)| {
361+
self.txout(txin.previous_output)
362+
.map(|(index, txout)| SpentTxOut {
363+
txout: txout.clone(),
364+
spending_input: txin.clone(),
365+
spending_input_index: u32::try_from(input_index)
366+
.expect("invalid input index"),
367+
spk_index: index.clone(),
368+
})
369+
})
370+
}
371+
372+
/// Returns the relevant [`CreatedTxOut`]s for a [`Transaction`]
373+
///
374+
/// TxOuts are *created* when an indexed script pubkey is found in one of the transaction's
375+
/// outputs. These are computed directly from the transaction outputs.
376+
///
377+
/// # Example
378+
/// Shows the addresses of the TxOut created by a Transaction relevant to spks in this index.
379+
///
380+
/// ```rust
381+
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
382+
/// # use bitcoin::{Address, Network, Transaction};
383+
/// # use std::str::FromStr;
384+
/// #
385+
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
386+
/// let mut index = SpkTxOutIndex::<u32>::default();
387+
///
388+
/// // ... scan transactions to populate the index ...
389+
/// # let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::locktime::absolute::LockTime::ZERO, input: vec![], output: vec![] };
390+
///
391+
/// // Get created txouts for a transaction for all indexed spks
392+
/// let created_txouts = index.created_txouts(&tx);
393+
///
394+
/// // Display addresses and amounts
395+
/// println!("Created:");
396+
/// for created in created_txouts {
397+
/// let address = Address::from_script(&created.txout.script_pubkey, Network::Bitcoin)?;
398+
/// println!("output {}: to {} + {}", &created.outpoint.vout, address, &created.txout.value.display_dynamic());
399+
/// }
400+
/// # Ok(())
401+
/// # }
402+
/// ```
403+
pub fn created_txouts<'a>(
404+
&'a self,
405+
tx: &'a Transaction,
406+
) -> impl Iterator<Item = CreatedTxOut<I>> + 'a {
407+
tx.output
408+
.iter()
409+
.enumerate()
410+
.filter_map(|(output_index, txout)| {
411+
self.index_of_spk(txout.script_pubkey.clone())
412+
.map(|index| CreatedTxOut {
413+
outpoint: OutPoint {
414+
txid: tx.compute_txid(),
415+
vout: u32::try_from(output_index).expect("invalid output index"),
416+
},
417+
txout: txout.clone(),
418+
spk_index: index.clone(),
419+
})
420+
})
421+
}
422+
321423
/// Computes the net value transfer effect of `tx` on the script pubkeys in `range`. Shorthand
322424
/// for calling [`sent_and_received`] and subtracting sent from received.
323425
///
@@ -367,3 +469,38 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
367469
spks_from_inputs.chain(spks_from_outputs).collect()
368470
}
369471
}
472+
473+
/// A transaction output that was spent by a transaction input.
474+
///
475+
/// Contains information about the spent output and the input that spent it.
476+
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
477+
pub struct SpentTxOut<I> {
478+
/// The transaction output that was spent.
479+
pub txout: TxOut,
480+
/// The transaction input that spent the output.
481+
pub spending_input: TxIn,
482+
/// The index of the spending input in the transaction.
483+
pub spending_input_index: u32,
484+
/// The script pubkey index associated with the spent output.
485+
pub spk_index: I,
486+
}
487+
488+
impl<I> SpentTxOut<I> {
489+
/// Returns the outpoint of the spent transaction output.
490+
pub fn outpoint(&self) -> OutPoint {
491+
self.spending_input.previous_output
492+
}
493+
}
494+
495+
/// A transaction output that was created by a transaction.
496+
///
497+
/// Contains information about the created output and its location.
498+
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
499+
pub struct CreatedTxOut<I> {
500+
/// The outpoint identifying the created output.
501+
pub outpoint: OutPoint,
502+
/// The transaction output that was created.
503+
pub txout: TxOut,
504+
/// The script pubkey index associated with the created output.
505+
pub spk_index: I,
506+
}

crates/chain/tests/test_spk_txout_index.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bdk_chain::spk_txout::{CreatedTxOut, SpentTxOut};
12
use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
23
use bitcoin::{
34
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
@@ -80,6 +81,121 @@ fn spk_txout_sent_and_received() {
8081
assert_eq!(index.net_value(&tx2, ..), SignedAmount::from_sat(8_000));
8182
}
8283

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

0 commit comments

Comments
 (0)