Skip to main content

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