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