Skip to main content

foundry_common/provider/
mod.rs

1//! Provider-related instantiation and usage utilities.
2
3pub mod curl_transport;
4pub mod fee;
5pub mod mpp;
6pub mod runtime_transport;
7
8use crate::{
9    ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
10    provider::{curl_transport::CurlTransport, runtime_transport::RuntimeTransportBuilder},
11};
12use alloy_chains::NamedChain;
13use alloy_json_rpc::{RequestPacket, ResponsePacket};
14use alloy_network::{Network, NetworkWallet};
15use alloy_provider::{
16    Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
17    fillers::{FillProvider, JoinFill, RecommendedFillers, WalletFiller},
18    network::{AnyNetwork, EthereumWallet},
19};
20use alloy_rpc_client::ClientBuilder;
21use alloy_transport::{
22    TransportError, TransportFut, layers::RetryBackoffLayer, utils::guess_local_url,
23};
24use eyre::{Result, WrapErr};
25use foundry_config::Config;
26use reqwest::Url;
27use std::{
28    marker::PhantomData,
29    net::SocketAddr,
30    path::{Path, PathBuf},
31    str::FromStr,
32    sync::{
33        Arc,
34        atomic::{AtomicUsize, Ordering},
35    },
36    task::{Context, Poll},
37    time::Duration,
38};
39use tower::Service;
40use url::ParseError;
41
42/// The assumed block time for unknown chains.
43/// We assume that these are chains have a faster block time.
44const DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME: Duration = Duration::from_secs(3);
45
46/// The factor to scale the block time by to get the poll interval.
47const POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR: f32 = 0.6;
48
49/// Helper type alias for a retry provider
50pub type RetryProvider<N = AnyNetwork> = RootProvider<N>;
51
52/// Helper type alias for a retry provider with a signer
53pub type RetryProviderWithSigner<N = AnyNetwork, W = EthereumWallet> = FillProvider<
54    JoinFill<JoinFill<Identity, <N as RecommendedFillers>::RecommendedFillers>, WalletFiller<W>>,
55    RootProvider<N>,
56    N,
57>;
58
59/// A round-robin transport that distributes requests across multiple transports.
60///
61/// Each request is sent to exactly one transport, rotating through the list.
62/// Failover on error is handled by the retry layer above this service.
63#[derive(Clone)]
64pub struct RoundRobinService<S> {
65    transports: Arc<Vec<S>>,
66    next: Arc<AtomicUsize>,
67}
68
69impl<S> RoundRobinService<S> {
70    /// Creates a new round-robin service from a non-empty list of transports.
71    ///
72    /// # Panics
73    ///
74    /// Panics if `transports` is empty.
75    pub fn new(transports: Vec<S>) -> Self {
76        assert!(!transports.is_empty(), "RoundRobinService requires at least one transport");
77        Self { transports: Arc::new(transports), next: Arc::new(AtomicUsize::new(0)) }
78    }
79}
80
81impl<S> Service<RequestPacket> for RoundRobinService<S>
82where
83    S: Service<
84            RequestPacket,
85            Response = ResponsePacket,
86            Error = TransportError,
87            Future = TransportFut<'static>,
88        > + Clone
89        + Send
90        + Sync
91        + 'static,
92{
93    type Response = ResponsePacket;
94    type Error = TransportError;
95    type Future = TransportFut<'static>;
96
97    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
98        Poll::Ready(Ok(()))
99    }
100
101    fn call(&mut self, req: RequestPacket) -> Self::Future {
102        let transports = self.transports.clone();
103        let idx = self.next.fetch_add(1, Ordering::Relaxed) % transports.len();
104        let mut transport = transports[idx].clone();
105        transport.call(req)
106    }
107}
108
109/// Helper type to construct a `RetryProvider`
110///
111/// This builder is generic over the network type `N`, defaulting to `AnyNetwork`.
112#[derive(Debug)]
113pub struct ProviderBuilder<N: Network = AnyNetwork> {
114    // Note: this is a result, so we can easily chain builder calls
115    url: Result<Url>,
116    chain: NamedChain,
117    max_retry: u32,
118    initial_backoff: u64,
119    timeout: Duration,
120    /// available CUPS
121    compute_units_per_second: u64,
122    /// JWT Secret
123    jwt: Option<String>,
124    headers: Vec<String>,
125    is_local: bool,
126    /// Whether to accept invalid certificates.
127    accept_invalid_certs: bool,
128    /// Whether to disable automatic proxy detection.
129    no_proxy: bool,
130    /// Whether to output curl commands instead of making requests.
131    curl_mode: bool,
132    /// Phantom data for the network type.
133    _network: PhantomData<N>,
134}
135
136impl<N: Network> ProviderBuilder<N> {
137    /// Creates a new ProviderBuilder helper instance.
138    pub fn new(url_str: &str) -> Self {
139        // a copy is needed for the next lines to work
140        let mut url_str = url_str;
141
142        // invalid url: non-prefixed URL scheme is not allowed, so we prepend the default http
143        // prefix
144        let storage;
145        if url_str.starts_with("localhost:") {
146            storage = format!("http://{url_str}");
147            url_str = storage.as_str();
148        }
149
150        let url = Url::parse(url_str)
151            .or_else(|err| match err {
152                ParseError::RelativeUrlWithoutBase => {
153                    if SocketAddr::from_str(url_str).is_ok() {
154                        Url::parse(&format!("http://{url_str}"))
155                    } else {
156                        let path = Path::new(url_str);
157
158                        if let Ok(path) = resolve_path(path) {
159                            Url::parse(&format!("file://{}", path.display()))
160                        } else {
161                            Err(err)
162                        }
163                    }
164                }
165                _ => Err(err),
166            })
167            .wrap_err_with(|| format!("invalid provider URL: {:?}", redact_url(url_str)));
168
169        // Use the final URL string to guess if it's a local URL.
170        let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
171
172        Self {
173            url,
174            chain: NamedChain::Mainnet,
175            max_retry: 8,
176            initial_backoff: 800,
177            timeout: REQUEST_TIMEOUT,
178            // alchemy max cpus <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
179            compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
180            jwt: None,
181            headers: vec![],
182            is_local,
183            accept_invalid_certs: false,
184            no_proxy: false,
185            curl_mode: false,
186            _network: PhantomData,
187        }
188    }
189
190    /// Constructs a [ProviderBuilder] instantiated using [Config] values.
191    ///
192    /// Defaults to `http://localhost:8545` and `Mainnet`.
193    pub fn from_config(config: &Config) -> Result<Self> {
194        let url = config.get_rpc_url_or_localhost_http()?;
195        let mut builder = Self::from_config_with_url(config, url.as_ref())?;
196
197        if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
198            builder = builder.chain(chain);
199        }
200
201        Ok(builder)
202    }
203
204    /// Constructs a [ProviderBuilder] for `url`, applying transport options from [Config].
205    pub fn from_config_with_url(config: &Config, url: &str) -> Result<Self> {
206        let mut builder = Self::new(url)
207            .accept_invalid_certs(config.eth_rpc_accept_invalid_certs)
208            .no_proxy(config.eth_rpc_no_proxy)
209            .curl_mode(config.eth_rpc_curl);
210
211        if let Some(jwt) = config.get_rpc_jwt_secret()? {
212            builder = builder.jwt(jwt.as_ref());
213        }
214
215        if let Some(rpc_timeout) = config.eth_rpc_timeout {
216            builder = builder.timeout(Duration::from_secs(rpc_timeout));
217        }
218
219        if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
220            builder = builder.headers(rpc_headers);
221        }
222
223        Ok(builder)
224    }
225
226    /// Enables a request timeout.
227    ///
228    /// The timeout is applied from when the request starts connecting until the
229    /// response body has finished.
230    ///
231    /// Default is no timeout.
232    pub const fn timeout(mut self, timeout: Duration) -> Self {
233        self.timeout = timeout;
234        self
235    }
236
237    /// Sets the chain of the node the provider will connect to
238    pub const fn chain(mut self, chain: NamedChain) -> Self {
239        self.chain = chain;
240        self
241    }
242
243    /// How often to retry a failed request
244    pub const fn max_retry(mut self, max_retry: u32) -> Self {
245        self.max_retry = max_retry;
246        self
247    }
248
249    /// How often to retry a failed request. If `None`, defaults to the already-set value.
250    pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
251        self.max_retry = max_retry.unwrap_or(self.max_retry);
252        self
253    }
254
255    /// The starting backoff delay to use after the first failed request. If `None`, defaults to
256    /// the already-set value.
257    pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
258        self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
259        self
260    }
261
262    /// The starting backoff delay to use after the first failed request
263    pub const fn initial_backoff(mut self, initial_backoff: u64) -> Self {
264        self.initial_backoff = initial_backoff;
265        self
266    }
267
268    /// Sets the number of assumed available compute units per second
269    ///
270    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
271    pub const fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
272        self.compute_units_per_second = compute_units_per_second;
273        self
274    }
275
276    /// Sets the number of assumed available compute units per second
277    ///
278    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
279    pub const fn compute_units_per_second_opt(
280        mut self,
281        compute_units_per_second: Option<u64>,
282    ) -> Self {
283        if let Some(cups) = compute_units_per_second {
284            self.compute_units_per_second = cups;
285        }
286        self
287    }
288
289    /// Sets the provider to be local.
290    ///
291    /// This is useful for local dev nodes.
292    pub const fn local(mut self, is_local: bool) -> Self {
293        self.is_local = is_local;
294        self
295    }
296
297    /// Sets aggressive `max_retry` and `initial_backoff` values
298    ///
299    /// This is only recommend for local dev nodes
300    pub const fn aggressive(self) -> Self {
301        self.max_retry(100).initial_backoff(100).local(true)
302    }
303
304    /// Sets the JWT secret
305    pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
306        self.jwt = Some(jwt.into());
307        self
308    }
309
310    /// Sets http headers
311    pub fn headers(mut self, headers: Vec<String>) -> Self {
312        self.headers = headers;
313
314        self
315    }
316
317    /// Sets http headers. If `None`, defaults to the already-set value.
318    pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
319        self.headers = headers.unwrap_or(self.headers);
320        self
321    }
322
323    /// Sets whether to accept invalid certificates.
324    pub const fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
325        self.accept_invalid_certs = accept_invalid_certs;
326        self
327    }
328
329    /// Sets whether to disable automatic proxy detection.
330    ///
331    /// This can help in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox)
332    /// where system proxy detection via SCDynamicStore causes crashes.
333    pub const fn no_proxy(mut self, no_proxy: bool) -> Self {
334        self.no_proxy = no_proxy;
335        self
336    }
337
338    /// Sets whether to output curl commands instead of making requests.
339    ///
340    /// When enabled, the provider will print equivalent curl commands to stdout
341    /// instead of actually executing the RPC requests.
342    pub const fn curl_mode(mut self, curl_mode: bool) -> Self {
343        self.curl_mode = curl_mode;
344        self
345    }
346
347    /// Constructs the `RetryProvider` taking all configs into account.
348    pub fn build(self) -> Result<RetryProvider<N>> {
349        let Self {
350            url,
351            chain,
352            max_retry,
353            initial_backoff,
354            timeout,
355            compute_units_per_second,
356            jwt,
357            headers,
358            is_local,
359            accept_invalid_certs,
360            no_proxy,
361            curl_mode,
362            ..
363        } = self;
364        let url = url?;
365        let no_proxy = no_proxy || is_local;
366
367        let retry_layer =
368            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
369
370        // If curl_mode is enabled, use CurlTransport instead of RuntimeTransport
371        if curl_mode {
372            let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
373            let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
374
375            let provider = AlloyProviderBuilder::<_, _, N>::default()
376                .connect_provider(RootProvider::new(client));
377
378            return Ok(provider);
379        }
380
381        let transport = RuntimeTransportBuilder::new(url)
382            .with_timeout(timeout)
383            .with_headers(headers)
384            .with_jwt(jwt)
385            .accept_invalid_certs(accept_invalid_certs)
386            .no_proxy(no_proxy)
387            .build();
388        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
389
390        if !is_local {
391            client.set_poll_interval(
392                chain
393                    .average_blocktime_hint()
394                    // we cap the poll interval because if not provided, chain would default to
395                    // mainnet
396                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
397                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
398                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
399            );
400        }
401
402        let provider =
403            AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
404
405        Ok(provider)
406    }
407}
408
409impl<N: Network> ProviderBuilder<N> {
410    /// Constructs a `RetryProvider` backed by multiple URLs using round-robin load balancing.
411    ///
412    /// Each request is sent to exactly one transport, rotating through the list via
413    /// [`RoundRobinService`]. There is no health scoring or endpoint deprioritization.
414    /// On failure, the `RetryBackoffLayer` retries the request, which naturally hits
415    /// the next transport in the rotation.
416    pub fn build_fallback(self, urls: Vec<String>) -> Result<RetryProvider<N>> {
417        let Self {
418            chain,
419            max_retry,
420            initial_backoff,
421            timeout,
422            compute_units_per_second,
423            jwt,
424            headers,
425            accept_invalid_certs,
426            no_proxy,
427            curl_mode,
428            ..
429        } = self;
430
431        eyre::ensure!(!urls.is_empty(), "at least one fork URL is required");
432        eyre::ensure!(!curl_mode, "curl mode is not supported with multiple fork URLs");
433
434        // Build a RuntimeTransport for each URL, using the same URL normalization
435        // as ProviderBuilder::new() (handles localhost:port, raw socket addrs, IPC paths)
436        let mut parsed_urls = Vec::with_capacity(urls.len());
437        let transports: Vec<_> = urls
438            .iter()
439            .map(|url_str| {
440                let builder = Self::new(url_str);
441                let url = builder.url?;
442                let transport_no_proxy = no_proxy || builder.is_local;
443                parsed_urls.push(url.clone());
444                Ok(RuntimeTransportBuilder::new(url)
445                    .with_timeout(timeout)
446                    .with_headers(headers.clone())
447                    .with_jwt(jwt.clone())
448                    .accept_invalid_certs(accept_invalid_certs)
449                    .no_proxy(transport_no_proxy)
450                    .build())
451            })
452            .collect::<Result<Vec<_>>>()?;
453
454        let round_robin = RoundRobinService::new(transports);
455
456        let retry_layer =
457            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
458        // Use normalized/parsed URLs for local detection, consistent with build()
459        let is_local = parsed_urls.iter().all(|url| guess_local_url(url.as_str()));
460        let client = ClientBuilder::default().layer(retry_layer).transport(round_robin, is_local);
461
462        if !is_local {
463            client.set_poll_interval(
464                chain
465                    .average_blocktime_hint()
466                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
467                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
468                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
469            );
470        }
471
472        let provider =
473            AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
474
475        Ok(provider)
476    }
477
478    /// Constructs the `RetryProvider` with a wallet.
479    pub fn build_with_wallet<W: NetworkWallet<N> + Clone>(
480        self,
481        wallet: W,
482    ) -> Result<RetryProviderWithSigner<N, W>>
483    where
484        N: RecommendedFillers,
485    {
486        let Self {
487            url,
488            chain,
489            max_retry,
490            initial_backoff,
491            timeout,
492            compute_units_per_second,
493            jwt,
494            headers,
495            is_local,
496            accept_invalid_certs,
497            no_proxy,
498            curl_mode,
499            ..
500        } = self;
501        let url = url?;
502        let no_proxy = no_proxy || is_local;
503
504        let retry_layer =
505            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
506
507        // If curl_mode is enabled, use CurlTransport instead of RuntimeTransport
508        if curl_mode {
509            let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
510            let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
511
512            let provider = AlloyProviderBuilder::<_, _, N>::default()
513                .with_recommended_fillers()
514                .wallet(wallet)
515                .connect_provider(RootProvider::new(client));
516
517            return Ok(provider);
518        }
519
520        let transport = RuntimeTransportBuilder::new(url)
521            .with_timeout(timeout)
522            .with_headers(headers)
523            .with_jwt(jwt)
524            .accept_invalid_certs(accept_invalid_certs)
525            .no_proxy(no_proxy)
526            .build();
527
528        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
529
530        if !is_local {
531            client.set_poll_interval(
532                chain
533                    .average_blocktime_hint()
534                    // we cap the poll interval because if not provided, chain would default to
535                    // mainnet
536                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
537                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
538                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
539            );
540        }
541
542        let provider = AlloyProviderBuilder::<_, _, N>::default()
543            .with_recommended_fillers()
544            .wallet(wallet)
545            .connect_provider(RootProvider::new(client));
546
547        Ok(provider)
548    }
549}
550
551/// Returns whether an RPC transport error reports JSON-RPC method-not-found.
552///
553/// Some providers encode JSON-RPC errors inside an HTTP error response instead of returning a
554/// normal JSON-RPC response. Only the exact `-32601` code is treated as method unavailability;
555/// authentication, internal, and transport errors must remain visible to callers.
556pub fn is_rpc_method_not_found(error: &TransportError) -> bool {
557    rpc_error_code(error) == Some(-32601)
558}
559
560/// Returns an RPC URL safe for display by retaining only its scheme, host, and port.
561pub fn redact_url(raw: &str) -> String {
562    let Ok(mut redacted) = Url::parse(raw) else {
563        return "<redacted>".to_owned();
564    };
565    let _ = redacted.set_username("");
566    let _ = redacted.set_password(None);
567    redacted.set_path("");
568    redacted.set_query(None);
569    redacted.set_fragment(None);
570    redacted.to_string()
571}
572
573fn rpc_error_code(error: &TransportError) -> Option<i64> {
574    if let Some(response) = error.as_error_resp() {
575        return Some(response.code);
576    }
577    let TransportError::Transport(error) = error else { return None };
578    error.as_http_error().and_then(|error| rpc_error_code_from_body(&error.body))
579}
580
581fn rpc_error_code_from_body(body: &str) -> Option<i64> {
582    // HTTP transports may append human-readable diagnostics after the JSON-RPC body. Parse the
583    // first complete JSON value instead of requiring the entire body to be JSON.
584    let value =
585        serde_json::Deserializer::from_str(body).into_iter::<serde_json::Value>().next()?.ok()?;
586    let error = value.get("error").unwrap_or(&value);
587    error.get("code")?.as_i64()
588}
589
590/// Constructs a provider with a 100 millisecond interval poll if it's a localhost URL (most likely
591/// an anvil or other dev node) and with the default, or 7 second otherwise.
592///
593/// See [`try_get_http_provider`] for more details.
594///
595/// # Panics
596///
597/// Panics if the URL is invalid.
598///
599/// # Examples
600///
601/// ```
602/// use foundry_common::provider::get_http_provider;
603///
604/// let retry_provider = get_http_provider("http://localhost:8545");
605/// ```
606#[inline]
607#[track_caller]
608pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
609    try_get_http_provider(builder).unwrap()
610}
611
612/// Constructs a provider with a 100 millisecond interval poll if it's a localhost URL (most likely
613/// an anvil or other dev node) and with the default, or 7 second otherwise.
614#[inline]
615pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
616    ProviderBuilder::new(builder.as_ref()).build()
617}
618
619#[cfg(not(windows))]
620fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
621    if path.is_absolute() {
622        Ok(path.to_path_buf())
623    } else {
624        std::env::current_dir().map(|d| d.join(path)).map_err(drop)
625    }
626}
627
628#[cfg(windows)]
629fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
630    if let Some(s) = path.to_str()
631        && s.starts_with(r"\\.\pipe\")
632    {
633        return Ok(path.to_path_buf());
634    }
635    if path.is_absolute() {
636        Ok(path.to_path_buf())
637    } else {
638        std::env::current_dir().map(|d| d.join(path)).map_err(drop)
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use alloy_json_rpc::ErrorPayload;
645
646    use super::*;
647
648    #[test]
649    fn redacts_url_credentials_and_resource() {
650        let url = "https://user:password@example.com:8545/private-key?token=secret#fragment";
651
652        assert_eq!(redact_url(url), "https://example.com:8545/");
653        assert_eq!(redact_url("not a URL with secret"), "<redacted>");
654    }
655
656    #[test]
657    fn invalid_provider_url_error_is_redacted() {
658        let builder = ProviderBuilder::<AnyNetwork>::new(
659            "https://example.com:bad/private-api-key?token=secret",
660        );
661
662        let error = builder.url.unwrap_err().to_string();
663        assert!(error.contains("<redacted>"));
664        assert!(!error.contains("private-api-key"));
665        assert!(!error.contains("secret"));
666    }
667
668    #[test]
669    fn method_not_found_classification_is_exact() {
670        let method_not_found = TransportError::ErrorResp(ErrorPayload::method_not_found());
671        let internal_error = TransportError::ErrorResp(ErrorPayload::internal_error());
672        let http_method_not_found = alloy_transport::TransportErrorKind::http_error(
673            403,
674            r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"method not allowed"}}"#
675                .to_string(),
676        );
677        let http_internal_error = alloy_transport::TransportErrorKind::http_error(
678            500,
679            r#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"internal error"}}"#.to_string(),
680        );
681        let http_method_not_found_with_diagnostics =
682            alloy_transport::TransportErrorKind::http_error(
683                403,
684                concat!(
685                    r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"method not allowed"}}"#,
686                    "\n\nHTTP diagnostics:\nstatus: 403 Forbidden"
687                )
688                .to_string(),
689            );
690        let transport_error = alloy_transport::TransportErrorKind::backend_gone();
691
692        assert!(is_rpc_method_not_found(&method_not_found));
693        assert!(is_rpc_method_not_found(&http_method_not_found));
694        assert!(is_rpc_method_not_found(&http_method_not_found_with_diagnostics));
695        assert!(!is_rpc_method_not_found(&internal_error));
696        assert!(!is_rpc_method_not_found(&http_internal_error));
697        assert!(!is_rpc_method_not_found(&transport_error));
698    }
699
700    #[test]
701    fn can_auto_correct_missing_prefix() {
702        let builder = ProviderBuilder::<AnyNetwork>::new("localhost:8545");
703        assert!(builder.url.is_ok());
704
705        let url = builder.url.unwrap();
706        assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
707    }
708
709    #[test]
710    fn from_config_applies_rpc_transport_options() {
711        let config = Config {
712            eth_rpc_url: Some("http://example.com".to_string()),
713            chain: Some(NamedChain::Polygon.into()),
714            eth_rpc_accept_invalid_certs: true,
715            eth_rpc_no_proxy: true,
716            eth_rpc_timeout: Some(7),
717            ..Default::default()
718        };
719
720        let builder = ProviderBuilder::<AnyNetwork>::from_config(&config).unwrap();
721
722        assert!(builder.accept_invalid_certs);
723        assert!(builder.no_proxy);
724        assert_eq!(builder.timeout, Duration::from_secs(7));
725        assert_eq!(builder.chain, NamedChain::Polygon);
726    }
727
728    #[test]
729    fn from_config_with_url_overrides_rpc_url() {
730        let config = Config {
731            eth_rpc_url: Some("http://configured.example".to_string()),
732            chain: Some(NamedChain::Polygon.into()),
733            eth_rpc_timeout: Some(7),
734            ..Default::default()
735        };
736
737        let builder =
738            ProviderBuilder::<AnyNetwork>::from_config_with_url(&config, "http://sequence.example")
739                .unwrap();
740
741        assert_eq!(builder.url.unwrap().as_str(), "http://sequence.example/");
742        assert_eq!(builder.timeout, Duration::from_secs(7));
743        assert_eq!(builder.chain, NamedChain::Mainnet);
744    }
745}