Skip to main content

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