Skip to main content

cast/cmd/
events.rs

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