use core::ops::RangeBounds;
use crate::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
Indexer,
};
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, 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: 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 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
}
}