use std::{
collections::HashSet,
fmt::{self, Display},
str::FromStr,
};
use eyre::{Result, bail, eyre};
use regex::Regex;
use revm::primitives::{Address, U256};
use super::mev_transaction::MEVTransaction;
use crate::misc::{
args_parsing::PositionRange, eth_unit_parser::parse_eth_value, shared_init::SharedOpts,
};
#[derive(Clone, Debug, clap::Parser)]
pub struct TxsFilterOpts {
#[arg(short = 'f', long, help = "Filter by tx source address or ENS name")]
pub from: Option<String>,
#[arg(
long,
help = "Filter by tx target address, ENS name, or CREATE transactions"
)]
pub to: Option<String>,
#[arg(short = 'p', long, help_heading = "Tx position or position range in a block (e.g., '0' or '0:10'", num_args(1..))]
pub position: Option<String>,
#[arg(
short = 't',
long,
help = "Filter by contracts with storage changed by the transaction"
)]
pub touching: Option<Address>,
#[arg(
alias = "e",
long,
help = "Include txs by event names matching the provided regex or signature and optionally an address"
)]
pub event: Vec<String>,
#[arg(
alias = "ne",
long,
help = "Exclude txs by event names matching the provided regex or signature and optionally an address"
)]
pub not_event: Option<String>,
#[arg(
alias = "m",
long,
help = "Include txs by root method names matching the provided regex, signature or signature hash"
)]
pub method: Option<String>,
#[arg(
long,
help = "Include txs by subcalls method names matching the provided regex, signature or signature hash"
)]
pub calls: Vec<String>,
#[arg(
alias = "tc",
long,
help = "Filter by tx cost (e.g., 'ge10000000000000000', 'le0.01ether')"
)]
pub tx_cost: Option<String>,
#[arg(
alias = "rtc",
long,
help = "Filter by real (including coinbase bribe) tx cost (e.g., 'ge10000000000000000', 'le0.01ether')"
)]
pub real_tx_cost: Option<String>,
#[arg(
alias = "gp",
long,
help = "Filter by effective gas price (e.g., 'ge2000000000', 'le5gwei')"
)]
pub gas_price: Option<String>,
#[arg(
alias = "rgp",
long,
help = "Filter by real (including coinbase bribe) effective gas price (e.g., 'ge2000000000', 'le5gwei')"
)]
pub real_gas_price: Option<String>,
#[arg(
long,
help = "Filter by transaction value (e.g., 'ge1ether', 'le0.1ether')"
)]
pub value: Option<String>,
#[arg(short, long, alias = "r", help = "Reverse the order of txs")]
pub reverse: bool,
#[arg(
long,
alias = "tm",
help = "Display block and txs metadata info on top"
)]
pub top_metadata: bool,
#[arg(long, help = "Filter by txs which failed to execute")]
pub failed: bool,
#[arg(
long,
help = "Filter by ERC20 Transfer events with specific address and optionally amount (e.g., '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' or '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|ge3ether')"
)]
pub erc20_transfer: Vec<String>,
}
#[derive(Debug)]
pub struct PriceQuery {
pub gas_price: U256,
pub operator: DiffOperator,
}
impl PriceQuery {
#[hotpath::measure(log = true)]
pub fn matches(&self, gas_price: U256) -> bool {
match self.operator {
DiffOperator::GreaterOrEq => gas_price >= self.gas_price,
DiffOperator::LessOrEq => gas_price <= self.gas_price,
}
}
}
impl FromStr for PriceQuery {
type Err = eyre::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (operator, gas_price) = parse_price_query(s)?;
Ok(PriceQuery {
operator,
gas_price,
})
}
}
#[derive(Debug)]
pub struct ERC20TransferQuery {
pub address: Address,
pub amount: Option<U256>,
pub operator: Option<DiffOperator>,
}
impl ERC20TransferQuery {
pub fn matches(&self, address: &Address, amount: &U256) -> bool {
if address != &self.address {
return false;
}
let (Some(filter_amount), Some(operator)) = (&self.amount, &self.operator) else {
return true;
};
match operator {
DiffOperator::GreaterOrEq => *amount >= *filter_amount,
DiffOperator::LessOrEq => *amount <= *filter_amount,
}
}
}
impl FromStr for ERC20TransferQuery {
type Err = eyre::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split('|').collect();
if parts.len() == 1 {
let address = parts[0].parse::<Address>()?;
Ok(ERC20TransferQuery {
address,
amount: None,
operator: None,
})
} else if parts.len() == 2 {
let address = parts[0].parse::<Address>()?;
let (operator, amount) = parse_price_query(parts[1])?;
Ok(ERC20TransferQuery {
address,
amount: Some(amount),
operator: Some(operator),
})
} else {
bail!(
"Invalid transfer query format. Expected 'address' or 'address|amount_filter' (e.g., '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' or '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|ge3ether')"
);
}
}
}
fn parse_price_query(s: &str) -> Result<(DiffOperator, U256)> {
let trimmed = s.trim();
if trimmed.len() < 3 {
eyre::bail!("Invalid value query: '{}'", s);
}
let op_str = &trimmed[0..2];
let value_str = &trimmed[2..];
let operator = DiffOperator::from_str(op_str).map_err(|e| eyre!("Parse error: {}", e))?;
let value = parse_eth_value(value_str)?;
Ok((operator, value))
}
#[derive(Debug)]
pub enum DiffOperator {
GreaterOrEq,
LessOrEq,
}
impl FromStr for DiffOperator {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ge" => Ok(DiffOperator::GreaterOrEq),
"le" => Ok(DiffOperator::LessOrEq),
_ => Err(format!(
"Invalid operator: '{s}' use 'le' (Less or Equal) or 'ge' (Greater or Equal)"
)),
}
}
}
#[derive(Debug)]
pub struct TxsFilter {
pub tx_indexes: Option<HashSet<u64>>,
pub tx_from: Option<AddressFilter>,
pub tx_to: Option<AddressFilter>,
pub touching: Option<Address>,
pub tx_position: Option<PositionRange>,
pub events: Vec<EventQuery>,
pub not_events: Vec<EventQuery>,
pub match_method: Option<SignatureQuery>,
pub match_calls: Vec<SignatureQuery>,
pub show_calls: bool,
pub tx_cost: Option<PriceQuery>,
pub real_tx_cost: Option<PriceQuery>,
pub gas_price: Option<PriceQuery>,
pub real_gas_price: Option<PriceQuery>,
pub value: Option<PriceQuery>,
pub reversed_order: bool,
pub failed: bool,
pub top_metadata: bool,
pub erc20_transfers: Vec<ERC20TransferQuery>,
pub show_opcodes: bool,
pub show_state_diff: bool,
}
impl TxsFilter {
pub fn new(
filter_opts: &TxsFilterOpts,
tx_indexes: Option<HashSet<u64>>,
shared_opts: &SharedOpts,
erc20_sort_token: Option<Address>,
) -> Result<Self> {
if shared_opts.evm_trace.is_none() {
if filter_opts.touching.is_some() {
eyre::bail!(
"'--touching' filter is supported only with --evm-trace [rpc|revm] enabled "
)
}
if filter_opts.real_tx_cost.is_some() {
eyre::bail!(
"'--real-tx-cost' filter is supported only with --evm-trace [rpc|revm] enabled "
)
}
if filter_opts.real_gas_price.is_some() {
eyre::bail!(
"'--real-gas-price' filter is supported only with --evm-trace [rpc|revm] enabled "
)
}
if shared_opts.evm_calls {
eyre::bail!("'--evm-calls' is supported only with --evm-trace [rpc|revm] enabled")
}
if shared_opts.evm_ops {
eyre::bail!("'--evm-ops' is supported only with --evm-trace [rpc|revm] enabled")
}
if shared_opts.evm_state_diff {
eyre::bail!(
"'--evm-state-diff' is supported only with --evm-trace [rpc|revm] enabled"
)
}
}
let mut events: Vec<EventQuery> = filter_opts
.event
.iter()
.map(|query| query.parse())
.collect::<Result<Vec<_>>>()?;
if let Some(token_address) = erc20_sort_token {
events.push(EventQuery {
signature: Some(SignatureQuery::NameOrHash(
"Transfer(address,address,uint256)".to_string(),
)),
address: Some(token_address),
});
}
Ok(Self {
tx_cost: match filter_opts.tx_cost {
Some(ref query) => Some(query.parse()?),
None => None,
},
gas_price: match filter_opts.gas_price {
Some(ref query) => Some(query.parse()?),
None => None,
},
real_tx_cost: match filter_opts.real_tx_cost {
Some(ref query) => Some(query.parse()?),
None => None,
},
real_gas_price: match filter_opts.real_gas_price {
Some(ref query) => Some(query.parse()?),
None => None,
},
value: match filter_opts.value {
Some(ref query) => Some(query.parse()?),
None => None,
},
tx_indexes,
tx_from: AddressFilter::new(filter_opts.from.as_deref())?,
tx_to: AddressFilter::new(filter_opts.to.as_deref())?,
touching: filter_opts.touching,
tx_position: match filter_opts.position {
Some(ref position) => Some(position.parse()?),
None => None,
},
events,
not_events: filter_opts
.not_event
.iter()
.map(|query| query.parse())
.collect::<Result<Vec<_>>>()?,
match_method: match filter_opts.method {
Some(ref query) => Some(query.parse()?),
None => None,
},
match_calls: filter_opts
.calls
.iter()
.map(|query| query.parse())
.collect::<Result<Vec<_>>>()?,
show_calls: shared_opts.evm_calls,
reversed_order: filter_opts.reverse,
top_metadata: filter_opts.top_metadata,
failed: filter_opts.failed,
erc20_transfers: filter_opts
.erc20_transfer
.iter()
.map(|query| query.parse())
.collect::<Result<Vec<_>>>()?,
show_opcodes: shared_opts.evm_ops,
show_state_diff: shared_opts.evm_state_diff,
})
}
pub fn prefetch_receipts(&self) -> bool {
self.tx_cost.is_some()
|| self.gas_price.is_some()
|| self.real_tx_cost.is_some()
|| self.real_gas_price.is_some()
}
pub fn tracing_should_exclude(&self, mev_tx: &MEVTransaction) -> bool {
if let Some(full_tx_cost) = &self.real_tx_cost
&& !full_tx_cost.matches(mev_tx.full_tx_cost().expect("must be traced"))
{
return true;
}
if let Some(full_effective_gas_price) = &self.real_gas_price
&& !full_effective_gas_price.matches(mev_tx.full_effective_gas_price())
{
return true;
}
if !self.match_calls.is_empty() {
if let Some(calls) = &mev_tx.calls {
let any_call_matches = calls.iter().any(|call| {
self.match_calls
.iter()
.any(|query| query.matches(&call.signature))
});
if !any_call_matches {
return true;
}
} else {
return true;
}
}
false
}
pub fn from_ens_query(&self) -> Option<String> {
if let Some(from) = &self.tx_from
&& let AddressFilter::ENSName(name) = from
{
return Some(name.clone());
}
None
}
pub fn to_ens_query(&self) -> Option<String> {
if let Some(to) = &self.tx_to
&& let AddressFilter::ENSName(name) = to
{
return Some(name.clone());
}
None
}
}
#[derive(Debug)]
pub struct EventQuery {
pub signature: Option<SignatureQuery>,
pub address: Option<Address>,
}
impl EventQuery {
pub fn matches(&self, signature: &str, address: &Address) -> bool {
let sig_matching = match self.signature {
Some(ref sig_query) => sig_query.matches(signature),
None => true,
};
let addr_matching = match &self.address {
Some(expected_address) => expected_address == address,
None => true,
};
sig_matching && addr_matching
}
}
impl FromStr for EventQuery {
type Err = eyre::Error;
fn from_str(input: &str) -> Result<Self> {
let parts: Vec<&str> = input.split('|').collect();
if parts.len() == 2 {
let signature = parts[0].parse::<SignatureQuery>()?;
let address = Some(parts[1].parse::<Address>()?);
Ok(EventQuery {
signature: Some(signature),
address,
})
} else if parts.len() == 1 {
if let Ok(address) = parts[0].parse::<Address>() {
return Ok(EventQuery {
signature: None,
address: Some(address),
});
}
if let Ok(signature) = parts[0].parse::<SignatureQuery>() {
return Ok(EventQuery {
signature: Some(signature),
address: None,
});
}
bail!("Invalid input: Must be either 'query|address' or a valid address")
} else {
bail!("Invalid input format")
}
}
}
#[derive(Debug)]
pub enum SignatureQuery {
NameOrHash(String),
Regex(Regex),
}
impl SignatureQuery {
pub fn matches(&self, signature: &str) -> bool {
match self {
SignatureQuery::NameOrHash(name) => name == signature,
SignatureQuery::Regex(regex) => regex.is_match(signature),
}
}
}
impl Display for SignatureQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SignatureQuery::NameOrHash(name) => write!(f, "{name}"),
SignatureQuery::Regex(regex) => write!(f, "/{regex}/"),
}
}
}
impl FromStr for SignatureQuery {
type Err = eyre::Error;
fn from_str(input: &str) -> Result<Self> {
if input.starts_with('/') && input.ends_with('/') {
let regex = Regex::new(&input[1..input.len() - 1])?;
Ok(SignatureQuery::Regex(regex))
} else {
Ok(SignatureQuery::NameOrHash(input.to_string()))
}
}
}
#[derive(Debug)]
pub enum AddressFilter {
Address(Address),
ENSName(String),
CreateCall,
}
#[hotpath::measure_all]
impl AddressFilter {
pub fn new(value: Option<&str>) -> Result<Option<Self>> {
let Some(value) = value else {
return Ok(None);
};
if value == "CREATE" {
return Ok(Some(AddressFilter::CreateCall));
}
if let Ok(address) = value.parse::<Address>() {
return Ok(Some(AddressFilter::Address(address)));
}
if value.ends_with(".eth") {
return Ok(Some(AddressFilter::ENSName(value.to_lowercase())));
}
eyre::bail!(
"Invalid input: '{}' is not an Ethereum address or ENS name",
value
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::misc::utils::GWEI_U128;
#[test]
fn test_gas_price_query_from_str() {
let query = PriceQuery::from_str("ge1000000000").unwrap();
assert!(
matches!(query.operator, DiffOperator::GreaterOrEq),
"Should be GreaterOrEq operator"
);
assert_eq!(
query.gas_price,
U256::from(1000000000),
"Should parse raw wei value correctly"
);
let query = PriceQuery::from_str("ge5gwei").unwrap();
assert!(
matches!(query.operator, DiffOperator::GreaterOrEq),
"Should be GreaterOrEq operator"
);
assert_eq!(
query.gas_price,
U256::from(GWEI_U128 * 5),
"Should convert 5 gwei to wei correctly"
);
let query = PriceQuery::from_str("le0.01ether").unwrap();
assert!(
matches!(query.operator, DiffOperator::LessOrEq),
"Should be LessOrEq operator"
);
let result = PriceQuery::from_str("xx5gwei");
assert!(result.is_err(), "Should reject invalid operators");
let query = PriceQuery::from_str("ge10gwei").unwrap();
assert!(
matches!(query.operator, DiffOperator::GreaterOrEq),
"Should parse GreaterOrEq operator"
);
assert_eq!(
query.gas_price,
U256::from(GWEI_U128 * 10),
"Should parse 10 gwei correctly"
);
let query = PriceQuery::from_str("le0.000001ether").unwrap();
assert!(
matches!(query.operator, DiffOperator::LessOrEq),
"Should parse LessOrEq operator"
);
assert_eq!(
query.gas_price,
U256::from(10).pow(U256::from(12)),
"Should parse 0.000001 ether correctly"
);
}
#[test]
fn test_matches_functionality() {
let tx_cost = PriceQuery {
gas_price: U256::from(GWEI_U128 * 5),
operator: DiffOperator::GreaterOrEq,
};
assert!(
tx_cost.matches(U256::from(GWEI_U128 * 6)),
"Should match when value is greater than threshold"
);
assert!(
tx_cost.matches(U256::from(GWEI_U128 * 5)),
"Should match when value is equal to threshold"
);
assert!(
!tx_cost.matches(U256::from(GWEI_U128 * 4)),
"Should not match when value is less than threshold"
);
let gas_price = PriceQuery {
gas_price: U256::from(GWEI_U128 * 10),
operator: DiffOperator::LessOrEq,
};
assert!(
!gas_price.matches(U256::from(GWEI_U128 * 11)),
"Should not match when value is greater than threshold"
);
assert!(
gas_price.matches(U256::from(GWEI_U128 * 10)),
"Should match when value is equal to threshold"
);
assert!(
gas_price.matches(U256::from(GWEI_U128 * 9)),
"Should match when value is less than threshold"
);
}
#[test]
fn test_transfer_query_from_str() {
let query =
ERC20TransferQuery::from_str("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|ge3ether")
.unwrap();
assert_eq!(
query.address,
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
.parse::<Address>()
.unwrap()
);
assert!(matches!(query.operator, Some(DiffOperator::GreaterOrEq)));
assert_eq!(
query.amount,
Some(U256::from(3) * U256::from(10).pow(U256::from(18)))
);
let query =
ERC20TransferQuery::from_str("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|le1000")
.unwrap();
assert_eq!(query.amount, Some(U256::from(1000)));
assert!(matches!(query.operator, Some(DiffOperator::LessOrEq)));
let query =
ERC20TransferQuery::from_str("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913").unwrap();
assert_eq!(
query.address,
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
.parse::<Address>()
.unwrap()
);
assert!(query.amount.is_none());
assert!(query.operator.is_none());
assert!(ERC20TransferQuery::from_str("invalid").is_err());
assert!(
ERC20TransferQuery::from_str("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|invalid")
.is_err()
);
}
#[test]
fn test_transfer_query_matches() {
let query = ERC20TransferQuery {
address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
.parse::<Address>()
.unwrap(),
amount: Some(U256::from(1000)),
operator: Some(DiffOperator::GreaterOrEq),
};
let target_address = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
.parse::<Address>()
.unwrap();
let other_address = "0x0000000000000000000000000000000000000001"
.parse::<Address>()
.unwrap();
assert!(query.matches(&target_address, &U256::from(1000)));
assert!(query.matches(&target_address, &U256::from(2000)));
assert!(!query.matches(&other_address, &U256::from(2000)));
assert!(!query.matches(&target_address, &U256::from(500)));
assert!(query.matches(&target_address, &U256::from(1000)));
assert!(query.matches(&target_address, &U256::from(1500)));
let address_only_query = ERC20TransferQuery {
address: target_address,
amount: None,
operator: None,
};
assert!(address_only_query.matches(&target_address, &U256::from(1)));
assert!(address_only_query.matches(&target_address, &U256::from(1000)));
assert!(address_only_query.matches(&target_address, &U256::from(999999)));
assert!(!address_only_query.matches(&other_address, &U256::from(1000)));
}
#[test]
fn test_multiple_transfer_queries() {
let queries = [
ERC20TransferQuery::from_str("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913|ge1000")
.unwrap(),
ERC20TransferQuery::from_str("0x0000000000000000000000000000000000000001|le500")
.unwrap(),
];
let addr1 = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
.parse::<Address>()
.unwrap();
let addr2 = "0x0000000000000000000000000000000000000001"
.parse::<Address>()
.unwrap();
let addr3 = "0x0000000000000000000000000000000000000002"
.parse::<Address>()
.unwrap();
assert!(queries.iter().any(|q| q.matches(&addr1, &U256::from(1000))));
assert!(queries.iter().any(|q| q.matches(&addr1, &U256::from(2000))));
assert!(!queries.iter().any(|q| q.matches(&addr1, &U256::from(500))));
assert!(queries.iter().any(|q| q.matches(&addr2, &U256::from(500))));
assert!(queries.iter().any(|q| q.matches(&addr2, &U256::from(100))));
assert!(!queries.iter().any(|q| q.matches(&addr2, &U256::from(1000))));
assert!(!queries.iter().any(|q| q.matches(&addr3, &U256::from(1000))));
}
}