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