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