foundry_common/provider/
mod.rs

1//! Provider-related instantiation and usage utilities.
2
3pub mod runtime_transport;
4
5use crate::{
6    provider::runtime_transport::RuntimeTransportBuilder, ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
7};
8use alloy_provider::{
9    fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller},
10    network::{AnyNetwork, EthereumWallet},
11    Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
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}
100
101impl ProviderBuilder {
102    /// Creates a new builder instance
103    pub fn new(url_str: &str) -> Self {
104        // a copy is needed for the next lines to work
105        let mut url_str = url_str;
106
107        // invalid url: non-prefixed URL scheme is not allowed, so we prepend the default http
108        // prefix
109        let storage;
110        if url_str.starts_with("localhost:") {
111            storage = format!("http://{url_str}");
112            url_str = storage.as_str();
113        }
114
115        let url = Url::parse(url_str)
116            .or_else(|err| match err {
117                ParseError::RelativeUrlWithoutBase => {
118                    if SocketAddr::from_str(url_str).is_ok() {
119                        Url::parse(&format!("http://{url_str}"))
120                    } else {
121                        let path = Path::new(url_str);
122
123                        if let Ok(path) = resolve_path(path) {
124                            Url::parse(&format!("file://{}", path.display()))
125                        } else {
126                            Err(err)
127                        }
128                    }
129                }
130                _ => Err(err),
131            })
132            .wrap_err_with(|| format!("invalid provider URL: {url_str:?}"));
133
134        // Use the final URL string to guess if it's a local URL.
135        let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
136
137        Self {
138            url,
139            chain: NamedChain::Mainnet,
140            max_retry: 8,
141            initial_backoff: 800,
142            timeout: REQUEST_TIMEOUT,
143            // alchemy max cpus <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
144            compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
145            jwt: None,
146            headers: vec![],
147            is_local,
148        }
149    }
150
151    /// Enables a request timeout.
152    ///
153    /// The timeout is applied from when the request starts connecting until the
154    /// response body has finished.
155    ///
156    /// Default is no timeout.
157    pub fn timeout(mut self, timeout: Duration) -> Self {
158        self.timeout = timeout;
159        self
160    }
161
162    /// Sets the chain of the node the provider will connect to
163    pub fn chain(mut self, chain: NamedChain) -> Self {
164        self.chain = chain;
165        self
166    }
167
168    /// How often to retry a failed request
169    pub fn max_retry(mut self, max_retry: u32) -> Self {
170        self.max_retry = max_retry;
171        self
172    }
173
174    /// How often to retry a failed request. If `None`, defaults to the already-set value.
175    pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
176        self.max_retry = max_retry.unwrap_or(self.max_retry);
177        self
178    }
179
180    /// The starting backoff delay to use after the first failed request. If `None`, defaults to
181    /// the already-set value.
182    pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
183        self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
184        self
185    }
186
187    /// The starting backoff delay to use after the first failed request
188    pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
189        self.initial_backoff = initial_backoff;
190        self
191    }
192
193    /// Sets the number of assumed available compute units per second
194    ///
195    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
196    pub fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
197        self.compute_units_per_second = compute_units_per_second;
198        self
199    }
200
201    /// Sets the number of assumed available compute units per second
202    ///
203    /// See also, <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
204    pub fn compute_units_per_second_opt(mut self, compute_units_per_second: Option<u64>) -> Self {
205        if let Some(cups) = compute_units_per_second {
206            self.compute_units_per_second = cups;
207        }
208        self
209    }
210
211    /// Sets the provider to be local.
212    ///
213    /// This is useful for local dev nodes.
214    pub fn local(mut self, is_local: bool) -> Self {
215        self.is_local = is_local;
216        self
217    }
218
219    /// Sets aggressive `max_retry` and `initial_backoff` values
220    ///
221    /// This is only recommend for local dev nodes
222    pub fn aggressive(self) -> Self {
223        self.max_retry(100).initial_backoff(100).local(true)
224    }
225
226    /// Sets the JWT secret
227    pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
228        self.jwt = Some(jwt.into());
229        self
230    }
231
232    /// Sets http headers
233    pub fn headers(mut self, headers: Vec<String>) -> Self {
234        self.headers = headers;
235
236        self
237    }
238
239    /// Sets http headers. If `None`, defaults to the already-set value.
240    pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
241        self.headers = headers.unwrap_or(self.headers);
242        self
243    }
244
245    /// Constructs the `RetryProvider` taking all configs into account.
246    pub fn build(self) -> Result<RetryProvider> {
247        let Self {
248            url,
249            chain,
250            max_retry,
251            initial_backoff,
252            timeout,
253            compute_units_per_second,
254            jwt,
255            headers,
256            is_local,
257        } = self;
258        let url = url?;
259
260        let retry_layer =
261            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
262
263        let transport = RuntimeTransportBuilder::new(url)
264            .with_timeout(timeout)
265            .with_headers(headers)
266            .with_jwt(jwt)
267            .build();
268        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
269
270        if !is_local {
271            client.set_poll_interval(
272                chain
273                    .average_blocktime_hint()
274                    // we cap the poll interval because if not provided, chain would default to
275                    // mainnet
276                    .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
277                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
278                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
279            );
280        }
281
282        let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
283            .on_provider(RootProvider::new(client));
284
285        Ok(provider)
286    }
287
288    /// Constructs the `RetryProvider` with a wallet.
289    pub fn build_with_wallet(self, wallet: EthereumWallet) -> Result<RetryProviderWithSigner> {
290        let Self {
291            url,
292            chain,
293            max_retry,
294            initial_backoff,
295            timeout,
296            compute_units_per_second,
297            jwt,
298            headers,
299            is_local,
300        } = self;
301        let url = url?;
302
303        let retry_layer =
304            RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
305
306        let transport = RuntimeTransportBuilder::new(url)
307            .with_timeout(timeout)
308            .with_headers(headers)
309            .with_jwt(jwt)
310            .build();
311
312        let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
313
314        if !is_local {
315            client.set_poll_interval(
316                chain
317                    .average_blocktime_hint()
318                    .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
319                    .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
320            );
321        }
322
323        let provider = AlloyProviderBuilder::<_, _, AnyNetwork>::default()
324            .with_recommended_fillers()
325            .wallet(wallet)
326            .on_provider(RootProvider::new(client));
327
328        Ok(provider)
329    }
330}
331
332#[cfg(not(windows))]
333fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
334    if path.is_absolute() {
335        Ok(path.to_path_buf())
336    } else {
337        std::env::current_dir().map(|d| d.join(path)).map_err(drop)
338    }
339}
340
341#[cfg(windows)]
342fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
343    if let Some(s) = path.to_str() {
344        if s.starts_with(r"\\.\pipe\") {
345            return Ok(path.to_path_buf());
346        }
347    }
348    Err(())
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn can_auto_correct_missing_prefix() {
357        let builder = ProviderBuilder::new("localhost:8545");
358        assert!(builder.url.is_ok());
359
360        let url = builder.url.unwrap();
361        assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
362    }
363}