use futures_util::{stream, StreamExt, TryStreamExt};
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
sync::Arc,
};
use tokio::{join, task::JoinHandle};
use anyhow::{Context, Result};
use bitcoin_hashes::{hex::ToHex, Hash};
use bitcoincash::Txid;
use rayon::prelude::*;
use sha2::{Digest, Sha256};
use crate::{
chaindef::{OutPointHash, ScriptHash, TokenID},
indexes::{outputindex::OutputIndexRow, scripthashindex::ScriptHashIndexRow, DBRow},
mempool::{ConfirmationState, Tracker},
query::{
queryutil::{outpoint_is_spent, token_from_outpoint},
unspent::{listunspent_at_tip, lisunspent_full_search},
BUFFER_SIZE, CHUNK_SIZE,
},
store::{DBContents, DBStore},
};
use super::{queryfilter::QueryFilter, queryutil::multi_get_utxo};
use crate::utilserialize::as_rpc_height;
#[cfg(bch)]
use crate::utilserialize::opt_as_hex;
#[derive(Serialize)]
pub struct HistoryItem {
pub tx_hash: Txid,
pub height: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee: Option<u64>, }
#[derive(Serialize)]
pub struct UnspentItem {
#[serde(rename = "tx_hash")]
pub txid: Txid,
#[serde(rename = "tx_pos")]
pub vout: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub outpoint_hash: Option<OutPointHash>,
#[serde(serialize_with = "as_rpc_height")]
pub height: u32,
pub value: u64,
#[cfg(nexa)]
#[serde(skip_serializing_if = "Option::is_none", rename = "token_id_hex")]
pub token_id: Option<TokenID>,
#[cfg(bch)]
#[serde(skip_serializing_if = "Option::is_none")]
pub token_id: Option<TokenID>,
#[cfg(bch)]
#[serde(skip_serializing_if = "Option::is_none", serialize_with = "opt_as_hex")]
pub commitment: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_amount: Option<i64>,
pub has_token: bool,
}
pub(crate) fn by_block_height(a: &HistoryItem, b: &HistoryItem) -> Ordering {
if a.height == b.height {
return b.tx_hash.cmp(&a.tx_hash);
}
if a.height > 0 && b.height > 0 {
return a.height.cmp(&b.height);
}
let mut a_height = a.height;
let mut b_height = b.height;
if a_height <= 0 {
a_height = 0xEE_EEEE + a_height.abs();
}
if b_height <= 0 {
b_height = 0xEE_EEEE + b_height.abs();
}
a_height.cmp(&b_height)
}
#[allow(clippy::clone_on_copy)]
pub async fn scan_for_outputs<'a>(
store: &'a Arc<DBStore>,
scripthash: ScriptHash,
filter: &'a QueryFilter,
) -> (
JoinHandle<Result<()>>,
impl futures::stream::Stream<Item = (OutputIndexRow, bool)> + 'a,
) {
let (scan_prefix, scan_bitmask) =
ScriptHashIndexRow::filter_by_outputs(scripthash.into_inner(), filter);
let (query, stream) = store
.scan(ScriptHashIndexRow::CF, scan_prefix, scan_bitmask)
.await;
let stream =
stream
.map(|row| ScriptHashIndexRow::from_row(&row))
.ready_chunks(CHUNK_SIZE)
.then(move |rows| {
let store = Arc::clone(store);
let filtered_rows: Vec<ScriptHashIndexRow> = rows
.into_iter()
.filter(|row| filter.height_within(row.get_height()))
.collect();
let outpoints: Vec<OutPointHash> =
filtered_rows.iter().map(|row| row.outpointhash()).collect();
async move {
let utxos = multi_get_utxo(&store, &outpoints).await;
stream::iter(utxos.into_iter().zip(filtered_rows.into_iter()).map(
|(utxo, r)| {
let utxo = utxo.expect("missing utxo");
(utxo, r.has_token())
},
))
}
})
.flatten_unordered(BUFFER_SIZE);
(query, stream)
}
fn merge_token_balance_maps(
mut a: HashMap<TokenID, i64>,
b: HashMap<TokenID, i64>,
) -> HashMap<TokenID, i64> {
for (token_id, amount) in b {
a.entry(token_id)
.and_modify(|a| {
*a += amount;
})
.or_insert(amount);
}
a
}
async fn calc_token_balance(
store: &Arc<DBStore>,
scripthash: ScriptHash,
filter_token: Option<TokenID>,
) -> Result<(HashMap<TokenID, i64>, Vec<OutPointHash>)> {
let filter = QueryFilter::filter_token_only();
let (outputs_query, outputs_stream) = scan_for_outputs(store, scripthash, &filter).await;
let outpoints: Vec<(TokenID, i64, OutPointHash)> = outputs_stream
.map(|(o, _)| o.take_hash())
.map(|outpoint| async move {
let (token_future, is_spent) = join!(
token_from_outpoint(store, &outpoint),
outpoint_is_spent(store, outpoint),
);
let (token_id, amount, _) = token_future?.context(format!(
"token info not found for outpoint {}",
outpoint.to_hex()
))?;
let valid_amount = if amount < 0 || is_spent { 0 } else { amount };
anyhow::Ok((token_id, valid_amount, outpoint))
})
.buffer_unordered(BUFFER_SIZE)
.try_collect::<Vec<(TokenID, i64, OutPointHash)>>()
.await?;
outputs_query.await??;
let outpoints = if let Some(filter) = filter_token {
outpoints
.into_iter()
.filter(|(token_id, _, _)| token_id.eq(&filter))
.collect()
} else {
outpoints
};
let outpoints_len = outpoints.len();
let (balance, outputs) = outpoints.into_iter().fold(
(
HashMap::<TokenID, i64>::default(),
Vec::with_capacity(outpoints_len),
),
|(mut map, mut ops): (HashMap<TokenID, i64>, Vec<OutPointHash>),
(token_id, amount, outpoint)| {
map.entry(token_id)
.and_modify(|a| *a += amount)
.or_insert(amount);
ops.push(outpoint);
(map, ops)
},
);
Ok((balance, outputs))
}
pub(crate) async fn unconfirmed_scripthash_token_balance(
mempool: &Arc<DBStore>,
index: &Arc<DBStore>,
confirmed_outputs: Vec<OutPointHash>,
scripthash: ScriptHash,
filter_token: Option<TokenID>,
) -> Result<HashMap<TokenID, i64>> {
assert!(mempool.contents == DBContents::MempoolIndex);
assert!(index.contents == DBContents::ConfirmedIndex);
let (balance, _) = calc_token_balance(mempool, scripthash, filter_token).await?;
let spends_of_confirmed: Vec<(TokenID, i64)> = stream::iter(confirmed_outputs)
.then(|outpoint| async move {
if outpoint_is_spent(mempool, outpoint).await {
let (token_id, amount, _) = token_from_outpoint(index, &outpoint)
.await?
.expect("token info found for outpoint {outpoint}");
anyhow::Ok((token_id, -amount))
} else {
Ok((TokenID::all_zeros(), 0))
}
})
.try_collect()
.await?;
let spends_of_confirmed = spends_of_confirmed.into_iter().fold(
HashMap::<TokenID, i64>::default(),
|mut map, (token_id, amount)| {
map.entry(token_id)
.and_modify(|a| {
*a += amount;
})
.or_insert(amount);
map
},
);
let mut balance = merge_token_balance_maps(balance, spends_of_confirmed);
balance.remove(&TokenID::all_zeros());
Ok(balance)
}
pub(crate) async fn confirmed_scripthash_token_balance(
store: &Arc<DBStore>,
scripthash: ScriptHash,
filter_token: Option<TokenID>,
) -> Result<(HashMap<TokenID, i64>, Vec<OutPointHash>)> {
calc_token_balance(store, scripthash, filter_token).await
}
pub(crate) async fn scripthash_transactions(
store: &Arc<DBStore>,
scripthash: ScriptHash,
filter: QueryFilter,
filter_token: Option<TokenID>,
) -> Result<HashMap<Txid, u32>> {
let (scan_prefix, scan_bitmask) =
ScriptHashIndexRow::filter_by_outputs_and_inputs(scripthash.into_inner(), &filter);
let (query, stream) = store
.scan(ScriptHashIndexRow::CF, scan_prefix, scan_bitmask)
.await;
let mut txids = HashMap::new();
let rows: Vec<ScriptHashIndexRow> = stream
.map(|row| ScriptHashIndexRow::from_row(&row))
.collect()
.await;
query.await??;
let candidate_rows: Vec<_> = rows
.into_iter()
.filter(|row| filter.height_within(row.get_height()))
.collect();
let mut valid_funding_outpoints = HashSet::new();
if let Some(requested_token) = &filter_token {
for row in &candidate_rows {
if !row.is_funding() {
continue;
}
let outpoint = row.outpointhash();
if let Ok(Some((actual_token, _, _))) = token_from_outpoint(store, &outpoint).await {
if &actual_token == requested_token {
valid_funding_outpoints.insert(outpoint);
}
}
}
}
for row in candidate_rows {
if filter_token.is_some() {
if row.is_funding() {
let outpoint = row.outpointhash();
if !valid_funding_outpoints.contains(&outpoint) {
continue;
}
} else {
let spent_outpoint = row.outpointhash();
if !valid_funding_outpoints.contains(&spent_outpoint) {
continue;
}
}
}
let txid = Txid::from_inner(row.get_txid());
txids.insert(txid, row.get_height());
}
Ok(txids)
}
async fn confirmed_history(
store: &Arc<DBStore>,
scripthash: ScriptHash,
filter: QueryFilter,
filter_token: Option<TokenID>,
) -> Result<Vec<HistoryItem>> {
let txids = scripthash_transactions(store, scripthash, filter, filter_token).await?;
let history: Vec<HistoryItem> = txids
.into_par_iter()
.map(|(txid, height)| HistoryItem {
tx_hash: txid,
height: height as i32,
fee: None,
})
.collect();
Ok(history)
}
pub async fn unconfirmed_history(
mempool: &Tracker,
scripthash: ScriptHash,
filter: QueryFilter,
filter_token: Option<TokenID>,
) -> Result<Vec<HistoryItem>> {
let txids = scripthash_transactions(mempool.index(), scripthash, filter, filter_token).await?;
Ok(stream::iter(txids)
.map(|(txid, _)| async move {
let height = match mempool.tx_confirmation_state(&txid, None).await {
ConfirmationState::InMempool => 0,
ConfirmationState::UnconfirmedParent => -1,
ConfirmationState::Indeterminate => {
debug_assert!(false, "Mempool tx's state cannot be indeterminate");
0
}
ConfirmationState::Confirmed => {
debug_assert!(false, "Mempool tx's state cannot be confirmed");
0
}
};
HistoryItem {
tx_hash: txid,
height,
fee: mempool.get_fee(&txid).await,
}
})
.buffer_unordered(BUFFER_SIZE)
.collect()
.await)
}
pub async fn scripthash_history(
store: &Arc<DBStore>,
mempool: &Tracker,
scripthash: ScriptHash,
filter: QueryFilter,
filter_token: Option<TokenID>,
) -> Result<Vec<HistoryItem>> {
let mut history = confirmed_history(store, scripthash, filter, filter_token.clone()).await?;
history.extend(unconfirmed_history(mempool, scripthash, filter, filter_token).await?);
let mut history = tokio::task::spawn_blocking(|| async move {
history.par_sort_unstable_by(by_block_height);
history
})
.await
.unwrap()
.await;
filter.filter_limit_offset(&mut history);
Ok(history)
}
pub fn hash_scripthash_history(history: &Vec<HistoryItem>) -> Option<[u8; 32]> {
if history.is_empty() {
None
} else {
let mut sha2 = Sha256::new();
let parts: Vec<String> = history
.into_par_iter()
.map(|t| format!("{}:{}:", t.tx_hash.to_hex(), t.height))
.collect();
parts.into_iter().for_each(|p| {
sha2.update(p.as_bytes());
});
Some(sha2.finalize().into())
}
}
pub(crate) async fn scripthash_listunspent(
store: &Arc<DBStore>,
mempool: &Arc<DBStore>,
scripthash: ScriptHash,
filter: QueryFilter,
filter_token: Option<TokenID>,
) -> Result<Vec<UnspentItem>> {
assert!(store.contents == DBContents::ConfirmedIndex);
assert!(mempool.contents == DBContents::MempoolIndex);
if filter_token.is_some() {
assert!(filter.token_only)
}
if !filter.has_height_filter() {
return listunspent_at_tip(store, mempool, &scripthash, &filter, &filter_token).await;
}
lisunspent_full_search(store, mempool, &scripthash, &filter, &filter_token).await
}