1use core::fmt;
19use std::{
20 future::Future,
21 num::NonZeroUsize,
22 ops::Deref,
23 task::{Context, Poll},
24 time::Duration,
25};
26
27#[cfg(feature = "web3-asserter")]
28use alloy::providers::mock::Asserter;
29use alloy::{
30 network::{Ethereum, EthereumWallet},
31 primitives::ChainId,
32 providers::{
33 DynProvider, PendingTransactionBuilder, PendingTransactionError, Provider, ProviderBuilder,
34 fillers::{BlobGasFiller, ChainIdFiller, NonceManager, SimpleNonceManager},
35 },
36 rpc::{
37 client::RpcClient,
38 json_rpc::{RequestPacket, ResponsePacket},
39 types::TransactionReceipt,
40 },
41 transports::{
42 RpcError, Transport, TransportError, TransportErrorKind, TransportFut,
43 http::{
44 Http,
45 reqwest::{self, IntoUrl, Url},
46 },
47 layers::{FallbackLayer, OrRetryPolicyFn, RateLimitRetryPolicy, RetryPolicy},
48 },
49};
50use backon::{BackoffBuilder, ExponentialBuilder, Retryable as _};
51use serde::Deserialize;
52use tower::{Layer, Service};
53
54use crate::Environment;
55
56pub use backon;
57
58pub mod erc165;
59pub mod event_stream;
60pub mod signers;
61
62#[derive(Clone)]
67pub struct HttpRpcProvider(DynProvider);
68
69#[derive(Clone, Deserialize)]
73#[serde(transparent)]
74pub struct UrlRedacted(Url);
75
76impl fmt::Debug for UrlRedacted {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.write_str("[REDACTED]")
79 }
80}
81
82#[derive(Debug, Clone, Deserialize)]
87#[non_exhaustive]
88pub struct HttpRpcProviderConfig {
89 pub http_urls: Vec<UrlRedacted>,
93 #[serde(default)]
98 pub chain_id: Option<ChainId>,
99 #[serde(default)]
104 #[serde(with = "humantime_serde")]
105 pub confirmations_poll_interval: Option<Duration>,
106 #[serde(default)]
108 pub retry_policy_config: RetryPolicyConfig,
109}
110
111#[derive(Debug, Clone, Deserialize)]
116#[non_exhaustive]
117pub struct RetryPolicyConfig {
118 #[serde(default = "RetryPolicyConfig::default_min_delay")]
122 #[serde(with = "humantime_serde")]
123 pub min_delay: Duration,
124
125 #[serde(default = "RetryPolicyConfig::default_max_delay")]
129 #[serde(with = "humantime_serde")]
130 pub max_delay: Duration,
131
132 #[serde(default = "RetryPolicyConfig::default_max_times")]
136 pub max_times: usize,
137}
138
139impl HttpRpcProviderConfig {
140 pub fn with_default_values<I, U>(http_urls: I) -> reqwest::Result<Self>
146 where
147 I: IntoIterator<Item = U>,
148 U: IntoUrl,
149 {
150 let http_urls = http_urls
151 .into_iter()
152 .map(|x| x.into_url().map(UrlRedacted))
153 .collect::<reqwest::Result<Vec<_>>>()?;
154 Ok(Self {
155 http_urls,
156 confirmations_poll_interval: None,
157 chain_id: None,
158 retry_policy_config: RetryPolicyConfig::default(),
159 })
160 }
161}
162
163impl RetryPolicyConfig {
164 fn default_min_delay() -> Duration {
166 Duration::from_secs(1)
167 }
168
169 fn default_max_delay() -> Duration {
171 Duration::from_secs(8)
172 }
173
174 fn default_max_times() -> usize {
176 5
177 }
178
179 fn with_default_values() -> Self {
181 Self {
182 min_delay: Self::default_min_delay(),
183 max_delay: Self::default_max_delay(),
184 max_times: Self::default_max_times(),
185 }
186 }
187}
188
189impl Default for RetryPolicyConfig {
190 fn default() -> Self {
191 Self::with_default_values()
192 }
193}
194
195fn build_transport_stack<S>(
196 transports: Vec<S>,
197 retry_policy_config: &RetryPolicyConfig,
198) -> impl Transport + Clone
199where
200 S: Service<RequestPacket, Response = ResponsePacket, Error = TransportError>
201 + Clone
202 + Send
203 + Sync
204 + 'static,
205 S::Future: Send,
206{
207 let retry_layer = RetryLayer::new(http_retry_policy(), retry_policy_config);
208 let retrying_transports = transports
209 .into_iter()
210 .map(|transport| retry_layer.layer(transport))
211 .collect::<Vec<_>>();
212 let transport_count =
213 NonZeroUsize::new(retrying_transports.len()).expect("transport stack must not be empty");
214
215 FallbackLayer::default()
218 .with_active_transport_count(transport_count)
219 .layer(retrying_transports)
220}
221
222fn http_retry_policy() -> OrRetryPolicyFn {
223 RateLimitRetryPolicy::default().or(|error: &TransportError| match error {
232 RpcError::Transport(TransportErrorKind::HttpError(e)) => {
233 matches!(e.status, 403 | 408 | 502 | 504)
234 }
235 RpcError::Transport(kind) => kind
236 .as_custom()
237 .and_then(|error| error.downcast_ref::<reqwest::Error>())
238 .is_some_and(reqwest::Error::is_timeout),
239 _ => false,
240 })
241}
242
243pub struct HttpRpcProviderBuilder {
248 http_urls: Vec<UrlRedacted>,
249 retry_policy_config: RetryPolicyConfig,
250 chain_id: Option<ChainId>,
251 confirmations_poll_interval: Option<Duration>,
252 is_local: bool,
253 wallet: Option<EthereumWallet>,
254 reqwest_client: Option<reqwest::Client>,
255}
256
257impl From<HttpRpcProviderConfig> for HttpRpcProviderBuilder {
258 fn from(value: HttpRpcProviderConfig) -> Self {
259 Self::from(&value)
260 }
261}
262
263impl From<&HttpRpcProviderConfig> for HttpRpcProviderBuilder {
264 fn from(value: &HttpRpcProviderConfig) -> Self {
265 Self::with_config(value)
266 }
267}
268
269impl HttpRpcProviderBuilder {
270 #[must_use]
277 pub fn with_config(config: &HttpRpcProviderConfig) -> Self {
278 assert!(!config.http_urls.is_empty(), "http URLs must not be empty");
279 Self {
280 http_urls: config.http_urls.clone(),
281 retry_policy_config: config.retry_policy_config.clone(),
282 chain_id: config.chain_id,
283 is_local: false,
284 wallet: None,
285 confirmations_poll_interval: config.confirmations_poll_interval,
286 reqwest_client: None,
287 }
288 }
289
290 pub fn with_default_values<I, U>(http_urls: I) -> reqwest::Result<Self>
305 where
306 I: IntoIterator<Item = U>,
307 U: IntoUrl,
308 {
309 Ok(Self::with_config(
310 &HttpRpcProviderConfig::with_default_values(http_urls)?,
311 ))
312 }
313
314 #[must_use]
318 pub fn reqwest_client(mut self, reqwest_client: reqwest::Client) -> Self {
319 self.reqwest_client = Some(reqwest_client);
320 self
321 }
322
323 #[must_use]
325 pub fn environment(mut self, environment: Environment) -> Self {
326 self.is_local = environment.is_dev();
327 self
328 }
329
330 #[must_use]
332 pub fn confirmations_poll_interval(mut self, confirmations_poll_interval: Duration) -> Self {
333 self.confirmations_poll_interval = Some(confirmations_poll_interval);
334 self
335 }
336
337 #[must_use]
339 pub fn chain_id(mut self, chain_id: ChainId) -> Self {
340 self.chain_id = Some(chain_id);
341 self
342 }
343
344 #[must_use]
346 pub fn retry_policy(mut self, retry_policy_config: RetryPolicyConfig) -> Self {
347 self.retry_policy_config = retry_policy_config;
348 self
349 }
350
351 #[must_use]
353 pub fn wallet(mut self, wallet: EthereumWallet) -> Self {
354 self.wallet = Some(wallet);
355 self
356 }
357
358 pub fn build(self) -> Result<HttpRpcProvider, TransportError> {
368 self.build_with_nonce_manager(SimpleNonceManager::default())
369 }
370
371 pub fn build_with_nonce_manager<N: NonceManager + 'static>(
381 self,
382 nonce_manager: N,
383 ) -> Result<HttpRpcProvider, TransportError> {
384 let HttpRpcProviderBuilder {
385 http_urls,
386 retry_policy_config,
387 chain_id,
388 is_local,
389 wallet,
390 confirmations_poll_interval,
391 reqwest_client,
392 } = self;
393
394 let reqwest = if let Some(reqwest_client) = reqwest_client {
395 reqwest_client
396 } else {
397 reqwest::ClientBuilder::new()
398 .build()
399 .map_err(TransportErrorKind::custom)?
400 };
401
402 let transports = http_urls
403 .into_iter()
404 .map(|url| Http::with_client(reqwest.clone(), url.0))
405 .collect::<Vec<_>>();
406 let transport = build_transport_stack(transports, &retry_policy_config);
407
408 let client = RpcClient::builder().transport(transport, is_local);
409 let client = if let Some(confirmations_poll_interval) = confirmations_poll_interval {
410 client.with_poll_interval(confirmations_poll_interval)
411 } else {
412 client
413 };
414
415 let http_provider_builder = ProviderBuilder::new()
416 .filler(ChainIdFiller::new(chain_id))
417 .filler(BlobGasFiller::default())
418 .with_nonce_management(nonce_manager)
419 .with_gas_estimation();
420
421 let provider = if let Some(wallet) = wallet {
422 http_provider_builder
423 .wallet(wallet)
424 .connect_client(client)
425 .erased()
426 } else {
427 http_provider_builder.connect_client(client).erased()
428 };
429
430 Ok(HttpRpcProvider(provider))
431 }
432}
433
434impl HttpRpcProvider {
435 #[must_use]
437 #[inline]
438 pub fn inner(&self) -> DynProvider {
439 self.0.clone()
440 }
441
442 #[cfg(feature = "web3-asserter")]
450 #[must_use]
451 pub fn with_mock_asserter(asserter: Asserter) -> Self {
452 Self(
453 ProviderBuilder::new()
454 .connect_mocked_client(asserter)
455 .erased(),
456 )
457 }
458}
459
460#[cfg(feature = "web3-asserter")]
461impl From<Asserter> for HttpRpcProvider {
462 fn from(value: Asserter) -> Self {
463 Self::with_mock_asserter(value)
464 }
465}
466
467impl AsRef<DynProvider> for HttpRpcProvider {
468 fn as_ref(&self) -> &DynProvider {
469 self
470 }
471}
472
473impl Deref for HttpRpcProvider {
474 type Target = DynProvider;
475
476 fn deref(&self) -> &Self::Target {
477 &self.0
478 }
479}
480
481#[derive(Debug, Clone)]
482struct RetryLayer {
483 policy: OrRetryPolicyFn,
484 backoff: ExponentialBuilder,
485}
486
487impl RetryLayer {
488 pub fn new(policy: OrRetryPolicyFn, config: &RetryPolicyConfig) -> Self {
498 let backoff = ExponentialBuilder::default()
499 .with_min_delay(config.min_delay)
500 .with_max_delay(config.max_delay)
501 .with_max_times(config.max_times)
502 .with_jitter();
503 Self { policy, backoff }
504 }
505}
506
507impl<S> Layer<S> for RetryLayer {
508 type Service = RetryService<S>;
509
510 fn layer(&self, inner: S) -> Self::Service {
511 RetryService {
512 inner,
513 policy: self.policy.clone(),
514 backoff: self.backoff,
515 }
516 }
517}
518
519#[derive(Debug, Clone)]
521struct RetryService<S> {
522 inner: S,
523 policy: OrRetryPolicyFn,
524 backoff: ExponentialBuilder,
525}
526
527impl<S> Service<RequestPacket> for RetryService<S>
528where
529 S: Service<RequestPacket, Response = ResponsePacket, Error = TransportError>
530 + Clone
531 + Send
532 + Sync
533 + 'static,
534 S::Future: Send,
535{
536 type Response = ResponsePacket;
537 type Error = TransportError;
538 type Future = TransportFut<'static>;
539
540 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
541 self.inner.poll_ready(cx)
542 }
543
544 fn call(&mut self, request: RequestPacket) -> Self::Future {
545 let service = self.clone();
546 let backoff = self.backoff;
547 let policy = self.policy.clone();
548
549 Box::pin(async move {
550 (|| service.clone().call_and_parse_error(request.clone()))
551 .retry(backoff)
552 .sleep(tokio::time::sleep)
553 .when(|e| policy.should_retry(e))
554 .notify(|err, duration| {
555 tracing::warn!(
556 ?err,
557 "Retrying RPC request after: {duration:?}. Reason: {err}"
558 );
559 })
560 .adjust(|e, dur| dur.and_then(|d| policy.backoff_hint(e).or(Some(d))))
565 .await
566 })
567 }
568}
569
570impl<S> RetryService<S>
571where
572 S: Service<RequestPacket, Response = ResponsePacket, Error = TransportError>
573 + Clone
574 + Send
575 + Sync
576 + 'static,
577 S::Future: Send,
578{
579 async fn call_and_parse_error(
580 mut self,
581 request: RequestPacket,
582 ) -> Result<ResponsePacket, RpcError<TransportErrorKind>> {
583 let resp = self.inner.call(request).await?;
584 if let Some(e) = resp.as_error() {
585 Err(TransportError::ErrorResp(e.to_owned()))
586 } else {
587 Ok(resp)
588 }
589 }
590}
591
592pub trait GetReceiptExt: Sized {
595 fn get_receipt_with_retry(
621 self,
622 builder: impl BackoffBuilder,
623 ) -> impl Future<Output = Result<TransactionReceipt, PendingTransactionError>> + Send;
624
625 fn get_receipt_with_default_retry(
649 self,
650 ) -> impl Future<Output = Result<TransactionReceipt, PendingTransactionError>> + Send {
651 self.get_receipt_with_retry(
652 backon::ConstantBuilder::new()
653 .with_delay(Duration::from_secs(2))
654 .with_max_times(10),
655 )
656 }
657}
658
659impl GetReceiptExt for PendingTransactionBuilder<Ethereum> {
660 async fn get_receipt_with_retry(
661 self,
662 builder: impl BackoffBuilder,
663 ) -> Result<TransactionReceipt, PendingTransactionError> {
664 let tx_hash = *self.tx_hash();
665 let provider = self.provider().clone();
666 let config = self.inner().clone();
667
668 match self.get_receipt().await {
669 Ok(receipt) => return Ok(receipt),
670 Err(err) => {
671 tracing::warn!("no receipt for transaction {tx_hash} yet ({err}), re-polling");
672 }
673 }
674
675 let poll = || async {
676 let pending = PendingTransactionBuilder::from_config(provider.clone(), config.clone());
677 pending.get_receipt().await
678 };
679
680 poll.retry(builder)
681 .sleep(tokio::time::sleep)
682 .notify(|err, dur| {
683 tracing::warn!(
684 "failed to fetch receipt for transaction {tx_hash} ({err}), retrying in {dur:?}"
685 );
686 })
687 .await
688 }
689}
690
691#[cfg(test)]
692pub(crate) mod tests;