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
167        if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
168            builder = builder.chain(chain);
169        }
170
171        if let Some(jwt) = config.get_rpc_jwt_secret()? {
172            builder = builder.jwt(jwt.as_ref());
173        }
174
175        if let Some(rpc_timeout) = config.eth_rpc_timeout {
176            builder = builder.timeout(Duration::from_secs(rpc_timeout));
177        }
178
179        if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
180            builder = builder.headers(rpc_headers);
181        }
182
183        Ok(builder)
184    }
185
186    /// Enables a request timeout.
187    ///
188    /// The timeout is applied from when the request starts connecting until the
189    /// response body has finished.
190    ///
191    /// Default is no timeout.
192    pub fn timeout(mut self, timeout: Duration) -> Self {
193        self.timeout = timeout;
194        self
195    }
196
197    /// Sets the chain of the node the provider will connect to
198    pub fn chain(mut self, chain: NamedChain) -> Self {
199        self.chain = chain;
200        self
201    }
202
203    /// How often to retry a failed request
204    pub fn max_retry(mut self, max_retry: u32) -> Self {
205        self.max_retry = max_retry;
206        self
207    }
208
209    /// How often to retry a failed request. If `None`, defaults to the already-set value.
210    pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
211        self.max_retry = max_retry.unwrap_or(self.max_retry);
212        self
213    }
214
215    /// The starting backoff delay to use after the first failed request. If `None`, defaults to
216    /// the already-set value.
217    pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
218        self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
219        self
220    }
221
222    /// The starting backoff delay to use after the first failed request
223    pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
224        self.initial_backoff = initial_backoff;
225        self
226    }
227
228    /// Sets the number of assumed available compute units per second
229    ///
230    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
231    pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
232        self.compute_units_per_second = compute_units_per_second;
233        self
234    }
235
236    /// Sets the number of assumed available compute units per second
237    ///
238    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
239    pub fn compute_units_per_second_opt(mut self, compute_units_per_second: Option<u64>) -> Self {
240        if let Some(cups) = compute_units_per_second {
241            self.compute_units_per_second = cups;
242        }
243        self
244    }
245
246    /// Sets the provider to be local.
247    ///
248    /// This is useful for local dev nodes.
249    pub fn local(mut self, is_local: bool) -> Self {
250        self.is_local = is_local;
251        self
252    }
253
254    /// Sets aggressive `max_retry` and `initial_backoff` values
255    ///
256    /// This is only recommend for local dev nodes
257    pub fn aggressive(self) -> Self {
258        self.max_retry(100).initial_backoff(100).local(true)
259    }
260
261    /// Sets the JWT secret
262    pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
263        self.jwt = Some(jwt.into());
264        self
265    }
266
267    /// Sets http headers
268    pub fn headers(mut self, headers: Vec<String>) -> Self {
269        self.headers = headers;
270
271        self
272    }
273
274    /// Sets http headers. If `None`, defaults to the already-set value.
275    pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
276        self.headers = headers.unwrap_or(self.headers);
277        self
278    }
279
280    /// Sets whether to accept invalid certificates.
281    pub fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
282        self.accept_invalid_certs = accept_invalid_certs;
283        self
284    }
285
286    /// Sets whether to disable automatic proxy detection.
287    ///
288    /// This can help in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox)
289    /// where system proxy detection via SCDynamicStore causes crashes.
290    pub fn no_proxy(mut self, no_proxy: bool) -> Self {
291        self.no_proxy = no_proxy;
292        self
293    }
294
295    /// Sets whether to output curl commands instead of making requests.
296    ///
297    /// When enabled, the provider will print equivalent curl commands to stdout
298    /// instead of actually executing the RPC requests.
299    pub fn curl_mode(mut self, curl_mode: bool) -> Self {
300        self.curl_mode = curl_mode;
301        self
302    }
303
304    /// Constructs the `RetryProvider` taking all configs into account.
305    pub fn build(self) -> Result<RetryProvider<N>> {
306        let Self {
307            url,
308            chain,
309            max_retry,
310            initial_backoff,
311            timeout,
312            compute_units_per_second,
313            jwt,
314            headers,
315            is_local,
316            accept_invalid_certs,
317            no_proxy,
318            curl_mode,
319            ..
320        } = self;
321        let url = url?;
322
323        let retry_layer =
324            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
325
326        // If curl_mode is enabled, use CurlTransport instead of RuntimeTransport
327        if curl_mode {
328            let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
329            let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
330
331            let provider = AlloyProviderBuilder::<_, _, N>::default()
332                .connect_provider(RootProvider::new(client));
333
334            return Ok(provider);
335        }
336
337        let transport = RuntimeTransportBuilder::new(url)
338            .with_timeout(timeout)
339            .with_headers(headers)
340            .with_jwt(jwt)
341            .accept_invalid_certs(accept_invalid_certs)
342            .no_proxy(no_proxy)
343            .build();
344        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
345
346        if !is_local {
347            client.set_poll_interval(
348                chain
349                    .average_blocktime_hint()
350                    // we cap the poll interval because if not provided, chain would default to
351                    // mainnet
352                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
353                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
354                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
355            );
356        }
357
358        let provider =
359            AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
360
361        Ok(provider)
362    }
363}
364
365impl<N: Network> ProviderBuilder<N> {
366    /// Constructs the `RetryProvider` with a wallet.
367    pub fn build_with_wallet<W: NetworkWallet<N> + Clone>(
368        self,
369        wallet: W,
370    ) -> Result<RetryProviderWithSigner<N, W>>
371    where
372        N: RecommendedFillers,
373    {
374        let Self {
375            url,
376            chain,
377            max_retry,
378            initial_backoff,
379            timeout,
380            compute_units_per_second,
381            jwt,
382            headers,
383            is_local,
384            accept_invalid_certs,
385            no_proxy,
386            curl_mode,
387            ..
388        } = self;
389        let url = url?;
390
391        let retry_layer =
392            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
393
394        // If curl_mode is enabled, use CurlTransport instead of RuntimeTransport
395        if curl_mode {
396            let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
397            let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
398
399            let provider = AlloyProviderBuilder::<_, _, N>::default()
400                .with_recommended_fillers()
401                .wallet(wallet)
402                .connect_provider(RootProvider::new(client));
403
404            return Ok(provider);
405        }
406
407        let transport = RuntimeTransportBuilder::new(url)
408            .with_timeout(timeout)
409            .with_headers(headers)
410            .with_jwt(jwt)
411            .accept_invalid_certs(accept_invalid_certs)
412            .no_proxy(no_proxy)
413            .build();
414
415        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
416
417        if !is_local {
418            client.set_poll_interval(
419                chain
420                    .average_blocktime_hint()
421                    // we cap the poll interval because if not provided, chain would default to
422                    // mainnet
423                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
424                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
425                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
426            );
427        }
428
429        let provider = AlloyProviderBuilder::<_, _, N>::default()
430            .with_recommended_fillers()
431            .wallet(wallet)
432            .connect_provider(RootProvider::new(client));
433
434        Ok(provider)
435    }
436}
437
438#[cfg(not(windows))]
439fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
440    if path.is_absolute() {
441        Ok(path.to_path_buf())
442    } else {
443        std::env::current_dir().map(|d| d.join(path)).map_err(drop)
444    }
445}
446
447#[cfg(windows)]
448fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
449    if let Some(s) = path.to_str()
450        && s.starts_with(r"\\.\pipe\")
451    {
452        return Ok(path.to_path_buf());
453    }
454    if path.is_absolute() {
455        Ok(path.to_path_buf())
456    } else {
457        std::env::current_dir().map(|d| d.join(path)).map_err(drop)
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn can_auto_correct_missing_prefix() {
467        let builder = ProviderBuilder::<AnyNetwork>::new("localhost:8545");
468        assert!(builder.url.is_ok());
469
470        let url = builder.url.unwrap();
471        assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
472    }
473}