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