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, RpcClient};
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#[derive(Clone)]
64pub struct RoundRobinService<S> {
65 transports: Arc<Vec<S>>,
66 next: Arc<AtomicUsize>,
67}
68
69impl<S> RoundRobinService<S> {
70 pub fn new(transports: Vec<S>) -> Self {
76 assert!(!transports.is_empty(), "RoundRobinService requires at least one transport");
77 Self { transports: Arc::new(transports), next: Arc::new(AtomicUsize::new(0)) }
78 }
79}
80
81impl<S> Service<RequestPacket> for RoundRobinService<S>
82where
83 S: Service<
84 RequestPacket,
85 Response = ResponsePacket,
86 Error = TransportError,
87 Future = TransportFut<'static>,
88 > + Clone
89 + Send
90 + Sync
91 + 'static,
92{
93 type Response = ResponsePacket;
94 type Error = TransportError;
95 type Future = TransportFut<'static>;
96
97 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
98 Poll::Ready(Ok(()))
99 }
100
101 fn call(&mut self, req: RequestPacket) -> Self::Future {
102 let transports = self.transports.clone();
103 let idx = self.next.fetch_add(1, Ordering::Relaxed) % transports.len();
104 let mut transport = transports[idx].clone();
105 transport.call(req)
106 }
107}
108
109#[derive(Debug)]
113pub struct ProviderBuilder<N: Network = AnyNetwork> {
114 url: Result<Url>,
116 chain: NamedChain,
117 max_retry: u32,
118 initial_backoff: u64,
119 timeout: Duration,
120 compute_units_per_second: u64,
122 jwt: Option<String>,
124 headers: Vec<String>,
125 is_local: bool,
126 accept_invalid_certs: bool,
128 no_proxy: bool,
130 curl_mode: bool,
132 _network: PhantomData<N>,
134}
135
136impl<N: Network> ProviderBuilder<N> {
137 pub fn new(url_str: &str) -> Self {
139 let mut url_str = url_str;
141
142 let storage;
145 if url_str.starts_with("localhost:") {
146 storage = format!("http://{url_str}");
147 url_str = storage.as_str();
148 }
149
150 let url = Url::parse(url_str)
151 .or_else(|err| match err {
152 ParseError::RelativeUrlWithoutBase => {
153 if SocketAddr::from_str(url_str).is_ok() {
154 Url::parse(&format!("http://{url_str}"))
155 } else {
156 let path = Path::new(url_str);
157
158 if let Ok(path) = resolve_path(path) {
159 Url::parse(&format!("file://{}", path.display()))
160 } else {
161 Err(err)
162 }
163 }
164 }
165 _ => Err(err),
166 })
167 .wrap_err_with(|| format!("invalid provider URL: {:?}", redact_url(url_str)));
168
169 let is_local = url.as_ref().is_ok_and(|url| guess_local_url(url.as_str()));
171
172 Self {
173 url,
174 chain: NamedChain::Mainnet,
175 max_retry: 8,
176 initial_backoff: 800,
177 timeout: REQUEST_TIMEOUT,
178 compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
180 jwt: None,
181 headers: vec![],
182 is_local,
183 accept_invalid_certs: false,
184 no_proxy: false,
185 curl_mode: false,
186 _network: PhantomData,
187 }
188 }
189
190 pub fn from_config(config: &Config) -> Result<Self> {
194 let url = config.get_rpc_url_or_localhost_http()?;
195 let mut builder = Self::from_config_with_url(config, url.as_ref())?;
196
197 if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
198 builder = builder.chain(chain);
199 }
200
201 Ok(builder)
202 }
203
204 pub fn from_config_with_url(config: &Config, url: &str) -> Result<Self> {
206 let mut builder = Self::new(url)
207 .accept_invalid_certs(config.eth_rpc_accept_invalid_certs)
208 .no_proxy(config.eth_rpc_no_proxy)
209 .curl_mode(config.eth_rpc_curl);
210
211 if let Some(jwt) = config.get_rpc_jwt_secret()? {
212 builder = builder.jwt(jwt.as_ref());
213 }
214
215 if let Some(rpc_timeout) = config.eth_rpc_timeout {
216 builder = builder.timeout(Duration::from_secs(rpc_timeout));
217 }
218
219 if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
220 builder = builder.headers(rpc_headers);
221 }
222
223 Ok(builder)
224 }
225
226 pub const fn timeout(mut self, timeout: Duration) -> Self {
233 self.timeout = timeout;
234 self
235 }
236
237 pub const fn chain(mut self, chain: NamedChain) -> Self {
239 self.chain = chain;
240 self
241 }
242
243 pub const fn max_retry(mut self, max_retry: u32) -> Self {
245 self.max_retry = max_retry;
246 self
247 }
248
249 pub fn maybe_max_retry(mut self, max_retry: Option<u32>) -> Self {
251 self.max_retry = max_retry.unwrap_or(self.max_retry);
252 self
253 }
254
255 pub fn maybe_initial_backoff(mut self, initial_backoff: Option<u64>) -> Self {
258 self.initial_backoff = initial_backoff.unwrap_or(self.initial_backoff);
259 self
260 }
261
262 pub const fn initial_backoff(mut self, initial_backoff: u64) -> Self {
264 self.initial_backoff = initial_backoff;
265 self
266 }
267
268 pub const fn compute_units_per_second(mut self, compute_units_per_second: u64) -> Self {
272 self.compute_units_per_second = compute_units_per_second;
273 self
274 }
275
276 pub const fn compute_units_per_second_opt(
280 mut self,
281 compute_units_per_second: Option<u64>,
282 ) -> Self {
283 if let Some(cups) = compute_units_per_second {
284 self.compute_units_per_second = cups;
285 }
286 self
287 }
288
289 pub const fn local(mut self, is_local: bool) -> Self {
293 self.is_local = is_local;
294 self
295 }
296
297 pub const fn aggressive(self) -> Self {
301 self.max_retry(100).initial_backoff(100).local(true)
302 }
303
304 pub fn jwt(mut self, jwt: impl Into<String>) -> Self {
306 self.jwt = Some(jwt.into());
307 self
308 }
309
310 pub fn headers(mut self, headers: Vec<String>) -> Self {
312 self.headers = headers;
313
314 self
315 }
316
317 pub fn maybe_headers(mut self, headers: Option<Vec<String>>) -> Self {
319 self.headers = headers.unwrap_or(self.headers);
320 self
321 }
322
323 pub const fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
325 self.accept_invalid_certs = accept_invalid_certs;
326 self
327 }
328
329 pub const fn no_proxy(mut self, no_proxy: bool) -> Self {
334 self.no_proxy = no_proxy;
335 self
336 }
337
338 pub const fn curl_mode(mut self, curl_mode: bool) -> Self {
343 self.curl_mode = curl_mode;
344 self
345 }
346
347 pub fn build(self) -> Result<RetryProvider<N>> {
349 Ok(RootProvider::new(self.build_client()?))
350 }
351
352 pub fn build_client(self) -> Result<RpcClient> {
354 let Self {
355 url,
356 chain,
357 max_retry,
358 initial_backoff,
359 timeout,
360 compute_units_per_second,
361 jwt,
362 headers,
363 is_local,
364 accept_invalid_certs,
365 no_proxy,
366 curl_mode,
367 ..
368 } = self;
369 let url = url?;
370 let no_proxy = no_proxy || is_local;
371
372 let retry_layer =
373 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
374
375 if curl_mode {
377 let transport = CurlTransport::new(url).with_headers(headers).with_jwt(jwt);
378 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
379
380 return Ok(client);
381 }
382
383 let transport = RuntimeTransportBuilder::new(url)
384 .with_timeout(timeout)
385 .with_headers(headers)
386 .with_jwt(jwt)
387 .accept_invalid_certs(accept_invalid_certs)
388 .no_proxy(no_proxy)
389 .build();
390 let client = ClientBuilder::default().layer(retry_layer).transport(transport, is_local);
391
392 if !is_local {
393 client.set_poll_interval(
394 chain
395 .average_blocktime_hint()
396 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
399 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
400 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
401 );
402 }
403
404 Ok(client)
405 }
406}
407
408impl<N: Network> ProviderBuilder<N> {
409 pub fn build_fallback(self, urls: Vec<String>) -> Result<RetryProvider<N>> {
416 let Self {
417 chain,
418 max_retry,
419 initial_backoff,
420 timeout,
421 compute_units_per_second,
422 jwt,
423 headers,
424 accept_invalid_certs,
425 no_proxy,
426 curl_mode,
427 ..
428 } = self;
429
430 eyre::ensure!(!urls.is_empty(), "at least one fork URL is required");
431 eyre::ensure!(!curl_mode, "curl mode is not supported with multiple fork URLs");
432
433 let mut parsed_urls = Vec::with_capacity(urls.len());
436 let transports: Vec<_> = urls
437 .iter()
438 .map(|url_str| {
439 let builder = Self::new(url_str);
440 let url = builder.url?;
441 let transport_no_proxy = no_proxy || builder.is_local;
442 parsed_urls.push(url.clone());
443 Ok(RuntimeTransportBuilder::new(url)
444 .with_timeout(timeout)
445 .with_headers(headers.clone())
446 .with_jwt(jwt.clone())
447 .accept_invalid_certs(accept_invalid_certs)
448 .no_proxy(transport_no_proxy)
449 .build())
450 })
451 .collect::<Result<Vec<_>>>()?;
452
453 let round_robin = RoundRobinService::new(transports);
454
455 let retry_layer =
456 RetryBackoffLayer::new(max_retry, initial_backoff, compute_units_per_second);
457 let is_local = parsed_urls.iter().all(|url| guess_local_url(url.as_str()));
459 let client = ClientBuilder::default().layer(retry_layer).transport(round_robin, is_local);
460
461 if !is_local {
462 client.set_poll_interval(
463 chain
464 .average_blocktime_hint()
465 .map(|hint| hint.min(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME))
466 .unwrap_or(DEFAULT_UNKNOWN_CHAIN_BLOCK_TIME)
467 .mul_f32(POLL_INTERVAL_BLOCK_TIME_SCALE_FACTOR),
468 );
469 }
470
471 let provider =
472 AlloyProviderBuilder::<_, _, N>::default().connect_provider(RootProvider::new(client));
473
474 Ok(provider)
475 }
476
477 pub fn build_with_wallet<W: NetworkWallet<N> + Clone>(
479 self,
480 wallet: W,
481 ) -> Result<RetryProviderWithSigner<N, W>>
482 where
483 N: RecommendedFillers,
484 {
485 let provider = self.build()?;
486 Ok(AlloyProviderBuilder::<_, _, N>::default()
487 .with_recommended_fillers()
488 .wallet(wallet)
489 .connect_provider(provider))
490 }
491}
492
493pub fn is_rpc_method_not_found(error: &TransportError) -> bool {
499 rpc_error_code(error) == Some(-32601)
500}
501
502pub fn redact_url(raw: &str) -> String {
504 let Ok(mut redacted) = Url::parse(raw) else {
505 return "<redacted>".to_owned();
506 };
507 let _ = redacted.set_username("");
508 let _ = redacted.set_password(None);
509 redacted.set_path("");
510 redacted.set_query(None);
511 redacted.set_fragment(None);
512 redacted.to_string()
513}
514
515fn rpc_error_code(error: &TransportError) -> Option<i64> {
516 if let Some(response) = error.as_error_resp() {
517 return Some(response.code);
518 }
519 let TransportError::Transport(error) = error else { return None };
520 error.as_http_error().and_then(|error| rpc_error_code_from_body(&error.body))
521}
522
523fn rpc_error_code_from_body(body: &str) -> Option<i64> {
524 let value =
527 serde_json::Deserializer::from_str(body).into_iter::<serde_json::Value>().next()?.ok()?;
528 let error = value.get("error").unwrap_or(&value);
529 error.get("code")?.as_i64()
530}
531
532#[inline]
549#[track_caller]
550pub fn get_http_provider(builder: impl AsRef<str>) -> RetryProvider {
551 try_get_http_provider(builder).unwrap()
552}
553
554#[inline]
557pub fn try_get_http_provider(builder: impl AsRef<str>) -> Result<RetryProvider> {
558 ProviderBuilder::new(builder.as_ref()).build()
559}
560
561#[cfg(not(windows))]
562fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
563 if path.is_absolute() {
564 Ok(path.to_path_buf())
565 } else {
566 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
567 }
568}
569
570#[cfg(windows)]
571fn resolve_path(path: &Path) -> Result<PathBuf, ()> {
572 if let Some(s) = path.to_str()
573 && s.starts_with(r"\\.\pipe\")
574 {
575 return Ok(path.to_path_buf());
576 }
577 if path.is_absolute() {
578 Ok(path.to_path_buf())
579 } else {
580 std::env::current_dir().map(|d| d.join(path)).map_err(drop)
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use alloy_json_rpc::ErrorPayload;
587
588 use super::*;
589
590 #[test]
591 fn redacts_url_credentials_and_resource() {
592 let url = "https://user:password@example.com:8545/private-key?token=secret#fragment";
593
594 assert_eq!(redact_url(url), "https://example.com:8545/");
595 assert_eq!(redact_url("not a URL with secret"), "<redacted>");
596 }
597
598 #[test]
599 fn invalid_provider_url_error_is_redacted() {
600 let builder = ProviderBuilder::<AnyNetwork>::new(
601 "https://example.com:bad/private-api-key?token=secret",
602 );
603
604 let error = builder.url.unwrap_err().to_string();
605 assert!(error.contains("<redacted>"));
606 assert!(!error.contains("private-api-key"));
607 assert!(!error.contains("secret"));
608 }
609
610 #[test]
611 fn method_not_found_classification_is_exact() {
612 let method_not_found = TransportError::ErrorResp(ErrorPayload::method_not_found());
613 let internal_error = TransportError::ErrorResp(ErrorPayload::internal_error());
614 let http_method_not_found = alloy_transport::TransportErrorKind::http_error(
615 403,
616 r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"method not allowed"}}"#
617 .to_string(),
618 );
619 let http_internal_error = alloy_transport::TransportErrorKind::http_error(
620 500,
621 r#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"internal error"}}"#.to_string(),
622 );
623 let http_method_not_found_with_diagnostics =
624 alloy_transport::TransportErrorKind::http_error(
625 403,
626 concat!(
627 r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"method not allowed"}}"#,
628 "\n\nHTTP diagnostics:\nstatus: 403 Forbidden"
629 )
630 .to_string(),
631 );
632 let transport_error = alloy_transport::TransportErrorKind::backend_gone();
633
634 assert!(is_rpc_method_not_found(&method_not_found));
635 assert!(is_rpc_method_not_found(&http_method_not_found));
636 assert!(is_rpc_method_not_found(&http_method_not_found_with_diagnostics));
637 assert!(!is_rpc_method_not_found(&internal_error));
638 assert!(!is_rpc_method_not_found(&http_internal_error));
639 assert!(!is_rpc_method_not_found(&transport_error));
640 }
641
642 #[test]
643 fn can_auto_correct_missing_prefix() {
644 let builder = ProviderBuilder::<AnyNetwork>::new("localhost:8545");
645 assert!(builder.url.is_ok());
646
647 let url = builder.url.unwrap();
648 assert_eq!(url, Url::parse("http://localhost:8545").unwrap());
649 }
650
651 #[test]
652 fn from_config_applies_rpc_transport_options() {
653 let config = Config {
654 eth_rpc_url: Some("http://example.com".to_string()),
655 chain: Some(NamedChain::Polygon.into()),
656 eth_rpc_accept_invalid_certs: true,
657 eth_rpc_no_proxy: true,
658 eth_rpc_timeout: Some(7),
659 ..Default::default()
660 };
661
662 let builder = ProviderBuilder::<AnyNetwork>::from_config(&config).unwrap();
663
664 assert!(builder.accept_invalid_certs);
665 assert!(builder.no_proxy);
666 assert_eq!(builder.timeout, Duration::from_secs(7));
667 assert_eq!(builder.chain, NamedChain::Polygon);
668 }
669
670 #[test]
671 fn from_config_with_url_overrides_rpc_url() {
672 let config = Config {
673 eth_rpc_url: Some("http://configured.example".to_string()),
674 chain: Some(NamedChain::Polygon.into()),
675 eth_rpc_timeout: Some(7),
676 ..Default::default()
677 };
678
679 let builder =
680 ProviderBuilder::<AnyNetwork>::from_config_with_url(&config, "http://sequence.example")
681 .unwrap();
682
683 assert_eq!(builder.url.unwrap().as_str(), "http://sequence.example/");
684 assert_eq!(builder.timeout, Duration::from_secs(7));
685 assert_eq!(builder.chain, NamedChain::Mainnet);
686 }
687}