ethl_cli/commands/
mod.rs

1use alloy::{json_abi::Event, primitives::Address, rpc::types::Filter};
2use anyhow::Result;
3use clap::{Parser, Subcommand, arg};
4use ethl::rpc::config::{ProviderOptions, ProviderSettings};
5
6pub mod cat;
7pub mod extract;
8
9#[derive(Debug, Subcommand)]
10pub enum Commands {
11    /// Extract a set of events from RPC nodes to parquet files to a given path (file://, s3://, etc)
12    #[command(arg_required_else_help = true)]
13    Extract(extract::ExtractArgs),
14
15    /// Stream latest logs (or parsed events) to console
16    #[command(arg_required_else_help = false)]
17    Cat(cat::CatArgs),
18}
19
20/// Common provider configuration for all commands
21#[derive(Debug, Parser)]
22pub struct ProviderArgs {
23    /// The chain ID to use for auto configuring providers (1=mainnet, 5=goerli, etc)
24    /// If not specified, defaults to Base mainnet (1)
25    #[arg(long, env, global = true)]
26    chain_id: Option<u64>,
27
28    /// Ankr API Key - Auto configures Ankr RPC and WSS endpoints for supported chain
29    #[arg(long, env, short, global = true)]
30    ankr_api_key: Option<String>,
31
32    /// Infura API Key - Auto configures Infura RPC and WSS endpoints for supported chain
33    #[arg(long, env, global = true)]
34    infura_api_key: Option<String>,
35
36    /// Quicknode API Key - Auto configures Quicknode RPC and WSS endpoints for supported chain
37    #[arg(long, env, global = true)]
38    quicknode_api_key: Option<String>,
39
40    /// Alchemy API Key - Auto configures Alchemy RPC and WSS endpoints for supported chain
41    #[arg(long, env, global = true)]
42    alchemy_api_key: Option<String>,
43
44    /// Comma separated list of custom HTTP RPC URLs
45    #[arg(long, env, global = true)]
46    rpc_url: Option<Vec<String>>,
47
48    /// Comma separated list of custom websocket URLs
49    #[arg(long, env, global = true)]
50    ws_url: Option<Vec<String>>,
51}
52
53impl TryFrom<ProviderArgs> for ProviderSettings {
54    type Error = anyhow::Error;
55    fn try_from(args: ProviderArgs) -> Result<Self, Self::Error> {
56        ProviderSettings::build(
57            ProviderOptions {
58                ankr_api_key: args.ankr_api_key,
59                infura_api_key: args.infura_api_key,
60                quicknode_api_key: args.quicknode_api_key,
61                alchemy_api_key: args.alchemy_api_key,
62                rpc_urls: args.rpc_url,
63                ws_urls: args.ws_url,
64            },
65            args.chain_id.unwrap_or(1), // Default to Base mainnet
66        )
67    }
68}
69
70#[derive(Debug, Parser)]
71pub struct FilterArgs {
72    /// Full event signatures to filter and parse against (e.g. "event Transfer(address from,address to,uint256 amount)")
73    #[arg(long, required = false)]
74    events: Option<Vec<String>>,
75
76    /// Comma separated list of contract addresses to filter logs by
77    #[arg(long, short, required = false, value_delimiter = ',')]
78    addresses: Option<Vec<Address>>,
79
80    /// Optional end block for the archive (default: latest)
81    #[arg(long)]
82    to_block: Option<u64>,
83
84    /// Optional start block for the archive (default: 0)
85    #[arg(long)]
86    from_block: Option<u64>,
87}
88
89impl TryInto<Option<Filter>> for &FilterArgs {
90    type Error = anyhow::Error;
91
92    fn try_into(self) -> Result<Option<Filter>> {
93        if self.addresses.is_none() && self.events.is_none() {
94            return Ok(None);
95        }
96
97        let mut filter = Filter::new();
98        if let Some(addresses) = &self.addresses {
99            filter = filter.address(addresses.clone());
100        }
101
102        let events: Option<Vec<Event>> = self.try_into()?;
103
104        if let Some(events) = events {
105            filter = filter.events(
106                events
107                    .iter()
108                    .map(|e| e.signature())
109                    .collect::<Vec<String>>(),
110            );
111        }
112
113        if let Some(from_block) = self.from_block {
114            filter = filter.from_block(from_block);
115        }
116
117        if let Some(to_block) = self.to_block {
118            filter = filter.to_block(to_block);
119        }
120
121        Ok(Some(filter))
122    }
123}
124
125impl TryInto<Option<Vec<Event>>> for &FilterArgs {
126    type Error = anyhow::Error;
127    fn try_into(self) -> Result<Option<Vec<Event>>> {
128        if self.events.is_none() {
129            return Ok(None);
130        }
131
132        let events = self
133            .events
134            .as_ref()
135            .unwrap()
136            .iter()
137            .map(|signature| Event::parse(signature))
138            .collect::<Result<Vec<Event>, _>>()?;
139
140        Ok(Some(events))
141    }
142}