foundry_common/provider/
mod.rs

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