mevlog 0.9.0

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

use alloy::{providers::Provider, sol};
use eyre::Result;
use revm::primitives::Address;
use tokio::sync::{
    RwLock,
    mpsc::{self, UnboundedSender},
};

use super::shared_init::init_provider;
use crate::{GenericProvider, models::mev_log_signature::MEVLogSignatureType};

sol! {
  #[sol(rpc)]
  interface IERC20 {
    function symbol() external view returns (string memory);
  }

  #[sol(rpc)]
  interface IUniswapPair {
    function token0() external view returns (address);
    function token1() external view returns (address);
  }
}

const MISSING_SYMBOL: &str = "E";

static SYMBOLS_MEMORY_CACHE: std::sync::LazyLock<RwLock<HashMap<Address, CachedEntry>>> =
    std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));

#[derive(Clone, Debug)]
pub enum CachedEntry {
    Known(String),
    KnownEmpty,
    Unknown,
}

pub enum ERC20SymbolsLookup {
    Async(ERC20SymbolLookupWorker),
    OnlyCached,
}

impl ERC20SymbolsLookup {
    pub fn lookup_mode(
        erc20_symbold_lookup_worker: ERC20SymbolLookupWorker,
        erc20_symbols_enabled: bool,
    ) -> ERC20SymbolsLookup {
        if erc20_symbols_enabled {
            ERC20SymbolsLookup::Async(erc20_symbold_lookup_worker)
        } else {
            ERC20SymbolsLookup::OnlyCached
        }
    }
}

pub type ERC20SymbolLookupWorker = UnboundedSender<(Address, MEVLogSignatureType)>;

#[allow(unused_mut)]
pub fn start_symbols_lookup_worker(rpc_url: &str) -> ERC20SymbolLookupWorker {
    let (tx, mut rx) = hotpath::channel!(
        mpsc::unbounded_channel::<(Address, MEVLogSignatureType)>(),
        log = true
    );

    let rpc_url = rpc_url.to_string();
    tokio::spawn(async move {
        let provider = init_provider(&rpc_url).await.unwrap();
        let provider = Arc::new(provider);
        while let Some(data) = rx.recv().await {
            let target = data.0;
            let sig_type = data.1;

            match sig_type {
                MEVLogSignatureType::ERC20 => match get_erc20_symbol(target, &provider).await {
                    Ok(_) => (),
                    Err(e) => {
                        tracing::error!("Error looking up ERC20 symbol: {}", e);
                    }
                },
                MEVLogSignatureType::UNIV2 | MEVLogSignatureType::UNIV3 => {
                    match get_uniswap_symbol(target, &provider).await {
                        Ok(_) => (),
                        Err(e) => {
                            tracing::error!("Error looking up UNI symbol: {}", e);
                        }
                    }
                }
            }
        }
    });

    tx
}

async fn get_uniswap_symbol(target: Address, provider: &Arc<GenericProvider>) -> Result<()> {
    match get_uniswap_symbol_core(target, provider).await {
        Ok(_) => (),
        Err(_e) => {
            write_symbol_cache(target, None).await?;
        }
    }
    Ok(())
}

async fn get_uniswap_symbol_core(target: Address, provider: &Arc<GenericProvider>) -> Result<()> {
    let pair = IUniswapPair::new(target, provider.clone());

    let multicall = provider.multicall().add(pair.token0()).add(pair.token1());

    let (token0, token1) = multicall.aggregate().await?;

    let itoken0 = IERC20::new(token0, provider.clone());
    let itoken1 = IERC20::new(token1, provider.clone());

    let multicall = provider
        .multicall()
        .add(itoken0.symbol())
        .add(itoken1.symbol());

    let (symbol0, symbol1) = multicall.aggregate().await?;
    write_symbol_cache(target, Some(format!("{symbol0}|{symbol1}"))).await?;

    Ok(())
}

async fn get_erc20_symbol(target: Address, provider: &Arc<GenericProvider>) -> Result<()> {
    match IERC20::new(target, provider.clone()).symbol().call().await {
        Ok(symbol) => {
            write_symbol_cache(target, Some(symbol)).await?;
        }
        Err(_e) => {
            write_symbol_cache(target, None).await?;
        }
    };

    Ok(())
}

#[hotpath::measure(log = true, future = true)]
async fn read_symbols_cache(target: Address) -> Result<CachedEntry> {
    {
        let cache = SYMBOLS_MEMORY_CACHE.read().await;
        if let Some(entry) = cache.get(&target) {
            return Ok(entry.clone());
        }
    }

    match cacache::read(&symbols_cache_dir(), target.to_string()).await {
        Ok(bytes) => {
            let name = String::from_utf8(bytes)
                .map_err(|e| eyre::eyre!("Invalid UTF-8 in cache: {}", e))?;
            let entry = if name.len() == 1 {
                CachedEntry::KnownEmpty
            } else {
                CachedEntry::Known(name)
            };

            {
                let mut cache = SYMBOLS_MEMORY_CACHE.write().await;
                cache.insert(target, entry.clone());
            }

            Ok(entry)
        }
        Err(_) => {
            {
                let mut cache = SYMBOLS_MEMORY_CACHE.write().await;
                cache.insert(target, CachedEntry::Unknown);
            }
            Ok(CachedEntry::Unknown)
        }
    }
}

pub async fn symbol_lookup_only_cached(target: Address) -> Result<Option<String>> {
    match read_symbols_cache(target).await? {
        CachedEntry::Known(name) => Ok(Some(name)),
        CachedEntry::KnownEmpty => Ok(None),
        CachedEntry::Unknown => Ok(None),
    }
}

pub async fn symbol_lookup_async(
    target: Address,
    signature_type: Option<MEVLogSignatureType>,
    symbols_lookup: &ERC20SymbolLookupWorker,
) -> Result<Option<String>> {
    let Some(signature_type) = signature_type else {
        return Ok(None);
    };

    match read_symbols_cache(target).await? {
        CachedEntry::Known(name) => Ok(Some(name)),
        CachedEntry::KnownEmpty => Ok(None),
        CachedEntry::Unknown => {
            symbols_lookup.send((target, signature_type))?;
            Ok(None)
        }
    }
}

async fn write_symbol_cache(target: Address, name: Option<String>) -> Result<()> {
    let name_record = match &name {
        Some(name) => name.as_str(),
        None => MISSING_SYMBOL,
    };

    let entry = match &name {
        Some(name) => CachedEntry::Known(name.clone()),
        None => CachedEntry::KnownEmpty,
    };

    {
        let mut cache = SYMBOLS_MEMORY_CACHE.write().await;
        cache.insert(target, entry);
    }

    match cacache::write(
        &symbols_cache_dir(),
        target.to_string(),
        name_record.as_bytes(),
    )
    .await
    {
        Ok(_) => (),
        Err(e) => {
            tracing::error!("Error writing ENS cache: {}", e);
        }
    };
    Ok(())
}

fn symbols_cache_dir() -> PathBuf {
    home::home_dir().unwrap().join(".mevlog/.symbols-cache")
}