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