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 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 pub fn timeout(mut self, timeout: Duration) -> Self {
194 self.timeout = timeout;
195 self
196 }
197
198 pub fn chain(mut self, chain: NamedChain) -> Self {
200 self.chain = chain;
201 self
202 }
203
204 pub fn max_retry(mut self, max_retry: u32) -> Self {
206 self.max_retry = max_retry;
207 self
208 }
209
210 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 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 pub fn initial_backoff(mut self, initial_backoff: u64) -> Self {
225 self.initial_backoff = initial_backoff;
226 self
227 }
228
229 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 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 pub fn local(mut self, is_local: bool) -> Self {
251 self.is_local = is_local;
252 self
253 }
254
255 pub fn aggressive(self) -> Self {
259 self.max_retry(100).initial_backoff(100).local(true)
260 }
261
262 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
264 self.jwt = Some(jwt.into());
265 self
266 }
267
268 pub fn headers(mut self, headers: Vec<String>) -> Self {
270 self.headers = headers;
271
272 self
273 }
274
275 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
277 self.headers = headers.unwrap_or(self.headers);
278 self
279 }
280
281 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 pub fn no_proxy(mut self, no_proxy: bool) -> Self {
292 self.no_proxy = no_proxy;
293 self
294 }
295
296 pub fn curl_mode(mut self, curl_mode: bool) -> Self {
301 self.curl_mode = curl_mode;
302 self
303 }
304
305 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 {
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 .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 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 {
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 .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}