mevlog 0.9.1

EVM transactions monitoring and querying CLI/TUI powered by Revm
Documentation
use std::collections::HashSet;
use std::time::Instant;

use alloy::providers::Provider;
use eyre::{Result, eyre};
use mevlog::{
    ChainInfoNoRpcsJson,
    misc::{
        args_parsing::PositionRange,
        data_fetch::fetch_blocks_batch,
        ens_utils::ENSLookup,
        shared_init::{ConnOpts, OutputFormat, SharedOpts, init_deps},
        symbol_utils::ERC20SymbolsLookup,
        utils::get_native_token_price,
    },
    models::{
        json::mev_transaction_json::{TxQueryParams, serialize_json_response},
        mev_block::{PreFetchedBlockData, generate_block},
        txs_filter::TxsFilter,
    },
};
use revm::primitives::FixedBytes;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MinimalTxInfo {
    block_number: Option<String>,
    transaction_index: Option<String>,
}

#[derive(Debug, clap::Parser)]
pub struct TxArgs {
    tx_hash: FixedBytes<32>,
    #[arg(
        long,
        short = 'b',
        help = "'before' means newer transactions (smaller indexes)"
    )]
    before: Option<u8>,
    #[arg(
        long,
        short = 'a',
        help = "'after' means older transactions (larger indexes)"
    )]
    after: Option<u8>,

    #[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,

    #[command(flatten)]
    shared_opts: SharedOpts,

    #[command(flatten)]
    conn_opts: ConnOpts,
}

impl TxArgs {
    pub async fn run(&self, format: OutputFormat) -> Result<()> {
        check_range(self.before, "--before")?;
        check_range(self.after, "--after")?;

        if self.shared_opts.evm_trace.is_none() {
            if self.shared_opts.evm_calls {
                eyre::bail!("'--evm-calls' is supported only with --evm-trace [rpc|revm] enabled")
            }
            if self.shared_opts.evm_ops {
                eyre::bail!("'--evm-ops' is supported only with --evm-trace [rpc|revm] enabled")
            }
            if self.shared_opts.evm_state_diff {
                eyre::bail!(
                    "'--evm-state-diff' is supported only with --evm-trace [rpc|revm] enabled"
                )
            }
        }

        let deps = init_deps(&self.conn_opts).await?;
        let start_time = Instant::now();

        let tx_info: MinimalTxInfo = deps
            .provider
            .client()
            .request("eth_getTransactionByHash", (self.tx_hash,))
            .await?;

        let block_number = tx_info
            .block_number
            .ok_or_else(|| eyre!("transaction not found or not mined"))?;
        let tx_index = tx_info
            .transaction_index
            .ok_or_else(|| eyre!("transaction index not found"))?;

        let (block_number, tx_index) = (
            u64::from_str_radix(block_number.trim_start_matches("0x"), 16)?,
            u64::from_str_radix(tx_index.trim_start_matches("0x"), 16)?,
        );

        let tx_indexes = get_matching_indexes(tx_index, self.before, self.after);

        let max_index = tx_indexes
            .clone()
            .into_iter()
            .max()
            .expect("tx_indexes must have at least one element");

        let position_range = Some(PositionRange {
            from: 0,
            to: max_index,
        });

        let native_token_price = get_native_token_price(
            &deps.chain,
            &deps.provider,
            self.shared_opts.native_token_price,
        )
        .await?;

        let txs_filter = TxsFilter {
            tx_indexes: Some(tx_indexes),
            tx_from: None,
            tx_to: None,
            touching: None,
            tx_position: position_range,
            events: vec![],
            not_events: vec![],
            match_method: None,
            tx_cost: None,
            gas_price: None,
            real_tx_cost: None,
            real_gas_price: None,
            value: None,
            reversed_order: self.reverse,
            top_metadata: self.top_metadata,
            match_calls: vec![],
            show_calls: self.shared_opts.evm_calls,
            failed: false,
            erc20_transfers: vec![],
            show_opcodes: self.shared_opts.evm_ops,
            show_state_diff: self.shared_opts.evm_state_diff,
        };

        let ens_lookup_mode = if deps.chain.is_mainnet() && self.shared_opts.ens {
            ENSLookup::Sync
        } else if deps.chain.is_mainnet() {
            ENSLookup::OnlyCached
        } else {
            ENSLookup::Disabled
        };

        let symbols_lookup = ERC20SymbolsLookup::lookup_mode(
            deps.symbols_lookup_worker,
            self.shared_opts.erc20_symbols,
        );

        let batch_data = fetch_blocks_batch(
            block_number,
            block_number,
            &deps.chain,
            &deps.sqlite,
            &symbols_lookup,
        )
        .await?;

        let pre_fetched = PreFetchedBlockData {
            txs_data: batch_data
                .txs_by_block
                .get(&block_number)
                .cloned()
                .unwrap_or_default(),
            logs_data: batch_data
                .logs_by_block
                .get(&block_number)
                .cloned()
                .unwrap_or_default(),
        };

        let json_opts = self.shared_opts.json_serialize_opts();
        let mev_block = generate_block(
            &deps.provider,
            &deps.sqlite,
            block_number,
            &ens_lookup_mode,
            &txs_filter,
            &self.shared_opts,
            &deps.chain,
            &deps.rpc_url,
            native_token_price,
            json_opts.include_logs,
            pre_fetched,
        )
        .await?;

        let txs = mev_block.transactions_json();
        let mut chain_info = ChainInfoNoRpcsJson::from_evm_chain(&deps.chain);
        chain_info.native_token_price = native_token_price;
        let duration_ns = start_time.elapsed().as_nanos() as u64;
        let pretty = matches!(format, OutputFormat::JsonPretty);
        let query = TxQueryParams {
            command: "tx",
            tx_hash: format!("{:#x}", self.tx_hash),
            before: self.before,
            after: self.after,
            reverse: self.reverse,
            evm_trace: self.shared_opts.evm_trace.clone(),
            evm_calls: self.shared_opts.evm_calls,
            evm_ops: self.shared_opts.evm_ops,
            evm_state_diff: self.shared_opts.evm_state_diff,
        };
        println!(
            "{}",
            serialize_json_response(&txs, json_opts, pretty, &chain_info, duration_ns, query)
                .unwrap()
        );

        // Allow async ENS and symbols lookups to finish
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        Ok(())
    }
}

fn get_matching_indexes(
    source_tx_index: u64,
    before: Option<u8>,
    after: Option<u8>,
) -> HashSet<u64> {
    let mut result = HashSet::new();
    result.insert(source_tx_index);

    if let Some(count) = after
        && count > 0
    {
        for i in 1..=count as u64 {
            result.insert(source_tx_index + i);
        }
    }

    if let Some(count) = before
        && count > 0
    {
        for i in 1..=count as u64 {
            if source_tx_index >= i {
                result.insert(source_tx_index - i);
            }
        }
    }

    result
}

fn check_range(value: Option<u8>, label: &str) -> Result<()> {
    if let Some(value) = value
        && value > 5
    {
        eyre::bail!("{} must be less than or equal 5", label);
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_matching_indexes() {
        let expected1: HashSet<u64> = [7, 8, 9, 10, 11, 12].into_iter().collect();
        assert_eq!(get_matching_indexes(10, Some(3), Some(2)), expected1);

        let expected2: HashSet<u64> = [15].into_iter().collect();

        assert_eq!(get_matching_indexes(15, None, None), expected2);

        let expected3 = [0, 1, 2].into_iter().collect();
        assert_eq!(get_matching_indexes(0, Some(5), Some(2)), expected3);
    }
}