Skip to main content

foundry_common/
selectors.rs

1//! Support for handling/identifying selectors.
2
3#![allow(missing_docs)]
4
5use crate::{abi::abi_decode_calldata, provider::runtime_transport::RuntimeTransportBuilder};
6use alloy_json_abi::JsonAbi;
7use alloy_primitives::{B256, Selector, map::HashMap};
8use eyre::Context;
9use itertools::Itertools;
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11use std::{
12    fmt,
13    sync::{
14        Arc,
15        atomic::{AtomicBool, AtomicUsize, Ordering},
16    },
17    time::Duration,
18};
19
20const BASE_URL: &str = "https://api.4byte.sourcify.dev";
21const SELECTOR_LOOKUP_URL: &str = "https://api.4byte.sourcify.dev/signature-database/v1/lookup";
22const SELECTOR_IMPORT_URL: &str = "https://api.4byte.sourcify.dev/signature-database/v1/import";
23
24/// The standard request timeout for API requests.
25const REQ_TIMEOUT: Duration = Duration::from_secs(15);
26
27/// How many request can time out before we decide this is a spurious connection.
28const MAX_TIMEDOUT_REQ: usize = 4usize;
29
30/// List of signatures for a given [`SelectorKind`].
31pub type OpenChainSignatures = Vec<String>;
32
33/// A client that can request API data from OpenChain.
34#[derive(Clone, Debug)]
35pub struct OpenChainClient {
36    inner: reqwest::Client,
37    /// Whether the connection is spurious, or API is down
38    spurious_connection: Arc<AtomicBool>,
39    /// How many requests timed out
40    timedout_requests: Arc<AtomicUsize>,
41    /// Max allowed request that can time out
42    max_timedout_requests: usize,
43}
44
45impl OpenChainClient {
46    /// Creates a new client with default settings.
47    pub fn new() -> eyre::Result<Self> {
48        let inner = RuntimeTransportBuilder::new(BASE_URL.parse().unwrap())
49            .with_timeout(REQ_TIMEOUT)
50            .build()
51            .reqwest_client()
52            .wrap_err("failed to build OpenChain client")?;
53        Ok(Self {
54            inner,
55            spurious_connection: Default::default(),
56            timedout_requests: Default::default(),
57            max_timedout_requests: MAX_TIMEDOUT_REQ,
58        })
59    }
60
61    async fn get_text(&self, url: impl reqwest::IntoUrl + fmt::Display) -> reqwest::Result<String> {
62        trace!(%url, "GET");
63        self.inner
64            .get(url)
65            .send()
66            .await
67            .inspect_err(|err| self.on_reqwest_err(err))?
68            .text()
69            .await
70            .inspect_err(|err| self.on_reqwest_err(err))
71    }
72
73    /// Sends a new post request
74    async fn post_json<T: Serialize + std::fmt::Debug, R: DeserializeOwned>(
75        &self,
76        url: &str,
77        body: &T,
78    ) -> reqwest::Result<R> {
79        trace!(%url, body=?serde_json::to_string(body), "POST");
80        self.inner
81            .post(url)
82            .json(body)
83            .send()
84            .await
85            .inspect_err(|err| self.on_reqwest_err(err))?
86            .json()
87            .await
88            .inspect_err(|err| self.on_reqwest_err(err))
89    }
90
91    fn on_reqwest_err(&self, err: &reqwest::Error) {
92        fn is_connectivity_err(err: &reqwest::Error) -> bool {
93            if err.is_timeout() || err.is_connect() {
94                return true;
95            }
96            // Error HTTP codes (5xx) are considered connectivity issues and will prompt retry
97            if let Some(status) = err.status() {
98                let code = status.as_u16();
99                if (500..600).contains(&code) {
100                    return true;
101                }
102            }
103            false
104        }
105
106        if is_connectivity_err(err) {
107            warn!("spurious network detected for OpenChain");
108            let previous = self.timedout_requests.fetch_add(1, Ordering::SeqCst);
109            if previous + 1 >= self.max_timedout_requests {
110                self.set_spurious();
111            }
112        }
113    }
114
115    /// Returns whether the connection was marked as spurious
116    fn is_spurious(&self) -> bool {
117        self.spurious_connection.load(Ordering::Relaxed)
118    }
119
120    /// Marks the connection as spurious
121    fn set_spurious(&self) {
122        self.spurious_connection.store(true, Ordering::Relaxed)
123    }
124
125    fn ensure_not_spurious(&self) -> eyre::Result<()> {
126        if self.is_spurious() {
127            eyre::bail!("Spurious connection detected");
128        }
129        Ok(())
130    }
131
132    /// Decodes the given function or event selector using OpenChain
133    pub async fn decode_selector(
134        &self,
135        selector: SelectorKind,
136    ) -> eyre::Result<OpenChainSignatures> {
137        Ok(self.decode_selectors(&[selector]).await?.pop().unwrap())
138    }
139
140    /// Decodes the given function, error or event selectors using OpenChain.
141    pub async fn decode_selectors(
142        &self,
143        selectors: &[SelectorKind],
144    ) -> eyre::Result<Vec<OpenChainSignatures>> {
145        if selectors.is_empty() {
146            return Ok(vec![]);
147        }
148
149        if enabled!(tracing::Level::TRACE) {
150            trace!(?selectors, "decoding selectors");
151        } else {
152            debug!(len = selectors.len(), "decoding selectors");
153        }
154
155        // Exit early if spurious connection.
156        self.ensure_not_spurious()?;
157
158        // Build the URL with the query string.
159        let mut url: url::Url = SELECTOR_LOOKUP_URL.parse().unwrap();
160        {
161            let mut query = url.query_pairs_mut();
162            let functions = selectors.iter().filter_map(SelectorKind::as_function);
163            if functions.clone().next().is_some() {
164                query.append_pair("function", &functions.format(",").to_string());
165            }
166            let events = selectors.iter().filter_map(SelectorKind::as_event);
167            if events.clone().next().is_some() {
168                query.append_pair("event", &events.format(",").to_string());
169            }
170            let _ = query.finish();
171        }
172
173        let text = self.get_text(url).await?;
174        let SignatureResponse { ok, result } = match serde_json::from_str(&text) {
175            Ok(response) => response,
176            Err(err) => {
177                eyre::bail!("could not decode response: {err}: {text}");
178            }
179        };
180        if !ok {
181            eyre::bail!("OpenChain returned an error: {text}");
182        }
183
184        Ok(selectors
185            .iter()
186            .map(|selector| {
187                let signatures = match selector {
188                    SelectorKind::Function(selector) | SelectorKind::Error(selector) => {
189                        result.function.get(selector)
190                    }
191                    SelectorKind::Event(hash) => result.event.get(hash),
192                };
193                signatures
194                    .map(Option::as_deref)
195                    .unwrap_or_default()
196                    .unwrap_or_default()
197                    .iter()
198                    .map(|sig| sig.name.clone())
199                    .collect()
200            })
201            .collect())
202    }
203
204    /// Fetches a function signature given the selector using OpenChain
205    pub async fn decode_function_selector(
206        &self,
207        selector: Selector,
208    ) -> eyre::Result<OpenChainSignatures> {
209        self.decode_selector(SelectorKind::Function(selector)).await
210    }
211
212    /// Fetches all possible signatures and attempts to abi decode the calldata
213    pub async fn decode_calldata(&self, calldata: &str) -> eyre::Result<OpenChainSignatures> {
214        let calldata = calldata.strip_prefix("0x").unwrap_or(calldata);
215        if calldata.len() < 8 {
216            eyre::bail!(
217                "Calldata too short: expected at least 8 characters (excluding 0x prefix), got {}.",
218                calldata.len()
219            );
220        }
221
222        let mut sigs = self.decode_function_selector(calldata[..8].parse()?).await?;
223        // Retain only signatures that can be decoded.
224        sigs.retain(|sig| abi_decode_calldata(sig, calldata, true, true).is_ok());
225        Ok(sigs)
226    }
227
228    /// Fetches an event signature given the 32 byte topic using OpenChain.
229    pub async fn decode_event_topic(&self, topic: B256) -> eyre::Result<OpenChainSignatures> {
230        self.decode_selector(SelectorKind::Event(topic)).await
231    }
232
233    /// Pretty print calldata and if available, fetch possible function signatures
234    ///
235    /// ```no_run
236    /// use foundry_common::selectors::OpenChainClient;
237    ///
238    /// # async fn foo() -> eyre::Result<()> {
239    /// let pretty_data = OpenChainClient::new()?
240    ///     .pretty_calldata(
241    ///         "0x70a08231000000000000000000000000d0074f4e6490ae3f888d1d4f7e3e43326bd3f0f5"
242    ///             .to_string(),
243    ///         false,
244    ///     )
245    ///     .await?;
246    /// println!("{}", pretty_data);
247    /// # Ok(())
248    /// # }
249    /// ```
250    pub async fn pretty_calldata(
251        &self,
252        calldata: impl AsRef<str>,
253        offline: bool,
254    ) -> eyre::Result<PossibleSigs> {
255        let mut possible_info = PossibleSigs::new();
256        let calldata = calldata.as_ref().trim_start_matches("0x");
257
258        let selector =
259            calldata.get(..8).ok_or_else(|| eyre::eyre!("calldata cannot be less that 4 bytes"))?;
260
261        let sigs = if offline {
262            vec![]
263        } else {
264            let selector = selector.parse()?;
265            self.decode_function_selector(selector).await.unwrap_or_default().into_iter().collect()
266        };
267        let (_, data) = calldata.split_at(8);
268
269        if !data.len().is_multiple_of(64) {
270            eyre::bail!("\nInvalid calldata size");
271        }
272
273        let row_length = data.len() / 64;
274
275        for row in 0..row_length {
276            possible_info.data.push(data[64 * row..64 * (row + 1)].to_string());
277        }
278        if sigs.is_empty() {
279            possible_info.method = SelectorOrSig::Selector(selector.to_string());
280        } else {
281            possible_info.method = SelectorOrSig::Sig(sigs);
282        }
283        Ok(possible_info)
284    }
285
286    /// uploads selectors to OpenChain using the given data
287    pub async fn import_selectors(
288        &self,
289        data: SelectorImportData,
290    ) -> eyre::Result<SelectorImportResponse> {
291        self.ensure_not_spurious()?;
292
293        let request = match data {
294            SelectorImportData::Abi(abis) => {
295                let functions_and_errors: OpenChainSignatures = abis
296                    .iter()
297                    .flat_map(|abi| {
298                        abi.functions()
299                            .map(|func| func.signature())
300                            .chain(abi.errors().map(|error| error.signature()))
301                            .collect::<Vec<_>>()
302                    })
303                    .collect();
304
305                let events = abis
306                    .iter()
307                    .flat_map(|abi| abi.events().map(|event| event.signature()))
308                    .collect::<Vec<_>>();
309
310                SelectorImportRequest { function: functions_and_errors, event: events }
311            }
312            SelectorImportData::Raw(raw) => {
313                let function_and_error =
314                    raw.function.iter().chain(raw.error.iter()).cloned().collect::<Vec<_>>();
315                SelectorImportRequest { function: function_and_error, event: raw.event }
316            }
317        };
318
319        Ok(self.post_json(SELECTOR_IMPORT_URL, &request).await?)
320    }
321}
322
323pub enum SelectorOrSig {
324    Selector(String),
325    Sig(OpenChainSignatures),
326}
327
328pub struct PossibleSigs {
329    method: SelectorOrSig,
330    data: OpenChainSignatures,
331}
332
333impl PossibleSigs {
334    fn new() -> Self {
335        Self { method: SelectorOrSig::Selector("0x00000000".to_string()), data: vec![] }
336    }
337}
338
339impl fmt::Display for PossibleSigs {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        match &self.method {
342            SelectorOrSig::Selector(selector) => {
343                writeln!(f, "\n Method: {selector}")?;
344            }
345            SelectorOrSig::Sig(sigs) => {
346                writeln!(f, "\n Possible methods:")?;
347                for sig in sigs {
348                    writeln!(f, " - {sig}")?;
349                }
350            }
351        }
352
353        writeln!(f, " ------------")?;
354        for (i, row) in self.data.iter().enumerate() {
355            let row_label_decimal = i * 32;
356            let row_label_hex = format!("{row_label_decimal:03x}");
357            writeln!(f, " [{row_label_hex}]: {row}")?;
358        }
359        Ok(())
360    }
361}
362
363/// The kind of selector to fetch from OpenChain.
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
365pub enum SelectorKind {
366    /// A function selector.
367    Function(Selector),
368    /// A custom error selector. Behaves the same as a function selector.
369    Error(Selector),
370    /// An event selector.
371    Event(B256),
372}
373
374impl SelectorKind {
375    /// Returns the function selector if it is a function OR custom error.
376    pub const fn as_function(&self) -> Option<Selector> {
377        match *self {
378            Self::Function(selector) | Self::Error(selector) => Some(selector),
379            _ => None,
380        }
381    }
382
383    /// Returns the event selector if it is an event.
384    pub const fn as_event(&self) -> Option<B256> {
385        match *self {
386            Self::Event(hash) => Some(hash),
387            _ => None,
388        }
389    }
390}
391
392/// Decodes the given function or event selector using OpenChain.
393pub async fn decode_selector(selector: SelectorKind) -> eyre::Result<OpenChainSignatures> {
394    OpenChainClient::new()?.decode_selector(selector).await
395}
396
397/// Decodes the given function or event selectors using OpenChain.
398pub async fn decode_selectors(
399    selectors: &[SelectorKind],
400) -> eyre::Result<Vec<OpenChainSignatures>> {
401    OpenChainClient::new()?.decode_selectors(selectors).await
402}
403
404/// Fetches a function signature given the selector using OpenChain.
405pub async fn decode_function_selector(selector: Selector) -> eyre::Result<OpenChainSignatures> {
406    OpenChainClient::new()?.decode_function_selector(selector).await
407}
408
409/// Fetches all possible signatures and attempts to abi decode the calldata using OpenChain.
410pub async fn decode_calldata(calldata: &str) -> eyre::Result<OpenChainSignatures> {
411    OpenChainClient::new()?.decode_calldata(calldata).await
412}
413
414/// Fetches an event signature given the 32 byte topic using OpenChain.
415pub async fn decode_event_topic(topic: B256) -> eyre::Result<OpenChainSignatures> {
416    OpenChainClient::new()?.decode_event_topic(topic).await
417}
418
419/// Pretty print calldata and if available, fetch possible function signatures.
420///
421/// ```no_run
422/// use foundry_common::selectors::pretty_calldata;
423///
424/// # async fn foo() -> eyre::Result<()> {
425/// let pretty_data = pretty_calldata(
426///     "0x70a08231000000000000000000000000d0074f4e6490ae3f888d1d4f7e3e43326bd3f0f5".to_string(),
427///     false,
428/// )
429/// .await?;
430/// println!("{}", pretty_data);
431/// # Ok(())
432/// # }
433/// ```
434pub async fn pretty_calldata(
435    calldata: impl AsRef<str>,
436    offline: bool,
437) -> eyre::Result<PossibleSigs> {
438    OpenChainClient::new()?.pretty_calldata(calldata, offline).await
439}
440
441#[derive(Debug, Default, PartialEq, Eq, Serialize)]
442pub struct RawSelectorImportData {
443    pub function: OpenChainSignatures,
444    pub event: OpenChainSignatures,
445    pub error: OpenChainSignatures,
446}
447
448impl RawSelectorImportData {
449    pub const fn is_empty(&self) -> bool {
450        self.function.is_empty() && self.event.is_empty() && self.error.is_empty()
451    }
452}
453
454#[derive(Serialize)]
455#[serde(untagged)]
456pub enum SelectorImportData {
457    Abi(Vec<JsonAbi>),
458    Raw(RawSelectorImportData),
459}
460
461#[derive(Debug, Default, Serialize)]
462struct SelectorImportRequest {
463    function: OpenChainSignatures,
464    event: OpenChainSignatures,
465}
466
467#[derive(Debug, Deserialize)]
468struct SelectorImportEffect {
469    imported: HashMap<String, String>,
470    duplicated: HashMap<String, String>,
471}
472
473#[derive(Debug, Deserialize)]
474struct SelectorImportResult {
475    function: SelectorImportEffect,
476    event: SelectorImportEffect,
477}
478
479#[derive(Debug, Deserialize)]
480pub struct SelectorImportResponse {
481    result: SelectorImportResult,
482}
483
484impl SelectorImportResponse {
485    /// Print info about the functions which were uploaded or already known
486    pub fn describe(&self) {
487        for (k, v) in &self.result.function.imported {
488            let _ = sh_println!("Imported: Function {k}: {v}");
489        }
490        for (k, v) in &self.result.event.imported {
491            let _ = sh_println!("Imported: Event {k}: {v}");
492        }
493        for (k, v) in &self.result.function.duplicated {
494            let _ = sh_println!("Duplicated: Function {k}: {v}");
495        }
496        for (k, v) in &self.result.event.duplicated {
497            let _ = sh_println!("Duplicated: Event {k}: {v}");
498        }
499
500        let _ = sh_println!("Selectors successfully uploaded to OpenChain");
501    }
502}
503
504/// uploads selectors to OpenChain using the given data
505pub async fn import_selectors(data: SelectorImportData) -> eyre::Result<SelectorImportResponse> {
506    OpenChainClient::new()?.import_selectors(data).await
507}
508
509#[derive(Debug, Default, PartialEq, Eq)]
510pub struct ParsedSignatures {
511    pub signatures: RawSelectorImportData,
512    pub abis: Vec<JsonAbi>,
513}
514
515#[derive(Deserialize)]
516struct Artifact {
517    abi: JsonAbi,
518}
519
520/// Parses a list of tokens into function, event, and error signatures.
521/// Also handles JSON artifact files
522/// Ignores invalid tokens
523pub fn parse_signatures(tokens: Vec<String>) -> ParsedSignatures {
524    // if any of the given tokens are json artifact files,
525    // Parse them and read in the ABI from the file
526    let abis = tokens
527        .iter()
528        .filter(|sig| sig.ends_with(".json"))
529        .filter_map(|filename| std::fs::read_to_string(filename).ok())
530        .filter_map(|file| serde_json::from_str(file.as_str()).ok())
531        .map(|artifact: Artifact| artifact.abi)
532        .collect();
533
534    // for tokens that are not json artifact files,
535    // try to parse them as raw signatures
536    let signatures = tokens.iter().filter(|sig| !sig.ends_with(".json")).fold(
537        RawSelectorImportData::default(),
538        |mut data, signature| {
539            let mut split = signature.split(' ');
540            #[allow(clippy::collapsible_match)]
541            match split.next() {
542                Some("function") => {
543                    if let Some(sig) = split.next() {
544                        data.function.push(sig.to_string())
545                    }
546                }
547                Some("event") => {
548                    if let Some(sig) = split.next() {
549                        data.event.push(sig.to_string())
550                    }
551                }
552                Some("error") => {
553                    if let Some(sig) = split.next() {
554                        data.error.push(sig.to_string())
555                    }
556                }
557                Some(signature) => {
558                    // if no type given, assume function
559                    data.function.push(signature.to_string());
560                }
561                None => {}
562            }
563            data
564        },
565    );
566
567    ParsedSignatures { signatures, abis }
568}
569
570/// [`SELECTOR_LOOKUP_URL`] response.
571#[derive(Deserialize)]
572struct SignatureResponse {
573    ok: bool,
574    result: SignatureResult,
575}
576
577#[derive(Deserialize)]
578struct SignatureResult {
579    event: HashMap<B256, Option<Vec<Signature>>>,
580    function: HashMap<Selector, Option<Vec<Signature>>>,
581}
582
583#[derive(Deserialize)]
584struct Signature {
585    name: String,
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    #[test]
593    fn test_parse_signatures() {
594        let result = parse_signatures(vec!["transfer(address,uint256)".to_string()]);
595        assert_eq!(
596            result,
597            ParsedSignatures {
598                signatures: RawSelectorImportData {
599                    function: vec!["transfer(address,uint256)".to_string()],
600                    ..Default::default()
601                },
602                ..Default::default()
603            }
604        );
605
606        let result = parse_signatures(vec![
607            "transfer(address,uint256)".to_string(),
608            "function approve(address,uint256)".to_string(),
609        ]);
610        assert_eq!(
611            result,
612            ParsedSignatures {
613                signatures: RawSelectorImportData {
614                    function: vec![
615                        "transfer(address,uint256)".to_string(),
616                        "approve(address,uint256)".to_string()
617                    ],
618                    ..Default::default()
619                },
620                ..Default::default()
621            }
622        );
623
624        let result = parse_signatures(vec![
625            "transfer(address,uint256)".to_string(),
626            "event Approval(address,address,uint256)".to_string(),
627            "error ERC20InsufficientBalance(address,uint256,uint256)".to_string(),
628        ]);
629        assert_eq!(
630            result,
631            ParsedSignatures {
632                signatures: RawSelectorImportData {
633                    function: vec!["transfer(address,uint256)".to_string()],
634                    event: vec!["Approval(address,address,uint256)".to_string()],
635                    error: vec!["ERC20InsufficientBalance(address,uint256,uint256)".to_string()]
636                },
637                ..Default::default()
638            }
639        );
640
641        // skips invalid
642        let result = parse_signatures(vec!["event".to_string()]);
643        assert_eq!(
644            result,
645            ParsedSignatures { signatures: Default::default(), ..Default::default() }
646        );
647    }
648
649    #[tokio::test]
650    async fn spurious_marked_on_timeout_threshold() {
651        // Use an unreachable local port to trigger a quick connect error.
652        let client = OpenChainClient::new().expect("client must build");
653        let url = "http://127.0.0.1:9"; // Discard port; typically closed and fails fast.
654
655        // After MAX_TIMEDOUT_REQ - 1 failures we should NOT be spurious.
656        for i in 0..(MAX_TIMEDOUT_REQ - 1) {
657            let _ = client.get_text(url).await; // expect an error and internal counter increment
658            assert!(!client.is_spurious(), "unexpected spurious after {} failed attempts", i + 1);
659        }
660
661        // The Nth failure (N == MAX_TIMEDOUT_REQ) should flip the spurious flag.
662        let _ = client.get_text(url).await;
663        assert!(client.is_spurious(), "expected spurious after threshold failures");
664    }
665}