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