use core::ops::RangeBounds;
use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
Indexer,
};
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid};
#[derive(Clone, Debug)]
pub struct SpkTxOutIndex<I> {
spks: BTreeMap<I, ScriptBuf>,
spk_indices: HashMap<ScriptBuf, I>,
unused: BTreeSet<I>,
txouts: BTreeMap<OutPoint, (I, TxOut)>,
spk_txouts: BTreeSet<(I, OutPoint)>,
}
impl<I> Default for SpkTxOutIndex<I> {
fn default() -> Self {
Self {
txouts: Default::default(),
spks: Default::default(),
spk_indices: Default::default(),
spk_txouts: Default::default(),
unused: Default::default(),
}
}
}
impl<I> AsRef<SpkTxOutIndex<I>> for SpkTxOutIndex<I> {
fn as_ref(&self) -> &SpkTxOutIndex<I> {
self
}
}
impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
type ChangeSet = ();
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
self.scan_txout(outpoint, txout);
Default::default()
}
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet {
self.scan(tx);
Default::default()
}
fn initial_changeset(&self) -> Self::ChangeSet {}
fn apply_changeset(&mut self, _changeset: Self::ChangeSet) {
}
fn is_tx_relevant(&self, tx: &Transaction) -> bool {
self.is_relevant(tx)
}
}
impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
let mut scanned_indices = BTreeSet::new();
let txid = tx.compute_txid();
for (i, txout) in tx.output.iter().enumerate() {
let op = OutPoint::new(txid, i as u32);
if let Some(spk_i) = self.scan_txout(op, txout) {
scanned_indices.insert(spk_i.clone());
}
}
scanned_indices
}
pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> {
let spk_i = self.spk_indices.get(&txout.script_pubkey);
if let Some(spk_i) = spk_i {
self.txouts.insert(op, (spk_i.clone(), txout.clone()));
self.spk_txouts.insert((spk_i.clone(), op));
self.unused.remove(spk_i);
}
spk_i
}
pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> {
&self.spk_txouts
}
pub fn txouts(
&self,
) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> + ExactSizeIterator {
self.txouts
.iter()
.map(|(op, (index, txout))| (index, *op, txout))
}
pub fn txouts_in_tx(
&self,
txid: Txid,
) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> {
self.txouts
.range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
.map(|(op, (index, txout))| (index, *op, txout))
}
pub fn outputs_in_range(
&self,
range: impl RangeBounds<I>,
) -> impl DoubleEndedIterator<Item = (&I, OutPoint)> {
use bitcoin::hashes::Hash;
use core::ops::Bound::*;
let min_op = OutPoint {
txid: Txid::all_zeros(),
vout: u32::MIN,
};
let max_op = OutPoint {
txid: Txid::from_byte_array([0xff; Txid::LEN]),
vout: u32::MAX,
};
let start = match range.start_bound() {
Included(index) => Included((index.clone(), min_op)),
Excluded(index) => Excluded((index.clone(), max_op)),
Unbounded => Unbounded,
};
let end = match range.end_bound() {
Included(index) => Included((index.clone(), max_op)),
Excluded(index) => Excluded((index.clone(), min_op)),
Unbounded => Unbounded,
};
self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op))
}
pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
self.txouts.get(&outpoint).map(|v| (&v.0, &v.1))
}
pub fn spk_at_index(&self, index: &I) -> Option<ScriptBuf> {
self.spks.get(index).cloned()
}
pub fn all_spks(&self) -> &BTreeMap<I, ScriptBuf> {
&self.spks
}
pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool {
match self.spk_indices.entry(spk.clone()) {
Entry::Vacant(value) => {
value.insert(index.clone());
self.spks.insert(index.clone(), spk);
self.unused.insert(index);
true
}
Entry::Occupied(_) => false,
}
}
pub fn unused_spks<R>(
&self,
range: R,
) -> impl DoubleEndedIterator<Item = (&I, ScriptBuf)> + Clone + '_
where
R: RangeBounds<I>,
{
self.unused
.range(range)
.map(move |index| (index, self.spk_at_index(index).expect("must exist")))
}
pub fn is_used(&self, index: &I) -> bool {
!self.unused.contains(index)
}
pub fn mark_used(&mut self, index: &I) -> bool {
self.unused.remove(index)
}
pub fn unmark_used(&mut self, index: &I) -> bool {
if !self.spks.contains_key(index) {
return false;
}
if self.outputs_in_range(index..=index).next().is_some() {
return false;
}
self.unused.insert(index.clone())
}
pub fn index_of_spk(&self, script: ScriptBuf) -> Option<&I> {
self.spk_indices.get(script.as_script())
}
pub fn sent_and_received(
&self,
tx: &Transaction,
range: impl RangeBounds<I>,
) -> (Amount, Amount) {
let mut sent = Amount::ZERO;
let mut received = Amount::ZERO;
for txin in &tx.input {
if let Some((index, txout)) = self.txout(txin.previous_output) {
if range.contains(index) {
sent += txout.value;
}
}
}
for txout in &tx.output {
if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
if range.contains(index) {
received += txout.value;
}
}
}
(sent, received)
}
pub fn spent_txouts<'a>(
&'a self,
tx: &'a Transaction,
) -> impl Iterator<Item = SpentTxOut<I>> + '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(),
})
})
}
pub fn created_txouts<'a>(
&'a self,
tx: &'a Transaction,
) -> impl Iterator<Item = CreatedTxOut<I>> + '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(),
})
})
}
pub fn net_value(&self, tx: &Transaction, range: impl RangeBounds<I>) -> SignedAmount {
let (sent, received) = self.sent_and_received(tx, range);
received.to_signed().expect("valid `SignedAmount`")
- sent.to_signed().expect("valid `SignedAmount`")
}
pub fn is_relevant(&self, tx: &Transaction) -> bool {
let input_matches = tx
.input
.iter()
.any(|input| self.txouts.contains_key(&input.previous_output));
let output_matches = tx
.output
.iter()
.any(|output| self.spk_indices.contains_key(&output.script_pubkey));
input_matches || output_matches
}
pub fn relevant_spks_of_tx(&self, tx: &Transaction) -> BTreeSet<(I, ScriptBuf)> {
let spks_from_inputs = tx.input.iter().filter_map(|txin| {
self.txouts
.get(&txin.previous_output)
.cloned()
.map(|(i, prev_txo)| (i, prev_txo.script_pubkey))
});
let spks_from_outputs = tx
.output
.iter()
.filter_map(|txout| self.spk_indices.get_key_value(&txout.script_pubkey))
.map(|(spk, i)| (i.clone(), spk.clone()));
spks_from_inputs.chain(spks_from_outputs).collect()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct SpentTxOut<I> {
pub txout: TxOut,
pub spending_input: TxIn,
pub spending_input_index: u32,
pub spk_index: I,
}
impl<I> SpentTxOut<I> {
pub fn outpoint(&self) -> OutPoint {
self.spending_input.previous_output
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct CreatedTxOut<I> {
pub outpoint: OutPoint,
pub txout: TxOut,
pub spk_index: I,
}