cast/cmd/
logs.rs

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