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