use std::{fmt, fmt::Display};
use alloy_primitives::TxHash;
use clickhouse::Row;
use colored::Colorize;
use itertools::Itertools;
use redefined::self_convert_redefined;
use reth_primitives::{Address, B256};
use serde::{Deserialize, Serialize};
use super::Node;
use crate::{
db::{
address_metadata::AddressMetadata, metadata::Metadata, searcher::SearcherInfo,
traits::LibmdbxReader,
},
normalized_actions::{
Action, MultiCallFrameClassification, NormalizedAction, NormalizedEthTransfer,
},
tree::types::NodeWithDataRef,
FastHashMap, FastHashSet, TreeSearchBuilder, TxInfo,
};
#[derive(Debug, Clone)]
pub struct NodeData<V: NormalizedAction>(pub Vec<Option<Vec<V>>>);
impl<V: NormalizedAction> NodeData<V> {
pub fn add(&mut self, data: Vec<V>) -> usize {
self.0.push(Some(data));
self.0.len() - 1
}
pub fn get_ref(&self, idx: usize) -> Option<&Vec<V>> {
self.0.get(idx).and_then(|f| f.as_ref())
}
pub fn get_mut(&mut self, idx: usize) -> Option<&mut Vec<V>> {
self.0.get_mut(idx).and_then(|f| f.as_mut())
}
pub fn remove(&mut self, idx: usize) -> Option<Vec<V>> {
self.0[idx].take()
}
pub fn replace(&mut self, idx: usize, value: Vec<V>) {
self.0[idx] = Some(value);
}
}
#[derive(Debug, Clone)]
pub struct Root<V: NormalizedAction> {
pub head: Node,
pub position: usize,
pub tx_hash: B256,
pub private: bool,
pub gas_details: GasDetails,
pub total_msg_value_transfers: Vec<NormalizedEthTransfer>,
pub data_store: NodeData<V>,
}
impl<V: NormalizedAction> Root<V> {
pub fn get_tx_info_batch(
&self,
block_number: u64,
eoa: &FastHashMap<Address, SearcherInfo>,
contract: &FastHashMap<Address, SearcherInfo>,
address_meta: &FastHashMap<Address, AddressMetadata>,
) -> eyre::Result<TxInfo> {
self.tx_info_internal(
block_number,
|eoa_addr| Ok(eoa.get(&eoa_addr).cloned()),
|contract_addr| Ok(contract.get(&contract_addr).cloned()),
|address_metadata| Ok(address_meta.get(&address_metadata).cloned()),
)
}
pub fn tx_must_contain_action(&self, f: impl Fn(&V) -> bool) -> bool {
self.data_store.0.iter().flatten().flatten().any(f)
}
pub fn get_tx_info<DB: LibmdbxReader>(
&self,
block_number: u64,
database: &DB,
) -> eyre::Result<TxInfo> {
self.tx_info_internal(
block_number,
|eoa_addr| database.try_fetch_searcher_eoa_info(eoa_addr),
|contract_addr| database.try_fetch_searcher_contract_info(contract_addr),
|address_meta| database.try_fetch_address_metadata(address_meta),
)
}
fn tx_info_internal(
&self,
block_number: u64,
eoa: impl Fn(Address) -> eyre::Result<Option<SearcherInfo>>,
contract: impl Fn(Address) -> eyre::Result<Option<SearcherInfo>>,
address: impl Fn(Address) -> eyre::Result<Option<AddressMetadata>>,
) -> eyre::Result<TxInfo> {
let to_address = self
.data_store
.get_ref(self.head.data)
.unwrap()
.clone()
.first()
.unwrap()
.get_action()
.get_to_address();
let address_meta =
address(to_address).map_err(|_| eyre::eyre!("Failed to fetch address metadata"))?;
let (is_verified_contract, contract_type) = match address_meta {
Some(meta) => {
let verified = meta.is_verified();
let contract_type = meta.get_contract_type();
(verified, Some(contract_type))
}
None => (false, None),
};
let is_classified = self
.data_store
.get_ref(self.head.data)
.map(|f| f.iter().any(|f| f.is_classified()))
.unwrap_or_default();
let emits_logs = self
.data_store
.get_ref(self.head.data)
.unwrap()
.iter()
.any(|a| a.get_action().emitted_logs());
let is_cex_dex_call = self
.data_store
.get_ref(self.head.data)
.unwrap()
.iter()
.any(|a| {
matches!(a.get_action(),
Action::Unclassified(data) if data.is_cex_dex_call()
)
});
let searcher_eoa_info = eoa(self.head.address)?;
let searcher_contract_info = contract(self.get_to_address())?;
if is_verified_contract
|| is_classified
|| emits_logs && searcher_contract_info.is_none()
|| contract_type
.as_ref()
.map_or(false, |ct| !ct.could_be_mev_contract())
{
return Ok(TxInfo::new(
block_number,
self.position as u64,
self.head.address,
None,
contract_type,
self.tx_hash,
self.gas_details,
is_classified,
is_cex_dex_call,
self.private,
is_verified_contract,
searcher_eoa_info,
None,
self.total_msg_value_transfers.clone(),
))
}
Ok(TxInfo::new(
block_number,
self.position as u64,
self.head.address,
Some(to_address),
contract_type,
self.tx_hash,
self.gas_details,
is_classified,
is_cex_dex_call,
self.private,
is_verified_contract,
searcher_eoa_info,
searcher_contract_info,
self.total_msg_value_transfers.clone(),
))
}
pub fn get_from_address(&self) -> Address {
self.head.address
}
pub fn try_get_to_address(&self) -> Option<Address> {
Some(
self.data_store
.get_ref(0)?
.first()?
.get_action()
.get_to_address(),
)
}
pub fn get_to_address(&self) -> Address {
self.data_store
.get_ref(0)
.unwrap()
.first()
.unwrap()
.get_action()
.get_to_address()
}
pub fn get_root_action(&self) -> &V {
self.data_store.get_ref(0).unwrap().first().unwrap()
}
pub fn get_block_position(&self) -> usize {
self.position
}
pub fn insert(&mut self, node: Node, data: Vec<V>) {
self.head.insert(node, data, &mut self.data_store);
}
pub fn collect_spans(&self, call: &TreeSearchBuilder<V>) -> Vec<Vec<V>> {
let mut result = Vec::new();
self.head.collect_spans(&mut result, call, &self.data_store);
result
}
pub fn modify_spans<F>(&mut self, find: &TreeSearchBuilder<V>, modify: &F)
where
F: Fn(Vec<&mut Node>, &mut NodeData<V>),
{
self.head
.modify_node_spans(find, modify, &mut self.data_store);
}
pub fn collect(&self, call: &TreeSearchBuilder<V>) -> Vec<V> {
let mut result = Vec::new();
self.head
.collect(&mut result, call, &|data| data.data.clone(), &self.data_store);
result.sort_by_key(|a| a.get_trace_index());
result
}
pub fn modify_node_if_contains_childs<F>(&mut self, find: &TreeSearchBuilder<V>, modify: &F)
where
F: Fn(&mut Node, &mut NodeData<V>),
{
self.head
.modify_node_if_contains_childs(find, modify, &mut self.data_store);
}
pub fn collect_child_traces_and_classify(&mut self, heads: &[MultiCallFrameClassification<V>]) {
heads.iter().for_each(|search_head| {
self.head
.get_all_children_for_complex_classification(search_head, &mut self.data_store)
});
}
pub fn finalize(&mut self) {
self.head.finalize();
}
pub fn is_private(&self) -> bool {
self.private
}
pub fn label_private_tx(&mut self, metadata: &Metadata) {
if metadata.private_flow.contains(&self.tx_hash) {
self.private = true;
}
}
pub fn remove_duplicate_data<C, T, R>(
&mut self,
find: &TreeSearchBuilder<V>,
classify: &C,
info: &T,
removal: &TreeSearchBuilder<V>,
) where
T: Fn(NodeWithDataRef<'_, V>) -> R + Sync,
C: Fn(&Vec<R>, &Node, &NodeData<V>) -> Vec<u64> + Sync,
{
let mut find_res = Vec::new();
self.head
.collect(&mut find_res, find, &|data| data.node.clone(), &self.data_store);
let mut bad_res: Vec<R> = Vec::new();
self.head
.collect(&mut bad_res, removal, info, &self.data_store);
let indexes = find_res
.into_iter()
.flat_map(|node| classify(&bad_res, &node, &self.data_store))
.collect::<FastHashSet<_>>();
indexes.into_iter().for_each(|index| {
self.head
.remove_node_and_children(index, &mut self.data_store)
});
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Serialize,
Deserialize,
Row,
Default,
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
)]
pub struct GasDetails {
pub coinbase_transfer: Option<u128>,
pub priority_fee: u128,
pub gas_used: u128,
pub effective_gas_price: u128,
}
impl Display for GasDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"GasDetails {{ coinbase_transfer: {:?}, priority_fee: {}, gas_used: {}, \
effective_gas_price: {} }}",
self.coinbase_transfer, self.priority_fee, self.gas_used, self.effective_gas_price
)
}
}
self_convert_redefined!(GasDetails);
impl GasDetails {
pub fn gas_paid(&self) -> u128 {
let mut gas = self.gas_used * self.effective_gas_price;
if let Some(coinbase) = self.coinbase_transfer {
gas += coinbase
}
gas
}
pub fn priority_fee(&self, base_fee: u128) -> u128 {
self.effective_gas_price - base_fee
}
pub fn priority_fee_paid(&self, base_fee: u128) -> u128 {
self.priority_fee(base_fee) * self.gas_used
}
pub fn coinbase_transfer(&self) -> u128 {
self.coinbase_transfer.unwrap_or_default()
}
pub fn merge(&mut self, other: &GasDetails) {
self.coinbase_transfer = Some(
self.coinbase_transfer.unwrap_or_default()
+ other.coinbase_transfer.unwrap_or_default(),
)
.filter(|&res| res != 0);
self.priority_fee += other.priority_fee;
self.gas_used += other.gas_used;
self.effective_gas_price += other.effective_gas_price;
}
pub fn pretty_print_with_spaces(&self, f: &mut fmt::Formatter, spaces: usize) -> fmt::Result {
let space_str = " ".repeat(spaces);
let labels = [
(
"Coinbase Transfer",
self.coinbase_transfer
.map(|amount| format!("{:.18} ETH", amount as f64 / 1e18))
.unwrap_or_else(|| "None".to_string()),
),
("Priority Fee", format!("{} Wei", self.priority_fee)),
("Gas Used", self.gas_used.to_string()),
("Effective Gas Price", format!("{} Wei", self.effective_gas_price)),
("Total Gas Paid in ETH", format!("{:.7} ETH", self.gas_paid() as f64 / 1e18)),
];
let max_label_length = labels
.iter()
.map(|(label, _)| label.len())
.max()
.unwrap_or(0);
for (label, value) in &labels {
writeln!(
f,
"{}",
self.format_line_with_spaces(label, value, max_label_length, &space_str)
)?;
}
Ok(())
}
fn format_line_with_spaces(
&self,
label: &str,
value: &str,
max_label_length: usize,
leading_spaces: &str,
) -> String {
let padded_label = format!("{:<width$} :", label, width = max_label_length);
let formatted_value = format!(" {}", value).bright_yellow();
format!("{}{}{}", leading_spaces, padded_label, formatted_value)
}
}
pub struct ClickhouseVecGasDetails {
pub tx_hash: Vec<String>,
pub coinbase_transfer: Vec<Option<u128>>,
pub priority_fee: Vec<u128>,
pub gas_used: Vec<u128>,
pub effective_gas_price: Vec<u128>,
}
impl From<(Vec<TxHash>, Vec<GasDetails>)> for ClickhouseVecGasDetails {
fn from(value: (Vec<TxHash>, Vec<GasDetails>)) -> Self {
let vec_vals = value
.0
.into_iter()
.zip(value.1)
.map(|(tx, gas)| {
(
format!("{:?}", tx),
gas.coinbase_transfer,
gas.priority_fee,
gas.gas_used,
gas.effective_gas_price,
)
})
.collect::<Vec<_>>();
ClickhouseVecGasDetails {
tx_hash: vec_vals.iter().map(|val| val.0.to_owned()).collect_vec(),
coinbase_transfer: vec_vals.iter().map(|val| val.1.to_owned()).collect_vec(),
priority_fee: vec_vals.iter().map(|val| val.2.to_owned()).collect_vec(),
gas_used: vec_vals.iter().map(|val| val.3.to_owned()).collect_vec(),
effective_gas_price: vec_vals.iter().map(|val| val.4.to_owned()).collect_vec(),
}
}
}
impl From<(Vec<Vec<TxHash>>, Vec<GasDetails>)> for ClickhouseVecGasDetails {
fn from(value: (Vec<Vec<TxHash>>, Vec<GasDetails>)) -> Self {
let tx_hashes = value.0.into_iter().flatten().collect_vec();
let gas_details = value.1;
(tx_hashes, gas_details).into()
}
}
pub enum FalsePositiveEntity {
MaestroBots,
}