Skip to main content

cast/cmd/
events.rs

1use super::{MAX_CONCURRENT_RPC_REQUESTS, fetch_code_via_rpc, logs::LogQueryArgs};
2use crate::traces::{
3    CallTraceDecoderBuilder, DecodedEvent,
4    identifier::{ExternalIdentifier, SignaturesIdentifier},
5};
6use alloy_dyn_abi::DynSolValue;
7use alloy_json_abi::JsonAbi;
8use alloy_network::Network;
9use alloy_primitives::{Address, B256, Bytes, TxHash};
10use alloy_provider::Provider;
11use alloy_rpc_types::{BlockId, Log};
12use clap::{ArgGroup, Parser};
13use eyre::Result;
14use foundry_cli::{
15    json::print_json_object,
16    opts::{EtherscanOpts, RpcOpts},
17    utils::{self, load_config_from_provider},
18};
19use foundry_common::{
20    ContractsByArtifact, compile::ProjectCompiler, fmt::serialize_value_as_json, shell,
21};
22use foundry_config::{Chain, Config};
23use futures::StreamExt;
24use itertools::Itertools;
25use serde::{Serialize, Serializer};
26use std::{
27    collections::{BTreeMap, BTreeSet},
28    fmt::Write as _,
29};
30
31foundry_config::impl_figment_convert!(EventsArgs, etherscan, rpc);
32
33/// CLI arguments for `cast events`.
34#[derive(Debug, Parser)]
35#[command(group(
36    ArgGroup::new("event_source")
37        .required(true)
38        .multiple(true)
39        .args(["tx_hash", "address", "from_block", "to_block", "sig_or_topic"])
40))]
41pub struct EventsArgs {
42    /// Get events emitted by this transaction.
43    #[arg(
44        long,
45        alias = "txhash",
46        value_name = "TX_HASH",
47        conflicts_with_all = [
48            "from_block",
49            "to_block",
50            "address",
51            "sig_or_topic",
52            "topics_or_args",
53            "query_size"
54        ]
55    )]
56    tx_hash: Option<TxHash>,
57
58    #[command(flatten)]
59    query: LogQueryArgs,
60
61    #[command(flatten)]
62    etherscan: EtherscanOpts,
63
64    #[command(flatten)]
65    rpc: RpcOpts,
66
67    /// Use current project artifacts for event decoding.
68    ///
69    /// Only supported when querying by transaction hash.
70    #[arg(long, visible_alias = "la")]
71    with_local_artifacts: bool,
72}
73
74impl EventsArgs {
75    pub async fn run(self) -> Result<()> {
76        let figment =
77            self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self.etherscan);
78        let mut config = load_config_from_provider(figment)?;
79        let Self { tx_hash, mut query, with_local_artifacts, .. } = self;
80        let tx_hash = tx_hash.or_else(|| query.take_transaction_hash());
81        if with_local_artifacts && tx_hash.is_none() {
82            eyre::bail!("--with-local-artifacts is only supported with a transaction hash");
83        }
84        let provider = utils::get_provider(&config)?;
85        let rpc_chain = Chain::from(provider.get_chain_id().await?);
86        let (decoder_chain, explorer_chain) = resolve_chains(config.chain, rpc_chain);
87        config.chain = Some(decoder_chain);
88
89        let logs = if let Some(tx_hash) = tx_hash {
90            provider
91                .get_transaction_receipt(tx_hash)
92                .await?
93                .ok_or_else(|| eyre::eyre!("tx receipt not found: {tx_hash}"))?
94                .inner
95                .logs()
96                .to_vec()
97        } else {
98            let (filter, query_size) = query.resolve(&provider).await?;
99            match query_size {
100                Some(chunk_size) => {
101                    crate::cmd::logs::get_logs_chunked(&provider, &filter, chunk_size).await?
102                }
103                None => provider.get_logs(&filter).await?,
104            }
105        };
106
107        let local_abis = if with_local_artifacts {
108            local_event_abis(&provider, &logs, &config).await?
109        } else {
110            BTreeMap::new()
111        };
112        let events = decode_logs(logs, &config, explorer_chain, local_abis).await?;
113        if shell::is_json() {
114            print_json_object(events)?;
115        } else {
116            // Bypass the shell verbosity layer so `--quiet` does not suppress the primary result.
117            let mut shell = shell::Shell::get();
118            let out = shell.out();
119            write!(out, "{}", format_events(&events))?;
120            out.flush()?;
121        }
122        Ok(())
123    }
124}
125
126async fn local_event_abis<N: Network, P: Provider<N>>(
127    provider: &P,
128    logs: &[Log],
129    config: &Config,
130) -> Result<BTreeMap<Address, JsonAbi>> {
131    let addresses = logs.iter().map(Log::address).collect::<BTreeSet<_>>();
132    let Some(block_number) = logs.first().and_then(|log| log.block_number) else {
133        return Ok(BTreeMap::new());
134    };
135
136    let bytecodes = fetch_code_via_rpc(&provider, addresses, BlockId::number(block_number)).await;
137
138    let output = ProjectCompiler::new().quiet(true).compile(&config.project()?)?;
139    let contracts = ContractsByArtifact::from(output);
140    Ok(bytecodes
141        .iter()
142        .filter_map(|(&address, code)| {
143            let (_, contract) = contracts.find_by_deployed_code_exact_unique(code)?;
144            Some((address, contract.abi.clone()))
145        })
146        .collect())
147}
148
149async fn decode_logs(
150    logs: Vec<Log>,
151    config: &Config,
152    explorer_chain: Chain,
153    local_abis: BTreeMap<Address, JsonAbi>,
154) -> Result<Vec<EventOutput>> {
155    let signature_identifier = SignaturesIdentifier::from_config(config)?;
156    let mut builder = CallTraceDecoderBuilder::new()
157        .with_signature_identifier(signature_identifier)
158        .with_networks(config.networks)
159        .with_chain_id(config.chain.map(|chain| chain.id()));
160
161    let mut externally_resolved = BTreeSet::new();
162    if let Some(mut identifier) = ExternalIdentifier::new(config, Some(explorer_chain))? {
163        let addresses =
164            logs.iter().map(Log::address).collect::<BTreeSet<_>>().into_iter().collect::<Vec<_>>();
165        for (address, result) in identifier.get_abis(&addresses).await {
166            match result {
167                Ok((abis, complete)) => {
168                    externally_resolved.insert(address);
169                    if !complete {
170                        sh_warn!("Only partially resolved proxy ABI chain for {address}")?;
171                    }
172                    for abi in abis {
173                        builder = builder.with_address_events(address, &abi);
174                    }
175                }
176                Err(err) => sh_warn!("Failed to fetch ABI for {address}: {err}")?,
177            }
178        }
179    }
180
181    for (address, abi) in local_abis {
182        if !externally_resolved.contains(&address) {
183            builder = builder.with_address_events(address, &abi);
184        }
185    }
186
187    let decoder = builder.build();
188    let events = futures::stream::iter(logs)
189        .map(|log| async {
190            let decoded =
191                decoder.decode_event_with_address_signature(log.address(), log.data()).await;
192            EventOutput::new(log, decoded)
193        })
194        .buffered(MAX_CONCURRENT_RPC_REQUESTS)
195        .collect()
196        .await;
197    Ok(events)
198}
199
200#[derive(Debug, Serialize)]
201#[serde(rename_all = "camelCase")]
202struct EventOutput {
203    address: Address,
204    block_hash: Option<B256>,
205    block_number: Option<u64>,
206    block_timestamp: Option<u64>,
207    transaction_hash: Option<TxHash>,
208    transaction_index: Option<u64>,
209    log_index: Option<u64>,
210    removed: bool,
211    event: Option<String>,
212    params: Option<Vec<EventParam>>,
213    topics: Vec<B256>,
214    data: Bytes,
215}
216
217impl EventOutput {
218    fn new(log: Log, decoded: DecodedEvent) -> Self {
219        let params = decoded.params.map(|params| {
220            params
221                .into_iter()
222                .enumerate()
223                .map(|(index, (name, display_value, value))| EventParam {
224                    name: if name.is_empty() { format!("param{index}") } else { name },
225                    value,
226                    display_value,
227                })
228                .collect()
229        });
230        Self {
231            address: log.address(),
232            block_hash: log.block_hash,
233            block_number: log.block_number,
234            block_timestamp: log.block_timestamp,
235            transaction_hash: log.transaction_hash,
236            transaction_index: log.transaction_index,
237            log_index: log.log_index,
238            removed: log.removed,
239            event: decoded.name,
240            params,
241            topics: log.topics().to_vec(),
242            data: log.data().data.clone(),
243        }
244    }
245}
246
247#[derive(Debug, Serialize)]
248struct EventParam {
249    name: String,
250    #[serde(serialize_with = "serialize_abi_value")]
251    value: DynSolValue,
252    #[serde(skip)]
253    display_value: String,
254}
255
256fn serialize_abi_value<S>(value: &DynSolValue, serializer: S) -> Result<S::Ok, S::Error>
257where
258    S: Serializer,
259{
260    serialize_value_as_json(value.clone(), None, true)
261        .map_err(serde::ser::Error::custom)?
262        .serialize(serializer)
263}
264
265/// Formats decoded and raw events for human-readable output.
266///
267/// # Example
268///
269/// ```text
270/// [block 1, tx 0xabc..., log 0] 0x123...::Transfer(address,uint256) { from: 0x456..., value: 1 }
271/// 0x789...
272///   topic 0: 0xdef...
273///   data: 0x
274/// ```
275fn format_events(events: &[EventOutput]) -> String {
276    let mut output = String::new();
277    for event in events {
278        let location = [
279            event.block_number.map(|n| format!("block {n}")),
280            event.transaction_hash.map(|hash| format!("tx {hash}")),
281            event.log_index.map(|i| format!("log {i}")),
282        ];
283        let mut location = location.iter().flatten().peekable();
284        if location.peek().is_some() {
285            let _ = write!(output, "[{}] ", location.format(", "));
286        }
287        if let Some(name) = &event.event {
288            let _ = write!(output, "{}::{name}", event.address);
289            if let Some(params) = &event.params {
290                let params =
291                    params.iter().map(|param| format!("{}: {}", param.name, param.display_value));
292                let _ = write!(output, " {{ {} }}", params.format(", "));
293            }
294            output.push('\n');
295        } else {
296            let _ = writeln!(output, "{}", event.address);
297            for (index, topic) in event.topics.iter().enumerate() {
298                let _ = writeln!(output, "  topic {index}: {topic}");
299            }
300            let _ = writeln!(output, "  data: {}", event.data);
301        }
302    }
303    output
304}
305
306/// Decodes against the RPC chain, but looks ABIs up on the configured explorer chain.
307fn resolve_chains(configured: Option<Chain>, rpc_chain: Chain) -> (Chain, Chain) {
308    (rpc_chain, configured.unwrap_or(rpc_chain))
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use alloy_primitives::{Function, U256};
315
316    #[test]
317    fn configured_chain_controls_explorer_lookup() {
318        let rpc_chain = Chain::from(31337);
319        assert_eq!(
320            resolve_chains(Some(Chain::mainnet()), rpc_chain),
321            (rpc_chain, Chain::mainnet())
322        );
323        assert_eq!(resolve_chains(None, rpc_chain), (rpc_chain, rpc_chain));
324    }
325
326    #[test]
327    fn validates_event_sources() {
328        assert!(EventsArgs::try_parse_from(["events"]).is_err());
329        assert!(
330            EventsArgs::try_parse_from(["events", "--tx-hash", &TxHash::ZERO.to_string()]).is_ok()
331        );
332        let EventsArgs { tx_hash, mut query, .. } =
333            EventsArgs::try_parse_from(["events", &TxHash::ZERO.to_string()]).unwrap();
334        assert_eq!(tx_hash.or_else(|| query.take_transaction_hash()), Some(TxHash::ZERO));
335        let EventsArgs { mut query, .. } = EventsArgs::try_parse_from([
336            "events",
337            &TxHash::ZERO.to_string(),
338            "--address",
339            &Address::ZERO.to_string(),
340        ])
341        .unwrap();
342        assert!(query.take_transaction_hash().is_none());
343        assert!(
344            EventsArgs::try_parse_from([
345                "events",
346                "--tx-hash",
347                &TxHash::ZERO.to_string(),
348                "--address",
349                &Address::ZERO.to_string(),
350            ])
351            .is_err()
352        );
353        assert!(
354            EventsArgs::try_parse_from([
355                "events",
356                "--address",
357                &Address::ZERO.to_string(),
358                "--from-block",
359                "1",
360                "--to-block",
361                "2",
362            ])
363            .is_ok()
364        );
365    }
366
367    #[test]
368    fn serializes_abi_values_and_formats_display_values() {
369        let output = EventOutput::new(
370            Log::default(),
371            DecodedEvent {
372                name: Some("Value(uint256)".to_string()),
373                params: Some(vec![(
374                    "value".to_string(),
375                    "19705728070 [1.97e10]".to_string(),
376                    DynSolValue::Uint(U256::from(19_705_728_070u64), 256),
377                )]),
378            },
379        );
380        let json = serde_json::to_value(&output).unwrap();
381        assert_eq!(json["params"][0]["value"], "19705728070");
382        assert!(format_events(&[output]).contains("19705728070 [1.97e10]"));
383
384        let output = EventOutput::new(
385            Log::default(),
386            DecodedEvent {
387                name: Some("Callback(function)".to_string()),
388                params: Some(vec![(
389                    "callback".to_string(),
390                    "0x111111111111111111111111111111111111111111111111".to_string(),
391                    DynSolValue::Function(Function::new([0x11; 24])),
392                )]),
393            },
394        );
395        assert!(format_events(&[output]).contains(
396            "Callback(function) { callback: 0x111111111111111111111111111111111111111111111111 }"
397        ));
398    }
399
400    #[test]
401    fn formats_decoded_and_raw_events() {
402        let decoded = EventOutput {
403            address: Address::repeat_byte(0xaa),
404            block_hash: Some(B256::repeat_byte(0x33)),
405            block_number: Some(7),
406            block_timestamp: Some(123),
407            transaction_hash: Some(TxHash::repeat_byte(0xbb)),
408            transaction_index: Some(2),
409            log_index: Some(3),
410            removed: false,
411            event: Some("Transfer(address,address,uint256)".to_string()),
412            params: Some(vec![EventParam {
413                name: "value".to_string(),
414                value: DynSolValue::Uint(U256::from(42), 256),
415                display_value: "42".to_string(),
416            }]),
417            topics: vec![B256::repeat_byte(0x11)],
418            data: Bytes::from_static(&[0x22]),
419        };
420        let raw = EventOutput {
421            address: Address::repeat_byte(0xbb),
422            block_hash: None,
423            block_number: None,
424            block_timestamp: None,
425            transaction_hash: None,
426            transaction_index: None,
427            log_index: None,
428            removed: false,
429            event: None,
430            params: None,
431            topics: vec![B256::repeat_byte(0x11)],
432            data: Bytes::from_static(&[0x22]),
433        };
434
435        let value = serde_json::to_value(&decoded).unwrap();
436        assert_eq!(value["blockNumber"], 7);
437        assert_eq!(value["event"], "Transfer(address,address,uint256)");
438        assert_eq!(value["params"][0]["name"], "value");
439        assert_eq!(value["params"][0]["value"], "42");
440        assert_eq!(value["data"], "0x22");
441
442        assert_eq!(
443            format_events(&[decoded, raw]),
444            concat!(
445                "[block 7, tx 0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, log 3] ",
446                "0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa::Transfer(address,address,uint256) { value: 42 }\n",
447                "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\n",
448                "  topic 0: 0x1111111111111111111111111111111111111111111111111111111111111111\n",
449                "  data: 0x22\n",
450            )
451        );
452    }
453}