1use std::{borrow::Cow, fmt::Debug, future::Future, time::Duration};
4
5use backon::{ExponentialBuilder, Retryable};
6use serde_json::value::RawValue;
7use tokio::time::timeout;
8
9use super::errors::{FailoverError, is_retryable_error};
10use alloy::{
11 consensus::{BlockHeader, TrieAccount},
12 eips::{BlockId, BlockNumberOrTag, eip1559::Eip1559Estimation},
13 network::{BlockResponse, Ethereum, Network},
14 primitives::{
15 Address, B256, BlockHash, BlockNumber, Bytes, StorageKey, StorageValue, TxHash, U256,
16 },
17 providers::{
18 PendingTransactionBuilder, Provider, RootProvider,
19 utils::{
20 EIP1559_FEE_ESTIMATION_PAST_BLOCKS, EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE,
21 Eip1559Estimator,
22 },
23 },
24 rpc::{
25 client::PollerBuilder,
26 json_rpc::{RpcRecv, RpcSend},
27 types::{
28 AccessListResult, AccountInfo, Bundle, EIP1186AccountProofResponse, EthCallResponse,
29 FeeHistory, FillTransaction, Filter, Log, SyncStatus,
30 erc4337::TransactionConditional,
31 simulate::{SimulatePayload, SimulatedBlock},
32 },
33 },
34 transports::{RpcError, TransportErrorKind},
35};
36
37use crate::{
38 Error, block_not_found_doc,
39 robust_provider::{RobustSubscription, subscription::SubscriptionBackend},
40};
41
42#[derive(Clone, Debug)]
47pub struct RobustProvider<N: Network = Ethereum> {
48 pub(crate) primary_provider: RootProvider<N>,
49 pub(crate) fallback_providers: Vec<RootProvider<N>>,
50 pub(crate) call_timeout: Duration,
51 pub(crate) subscription_timeout: Duration,
52 pub(crate) max_retries: usize,
53 pub(crate) min_delay: Duration,
54 pub(crate) reconnect_interval: Duration,
55 pub(crate) subscription_buffer_capacity: usize,
56 #[cfg(feature = "http-subscription")]
58 pub(crate) poll_interval: Duration,
59 #[cfg(feature = "http-subscription")]
61 pub(crate) allow_http_subscriptions: bool,
62}
63
64impl<N: Network> RobustProvider<N> {
65 #[must_use]
66 pub fn primary(&self) -> &RootProvider<N> {
67 &self.primary_provider
68 }
69
70 #[must_use]
71 pub fn fallback_providers(&self) -> &[RootProvider<N>] {
72 &self.fallback_providers
73 }
74
75 #[must_use]
76 pub fn call_timeout(&self) -> Duration {
77 self.call_timeout
78 }
79
80 #[must_use]
81 pub fn max_retries(&self) -> usize {
82 self.max_retries
83 }
84
85 #[must_use]
86 pub fn min_delay(&self) -> Duration {
87 self.min_delay
88 }
89
90 robust_rpc!(fn get_accounts() -> Vec<Address>);
91 robust_rpc!(fn get_blob_base_fee() -> u128);
92
93 robust_rpc!(
94 doc_args = [(tx, "The transaction request to simulate.")]
95 @clone [tx]
96 fn call(tx: N::TransactionRequest) -> Bytes
97 );
98
99 robust_rpc!(
100 doc_args = [(bundles, "A slice of transaction bundles to execute.")]
101 fn call_many(bundles: &[Bundle]) -> Vec<Vec<EthCallResponse>>
102 );
103
104 robust_rpc!(fn get_chain_id() -> u64);
105
106 robust_rpc!(fn get_net_version() -> u64);
107
108 robust_rpc!(
109 doc_args = [(data, "The data to hash.")]
110 fn get_sha3(data: &[u8]) -> B256
111 );
112
113 robust_rpc!(
114 doc_args = [(request, "The transaction request to create an access list for.")]
115 fn create_access_list(request: &N::TransactionRequest) -> AccessListResult
116 );
117
118 robust_rpc!(
119 doc_args = [(tx, "The transaction request to estimate gas for.")]
120 @clone [tx]
121 fn estimate_gas(tx: N::TransactionRequest) -> u64
122 );
123
124 robust_rpc!(
125 fn estimate_eip1559_fees() -> Eip1559Estimation
126 );
127
128 robust_rpc!(
129 doc_args = [
130 (block_count, "The number of blocks to include in the fee history."),
131 (last_block, "The last block to include in the fee history."),
132 (reward_percentiles, "A list of percentiles to compute reward values for.")
133 ]
134 fn get_fee_history(block_count: u64, last_block: BlockNumberOrTag, reward_percentiles: &[f64]) -> FeeHistory
135 );
136
137 robust_rpc!(fn get_gas_price() -> u128);
138 robust_rpc!(fn get_max_priority_fee_per_gas() -> u128);
139
140 robust_rpc!(
141 doc_args = [(address, "The address for which to get the account info.")]
142 fn get_account_info(address: Address) -> AccountInfo
143 );
144
145 robust_rpc!(
146 doc_args = [(address, "The address to get the account for.")]
147 fn get_account(address: Address) -> TrieAccount
148 );
149
150 robust_rpc!(
151 doc_args = [(address, "The address to get the balance for.")]
152 fn get_balance(address: Address) -> U256
153 );
154
155 robust_rpc!(
156 doc_include_error = [block_not_found_doc!()]
157 doc_args = [(number, "The block number or tag.")]
158 fn get_block_by_number(number: BlockNumberOrTag) -> N::BlockResponse; or BlockNotFound
159 );
160
161 robust_rpc!(
162 doc_include_error = [block_not_found_doc!()]
163 doc_args = [(id, "The block identifier.")]
164 fn get_block(id: BlockId) -> N::BlockResponse; or BlockNotFound
165 );
166
167 robust_rpc!(fn get_block_number() -> BlockNumber);
168
169 robust_rpc!(
170 doc_include_error = [block_not_found_doc!()]
171 doc_args = [(block_id, "The block identifier to fetch the block number for.")]
172 fn get_block_number_by_id(block_id: BlockId) -> BlockNumber; or BlockNotFound
173 );
174
175 robust_rpc!(
176 doc_include_error = [block_not_found_doc!()]
177 doc_args = [(hash, "The block hash.")]
178 fn get_block_by_hash(hash: BlockHash) -> N::BlockResponse; or BlockNotFound
179 );
180
181 robust_rpc!(
182 doc_include_error = [block_not_found_doc!()]
183 doc_args = [(block, "The block identifier (hash, number, or tag).")]
184 fn get_block_receipts(block: BlockId) -> Vec<N::ReceiptResponse>; or BlockNotFound
185 );
186
187 robust_rpc!(
188 doc_include_error = [block_not_found_doc!()]
189 doc_args = [(hash, "The block hash.")]
190 fn get_block_transaction_count_by_hash(hash: BlockHash) -> u64; or BlockNotFound
191 );
192
193 robust_rpc!(
194 doc_include_error = [block_not_found_doc!()]
195 doc_args = [(block_number, "The block number or tag.")]
196 fn get_block_transaction_count_by_number(block_number: BlockNumberOrTag) -> u64; or BlockNotFound
197 );
198
199 robust_rpc!(
200 doc_args = [(filter, "The log filter.")]
201 fn get_logs(filter: &Filter) -> Vec<Log>
202 );
203
204 robust_rpc!(
205 doc_args = [(address, "The address to get the code for.")]
206 fn get_code_at(address: Address) -> Bytes
207 );
208
209 robust_rpc!(
210 doc_args = [(filter_id, "The filter ID to fetch logs for.")]
211 fn get_filter_logs(filter_id: U256) -> Vec<Log>
212 );
213
214 robust_rpc!(
215 doc_args = [(filter_id, "The filter ID to get changes for.")]
216 fn get_filter_changes<R: RpcRecv>(filter_id: U256) -> Vec<R>
217 );
218
219 robust_rpc!(
220 doc_args = [(filter, "The filter to create.")]
221 fn new_filter(filter: &Filter) -> U256
222 );
223
224 robust_rpc!(fn new_block_filter() -> U256);
225
226 robust_rpc!(
227 doc_args = [(full, "Whether to include full transaction objects.")]
228 fn new_pending_transactions_filter(full: bool) -> U256
229 );
230
231 robust_rpc!(
232 doc_args = [
233 (tx, "The transaction request to sign.")
234 ]
235 @clone [tx]
236 fn sign_transaction(tx: N::TransactionRequest) -> Bytes
237 );
238
239 robust_rpc!(
240 doc_args = [
241 (tx, "The transaction request to fill.")
242 ]
243 @clone [tx]
244 fn fill_transaction(tx: N::TransactionRequest) -> FillTransaction<N::TxEnvelope>
245 where N::TxEnvelope: RpcRecv
246 );
247
248 robust_rpc!(
249 doc_args = [
250 (address, "The address of the account."),
251 (keys, "A vector of storage keys to include in the proof.")
252 ]
253 @clone [keys]
254 fn get_proof(address: Address, keys: Vec<StorageKey>) -> EIP1186AccountProofResponse
255 );
256
257 robust_rpc!(
258 doc_args = [
259 (address, "The address of the storage."),
260 (key, "The position in the storage.")
261 ]
262 fn get_storage_at(address: Address, key: U256) -> StorageValue
263 );
264
265 robust_rpc!(
266 doc_args = [
267 (block_hash, "The hash of the block."),
268 (index, "The transaction index position.")
269 ]
270 fn get_transaction_by_block_hash_and_index(block_hash: B256, index: usize) -> Option<N::TransactionResponse>
271 );
272
273 robust_rpc!(
274 doc_args = [
275 (block_number, "The block number or tag."),
276 (index, "The transaction index position.")
277 ]
278 fn get_transaction_by_block_number_and_index(block_number: BlockNumberOrTag, index: usize) -> Option<N::TransactionResponse>
279 );
280
281 robust_rpc!(
282 doc_args = [(hash, "The transaction hash.")]
283 fn get_transaction_by_hash(hash: TxHash) -> Option<N::TransactionResponse>
284 );
285
286 robust_rpc!(
287 doc_args = [(hash, "The transaction hash.")]
288 fn get_raw_transaction_by_hash(hash: TxHash) -> Option<Bytes>
289 );
290
291 robust_rpc!(
292 doc_args = [(address, "The address to get the transaction count for.")]
293 fn get_transaction_count(address: Address) -> u64
294 );
295
296 robust_rpc!(
297 doc_args = [(hash, "The transaction hash.")]
298 fn get_transaction_receipt(hash: TxHash) -> Option<N::ReceiptResponse>
299 );
300
301 robust_rpc!(
302 doc_args = [(block, "The block identifier (hash or number).")]
303 fn get_uncle_count(block: BlockId) -> u64
304 );
305
306 robust_rpc!(
307 doc_args = [
308 (tag, "The block identifier (hash or number)."),
309 (idx, "The uncle index position.")
310 ]
311 fn get_uncle(tag: BlockId, idx: u64) -> Option<N::BlockResponse>
312 );
313
314 robust_rpc!(
315 doc_args = [(request, "The simulation request")]
316 fn simulate(request: &SimulatePayload) -> Vec<SimulatedBlock<N::BlockResponse>>
317 );
318
319 robust_rpc!(fn syncing() -> SyncStatus);
320
321 robust_rpc!(
322 doc_args = [(id, "The filter ID to uninstall.")]
323 fn uninstall_filter(id: U256) -> bool
324 );
325
326 robust_rpc!(
327 doc_args = [(encoded_tx, "The RLP-encoded signed transaction bytes")]
328 fn send_raw_transaction(encoded_tx: &[u8]) -> PendingTransactionBuilder<N>
329 );
330
331 robust_rpc!(
332 doc_args = [(encoded_tx, "The RLP-encoded signed transaction bytes")]
333 fn send_raw_transaction_sync(encoded_tx: &[u8]) -> N::ReceiptResponse
334 );
335
336 robust_rpc!(
337 doc_args = [
338 (encoded_tx, "The RLP-encoded signed transaction bytes"),
339 (conditional, "The transaction conditional to apply")
340 ]
341 @clone [conditional]
342 fn send_raw_transaction_conditional(encoded_tx: &[u8], conditional: TransactionConditional) -> PendingTransactionBuilder<N>
343 );
344
345 robust_rpc!(
346 doc_args = [(tx, "The transaction request to send")]
347 @clone [tx]
348 fn send_transaction(tx: N::TransactionRequest) -> PendingTransactionBuilder<N>
349 );
350
351 robust_rpc!(
352 doc_args = [
353 (tx, "The signed transaction envelope to send.")
354 ]
355 @clone [tx]
356 fn send_tx_envelope(tx: N::TxEnvelope) -> PendingTransactionBuilder<N>
357 where N::TxEnvelope: Clone
358 );
359
360 robust_rpc!(
361 doc_args = [(tx, "The transaction request to send synchronously")]
362 @clone [tx]
363 fn send_transaction_sync(tx: N::TransactionRequest) -> N::ReceiptResponse
364 );
365
366 robust_rpc!(
367 doc_args = [
368 (sender, "The sender address"),
369 (nonce, "The nonce of the transaction")
370 ]
371 fn get_transaction_by_sender_nonce(sender: Address, nonce: u64) -> Option<N::TransactionResponse>
372 );
373
374 robust_rpc!(
375 doc_args = [
376 (block_hash, "The hash of the block"),
377 (index, "The transaction index position")
378 ]
379 fn get_raw_transaction_by_block_hash_and_index(block_hash: B256, index: usize) -> Option<Bytes>
380 );
381
382 robust_rpc!(
383 doc_args = [
384 (block_number, "The block number or tag"),
385 (index, "The transaction index position")
386 ]
387 fn get_raw_transaction_by_block_number_and_index(block_number: BlockNumberOrTag, index: usize) -> Option<Bytes>
388 );
389
390 robust_rpc!(
391 doc_alias = "web3_client_version"
392 fn get_client_version() -> String
393 );
394
395 pub async fn estimate_eip1559_fees_with(
432 &self,
433 estimator: Eip1559Estimator,
434 ) -> Result<Eip1559Estimation, Error> {
435 let fee_history = self
436 .get_fee_history(
437 EIP1559_FEE_ESTIMATION_PAST_BLOCKS,
438 BlockNumberOrTag::Latest,
439 &[EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE],
440 )
441 .await?;
442
443 let base_fee_per_gas = match fee_history.latest_block_base_fee() {
446 Some(base_fee) if base_fee != 0 => base_fee,
447 _ => {
448 self.get_block_by_number(BlockNumberOrTag::Latest)
450 .await
451 .map_err(|e| {
452 if matches!(e, Error::BlockNotFound) {
455 RpcError::NullResp.into()
456 } else {
457 e
458 }
459 })?
460 .header()
461 .as_ref()
462 .base_fee_per_gas()
463 .ok_or(RpcError::UnsupportedFeature("eip1559"))?
464 .into()
465 }
466 };
467
468 Ok(estimator.estimate(base_fee_per_gas, &fee_history.reward.unwrap_or_default()))
469 }
470
471 robust_rpc!(
472 @clone [method, params]
473 fn raw_request<P, R>(method: Cow<'static, str>, params: P) -> R
474 where P: RpcSend, R: RpcRecv
475 );
476
477 robust_rpc!(
478 @clone [method]
479 fn raw_request_dyn(method: Cow<'static, str>, params: &RawValue) -> Box<RawValue>
480 );
481
482 pub async fn subscribe_blocks(&self) -> Result<RobustSubscription<N>, Error> {
502 let subscription: SubscriptionBackend<N> = self
503 .try_operation_with_failover(move |provider| async move {
504 #[cfg(feature = "http-subscription")]
511 {
512 let not_pubsub = provider.client().pubsub_frontend().is_none();
513 if not_pubsub && self.allow_http_subscriptions {
514 return provider.watch_blocks().await.map(|builder| {
515 builder
516 .with_poll_interval(self.poll_interval)
517 .with_channel_size(self.subscription_buffer_capacity)
518 .into()
519 });
520 }
521 }
522 provider
523 .subscribe_blocks()
524 .channel_size(self.subscription_buffer_capacity)
525 .await
526 .map(Into::<SubscriptionBackend<N>>::into)
527 })
528 .await?;
529
530 Ok(RobustSubscription::new(subscription, self.clone()))
531 }
532
533 robust_rpc!(fn watch_blocks() -> PollerBuilder<(U256,), Vec<BlockHash>>);
535
536 pub async fn try_operation_with_failover<T: Debug, F, Fut>(
552 &self,
553 operation: F,
554 ) -> Result<T, FailoverError>
555 where
556 F: Fn(RootProvider<N>) -> Fut,
557 Fut: Future<Output = Result<T, RpcError<TransportErrorKind>>>,
558 {
559 let primary = self.primary();
560
561 match self.try_provider_with_timeout(primary, &operation).await {
562 Ok(value) => Ok(value),
563 Err(last_error) => self
564 .try_fallback_providers_from(&operation, last_error, 0)
565 .await
566 .map(|(value, _)| value),
567 }
568 }
569
570 pub(crate) async fn try_fallback_providers_from<T: Debug, F, Fut>(
572 &self,
573 operation: F,
574 mut last_error: FailoverError,
575 start_index: usize,
576 ) -> Result<(T, usize), FailoverError>
577 where
578 F: Fn(RootProvider<N>) -> Fut,
579 Fut: Future<Output = Result<T, RpcError<TransportErrorKind>>>,
580 {
581 let fallback_providers = self.fallback_providers();
582
583 debug!(
584 start_index = start_index,
585 total_fallbacks = fallback_providers.len(),
586 "Primary provider failed, attempting fallback providers"
587 );
588
589 let fallback_iter = fallback_providers.iter().enumerate().skip(start_index);
590 for (fallback_idx, provider) in fallback_iter {
591 trace!(
592 fallback_index = fallback_idx,
593 total_fallbacks = fallback_providers.len(),
594 "Attempting fallback provider"
595 );
596
597 match self.try_provider_with_timeout(provider, &operation).await {
598 Ok(value) => {
599 info!(
600 fallback_index = fallback_idx,
601 total_fallbacks = fallback_providers.len(),
602 "Switched to fallback provider"
603 );
604 return Ok((value, fallback_idx));
605 }
606 Err(e) => {
607 warn!(
608 fallback_index = fallback_idx,
609 error = %e,
610 "Fallback provider failed"
611 );
612 last_error = e;
613 }
614 }
615 }
616
617 error!(attempted_providers = fallback_providers.len() + 1, "All providers exhausted");
618
619 Err(last_error)
620 }
621
622 pub(crate) fn try_provider_with_timeout<T, F, Fut>(
624 &self,
625 provider: &RootProvider<N>,
626 operation: F,
627 ) -> impl Future<Output = Result<T, FailoverError>>
628 where
629 F: Fn(RootProvider<N>) -> Fut,
630 Fut: Future<Output = Result<T, RpcError<TransportErrorKind>>>,
631 {
632 let retry_strategy = ExponentialBuilder::default()
633 .with_max_times(self.max_retries())
634 .with_min_delay(self.min_delay());
635
636 let call_timeout = self.call_timeout();
637 let provider = provider.clone();
638
639 async move {
640 timeout(
641 call_timeout,
642 (|| operation(provider.clone()))
643 .retry(retry_strategy)
644 .when(|e| match e {
645 RpcError::ErrorResp(err_resp) if err_resp.is_retry_err() => true,
647 RpcError::ErrorResp(err_resp) => {
650 is_retryable_error(err_resp.code, err_resp.message.as_ref())
651 }
652 RpcError::Transport(tr_err) => tr_err.is_retry_err(),
654 _ => true,
656 })
657 .sleep(tokio::time::sleep),
658 )
659 .await
660 .map_err(FailoverError::from)?
661 .map_err(FailoverError::from)
662 }
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use std::sync::atomic::{AtomicUsize, Ordering};
669
670 use alloy::{
671 node_bindings::Anvil,
672 providers::{ProviderBuilder, WsConnect},
673 };
674 use tokio::time::sleep;
675
676 use crate::{
677 DEFAULT_RECONNECT_INTERVAL, DEFAULT_SUBSCRIPTION_BUFFER_CAPACITY,
678 DEFAULT_SUBSCRIPTION_TIMEOUT, Error, RobustProvider, RobustProviderBuilder,
679 };
680
681 use super::*;
682
683 fn test_provider(timeout: u64, max_retries: usize, min_delay: u64) -> RobustProvider {
684 RobustProvider {
685 primary_provider: RootProvider::new_http("http://localhost:8545".parse().unwrap()),
686 fallback_providers: vec![],
687 call_timeout: Duration::from_millis(timeout),
688 subscription_timeout: DEFAULT_SUBSCRIPTION_TIMEOUT,
689 max_retries,
690 min_delay: Duration::from_millis(min_delay),
691 reconnect_interval: DEFAULT_RECONNECT_INTERVAL,
692 subscription_buffer_capacity: DEFAULT_SUBSCRIPTION_BUFFER_CAPACITY,
693 #[cfg(feature = "http-subscription")]
694 poll_interval: crate::DEFAULT_POLL_INTERVAL,
695 #[cfg(feature = "http-subscription")]
696 allow_http_subscriptions: false,
697 }
698 }
699
700 #[tokio::test]
701 async fn test_retry_with_timeout_succeeds_on_first_attempt() {
702 let provider = test_provider(100, 3, 10);
703
704 let call_count = AtomicUsize::new(0);
705
706 let result = provider
707 .try_operation_with_failover(|_| async {
708 call_count.fetch_add(1, Ordering::SeqCst);
709 let count = call_count.load(Ordering::SeqCst);
710 Ok(count)
711 })
712 .await;
713
714 assert!(matches!(result, Ok(1)));
715 }
716
717 #[tokio::test]
718 async fn test_retry_with_timeout_retries_on_error() {
719 let provider = test_provider(100, 3, 10);
720
721 let call_count = AtomicUsize::new(0);
722
723 let result = provider
724 .try_operation_with_failover(|_| async {
725 call_count.fetch_add(1, Ordering::SeqCst);
726 let count = call_count.load(Ordering::SeqCst);
727 match count {
728 3 => Ok(count),
729 _ => Err(TransportErrorKind::custom_str("429 Too Many Requests")),
731 }
732 })
733 .await;
734
735 assert!(matches!(result, Ok(3)));
736 }
737
738 #[tokio::test]
739 async fn test_retry_with_timeout_fails_after_max_retries() {
740 let provider = test_provider(100, 2, 10);
741
742 let call_count = AtomicUsize::new(0);
743
744 let result: Result<(), FailoverError> = provider
745 .try_operation_with_failover(|_| async {
746 call_count.fetch_add(1, Ordering::SeqCst);
747 Err(TransportErrorKind::custom_str("429 Too Many Requests"))
749 })
750 .await;
751
752 assert!(matches!(result, Err(FailoverError::RpcError(_))));
753 assert_eq!(call_count.load(Ordering::SeqCst), 3);
754 }
755
756 #[tokio::test]
757 async fn test_retry_with_timeout_respects_call_timeout() {
758 let call_timeout = 50;
759 let provider = test_provider(call_timeout, 10, 1);
760
761 let result = provider
762 .try_operation_with_failover(move |_provider| async move {
763 sleep(Duration::from_millis(call_timeout + 10)).await;
764 Ok(42)
765 })
766 .await;
767
768 assert!(matches!(result, Err(FailoverError::Timeout)));
769 }
770
771 #[tokio::test]
772 async fn test_subscribe_fails_when_all_providers_lack_pubsub() -> anyhow::Result<()> {
773 let anvil = Anvil::new().try_spawn()?;
774
775 let http_provider = ProviderBuilder::new().connect_http(anvil.endpoint_url());
776
777 let robust = RobustProviderBuilder::new(http_provider.clone())
778 .fallback(http_provider)
779 .call_timeout(Duration::from_secs(5))
780 .min_delay(Duration::from_millis(100))
781 .build()
782 .await?;
783
784 let err = robust.subscribe_blocks().await.unwrap_err();
785
786 assert!(
787 matches!(
788 err,
789 Error::RpcError(RpcError::Transport(TransportErrorKind::PubsubUnavailable))
790 ),
791 "expected TransportErrorKind::PubsubUnavailable error type, got: {err:?}"
792 );
793
794 Ok(())
795 }
796
797 #[tokio::test]
798 async fn test_subscribe_succeeds_if_primary_provider_lacks_pubsub_but_fallback_supports_it()
799 -> anyhow::Result<()> {
800 let anvil = Anvil::new().try_spawn()?;
801
802 let http_provider = ProviderBuilder::new().connect_http(anvil.endpoint_url());
803 let ws_provider = ProviderBuilder::new()
804 .connect_ws(WsConnect::new(anvil.ws_endpoint_url().as_str()))
805 .await?;
806
807 let robust = RobustProviderBuilder::fragile(http_provider)
808 .fallback(ws_provider)
809 .call_timeout(Duration::from_secs(5))
810 .build()
811 .await?;
812
813 let result = robust.subscribe_blocks().await;
814 assert!(result.is_ok());
815
816 Ok(())
817 }
818}