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#[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 #[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 #[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 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
280fn 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}