1pub 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
31const DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME: Duration = Duration::from_secs(3);
34
35const POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR: f32 = 0.6;
37
38pub type RetryProvider<N = AnyNetwork> = RootProvider<N>;
40
41pub 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#[inline]
65#[track_caller]
66pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
67 try_get_http_provider(builder).unwrap()
68}
69
70#[inline]
73pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
74 ProviderBuilder::new(builder.as_ref()).build()
75}
76
77#[derive(Debug)]
81pub struct ProviderBuilder<N: Network = AnyNetwork> {
82 url: Result<Url>,
84 chain: NamedChain,
85 max_retry: u32,
86 initial_backoff: u64,
87 timeout: Duration,
88 compute_units_per_second: u64,
90 jwt: Option<String>,
92 headers: Vec<String>,
93 is_local: bool,
94 accept_invalid_certs: bool,
96 no_proxy: bool,
98 curl_mode: bool,
100 _network: PhantomData<N>,
102}
103
104impl<N: Network> ProviderBuilder<N> {
105 pub fn new(url_str: &str) -> Self {
107 let mut url_str = url_str;
109
110 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 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 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 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 pub fn timeout(mut self, timeout: Duration) -> Self {
193 self.timeout = timeout;
194 self
195 }
196
197 pub fn chain(mut self, chain: NamedChain) -> Self {
199 self.chain = chain;
200 self
201 }
202
203 pub fn max_retry(mut self, max_retry: u32) -> Self {
205 self.max_retry = max_retry;
206 self
207 }
208
209 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 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 pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
224 self.initial_backoff = initial_backoff;
225 self
226 }
227
228 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 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 pub fn local(mut self, is_local: bool) -> Self {
250 self.is_local = is_local;
251 self
252 }
253
254 pub fn aggressive(self) -> Self {
258 self.max_retry(100).initial_backoff(100).local(true)
259 }
260
261 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
263 self.jwt = Some(jwt.into());
264 self
265 }
266
267 pub fn headers(mut self, headers: Vec<String>) -> Self {
269 self.headers = headers;
270
271 self
272 }
273
274 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
276 self.headers = headers.unwrap_or(self.headers);
277 self
278 }
279
280 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 pub fn no_proxy(mut self, no_proxy: bool) -> Self {
291 self.no_proxy = no_proxy;
292 self
293 }
294
295 pub fn curl_mode(mut self, curl_mode: bool) -> Self {
300 self.curl_mode = curl_mode;
301 self
302 }
303
304 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 {
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 .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 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 {
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 .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}