1pub mod curl_transport;
4pub mod mpp;
5pub mod runtime_transport;
6
7use crate::{
8 ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT,
9 provider::{curl_transport::CurlTransport, runtime_transport::RuntimeTransportBuilder},
10};
11use alloy_chains::NamedChain;
12use alloy_json_rpc::{RequestPacket, ResponsePacket};
13use alloy_network::{Network, NetworkWallet};
14use alloy_provider::{
15 Identity, ProviderBuilder as AlloyProviderBuilder, RootProvider,
16 fillers::{FillProvider, JoinFill, RecommendedFillers, WalletFiller},
17 network::{AnyNetwork, EthereumWallet},
18};
19use alloy_rpc_client::ClientBuilder;
20use alloy_transport::{
21 TransportError, TransportFut, layers::RetryBackoffLayer, utils::guess_local_url,
22};
23use eyre::{Result, WrapErr};
24use foundry_config::Config;
25use reqwest::Url;
26use std::{
27 marker::PhantomData,
28 net::SocketAddr,
29 path::{Path, PathBuf},
30 str::FromStr,
31 sync::{
32 Arc,
33 atomic::{AtomicUsize, Ordering},
34 },
35 task::{Context, Poll},
36 time::Duration,
37};
38use tower::Service;
39use url::ParseError;
40
41const DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME: Duration = Duration::from_secs(3);
44
45const POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR: f32 = 0.6;
47
48pub type RetryProvider<N = AnyNetwork> = RootProvider<N>;
50
51pub type RetryProviderWithSigner<N = AnyNetwork, W = EthereumWallet> = FillProvider<
53 JoinFill<JoinFill<Identity, <N as RecommendedFillers>::RecommendedFillers>, WalletFiller<W>>,
54 RootProvider<N>,
55 N,
56>;
57
58#[inline]
75#[track_caller]
76pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
77 try_get_http_provider(builder).unwrap()
78}
79
80#[inline]
83pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
84 ProviderBuilder::new(builder.as_ref()).build()
85}
86
87#[derive(Clone)]
92pub struct RoundRobinService<S> {
93 transports: Arc<Vec<S>>,
94 next: Arc<AtomicUsize>,
95}
96
97impl<S> RoundRobinService<S> {
98 pub fn new(transports: Vec<S>) -> Self {
104 assert!(!transports.is_empty(), "RoundRobinService requires at least one transport");
105 Self { transports: Arc::new(transports), next: Arc::new(AtomicUsize::new(0)) }
106 }
107}
108
109impl<S> Service<RequestPacket> for RoundRobinService<S>
110where
111 S: Service<
112 RequestPacket,
113 Response = ResponsePacket,
114 Error = TransportError,
115 Future = TransportFut<'static>,
116 > + Clone
117 + Send
118 + Sync
119 + 'static,
120{
121 type Response = ResponsePacket;
122 type Error = TransportError;
123 type Future = TransportFut<'static>;
124
125 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
126 Poll::Ready(Ok(()))
127 }
128
129 fn call(&mut self, req: RequestPacket) -> Self::Future {
130 let transports = self.transports.clone();
131 let idx = self.next.fetch_add(1, Ordering::Relaxed) % transports.len();
132 let mut transport = transports[idx].clone();
133 transport.call(req)
134 }
135}
136
137#[derive(Debug)]
141pub struct ProviderBuilder<N: Network = AnyNetwork> {
142 url: Result<Url>,
144 chain: NamedChain,
145 max_retry: u32,
146 initial_backoff: u64,
147 timeout: Duration,
148 compute_units_per_second: u64,
150 jwt: Option<String>,
152 headers: Vec<String>,
153 is_local: bool,
154 accept_invalid_certs: bool,
156 no_proxy: bool,
158 curl_mode: bool,
160 _network: PhantomData<N>,
162}
163
164impl<N: Network> ProviderBuilder<N> {
165 pub fn new(url_str: &str) -> Self {
167 let mut url_str = url_str;
169
170 let storage;
173 if url_str.starts_with("localhost:") {
174 storage = format!("http://{url_str}");
175 url_str = storage.as_str();
176 }
177
178 let url = Url::parse(url_str)
179 .or_else(|err| match err {
180 ParseError::RelativeUrlWithoutBase => {
181 if SocketAddr::from_str(url_str).is_ok() {
182 Url::parse(&format!("http://{url_str}"))
183 } else {
184 let path = Path::new(url_str);
185
186 if let Ok(path) = resolve_path(path) {
187 Url::parse(&format!("file://{}", path.display()))
188 } else {
189 Err(err)
190 }
191 }
192 }
193 _ => Err(err),
194 })
195 .wrap_err_with(|| format!("invalid provider URL: {url_str:?}"));
196
197 let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
199
200 Self {
201 url,
202 chain: NamedChain::Mainnet,
203 max_retry: 8,
204 initial_backoff: 800,
205 timeout: REQUEST_TIMEOUT,
206 compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
208 jwt: None,
209 headers: vec![],
210 is_local,
211 accept_invalid_certs: false,
212 no_proxy: false,
213 curl_mode: false,
214 _network: PhantomData,
215 }
216 }
217
218 pub fn from_config(config: &Config) -> Result<Self> {
222 let url = config.get_rpc_url_or_localhost_http()?;
223 let mut builder = Self::new(url.as_ref());
224
225 builder = builder.accept_invalid_certs(config.eth_rpc_accept_invalid_certs);
226 builder = builder.curl_mode(config.eth_rpc_curl);
227
228 if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
229 builder = builder.chain(chain);
230 }
231
232 if let Some(jwt) = config.get_rpc_jwt_secret()? {
233 builder = builder.jwt(jwt.as_ref());
234 }
235
236 if let Some(rpc_timeout) = config.eth_rpc_timeout {
237 builder = builder.timeout(Duration::from_secs(rpc_timeout));
238 }
239
240 if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
241 builder = builder.headers(rpc_headers);
242 }
243
244 Ok(builder)
245 }
246
247 pub const fn timeout(mut self, timeout: Duration) -> Self {
254 self.timeout = timeout;
255 self
256 }
257
258 pub const fn chain(mut self, chain: NamedChain) -> Self {
260 self.chain = chain;
261 self
262 }
263
264 pub const fn max_retry(mut self, max_retry: u32) -> Self {
266 self.max_retry = max_retry;
267 self
268 }
269
270 pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
272 self.max_retry = max_retry.unwrap_or(self.max_retry);
273 self
274 }
275
276 pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
279 self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
280 self
281 }
282
283 pub const fn initial_backoff(mut self, initial_backoff: u64) -> Self {
285 self.initial_backoff = initial_backoff;
286 self
287 }
288
289 pub const fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
293 self.compute_units_per_second = compute_units_per_second;
294 self
295 }
296
297 pub const fn compute_units_per_second_opt(
301 mut self,
302 compute_units_per_second: Option<u64>,
303 ) -> Self {
304 if let Some(cups) = compute_units_per_second {
305 self.compute_units_per_second = cups;
306 }
307 self
308 }
309
310 pub const fn local(mut self, is_local: bool) -> Self {
314 self.is_local = is_local;
315 self
316 }
317
318 pub const fn aggressive(self) -> Self {
322 self.max_retry(100).initial_backoff(100).local(true)
323 }
324
325 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
327 self.jwt = Some(jwt.into());
328 self
329 }
330
331 pub fn headers(mut self, headers: Vec<String>) -> Self {
333 self.headers = headers;
334
335 self
336 }
337
338 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
340 self.headers = headers.unwrap_or(self.headers);
341 self
342 }
343
344 pub const fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
346 self.accept_invalid_certs = accept_invalid_certs;
347 self
348 }
349
350 pub const fn no_proxy(mut self, no_proxy: bool) -> Self {
355 self.no_proxy = no_proxy;
356 self
357 }
358
359 pub const fn curl_mode(mut self, curl_mode: bool) -> Self {
364 self.curl_mode = curl_mode;
365 self
366 }
367
368 pub fn build(self) -> Result<RetryProvider<N>> {
370 let Self {
371 url,
372 chain,
373 max_retry,
374 initial_backoff,
375 timeout,
376 compute_units_per_second,
377 jwt,
378 headers,
379 is_local,
380 accept_invalid_certs,
381 no_proxy,
382 curl_mode,
383 ..
384 } = self;
385 let url = url?;
386
387 let retry_layer =
388 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
389
390 if curl_mode {
392 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
393 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
394
395 let provider = AlloyProviderBuilder::<_, _, N>::default()
396 .connect_provider(RootProvider::new(client));
397
398 return Ok(provider);
399 }
400
401 let transport = RuntimeTransportBuilder::new(url)
402 .with_timeout(timeout)
403 .with_headers(headers)
404 .with_jwt(jwt)
405 .accept_invalid_certs(accept_invalid_certs)
406 .no_proxy(no_proxy)
407 .build();
408 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
409
410 if !is_local {
411 client.set_poll_interval(
412 chain
413 .average_blocktime_hint()
414 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
417 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
418 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
419 );
420 }
421
422 let provider =
423 AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
424
425 Ok(provider)
426 }
427}
428
429impl<N: Network> ProviderBuilder<N> {
430 pub fn build_fallback(self, urls: Vec<String>) -> Result<RetryProvider<N>> {
437 let Self {
438 chain,
439 max_retry,
440 initial_backoff,
441 timeout,
442 compute_units_per_second,
443 jwt,
444 headers,
445 accept_invalid_certs,
446 no_proxy,
447 curl_mode,
448 ..
449 } = self;
450
451 eyre::ensure!(!urls.is_empty(), "at least one fork URL is required");
452 eyre::ensure!(!curl_mode, "curl mode is not supported with multiple fork URLs");
453
454 let mut parsed_urls = Vec::with_capacity(urls.len());
457 let transports: Vec<_> = urls
458 .iter()
459 .map(|url_str| {
460 let builder = Self::new(url_str);
461 let url = builder.url?;
462 parsed_urls.push(url.clone());
463 Ok(RuntimeTransportBuilder::new(url)
464 .with_timeout(timeout)
465 .with_headers(headers.clone())
466 .with_jwt(jwt.clone())
467 .accept_invalid_certs(accept_invalid_certs)
468 .no_proxy(no_proxy)
469 .build())
470 })
471 .collect::<Result<Vec<_>>>()?;
472
473 let round_robin = RoundRobinService::new(transports);
474
475 let retry_layer =
476 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
477 let is_local = parsed_urls.iter().all(|url| guess_local_url(url.as_str()));
479 let client = ClientBuilder::default().layer(retry_layer).transport(round_robin, is_local);
480
481 if !is_local {
482 client.set_poll_interval(
483 chain
484 .average_blocktime_hint()
485 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
486 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
487 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
488 );
489 }
490
491 let provider =
492 AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
493
494 Ok(provider)
495 }
496
497 pub fn build_with_wallet<W: NetworkWallet<N> + Clone>(
499 self,
500 wallet: W,
501 ) -> Result<RetryProviderWithSigner<N, W>>
502 where
503 N: RecommendedFillers,
504 {
505 let Self {
506 url,
507 chain,
508 max_retry,
509 initial_backoff,
510 timeout,
511 compute_units_per_second,
512 jwt,
513 headers,
514 is_local,
515 accept_invalid_certs,
516 no_proxy,
517 curl_mode,
518 ..
519 } = self;
520 let url = url?;
521
522 let retry_layer =
523 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
524
525 if curl_mode {
527 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
528 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
529
530 let provider = AlloyProviderBuilder::<_, _, N>::default()
531 .with_recommended_fillers()
532 .wallet(wallet)
533 .connect_provider(RootProvider::new(client));
534
535 return Ok(provider);
536 }
537
538 let transport = RuntimeTransportBuilder::new(url)
539 .with_timeout(timeout)
540 .with_headers(headers)
541 .with_jwt(jwt)
542 .accept_invalid_certs(accept_invalid_certs)
543 .no_proxy(no_proxy)
544 .build();
545
546 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
547
548 if !is_local {
549 client.set_poll_interval(
550 chain
551 .average_blocktime_hint()
552 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
555 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
556 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
557 );
558 }
559
560 let provider = AlloyProviderBuilder::<_, _, N>::default()
561 .with_recommended_fillers()
562 .wallet(wallet)
563 .connect_provider(RootProvider::new(client));
564
565 Ok(provider)
566 }
567}
568
569#[cfg(not(windows))]
570fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
571 if path.is_absolute() {
572 Ok(path.to_path_buf())
573 } else {
574 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
575 }
576}
577
578#[cfg(windows)]
579fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
580 if let Some(s) = path.to_str()
581 && s.starts_with(r"\\.\pipe\")
582 {
583 return Ok(path.to_path_buf());
584 }
585 if path.is_absolute() {
586 Ok(path.to_path_buf())
587 } else {
588 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn can_auto_correct_missing_prefix() {
598 let builder = ProviderBuilder::<AnyNetwork>::new("localhost:8545");
599 assert!(builder.url.is_ok());
600
601 let url = builder.url.unwrap();
602 assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
603 }
604}