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