foundry_common/provider/
mod.rs

1//! Provider-related instantiation and usage utilities.
2
3pub mod curl_transport;
4pub mod runtime_transport;
5
6use crate::{
7    ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
8    provider::{curl_transport::CurlTransport, runtime_transport::RuntimeTransportBuilder},
9};
10use alloy_chains::NamedChain;
11use alloy_provider::{
12    Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
13    fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller},
14    network::{AnyNetwork, EthereumWallet},
15};
16use alloy_rpc_client::ClientBuilder;
17use alloy_transport::{layers::RetryBackoffLayer, utils::guess_local_url};
18use eyre::{Result, WrapErr};
19use reqwest::Url;
20use std::{
21    net::SocketAddr,
22    path::{Path, PathBuf},
23    str::FromStr,
24    time::Duration,
25};
26use url::ParseError;
27
28/// The assumed block time for unknown chains.
29/// We assume that these are chains have a faster block time.
30const DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME: Duration = Duration::from_secs(3);
31
32/// The factor to scale the block time by to get the poll interval.
33const POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR: f32 = 0.6;
34
35/// Helper type alias for a retry provider
36pub type RetryProvider<N = AnyNetwork> = RootProvider<N>;
37
38/// Helper type alias for a retry provider with a signer
39pub type RetryProviderWithSigner<N = AnyNetwork> = FillProvider<
40    JoinFill<
41        JoinFill<
42            Identity,
43            JoinFill<
44                GasFiller,
45                JoinFill<
46                    alloy_provider::fillers::BlobGasFiller,
47                    JoinFill<NonceFiller, ChainIdFiller>,
48                >,
49            >,
50        >,
51        WalletFiller<EthereumWallet>,
52    >,
53    RootProvider<N>,
54    N,
55>;
56
57/// Constructs a provider with a 100 millisecond interval poll if it's a localhost URL (most likely
58/// an anvil or other dev node) and with the default, or 7 second otherwise.
59///
60/// See [`try_get_http_provider`] for more details.
61///
62/// # Panics
63///
64/// Panics if the URL is invalid.
65///
66/// # Examples
67///
68/// ```
69/// use foundry_common::provider::get_http_provider;
70///
71/// let retry_provider = get_http_provider("http://localhost:8545");
72/// ```
73#[inline]
74#[track_caller]
75pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
76    try_get_http_provider(builder).unwrap()
77}
78
79/// Constructs a provider with a 100 millisecond interval poll if it's a localhost URL (most likely
80/// an anvil or other dev node) and with the default, or 7 second otherwise.
81#[inline]
82pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
83    ProviderBuilder::new(builder.as_ref()).build()
84}
85
86/// Helper type to construct a `RetryProvider`
87#[derive(Debug)]
88pub struct ProviderBuilder {
89    // Note: this is a result, so we can easily chain builder calls
90    url: Result<Url>,
91    chain: NamedChain,
92    max_retry: u32,
93    initial_backoff: u64,
94    timeout: Duration,
95    /// available CUPS
96    compute_units_per_second: u64,
97    /// JWT Secret
98    jwt: Option<String>,
99    headers: Vec<String>,
100    is_local: bool,
101    /// Whether to accept invalid certificates.
102    accept_invalid_certs: bool,
103    /// Whether to disable automatic proxy detection.
104    no_proxy: bool,
105    /// Whether to output curl commands instead of making requests.
106    curl_mode: bool,
107}
108
109impl ProviderBuilder {
110    /// Creates a new builder instance
111    pub fn new(url_str: &str) -> Self {
112        // a copy is needed for the next lines to work
113        let mut url_str = url_str;
114
115        // invalid url: non-prefixed URL scheme is not allowed, so we prepend the default http
116        // prefix
117        let storage;
118        if url_str.starts_with("localhost:") {
119            storage = format!("http://{url_str}");
120            url_str = storage.as_str();
121        }
122
123        let url = Url::parse(url_str)
124            .or_else(|err| match err {
125                ParseError::RelativeUrlWithoutBase => {
126                    if SocketAddr::from_str(url_str).is_ok() {
127                        Url::parse(&format!("http://{url_str}"))
128                    } else {
129                        let path = Path::new(url_str);
130
131                        if let Ok(path) = resolve_path(path) {
132                            Url::parse(&format!("file://{}", path.display()))
133                        } else {
134                            Err(err)
135                        }
136                    }
137                }
138                _ => Err(err),
139            })
140            .wrap_err_with(|| format!("invalid provider URL: {url_str:?}"));
141
142        // Use the final URL string to guess if it's a local URL.
143        let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
144
145        Self {
146            url,
147            chain: NamedChain::Mainnet,
148            max_retry: 8,
149            initial_backoff: 800,
150            timeout: REQUEST_TIMEOUT,
151            // alchemy max cpus <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
152            compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
153            jwt: None,
154            headers: vec![],
155            is_local,
156            accept_invalid_certs: false,
157            no_proxy: false,
158            curl_mode: false,
159        }
160    }
161
162    /// Enables a request timeout.
163    ///
164    /// The timeout is applied from when the request starts connecting until the
165    /// response body has finished.
166    ///
167    /// Default is no timeout.
168    pub fn timeout(mut self, timeout: Duration) -> Self {
169        self.timeout = timeout;
170        self
171    }
172
173    /// Sets the chain of the node the provider will connect to
174    pub fn chain(mut self, chain: NamedChain) -> Self {
175        self.chain = chain;
176        self
177    }
178
179    /// How often to retry a failed request
180    pub fn max_retry(mut self, max_retry: u32) -> Self {
181        self.max_retry = max_retry;
182        self
183    }
184
185    /// How often to retry a failed request. If `None`, defaults to the already-set value.
186    pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
187        self.max_retry = max_retry.unwrap_or(self.max_retry);
188        self
189    }
190
191    /// The starting backoff delay to use after the first failed request. If `None`, defaults to
192    /// the already-set value.
193    pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
194        self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
195        self
196    }
197
198    /// The starting backoff delay to use after the first failed request
199    pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
200        self.initial_backoff = initial_backoff;
201        self
202    }
203
204    /// Sets the number of assumed available compute units per second
205    ///
206    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
207    pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
208        self.compute_units_per_second = compute_units_per_second;
209        self
210    }
211
212    /// Sets the number of assumed available compute units per second
213    ///
214    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
215    pub fn compute_units_per_second_opt(mut self, compute_units_per_second: Option<u64>) -> Self {
216        if let Some(cups) = compute_units_per_second {
217            self.compute_units_per_second = cups;
218        }
219        self
220    }
221
222    /// Sets the provider to be local.
223    ///
224    /// This is useful for local dev nodes.
225    pub fn local(mut self, is_local: bool) -> Self {
226        self.is_local = is_local;
227        self
228    }
229
230    /// Sets aggressive `max_retry` and `initial_backoff` values
231    ///
232    /// This is only recommend for local dev nodes
233    pub fn aggressive(self) -> Self {
234        self.max_retry(100).initial_backoff(100).local(true)
235    }
236
237    /// Sets the JWT secret
238    pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
239        self.jwt = Some(jwt.into());
240        self
241    }
242
243    /// Sets http headers
244    pub fn headers(mut self, headers: Vec<String>) -> Self {
245        self.headers = headers;
246
247        self
248    }
249
250    /// Sets http headers. If `None`, defaults to the already-set value.
251    pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
252        self.headers = headers.unwrap_or(self.headers);
253        self
254    }
255
256    /// Sets whether to accept invalid certificates.
257    pub fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
258        self.accept_invalid_certs = accept_invalid_certs;
259        self
260    }
261
262    /// Sets whether to disable automatic proxy detection.
263    ///
264    /// This can help in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox)
265    /// where system proxy detection via SCDynamicStore causes crashes.
266    pub fn no_proxy(mut self, no_proxy: bool) -> Self {
267        self.no_proxy = no_proxy;
268        self
269    }
270
271    /// Sets whether to output curl commands instead of making requests.
272    ///
273    /// When enabled, the provider will print equivalent curl commands to stdout
274    /// instead of actually executing the RPC requests.
275    pub fn curl_mode(mut self, curl_mode: bool) -> Self {
276        self.curl_mode = curl_mode;
277        self
278    }
279
280    /// Constructs the `RetryProvider` taking all configs into account.
281    pub fn build(self) -> Result<RetryProvider> {
282        let Self {
283            url,
284            chain,
285            max_retry,
286            initial_backoff,
287            timeout,
288            compute_units_per_second,
289            jwt,
290            headers,
291            is_local,
292            accept_invalid_certs,
293            no_proxy,
294            curl_mode,
295        } = self;
296        let url = url?;
297
298        let retry_layer =
299            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
300
301        // If curl_mode is enabled, use CurlTransport instead of RuntimeTransport
302        if curl_mode {
303            let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
304            let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
305
306            let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
307                .connect_provider(RootProvider::new(client));
308
309            return Ok(provider);
310        }
311
312        let transport = RuntimeTransportBuilder::new(url)
313            .with_timeout(timeout)
314            .with_headers(headers)
315            .with_jwt(jwt)
316            .accept_invalid_certs(accept_invalid_certs)
317            .no_proxy(no_proxy)
318            .build();
319        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
320
321        if !is_local {
322            client.set_poll_interval(
323                chain
324                    .average_blocktime_hint()
325                    // we cap the poll interval because if not provided, chain would default to
326                    // mainnet
327                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
328                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
329                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
330            );
331        }
332
333        let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
334            .connect_provider(RootProvider::new(client));
335
336        Ok(provider)
337    }
338
339    /// Constructs the `RetryProvider` with a wallet.
340    pub fn build_with_wallet(self, wallet: EthereumWallet) -> Result<RetryProviderWithSigner> {
341        let Self {
342            url,
343            chain,
344            max_retry,
345            initial_backoff,
346            timeout,
347            compute_units_per_second,
348            jwt,
349            headers,
350            is_local,
351            accept_invalid_certs,
352            no_proxy,
353            curl_mode,
354        } = self;
355        let url = url?;
356
357        let retry_layer =
358            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
359
360        // If curl_mode is enabled, use CurlTransport instead of RuntimeTransport
361        if curl_mode {
362            let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
363            let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
364
365            let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
366                .with_recommended_fillers()
367                .wallet(wallet)
368                .connect_provider(RootProvider::new(client));
369
370            return Ok(provider);
371        }
372
373        let transport = RuntimeTransportBuilder::new(url)
374            .with_timeout(timeout)
375            .with_headers(headers)
376            .with_jwt(jwt)
377            .accept_invalid_certs(accept_invalid_certs)
378            .no_proxy(no_proxy)
379            .build();
380
381        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
382
383        if !is_local {
384            client.set_poll_interval(
385                chain
386                    .average_blocktime_hint()
387                    // we cap the poll interval because if not provided, chain would default to
388                    // mainnet
389                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
390                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
391                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
392            );
393        }
394
395        let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
396            .with_recommended_fillers()
397            .wallet(wallet)
398            .connect_provider(RootProvider::new(client));
399
400        Ok(provider)
401    }
402}
403
404#[cfg(not(windows))]
405fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
406    if path.is_absolute() {
407        Ok(path.to_path_buf())
408    } else {
409        std::env::current_dir().map(|d| d.join(path)).map_err(drop)
410    }
411}
412
413#[cfg(windows)]
414fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
415    if let Some(s) = path.to_str()
416        && s.starts_with(r"\\.\pipe\")
417    {
418        return Ok(path.to_path_buf());
419    }
420    Err(())
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn can_auto_correct_missing_prefix() {
429        let builder = ProviderBuilder::new("localhost:8545");
430        assert!(builder.url.is_ok());
431
432        let url = builder.url.unwrap();
433        assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
434    }
435}