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