mevlog 0.9.3

EVM transactions monitoring and querying CLI/TUI powered by Revm
Documentation
use std::{path::PathBuf, str::FromStr, sync::Arc};

use alloy::{
    providers::{Provider, ProviderBuilder},
    rpc::client::RpcClient,
    transports::layers::RetryBackoffLayer,
};
use eyre::{Result, bail};
use revm::primitives::Address;
use sqlx::SqlitePool;
use tokio::sync::mpsc::UnboundedSender;
use tracing::debug;

use crate::{
    GenericProvider,
    misc::db_actions::download_db_file,
    models::{db_chain::DBChain, evm_chain::EVMChain},
};
use crate::{
    misc::{
        config::Config,
        database::sqlite_conn,
        db_actions::{check_and_create_indexes, db_file_exists},
        ens_utils::start_ens_lookup_worker,
        rpc_urls::get_chain_info,
        symbol_utils::{ERC20SymbolLookupWorker, start_symbols_lookup_worker},
    },
    models::json::mev_transaction_json::JsonSerializeOpts,
};

pub struct SharedDeps {
    pub sqlite: SqlitePool,
    pub ens_lookup_worker: UnboundedSender<Address>,
    pub symbols_lookup_worker: ERC20SymbolLookupWorker,
    pub provider: Arc<GenericProvider>,
    pub chain: Arc<EVMChain>,
    pub rpc_url: String,
}

pub struct ResolvedConn {
    pub provider: Arc<GenericProvider>,
    pub rpc_url: String,
    pub chain_id: u64,
}

pub async fn resolve_conn(conn_opts: &ConnOpts) -> Result<ResolvedConn> {
    Config::init_if_missing()?;
    let config = Config::load()?;

    let rpc_url = match (&conn_opts.rpc_url, conn_opts.chain_id) {
        (Some(url), _) => url.clone(),
        (None, Some(chain_id)) => {
            if let Some(chain_cfg) = config.get_chain(chain_id) {
                chain_cfg.rpc_url.clone()
            } else {
                let chain_info = get_chain_info(chain_id, conn_opts.rpc_timeout_ms, 1).await?;
                if chain_info.benchmarked_rpc_urls.is_empty() {
                    bail!("No working RPC URLs found for chain ID {}", chain_id)
                }
                chain_info.benchmarked_rpc_urls[0].0.clone()
            }
        }
        _ => {
            bail!("Either --rpc-url or --chain-id must be specified")
        }
    };

    let provider = init_provider(&rpc_url).await?;
    let provider = Arc::new(provider);

    let chain_id = match (conn_opts.rpc_url.as_ref(), conn_opts.chain_id) {
        (Some(_), Some(expected_chain_id)) => {
            if conn_opts.skip_verify_chain_id {
                expected_chain_id
            } else {
                let chain_id = provider.get_chain_id().await?;
                if chain_id != expected_chain_id {
                    bail!(
                        "Chain ID mismatch --chain-id {} != --chain-id from --rpc-url {chain_id}",
                        expected_chain_id,
                    );
                }
                chain_id
            }
        }
        (_, Some(chain_id)) => chain_id,
        (_, None) => provider.get_chain_id().await?,
    };

    Ok(ResolvedConn {
        provider,
        rpc_url,
        chain_id,
    })
}

#[hotpath::measure(future = true)]
pub async fn init_deps(conn_opts: &ConnOpts) -> Result<SharedDeps> {
    let resolved = resolve_conn(conn_opts).await?;

    if !db_file_exists() {
        let _ = std::fs::create_dir_all(config_path());
        println!("Database file missing");
        download_db_file().await?;
    }

    let sqlite = sqlite_conn(None).await?;
    check_and_create_indexes(&sqlite).await?;
    let ens_lookup_worker = start_ens_lookup_worker(&resolved.rpc_url);
    let symbols_lookup_worker = start_symbols_lookup_worker(&resolved.rpc_url);

    let db_chain = DBChain::find(resolved.chain_id as i64, &sqlite)
        .await?
        .unwrap_or(DBChain::unknown(resolved.chain_id as i64));
    let chain = Arc::new(EVMChain::new(db_chain, resolved.rpc_url.clone())?);

    Ok(SharedDeps {
        sqlite,
        ens_lookup_worker,
        symbols_lookup_worker,
        provider: resolved.provider,
        chain,
        rpc_url: resolved.rpc_url,
    })
}

pub async fn init_provider(rpc_url: &str) -> Result<GenericProvider> {
    let max_retry = 10;
    let backoff = 1000;
    let cups = 100;
    let retry_layer = RetryBackoffLayer::new(max_retry, backoff, cups);

    debug!("Initializing HTTP provider");
    let client = RpcClient::builder()
        .layer(retry_layer)
        .http(rpc_url.parse()?);

    Ok(ProviderBuilder::new().connect_client(client))
}

pub fn config_path() -> PathBuf {
    home::home_dir().unwrap().join(".mevlog")
}

pub fn mevlog_cmd_path() -> PathBuf {
    std::env::var_os("MEVLOG_CMD_PATH")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("mevlog"))
}

#[derive(Clone, Debug, clap::Parser)]
pub struct SharedOpts {
    #[arg(long, help = "EVM tracing mode ('revm' or 'rpc')")]
    pub evm_trace: Option<TraceMode>,

    #[arg(long, help = "Show detailed tx calls info")]
    pub evm_calls: bool,

    #[arg(long, help = "Display EVM opcodes executed by the transaction")]
    pub evm_ops: bool,

    #[arg(
        long,
        help = "Display storage slot changes (state diff) for the transaction"
    )]
    pub evm_state_diff: bool,

    #[arg(long, help = "Display amounts in ERC20 Transfer event logs")]
    pub erc20_transfer_amount: bool,

    #[arg(long, help = "Enable ENS domains lookup")]
    pub ens: bool,

    #[arg(long, help = "Enable ERC20 symbols lookup")]
    pub erc20_symbols: bool,

    #[arg(long, help = "Include event logs in output")]
    pub logs: bool,

    #[arg(
        long,
        help = "Provide native token price in USD instead of reading it from price oracle"
    )]
    pub native_token_price: Option<f64>,
}

impl SharedOpts {
    pub fn json_serialize_opts(&self) -> JsonSerializeOpts {
        JsonSerializeOpts {
            include_logs: self.logs,
            include_evm_calls: self.evm_calls,
            include_evm_opcodes: self.evm_ops,
            include_evm_state_diff: self.evm_state_diff,
        }
    }
}

#[derive(Clone, Debug, clap::Parser)]
pub struct ConnOpts {
    #[arg(long, help = "The URL of the HTTP provider")]
    pub rpc_url: Option<String>,

    #[arg(long, help = "Chain ID to automatically select RPC URL from ChainList")]
    pub chain_id: Option<u64>,

    #[arg(
        long,
        help = "Timeout in milliseconds for filtering RPC URLs",
        default_value = "1000"
    )]
    pub rpc_timeout_ms: u64,

    #[arg(
        long,
        help = "Timeout in milliseconds for block fetching",
        default_value = "10000"
    )]
    pub block_timeout_ms: u64,

    #[arg(long, help = "Skip verifying --chain-id with data from --rpc-url")]
    pub skip_verify_chain_id: bool,
}

#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TraceMode {
    Revm,
    #[serde(rename = "rpc")]
    RPC,
}

impl std::fmt::Display for TraceMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Revm => write!(f, "revm"),
            Self::RPC => write!(f, "rpc"),
        }
    }
}

impl FromStr for TraceMode {
    type Err = eyre::Error;

    fn from_str(s: &str) -> Result<Self> {
        match s {
            "revm" => Ok(Self::Revm),
            "rpc" => Ok(Self::RPC),
            _ => Err(eyre::eyre!("Invalid tracing mode")),
        }
    }
}

#[derive(Debug, Clone, clap::ValueEnum, PartialEq)]
pub enum OutputFormat {
    Json,
    JsonPretty,
}

impl FromStr for OutputFormat {
    type Err = eyre::Error;

    fn from_str(s: &str) -> Result<Self> {
        match s {
            "json" => Ok(Self::Json),
            "json-pretty" => Ok(Self::JsonPretty),
            _ => Err(eyre::eyre!("Invalid output format")),
        }
    }
}