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