Skip to main content

cast/cmd/
logs.rs

1use crate::{Cast, encode_event_topic};
2use alloy_dyn_abi::{DynSolType, DynSolValue, Specifier};
3use alloy_ens::NameOrAddress;
4use alloy_json_abi::Event;
5use alloy_network::{AnyNetwork, Network};
6use alloy_primitives::{Address, B256, TxHash, hex::FromHex};
7use alloy_provider::Provider;
8use alloy_rpc_types::{BlockId, BlockNumberOrTag, Filter, FilterBlockOption, FilterSet, Topic};
9use clap::Parser;
10use eyre::Result;
11use foundry_cli::{
12    opts::RpcOpts,
13    utils::{self, LoadConfig},
14};
15use itertools::Itertools;
16use std::{io, str::FromStr};
17
18/// CLI arguments for `cast logs`.
19#[derive(Debug, Parser)]
20pub struct LogsArgs {
21    #[command(flatten)]
22    query: LogQueryArgs,
23
24    /// If the RPC type and endpoints supports `eth_subscribe` stream logs instead of printing and
25    /// exiting. Will continue until interrupted or TO_BLOCK is reached.
26    #[arg(long)]
27    subscribe: bool,
28
29    #[command(flatten)]
30    rpc: RpcOpts,
31}
32
33/// Arguments shared by commands that query logs with `eth_getLogs`.
34#[derive(Debug, Parser)]
35pub struct LogQueryArgs {
36    /// The block height to start query at.
37    ///
38    /// Can also be the tags earliest, finalized, safe, latest, or pending.
39    #[arg(long)]
40    from_block: Option<BlockId>,
41
42    /// The block height to stop query at.
43    ///
44    /// Can also be the tags earliest, finalized, safe, latest, or pending.
45    #[arg(long)]
46    to_block: Option<BlockId>,
47
48    /// The contract address to filter on.
49    #[arg(long, value_parser = NameOrAddress::from_str)]
50    address: Option<Vec<NameOrAddress>>,
51
52    /// The signature of the event to filter logs by which will be converted to the first topic or
53    /// a topic to filter on.
54    #[arg(value_name = "SIG_OR_TOPIC")]
55    sig_or_topic: Option<String>,
56
57    /// If used with a signature, the indexed fields of the event to filter by. Otherwise, the
58    /// remaining topics of the filter.
59    #[arg(value_name = "TOPICS_OR_ARGS")]
60    topics_or_args: Vec<String>,
61
62    /// Split the query into chunks of this many blocks to work around provider range/result
63    /// limits.
64    ///
65    /// When omitted, the range is queried in a single request. Pass a value (e.g. `10000`) to
66    /// fetch the logs in `query-size`-block chunks instead.
67    #[arg(long, value_name = "BLOCKS")]
68    query_size: Option<u64>,
69}
70
71impl LogsArgs {
72    pub async fn run(self) -> Result<()> {
73        let Self { query, subscribe, rpc } = self;
74
75        let config = rpc.load_config()?;
76        let provider = utils::get_provider(&config)?;
77        let (filter, query_size) = query.resolve(&provider).await?;
78        let cast = Cast::new(&provider);
79
80        if !subscribe {
81            let logs = match query_size {
82                Some(chunk_size) => cast.filter_logs_chunked(filter, chunk_size).await?,
83                None => cast.filter_logs(filter).await?,
84            };
85            sh_println!("{logs}")?;
86            return Ok(());
87        }
88
89        // JSON envelope intentionally unsupported for streaming: --subscribe emits NDJSON events
90        // continuously; a terminal JsonEnvelope is pointless.
91        // FIXME: this is a hotfix for <https://github.com/foundry-rs/foundry/issues/7682>
92        //  currently the alloy `eth_subscribe` impl does not work with all transports, so we use
93        // the builtin transport here for now
94        let url = config.get_rpc_url_or_localhost_http()?;
95        let provider = alloy_provider::ProviderBuilder::<_, _, AnyNetwork>::default()
96            .connect(url.as_ref())
97            .await?;
98        let cast = Cast::new(&provider);
99        let mut stdout = io::stdout();
100        cast.subscribe(filter, &mut stdout).await?;
101
102        Ok(())
103    }
104}
105
106impl LogQueryArgs {
107    /// Takes a lone positional transaction hash, if present.
108    pub(super) fn take_transaction_hash(&mut self) -> Option<TxHash> {
109        if self.from_block.is_none()
110            && self.to_block.is_none()
111            && self.address.is_none()
112            && self.topics_or_args.is_empty()
113            && self.query_size.is_none()
114            && let Some(tx_hash) = self.sig_or_topic.as_deref().and_then(|value| value.parse().ok())
115        {
116            self.sig_or_topic = None;
117            return Some(tx_hash);
118        }
119        None
120    }
121
122    /// Resolves names and block tags and builds the RPC filter.
123    pub async fn resolve<P, N>(self, provider: &P) -> Result<(Filter, Option<u64>)>
124    where
125        P: Provider<N> + Clone + Unpin,
126        N: Network,
127    {
128        let Self { from_block, to_block, address, sig_or_topic, topics_or_args, query_size } = self;
129
130        let cast = Cast::new(&provider);
131        let addresses = match address {
132            Some(addresses) => Some(
133                futures::future::try_join_all(addresses.into_iter().map(|address| {
134                    let provider = provider.clone();
135                    async move { address.resolve(&provider).await }
136                }))
137                .await?,
138            ),
139            None => None,
140        };
141
142        let from_block =
143            cast.convert_block_number(Some(from_block.unwrap_or_else(BlockId::earliest))).await?;
144        let to_block =
145            cast.convert_block_number(Some(to_block.unwrap_or_else(BlockId::latest))).await?;
146        let filter = build_filter(from_block, to_block, addresses, sig_or_topic, topics_or_args)?;
147
148        Ok((filter, query_size))
149    }
150}
151
152/// Builds a Filter by first trying to parse the `sig_or_topic` as an event signature. If
153/// successful, `topics_or_args` is parsed as indexed inputs and converted to topics. Otherwise,
154/// `sig_or_topic` is prepended to `topics_or_args` and used as raw topics.
155fn build_filter(
156    from_block: Option<BlockNumberOrTag>,
157    to_block: Option<BlockNumberOrTag>,
158    address: Option<Vec<Address>>,
159    sig_or_topic: Option<String>,
160    topics_or_args: Vec<String>,
161) -> Result<Filter, eyre::Error> {
162    let block_option = FilterBlockOption::Range { from_block, to_block };
163    let filter = match sig_or_topic {
164        // Try and parse the signature as an event signature
165        Some(sig_or_topic) => match foundry_common::abi::get_event(sig_or_topic.as_str()) {
166            Ok(event) => build_filter_event_sig(event, topics_or_args)?,
167            Err(_) => {
168                let topics = [vec![sig_or_topic], topics_or_args].concat();
169                build_filter_topics(topics)?
170            }
171        },
172        None => Filter::default(),
173    };
174
175    let mut filter = filter.select(block_option);
176
177    if let Some(address) = address {
178        filter = filter.address(address)
179    }
180
181    Ok(filter)
182}
183
184/// Creates a [Filter] from the given event signature and arguments.
185fn build_filter_event_sig(event: Event, args: Vec<String>) -> Result<Filter, eyre::Error> {
186    let args = args.iter().map(|arg| arg.as_str()).collect::<Vec<_>>();
187
188    // Match the args to indexed inputs. Enumerate so that the ordering can be restored
189    // when merging the inputs with arguments and without arguments
190    let (with_args, without_args): (Vec<_>, Vec<_>) = event
191        .inputs
192        .iter()
193        .filter(|input| input.indexed)
194        .zip(args)
195        .map(|(input, arg)| {
196            let kind = input.resolve()?;
197            Ok((kind, arg))
198        })
199        .collect::<Result<Vec<(DynSolType, &str)>>>()?
200        .into_iter()
201        .enumerate()
202        .partition(|(_, (_, arg))| !arg.is_empty());
203
204    // Only parse the inputs with arguments
205    let indexed_tokens = with_args
206        .iter()
207        .map(|(_, (kind, arg))| kind.coerce_str(arg))
208        .collect::<Result<Vec<DynSolValue>, _>>()?;
209
210    // Merge the inputs restoring the original ordering
211    let mut topics = with_args
212        .into_iter()
213        .zip(indexed_tokens)
214        .map(|((i, _), t)| (i, Some(t)))
215        .chain(without_args.into_iter().map(|(i, _)| (i, None)))
216        .sorted_by(|(i1, _), (i2, _)| i1.cmp(i2))
217        .map(|(_, token)| {
218            token.map(|token| Topic::from(encode_event_topic(&token))).unwrap_or(Topic::default())
219        })
220        .collect::<Vec<Topic>>();
221
222    topics.resize(3, Topic::default());
223
224    let filter = Filter::new()
225        .event_signature(event.selector())
226        .topic1(topics[0].clone())
227        .topic2(topics[1].clone())
228        .topic3(topics[2].clone());
229
230    Ok(filter)
231}
232
233/// Creates a [Filter] from raw topic hashes.
234fn build_filter_topics(topics: Vec<String>) -> Result<Filter, eyre::Error> {
235    let mut topics = topics
236        .into_iter()
237        .map(|topic| {
238            if topic.is_empty() {
239                Ok(Topic::default())
240            } else {
241                Ok(Topic::from(B256::from_hex(topic.as_str())?))
242            }
243        })
244        .collect::<Result<Vec<FilterSet<_>>>>()?;
245
246    topics.resize(4, Topic::default());
247
248    let filter = Filter::new()
249        .event_signature(topics[0].clone())
250        .topic1(topics[1].clone())
251        .topic2(topics[2].clone())
252        .topic3(topics[3].clone());
253
254    Ok(filter)
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use alloy_primitives::{U160, U256, keccak256};
261    use alloy_rpc_types::ValueOrArray;
262
263    const ADDRESS: &str = "0x4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38";
264    const TRANSFER_SIG: &str = "Transfer(address indexed,address indexed,uint256)";
265    const TRANSFER_TOPIC: &str =
266        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
267
268    #[test]
269    fn test_build_filter_basic() {
270        let from_block = Some(BlockNumberOrTag::from(1337));
271        let to_block = Some(BlockNumberOrTag::Latest);
272        let address = Address::from_str(ADDRESS).ok();
273        let expected = Filter {
274            block_option: FilterBlockOption::Range { from_block, to_block },
275            address: ValueOrArray::Value(address.unwrap()).into(),
276            topics: [vec![].into(), vec![].into(), vec![].into(), vec![].into()],
277        };
278        let filter =
279            build_filter(from_block, to_block, address.map(|addr| vec![addr]), None, vec![])
280                .unwrap();
281        assert_eq!(filter, expected)
282    }
283
284    #[test]
285    fn test_build_filter_sig() {
286        let expected = Filter {
287            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
288            address: vec![].into(),
289            topics: [
290                B256::from_str(TRANSFER_TOPIC).unwrap().into(),
291                vec![].into(),
292                vec![].into(),
293                vec![].into(),
294            ],
295        };
296        let filter =
297            build_filter(None, None, None, Some(TRANSFER_SIG.to_string()), vec![]).unwrap();
298        assert_eq!(filter, expected)
299    }
300
301    #[test]
302    fn test_build_filter_mismatch() {
303        let expected = Filter {
304            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
305            address: vec![].into(),
306            topics: [
307                B256::from_str(TRANSFER_TOPIC).unwrap().into(),
308                vec![].into(),
309                vec![].into(),
310                vec![].into(),
311            ],
312        };
313        let filter = build_filter(
314            None,
315            None,
316            None,
317            Some("Swap(address indexed from, address indexed to, uint256 value)".to_string()), // Change signature, should result in error
318            vec![],
319        )
320        .unwrap();
321        assert_ne!(filter, expected)
322    }
323
324    #[test]
325    fn test_build_filter_sig_with_arguments() {
326        let addr = Address::from_str(ADDRESS).unwrap();
327        let addr = U256::from(U160::from_be_bytes(addr.0.0));
328        let expected = Filter {
329            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
330            address: vec![].into(),
331            topics: [
332                B256::from_str(TRANSFER_TOPIC).unwrap().into(),
333                addr.into(),
334                vec![].into(),
335                vec![].into(),
336            ],
337        };
338        let filter = build_filter(
339            None,
340            None,
341            None,
342            Some(TRANSFER_SIG.to_string()),
343            vec![ADDRESS.to_string()],
344        )
345        .unwrap();
346        assert_eq!(filter, expected)
347    }
348
349    #[test]
350    fn test_build_filter_sig_with_non_indexed_input_first() {
351        let event = Event::parse("event Owned(uint256 value, address indexed owner)").unwrap();
352        let address = Address::from_str(ADDRESS).unwrap();
353        let expected = Filter {
354            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
355            address: vec![].into(),
356            topics: [
357                event.selector().into(),
358                B256::left_padding_from(address.as_slice()).into(),
359                vec![].into(),
360                vec![].into(),
361            ],
362        };
363
364        let filter = build_filter_event_sig(event, vec![ADDRESS.to_string()]).unwrap();
365
366        assert_eq!(filter, expected);
367    }
368
369    #[test]
370    fn test_build_filter_sig_with_dynamic_indexed_input() {
371        let event = Event::parse("event Message(string indexed value)").unwrap();
372        let expected = Filter {
373            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
374            address: vec![].into(),
375            topics: [
376                event.selector().into(),
377                keccak256("hello").into(),
378                vec![].into(),
379                vec![].into(),
380            ],
381        };
382
383        let filter = build_filter_event_sig(event, vec!["hello".to_string()]).unwrap();
384
385        assert_eq!(filter, expected);
386    }
387
388    #[test]
389    fn test_build_filter_sig_with_skipped_arguments() {
390        let addr = Address::from_str(ADDRESS).unwrap();
391        let addr = U256::from(U160::from_be_bytes(addr.0.0));
392        let expected = Filter {
393            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
394            address: vec![].into(),
395            topics: [
396                vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
397                vec![].into(),
398                addr.into(),
399                vec![].into(),
400            ],
401        };
402        let filter = build_filter(
403            None,
404            None,
405            None,
406            Some(TRANSFER_SIG.to_string()),
407            vec![String::new(), ADDRESS.to_string()],
408        )
409        .unwrap();
410        assert_eq!(filter, expected)
411    }
412
413    #[test]
414    fn test_build_filter_with_topics() {
415        let expected = Filter {
416            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
417            address: vec![].into(),
418            topics: [
419                vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
420                vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
421                vec![].into(),
422                vec![].into(),
423            ],
424        };
425        let filter = build_filter(
426            None,
427            None,
428            None,
429            Some(TRANSFER_TOPIC.to_string()),
430            vec![TRANSFER_TOPIC.to_string()],
431        )
432        .unwrap();
433
434        assert_eq!(filter, expected)
435    }
436
437    #[test]
438    fn test_build_filter_with_skipped_topic() {
439        let expected = Filter {
440            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
441            address: vec![].into(),
442            topics: [
443                vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
444                vec![].into(),
445                vec![B256::from_str(TRANSFER_TOPIC).unwrap()].into(),
446                vec![].into(),
447            ],
448        };
449        let filter = build_filter(
450            None,
451            None,
452            None,
453            Some(TRANSFER_TOPIC.to_string()),
454            vec![String::new(), TRANSFER_TOPIC.to_string()],
455        )
456        .unwrap();
457
458        assert_eq!(filter, expected)
459    }
460
461    #[test]
462    fn test_build_filter_with_multiple_addresses() {
463        let expected = Filter {
464            block_option: FilterBlockOption::Range { from_block: None, to_block: None },
465            address: vec![Address::ZERO, ADDRESS.parse().unwrap()].into(),
466            topics: [
467                vec![TRANSFER_TOPIC.parse().unwrap()].into(),
468                vec![].into(),
469                vec![].into(),
470                vec![].into(),
471            ],
472        };
473        let filter = build_filter(
474            None,
475            None,
476            Some(vec![Address::ZERO, ADDRESS.parse().unwrap()]),
477            Some(TRANSFER_TOPIC.to_string()),
478            vec![],
479        )
480        .unwrap();
481        assert_eq!(filter, expected)
482    }
483
484    #[test]
485    fn test_build_filter_sig_with_mismatched_argument() {
486        let err = build_filter(
487            None,
488            None,
489            None,
490            Some(TRANSFER_SIG.to_string()),
491            vec!["1234".to_string()],
492        )
493        .err()
494        .unwrap()
495        .to_string()
496        .to_lowercase();
497
498        assert_eq!(err, "parser error:\n1234\n^\ninvalid string length");
499    }
500
501    #[test]
502    fn test_build_filter_with_invalid_sig_or_topic() {
503        let err = build_filter(None, None, None, Some("asdasdasd".to_string()), vec![])
504            .err()
505            .unwrap()
506            .to_string()
507            .to_lowercase();
508
509        assert_eq!(err, "odd number of digits");
510    }
511
512    #[test]
513    fn test_build_filter_with_invalid_sig_or_topic_hex() {
514        let err = build_filter(None, None, None, Some(ADDRESS.to_string()), vec![])
515            .err()
516            .unwrap()
517            .to_string()
518            .to_lowercase();
519
520        assert_eq!(err, "invalid string length");
521    }
522
523    #[test]
524    fn test_build_filter_with_invalid_topic() {
525        let err = build_filter(
526            None,
527            None,
528            None,
529            Some(TRANSFER_TOPIC.to_string()),
530            vec!["1234".to_string()],
531        )
532        .err()
533        .unwrap()
534        .to_string()
535        .to_lowercase();
536
537        assert_eq!(err, "invalid string length");
538    }
539}