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 .accept_invalid_certs(config.eth_rpc_accept_invalid_certs)
225 .no_proxy(config.eth_rpc_no_proxy)
226 .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 let no_proxy = no_proxy || is_local;
387
388 let retry_layer =
389 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
390
391 if curl_mode {
393 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
394 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
395
396 let provider = AlloyProviderBuilder::<_, _, N>::default()
397 .connect_provider(RootProvider::new(client));
398
399 return Ok(provider);
400 }
401
402 let transport = RuntimeTransportBuilder::new(url)
403 .with_timeout(timeout)
404 .with_headers(headers)
405 .with_jwt(jwt)
406 .accept_invalid_certs(accept_invalid_certs)
407 .no_proxy(no_proxy)
408 .build();
409 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
410
411 if !is_local {
412 client.set_poll_interval(
413 chain
414 .average_blocktime_hint()
415 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
418 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
419 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
420 );
421 }
422
423 let provider =
424 AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
425
426 Ok(provider)
427 }
428}
429
430impl<N: Network> ProviderBuilder<N> {
431 pub fn build_fallback(self, urls: Vec<String>) -> Result<RetryProvider<N>> {
438 let Self {
439 chain,
440 max_retry,
441 initial_backoff,
442 timeout,
443 compute_units_per_second,
444 jwt,
445 headers,
446 accept_invalid_certs,
447 no_proxy,
448 curl_mode,
449 ..
450 } = self;
451
452 eyre::ensure!(!urls.is_empty(), "at least one fork URL is required");
453 eyre::ensure!(!curl_mode, "curl mode is not supported with multiple fork URLs");
454
455 let mut parsed_urls = Vec::with_capacity(urls.len());
458 let transports: Vec<_> = urls
459 .iter()
460 .map(|url_str| {
461 let builder = Self::new(url_str);
462 let url = builder.url?;
463 let transport_no_proxy = no_proxy || builder.is_local;
464 parsed_urls.push(url.clone());
465 Ok(RuntimeTransportBuilder::new(url)
466 .with_timeout(timeout)
467 .with_headers(headers.clone())
468 .with_jwt(jwt.clone())
469 .accept_invalid_certs(accept_invalid_certs)
470 .no_proxy(transport_no_proxy)
471 .build())
472 })
473 .collect::<Result<Vec<_>>>()?;
474
475 let round_robin = RoundRobinService::new(transports);
476
477 let retry_layer =
478 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
479 let is_local = parsed_urls.iter().all(|url| guess_local_url(url.as_str()));
481 let client = ClientBuilder::default().layer(retry_layer).transport(round_robin, is_local);
482
483 if !is_local {
484 client.set_poll_interval(
485 chain
486 .average_blocktime_hint()
487 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
488 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
489 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
490 );
491 }
492
493 let provider =
494 AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
495
496 Ok(provider)
497 }
498
499 pub fn build_with_wallet<W: NetworkWallet<N> + Clone>(
501 self,
502 wallet: W,
503 ) -> Result<RetryProviderWithSigner<N, W>>
504 where
505 N: RecommendedFillers,
506 {
507 let Self {
508 url,
509 chain,
510 max_retry,
511 initial_backoff,
512 timeout,
513 compute_units_per_second,
514 jwt,
515 headers,
516 is_local,
517 accept_invalid_certs,
518 no_proxy,
519 curl_mode,
520 ..
521 } = self;
522 let url = url?;
523 let no_proxy = no_proxy || is_local;
524
525 let retry_layer =
526 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
527
528 if curl_mode {
530 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
531 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
532
533 let provider = AlloyProviderBuilder::<_, _, N>::default()
534 .with_recommended_fillers()
535 .wallet(wallet)
536 .connect_provider(RootProvider::new(client));
537
538 return Ok(provider);
539 }
540
541 let transport = RuntimeTransportBuilder::new(url)
542 .with_timeout(timeout)
543 .with_headers(headers)
544 .with_jwt(jwt)
545 .accept_invalid_certs(accept_invalid_certs)
546 .no_proxy(no_proxy)
547 .build();
548
549 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
550
551 if !is_local {
552 client.set_poll_interval(
553 chain
554 .average_blocktime_hint()
555 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
558 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
559 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
560 );
561 }
562
563 let provider = AlloyProviderBuilder::<_, _, N>::default()
564 .with_recommended_fillers()
565 .wallet(wallet)
566 .connect_provider(RootProvider::new(client));
567
568 Ok(provider)
569 }
570}
571
572#[cfg(not(windows))]
573fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
574 if path.is_absolute() {
575 Ok(path.to_path_buf())
576 } else {
577 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
578 }
579}
580
581#[cfg(windows)]
582fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
583 if let Some(s) = path.to_str()
584 && s.starts_with(r"\\.\pipe\")
585 {
586 return Ok(path.to_path_buf());
587 }
588 if path.is_absolute() {
589 Ok(path.to_path_buf())
590 } else {
591 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn can_auto_correct_missing_prefix() {
601 let builder = ProviderBuilder::<AnyNetwork>::new("localhost:8545");
602 assert!(builder.url.is_ok());
603
604 let url = builder.url.unwrap();
605 assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
606 }
607
608 #[test]
609 fn from_config_applies_rpc_transport_options() {
610 let config = Config {
611 eth_rpc_url: Some("http://example.com".to_string()),
612 eth_rpc_accept_invalid_certs: true,
613 eth_rpc_no_proxy: true,
614 eth_rpc_timeout: Some(7),
615 ..Default::default()
616 };
617
618 let builder = ProviderBuilder::<AnyNetwork>::from_config(&config).unwrap();
619
620 assert!(builder.accept_invalid_certs);
621 assert!(builder.no_proxy);
622 assert_eq!(builder.timeout, Duration::from_secs(7));
623 }
624}