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