ethl-cli 0.1.31

Tools for capturing, processing, archiving, and replaying Ethereum events
Documentation
use alloy::{
    dyn_abi::{DecodedEvent, DynSolValue, EventExt},
    hex,
    json_abi::{Event, EventParam},
    rpc::types::{Filter, Log},
};
use anyhow::Result;
use clap::Parser;
use ethl::rpc::{config::ProviderSettings, stream::stream_logs};
use futures_util::{StreamExt, pin_mut};
use tracing::info;

use crate::commands::FilterArgs;

#[derive(Parser, Debug)]
pub struct CatArgs {
    /// Filter arguments for scoping log queries
    #[command(flatten)]
    filter_args: FilterArgs,
}

pub async fn run_cat_command(providers: &ProviderSettings, args: CatArgs) -> Result<()> {
    info!("Watching logs");

    let filter: Option<Filter> = (&args.filter_args).try_into()?;
    let events: Option<Vec<Event>> = (&args.filter_args).try_into()?;
    let printers: Option<Vec<EventDisplay>> = events
        .as_ref()
        .map(|evs| evs.iter().cloned().map(EventDisplay::new).collect());

    let stream = stream_logs(providers, filter.as_ref()).await;
    pin_mut!(stream);
    while let Some(result) = stream.next().await {
        let (from_block, to_block, logs) = result?;
        info!(
            "observed {:05} logs for blocks {:012}-{:012}",
            logs.len(),
            from_block,
            to_block
        );
        match &printers {
            Some(evs) => {
                for log in logs {
                    for ev in evs {
                        if let Some(formatted) = ev.maybe_format(&log) {
                            info!("{}", formatted);
                        }
                    }
                }
            }
            None => {
                for log in logs {
                    info!("{:?}", log);
                }
            }
        }
    }

    Ok(())
}

struct EventDisplay {
    event: Event,
    indexed: Vec<EventParam>,
    data: Vec<EventParam>,
}

impl EventDisplay {
    fn new(event: Event) -> Self {
        let indexed = event
            .inputs
            .iter()
            .filter(|param| param.indexed)
            .cloned()
            .collect();
        let data = event
            .inputs
            .iter()
            .filter(|param| !param.indexed)
            .cloned()
            .collect();
        Self {
            event,
            indexed,
            data,
        }
    }

    pub fn maybe_format(&self, log: &Log) -> Option<String> {
        if log.topics().first() == Some(&self.event.selector()) {
            let decoded = self.event.decode_log(log.data());
            if decoded.is_err() {
                return Some(format!("Failed to decode event: {}", self.event.name));
            }
            return Some(self.format(&decoded.unwrap()));
        }
        None
    }

    pub fn format(&self, decoded: &DecodedEvent) -> String {
        let mut pairs = Vec::new();
        self.indexed.iter().enumerate().for_each(|(i, param)| {
            pairs.push(format!(
                "{}={}",
                param.name,
                decoded
                    .indexed
                    .get(i)
                    .map(dyn_sol_value_to_string)
                    .unwrap_or("n/a".to_string())
            ));
        });
        self.data.iter().enumerate().for_each(|(i, param)| {
            pairs.push(format!(
                "{}={}",
                param.name,
                decoded
                    .body
                    .get(i)
                    .map(dyn_sol_value_to_string)
                    .unwrap_or("n/a".to_string())
            ));
        });
        format!("{}({})", self.event.name, pairs.join(", "),)
    }
}

fn dyn_sol_value_to_string(value: &DynSolValue) -> String {
    match value {
        DynSolValue::Address(v) => v.to_string(),
        DynSolValue::Bytes(v) => hex::encode_prefixed(v),
        DynSolValue::Int(v, _) => v.to_string(),
        DynSolValue::Uint(v, _) => v.to_string(),
        DynSolValue::Bool(b) => b.to_string(),
        DynSolValue::String(s) => s.clone(),
        DynSolValue::FixedBytes(v, _) => hex::encode_prefixed(v),
        DynSolValue::Array(v) | DynSolValue::FixedArray(v) => {
            format!(
                "[{}]",
                v.iter()
                    .map(dyn_sol_value_to_string)
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        }
        DynSolValue::Tuple(v) => {
            format!(
                "({})",
                v.iter()
                    .map(dyn_sol_value_to_string)
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        }
        DynSolValue::Function(_) => "fn".to_string(),
    }
}