1use crate::Cast;
2use alloy_dyn_abi::{DynSolType, DynSolValue, Specifier};
3use alloy_ens::NameOrAddress;
4use alloy_json_abi::Event;
5use alloy_network::AnyNetwork;
6use alloy_primitives::{Address, B256, hex::FromHex};
7use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter, FilterBlockOption, FilterSet, Topic};
8use clap::Parser;
9use eyre::Result;
10use foundry_cli::{
11 opts::RpcOpts,
12 utils::{self, LoadConfig},
13};
14use itertools::Itertools;
15use std::{io, str::FromStr};
16
17#[derive(Debug, Parser)]
19pub struct LogsArgs {
20 #[arg(long)]
24 from_block: Option<BlockId>,
25
26 #[arg(long)]
30 to_block: Option<BlockId>,
31
32 #[arg(long, value_parser = NameOrAddress::from_str)]
34 address: Option<Vec<NameOrAddress>>,
35
36 #[arg(value_name = "SIG_OR_TOPIC")]
39 sig_or_topic: Option<String>,
40
41 #[arg(value_name = "TOPICS_OR_ARGS")]
44 topics_or_args: Vec<String>,
45
46 #[arg(long)]
49 subscribe: bool,
50
51 #[arg(long, value_name = "BLOCKS")]
57 query_size: Option<u64>,
58
59 #[command(flatten)]
60 rpc: RpcOpts,
61}
62
63impl LogsArgs {
64 pub async fn run(self) -> Result<()> {
65 let Self {
66 from_block,
67 to_block,
68 address,
69 sig_or_topic,
70 topics_or_args,
71 subscribe,
72 query_size,
73 rpc,
74 } = self;
75
76 let config = rpc.load_config()?;
77 let provider = utils::get_provider(&config)?;
78
79 let cast = Cast::new(&provider);
80 let addresses = match address {
81 Some(addresses) => Some(
82 futures::future::try_join_all(addresses.into_iter().map(|address| {
83 let provider = provider.clone();
84 async move { address.resolve(&provider).await }
85 }))
86 .await?,
87 ),
88 None => None,
89 };
90
91 let from_block =
92 cast.convert_block_number(Some(from_block.unwrap_or_else(BlockId::earliest))).await?;
93 let to_block =
94 cast.convert_block_number(Some(to_block.unwrap_or_else(BlockId::latest))).await?;
95
96 let filter = build_filter(from_block, to_block, addresses, sig_or_topic, topics_or_args)?;
97
98 if !subscribe {
99 let logs = match query_size {
100 Some(chunk_size) => cast.filter_logs_chunked(filter, chunk_size).await?,
101 None => cast.filter_logs(filter).await?,
102 };
103 sh_println!("{logs}")?;
104 return Ok(());
105 }
106
107 let url = config.get_rpc_url_or_localhost_http()?;
113 let provider = alloy_provider::ProviderBuilder::<_, _, AnyNetwork>::default()
114 .connect(url.as_ref())
115 .await?;
116 let cast = Cast::new(&provider);
117 let mut stdout = io::stdout();
118 cast.subscribe(filter, &mut stdout).await?;
119
120 Ok(())
121 }
122}
123
124fn build_filter(
128 from_block: Option<BlockNumberOrTag>,
129 to_block: Option<BlockNumberOrTag>,
130 address: Option<Vec<Address>>,
131 sig_or_topic: Option<String>,
132 topics_or_args: Vec<String>,
133) -> Result<Filter, eyre::Error> {
134 let block_option = FilterBlockOption::Range { from_block, to_block };
135 let filter = match sig_or_topic {
136 Some(sig_or_topic) => match foundry_common::abi::get_event(sig_or_topic.as_str()) {
138 Ok(event) => build_filter_event_sig(event, topics_or_args)?,
139 Err(_) => {
140 let topics = [vec![sig_or_topic], topics_or_args].concat();
141 build_filter_topics(topics)?
142 }
143 },
144 None => Filter::default(),
145 };
146
147 let mut filter = filter.select(block_option);
148
149 if let Some(address) = address {
150 filter = filter.address(address)
151 }
152
153 Ok(filter)
154}
155
156fn build_filter_event_sig(event: Event, args: Vec<String>) -> Result<Filter, eyre::Error> {
158 let args = args.iter().map(|arg| arg.as_str()).collect::<Vec<_>>();
159
160 let (with_args, without_args): (Vec<_>, Vec<_>) = event
163 .inputs
164 .iter()
165 .zip(args)
166 .filter(|(input, _)| input.indexed)
167 .map(|(input, arg)| {
168 let kind = input.resolve()?;
169 Ok((kind, arg))
170 })
171 .collect::<Result<Vec<(DynSolType, &str)>>>()?
172 .into_iter()
173 .enumerate()
174 .partition(|(_, (_, arg))| !arg.is_empty());
175
176 let indexed_tokens = with_args
178 .iter()
179 .map(|(_, (kind, arg))| kind.coerce_str(arg))
180 .collect::<Result<Vec<DynSolValue>, _>>()?;
181
182 let mut topics = with_args
184 .into_iter()
185 .zip(indexed_tokens)
186 .map(|((i, _), t)| (i, Some(t)))
187 .chain(without_args.into_iter().map(|(i, _)| (i, None)))
188 .sorted_by(|(i1, _), (i2, _)| i1.cmp(i2))
189 .map(|(_, token)| {
190 token
191 .map(|token| Topic::from(B256::from_slice(token.abi_encode().as_slice())))
192 .unwrap_or(Topic::default())
193 })
194 .collect::<Vec<Topic>>();
195
196 topics.resize(3, Topic::default());
197
198 let filter = Filter::new()
199 .event_signature(event.selector())
200 .topic1(topics[0].clone())
201 .topic2(topics[1].clone())
202 .topic3(topics[2].clone());
203
204 Ok(filter)
205}
206
207fn build_filter_topics(topics: Vec<String>) -> Result<Filter, eyre::Error> {
209 let mut topics = topics
210 .into_iter()
211 .map(|topic| {
212 if topic.is_empty() {
213 Ok(Topic::default())
214 } else {
215 Ok(Topic::from(B256::from_hex(topic.as_str())?))
216 }
217 })
218 .collect::<Result<Vec<FilterSet<_>>>>()?;
219
220 topics.resize(4, Topic::default());
221
222 let filter = Filter::new()
223 .event_signature(topics[0].clone())
224 .topic1(topics[1].clone())
225 .topic2(topics[2].clone())
226 .topic3(topics[3].clone());
227
228 Ok(filter)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use alloy_primitives::{U160, U256};
235 use alloy_rpc_types::ValueOrArray;
236
237 const ADDRESS: &str = "0x4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38";
238 const TRANSFER_SIG: &str = "Transfer(address indexed,address indexed,uint256)";
239 const TRANSFER_TOPIC: &str =
240 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
241
242 #[test]
243 fn test_build_filter_basic() {
244 let from_block = Some(BlockNumberOrTag::from(1337));
245 let to_block = Some(BlockNumberOrTag::Latest);
246 let address = Address::from_str(ADDRESS).ok();
247 let expected = Filter {
248 block_option: FilterBlockOption::Range { from_block, to_block },
249 address: ValueOrArray::Value(address.unwrap()).into(),
250 topics: [vec![].into(), vec![].into(), vec![].into(), vec![].into()],
251 };
252 let filter =
253 build_filter(from_block, to_block, address.map(|addr| vec![addr]), None, vec![])
254 .unwrap();
255 assert_eq!(filter, expected)
256 }
257
258 #[test]
259 fn test_build_filter_sig() {
260 let expected = Filter {
261 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
262 address: vec![].into(),
263 topics: [
264 B256::from_str(TRANSFER_TOPIC).unwrap().into(),
265 vec![].into(),
266 vec![].into(),
267 vec![].into(),
268 ],
269 };
270 let filter =
271 build_filter(None, None, None, Some(TRANSFER_SIG.to_string()), vec![]).unwrap();
272 assert_eq!(filter, expected)
273 }
274
275 #[test]
276 fn test_build_filter_mismatch() {
277 let expected = Filter {
278 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
279 address: vec![].into(),
280 topics: [
281 B256::from_str(TRANSFER_TOPIC).unwrap().into(),
282 vec![].into(),
283 vec![].into(),
284 vec![].into(),
285 ],
286 };
287 let filter = build_filter(
288 None,
289 None,
290 None,
291 Some("Swap(address indexed from, address indexed to, uint256 value)".to_string()), vec![],
293 )
294 .unwrap();
295 assert_ne!(filter, expected)
296 }
297
298 #[test]
299 fn test_build_filter_sig_with_arguments() {
300 let addr = Address::from_str(ADDRESS).unwrap();
301 let addr = U256::from(U160::from_be_bytes(addr.0.0));
302 let expected = Filter {
303 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
304 address: vec![].into(),
305 topics: [
306 B256::from_str(TRANSFER_TOPIC).unwrap().into(),
307 addr.into(),
308 vec![].into(),
309 vec![].into(),
310 ],
311 };
312 let filter = build_filter(
313 None,
314 None,
315 None,
316 Some(TRANSFER_SIG.to_string()),
317 vec![ADDRESS.to_string()],
318 )
319 .unwrap();
320 assert_eq!(filter, expected)
321 }
322
323 #[test]
324 fn test_build_filter_sig_with_skipped_arguments() {
325 let addr = Address::from_str(ADDRESS).unwrap();
326 let addr = U256::from(U160::from_be_bytes(addr.0.0));
327 let expected = Filter {
328 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
329 address: vec![].into(),
330 topics: [
331 vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
332 vec![].into(),
333 addr.into(),
334 vec![].into(),
335 ],
336 };
337 let filter = build_filter(
338 None,
339 None,
340 None,
341 Some(TRANSFER_SIG.to_string()),
342 vec![String::new(), ADDRESS.to_string()],
343 )
344 .unwrap();
345 assert_eq!(filter, expected)
346 }
347
348 #[test]
349 fn test_build_filter_with_topics() {
350 let expected = Filter {
351 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
352 address: vec![].into(),
353 topics: [
354 vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
355 vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
356 vec![].into(),
357 vec![].into(),
358 ],
359 };
360 let filter = build_filter(
361 None,
362 None,
363 None,
364 Some(TRANSFER_TOPIC.to_string()),
365 vec![TRANSFER_TOPIC.to_string()],
366 )
367 .unwrap();
368
369 assert_eq!(filter, expected)
370 }
371
372 #[test]
373 fn test_build_filter_with_skipped_topic() {
374 let expected = Filter {
375 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
376 address: vec![].into(),
377 topics: [
378 vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
379 vec![].into(),
380 vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
381 vec![].into(),
382 ],
383 };
384 let filter = build_filter(
385 None,
386 None,
387 None,
388 Some(TRANSFER_TOPIC.to_string()),
389 vec![String::new(), TRANSFER_TOPIC.to_string()],
390 )
391 .unwrap();
392
393 assert_eq!(filter, expected)
394 }
395
396 #[test]
397 fn test_build_filter_with_multiple_addresses() {
398 let expected = Filter {
399 block_option: FilterBlockOption::Range { from_block: None, to_block: None },
400 address: vec![Address::ZERO, ADDRESS.parse().unwrap()].into(),
401 topics: [
402 vec![TRANSFER_TOPIC.parse().unwrap()].into(),
403 vec![].into(),
404 vec![].into(),
405 vec![].into(),
406 ],
407 };
408 let filter = build_filter(
409 None,
410 None,
411 Some(vec![Address::ZERO, ADDRESS.parse().unwrap()]),
412 Some(TRANSFER_TOPIC.to_string()),
413 vec![],
414 )
415 .unwrap();
416 assert_eq!(filter, expected)
417 }
418
419 #[test]
420 fn test_build_filter_sig_with_mismatched_argument() {
421 let err = build_filter(
422 None,
423 None,
424 None,
425 Some(TRANSFER_SIG.to_string()),
426 vec!["1234".to_string()],
427 )
428 .err()
429 .unwrap()
430 .to_string()
431 .to_lowercase();
432
433 assert_eq!(err, "parser error:\n1234\n^\ninvalid string length");
434 }
435
436 #[test]
437 fn test_build_filter_with_invalid_sig_or_topic() {
438 let err = build_filter(None, None, None, Some("asdasdasd".to_string()), vec![])
439 .err()
440 .unwrap()
441 .to_string()
442 .to_lowercase();
443
444 assert_eq!(err, "odd number of digits");
445 }
446
447 #[test]
448 fn test_build_filter_with_invalid_sig_or_topic_hex() {
449 let err = build_filter(None, None, None, Some(ADDRESS.to_string()), vec![])
450 .err()
451 .unwrap()
452 .to_string()
453 .to_lowercase();
454
455 assert_eq!(err, "invalid string length");
456 }
457
458 #[test]
459 fn test_build_filter_with_invalid_topic() {
460 let err = build_filter(
461 None,
462 None,
463 None,
464 Some(TRANSFER_TOPIC.to_string()),
465 vec!["1234".to_string()],
466 )
467 .err()
468 .unwrap()
469 .to_string()
470 .to_lowercase();
471
472 assert_eq!(err, "invalid string length");
473 }
474}