cast/cmd/
logs.rs

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/// CLI arguments for `cast logs`.
15#[derive(Debug, Parser)]
16pub struct LogsArgs {
17    /// The block height to start query at.
18    ///
19    /// Can also be the tags earliest, finalized, safe, latest, or pending.
20    #[arg(long)]
21    from_block: Option<BlockId>,
22
23    /// The block height to stop query at.
24    ///
25    /// Can also be the tags earliest, finalized, safe, latest, or pending.
26    #[arg(long)]
27    to_block: Option<BlockId>,
28
29    /// The contract address to filter on.
30    #[arg(long, value_parser = NameOrAddress::from_str)]
31    address: Option<Vec<NameOrAddress>>,
32
33    /// The signature of the event to filter logs by which will be converted to the first topic or
34    /// a topic to filter on.
35    #[arg(value_name = "SIG_OR_TOPIC")]
36    sig_or_topic: Option<String>,
37
38    /// If used with a signature, the indexed fields of the event to filter by. Otherwise, the
39    /// remaining topics of the filter.
40    #[arg(value_name = "TOPICS_OR_ARGS")]
41    topics_or_args: Vec<String>,
42
43    /// If the RPC type and endpoints supports `eth_subscribe` stream logs instead of printing and
44    /// exiting. Will continue until interrupted or TO_BLOCK is reached.
45    #[arg(long)]
46    subscribe: bool,
47
48    /// Number of blocks to query in each chunk when the provider has range limits.
49    /// Defaults to 10000 blocks per chunk.
50    #[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        // FIXME: this is a hotfix for <https://github.com/foundry-rs/foundry/issues/7682>
99        //  currently the alloy `eth_subscribe` impl does not work with all transports, so we use
100        // the builtin transport here for now
101        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
113/// Builds a Filter by first trying to parse the `sig_or_topic` as an event signature. If
114/// successful, `topics_or_args` is parsed as indexed inputs and converted to topics. Otherwise,
115/// `sig_or_topic` is prepended to `topics_or_args` and used as raw topics.
116fn 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        // Try and parse the signature as an event signature
126        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
145/// Creates a [Filter] from the given event signature and arguments.
146fn 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    // Match the args to indexed inputs. Enumerate so that the ordering can be restored
150    // when merging the inputs with arguments and without arguments
151    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    // Only parse the inputs with arguments
166    let indexed_tokens = with_args
167        .iter()
168        .map(|(_, (kind, arg))| kind.coerce_str(arg))
169        .collect::<Result<Vec<DynSolValue>, _>>()?;
170
171    // Merge the inputs restoring the original ordering
172    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
196/// Creates a [Filter] from raw topic hashes.
197fn 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()), // Change signature, should result in error
281            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}