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