Skip to content

Commit 6015bc4

Browse files
committed
Merge branch 'main' into add-robust-subscription
2 parents d1202fe + 21cba9f commit 6015bc4

File tree

1 file changed

+276
-5
lines changed

1 file changed

+276
-5
lines changed

src/robust_provider/provider.rs

Lines changed: 276 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{fmt::Debug, time::Duration};
33
use alloy::{
44
eips::{BlockId, BlockNumberOrTag},
55
network::{Ethereum, Network},
6-
primitives::BlockHash,
6+
primitives::{BlockHash, BlockNumber},
77
providers::{Provider, RootProvider},
88
rpc::types::{Filter, Log},
99
transports::{RpcError, TransportErrorKind},
@@ -89,7 +89,7 @@ impl<N: Network> RobustProvider<N> {
8989
/// # Errors
9090
///
9191
/// See [retry errors](#retry-errors).
92-
pub async fn get_block_number(&self) -> Result<u64, Error> {
92+
pub async fn get_block_number(&self) -> Result<BlockNumber, Error> {
9393
info!("eth_getBlockNumber called");
9494
let result = self
9595
.try_operation_with_failover(
@@ -114,7 +114,7 @@ impl<N: Network> RobustProvider<N> {
114114
/// # Errors
115115
///
116116
/// See [retry errors](#retry-errors).
117-
pub async fn get_block_number_by_id(&self, block_id: BlockId) -> Result<u64, Error> {
117+
pub async fn get_block_number_by_id(&self, block_id: BlockId) -> Result<BlockNumber, Error> {
118118
info!("get_block_number_by_id called");
119119
let result = self
120120
.try_operation_with_failover(
@@ -358,11 +358,31 @@ mod tests {
358358
RobustProviderBuilder, builder::DEFAULT_SUBSCRIPTION_TIMEOUT,
359359
subscription::DEFAULT_RECONNECT_INTERVAL,
360360
};
361-
use alloy::providers::{ProviderBuilder, WsConnect};
362-
use alloy_node_bindings::Anvil;
361+
use alloy::providers::{ProviderBuilder, WsConnect, ext::AnvilApi};
362+
use alloy_node_bindings::{Anvil, AnvilInstance};
363363
use std::sync::atomic::{AtomicUsize, Ordering};
364364
use tokio::time::sleep;
365365

366+
async fn setup_anvil() -> anyhow::Result<(AnvilInstance, RobustProvider, impl Provider)> {
367+
let anvil = Anvil::new().try_spawn()?;
368+
let alloy_provider = ProviderBuilder::new().connect_http(anvil.endpoint_url());
369+
370+
let robust = RobustProviderBuilder::new(alloy_provider.clone())
371+
.call_timeout(Duration::from_secs(5))
372+
.build()
373+
.await?;
374+
375+
Ok((anvil, robust, alloy_provider))
376+
}
377+
378+
async fn setup_anvil_with_blocks(
379+
num_blocks: u64,
380+
) -> anyhow::Result<(AnvilInstance, RobustProvider, impl Provider)> {
381+
let (anvil, robust, alloy_provider) = setup_anvil().await?;
382+
alloy_provider.anvil_mine(Some(num_blocks), None).await?;
383+
Ok((anvil, robust, alloy_provider))
384+
}
385+
366386
fn test_provider(timeout: u64, max_retries: usize, min_delay: u64) -> RobustProvider {
367387
RobustProvider {
368388
primary_provider: RootProvider::new_http("http://localhost:8545".parse().unwrap()),
@@ -505,4 +525,255 @@ mod tests {
505525

506526
Ok(())
507527
}
528+
529+
#[tokio::test]
530+
async fn test_ws_fails_http_fallback_returns_primary_error() -> anyhow::Result<()> {
531+
let anvil_1 = Anvil::new().try_spawn()?;
532+
533+
let ws_provider =
534+
ProviderBuilder::new().connect(anvil_1.ws_endpoint_url().as_str()).await?;
535+
536+
let anvil_2 = Anvil::new().try_spawn()?;
537+
let http_provider = ProviderBuilder::new().connect_http(anvil_2.endpoint_url());
538+
539+
let robust = RobustProviderBuilder::fragile(ws_provider.clone())
540+
.fallback(http_provider)
541+
.call_timeout(Duration::from_millis(500))
542+
.build()
543+
.await?;
544+
545+
// force ws_provider to fail and return BackendGone
546+
drop(anvil_1);
547+
548+
let err = robust.subscribe_blocks().await.unwrap_err();
549+
550+
// The error should be either a Timeout or BackendGone from the primary WS provider,
551+
// NOT a PubsubUnavailable error (which would indicate HTTP fallback was attempted)
552+
match err {
553+
Error::Timeout => {}
554+
Error::RpcError(e) => {
555+
assert!(matches!(e.as_ref(), RpcError::Transport(TransportErrorKind::BackendGone)));
556+
}
557+
Error::BlockNotFound(id) => panic!("Unexpected error type: BlockNotFound({id})"),
558+
}
559+
560+
Ok(())
561+
}
562+
563+
#[tokio::test]
564+
async fn test_get_block_by_number_succeeds() -> anyhow::Result<()> {
565+
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(100).await?;
566+
567+
let tags = [
568+
BlockNumberOrTag::Number(50),
569+
BlockNumberOrTag::Latest,
570+
BlockNumberOrTag::Earliest,
571+
BlockNumberOrTag::Safe,
572+
BlockNumberOrTag::Finalized,
573+
];
574+
575+
for tag in tags {
576+
let robust_block = robust.get_block_by_number(tag).await?;
577+
let alloy_block =
578+
alloy_provider.get_block_by_number(tag).await?.expect("block should exist");
579+
580+
assert_eq!(robust_block.header.number, alloy_block.header.number);
581+
assert_eq!(robust_block.header.hash, alloy_block.header.hash);
582+
}
583+
584+
Ok(())
585+
}
586+
587+
#[tokio::test]
588+
async fn test_get_block_by_number_future_block_fails() -> anyhow::Result<()> {
589+
let (_anvil, robust, _alloy_provider) = setup_anvil().await?;
590+
591+
let future_block = 999_999;
592+
let result = robust.get_block_by_number(BlockNumberOrTag::Number(future_block)).await;
593+
594+
assert!(matches!(result, Err(Error::BlockNotFound(_))));
595+
596+
Ok(())
597+
}
598+
599+
#[tokio::test]
600+
async fn test_get_block_succeeds() -> anyhow::Result<()> {
601+
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(100).await?;
602+
603+
let block_ids = [
604+
BlockId::number(50),
605+
BlockId::latest(),
606+
BlockId::earliest(),
607+
BlockId::safe(),
608+
BlockId::finalized(),
609+
];
610+
611+
for block_id in block_ids {
612+
let robust_block = robust.get_block(block_id).await?;
613+
let alloy_block =
614+
alloy_provider.get_block(block_id).await?.expect("block should exist");
615+
616+
assert_eq!(robust_block.header.number, alloy_block.header.number);
617+
assert_eq!(robust_block.header.hash, alloy_block.header.hash);
618+
}
619+
620+
// test block hash
621+
let block = alloy_provider
622+
.get_block_by_number(BlockNumberOrTag::Number(50))
623+
.await?
624+
.expect("block should exist");
625+
let block_hash = block.header.hash;
626+
let block_id = BlockId::hash(block_hash);
627+
let robust_block = robust.get_block(block_id).await?;
628+
assert_eq!(robust_block.header.hash, block_hash);
629+
assert_eq!(robust_block.header.number, 50);
630+
631+
Ok(())
632+
}
633+
634+
#[tokio::test]
635+
async fn test_get_block_fails() -> anyhow::Result<()> {
636+
let (_anvil, robust, _alloy_provider) = setup_anvil().await?;
637+
638+
// Future block number
639+
let result = robust.get_block(BlockId::number(999_999)).await;
640+
assert!(matches!(result, Err(Error::BlockNotFound(_))));
641+
642+
// Non-existent hash
643+
let result = robust.get_block(BlockId::hash(BlockHash::ZERO)).await;
644+
assert!(matches!(result, Err(Error::BlockNotFound(_))));
645+
646+
Ok(())
647+
}
648+
649+
#[tokio::test]
650+
async fn test_get_block_number_succeeds() -> anyhow::Result<()> {
651+
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(100).await?;
652+
653+
let robust_block_num = robust.get_block_number().await?;
654+
let alloy_block_num = alloy_provider.get_block_number().await?;
655+
assert_eq!(robust_block_num, alloy_block_num);
656+
assert_eq!(robust_block_num, 100);
657+
658+
alloy_provider.anvil_mine(Some(10), None).await?;
659+
let new_block = robust.get_block_number().await?;
660+
assert_eq!(new_block, 110);
661+
662+
Ok(())
663+
}
664+
665+
#[tokio::test]
666+
async fn test_get_block_number_by_id_succeeds() -> anyhow::Result<()> {
667+
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(100).await?;
668+
669+
let block_num = robust.get_block_number_by_id(BlockId::number(50)).await?;
670+
assert_eq!(block_num, 50);
671+
672+
let block = alloy_provider
673+
.get_block_by_number(BlockNumberOrTag::Number(50))
674+
.await?
675+
.expect("block should exist");
676+
let block_num = robust.get_block_number_by_id(BlockId::hash(block.header.hash)).await?;
677+
assert_eq!(block_num, 50);
678+
679+
let block_num = robust.get_block_number_by_id(BlockId::latest()).await?;
680+
assert_eq!(block_num, 100);
681+
682+
let block_num = robust.get_block_number_by_id(BlockId::earliest()).await?;
683+
assert_eq!(block_num, 0);
684+
685+
// Returns block number even if it doesnt 'exist' on chain
686+
let block_num = robust.get_block_number_by_id(BlockId::number(999_999)).await?;
687+
let alloy_block_num = alloy_provider
688+
.get_block_number_by_id(BlockId::number(999_999))
689+
.await?
690+
.expect("Should return block num");
691+
assert_eq!(alloy_block_num, block_num);
692+
assert_eq!(block_num, 999_999);
693+
694+
Ok(())
695+
}
696+
697+
#[tokio::test]
698+
async fn test_get_block_number_by_id_fails() -> anyhow::Result<()> {
699+
let (_anvil, robust, _alloy_provider) = setup_anvil().await?;
700+
701+
let result = robust.get_block_number_by_id(BlockId::hash(BlockHash::ZERO)).await;
702+
assert!(matches!(result, Err(Error::BlockNotFound(_))));
703+
704+
Ok(())
705+
}
706+
707+
#[tokio::test]
708+
async fn test_get_latest_confirmed_succeeds() -> anyhow::Result<()> {
709+
let (_anvil, robust, _alloy_provider) = setup_anvil_with_blocks(100).await?;
710+
711+
// With confirmations
712+
let confirmed_block = robust.get_latest_confirmed(10).await?;
713+
assert_eq!(confirmed_block, 90);
714+
715+
// Zero confirmations returns latest
716+
let confirmed_block = robust.get_latest_confirmed(0).await?;
717+
assert_eq!(confirmed_block, 100);
718+
719+
// Single confirmation
720+
let confirmed_block = robust.get_latest_confirmed(1).await?;
721+
assert_eq!(confirmed_block, 99);
722+
723+
// confirmations = latest - 1
724+
let confirmed_block = robust.get_latest_confirmed(99).await?;
725+
assert_eq!(confirmed_block, 1);
726+
727+
// confirmations = latest (should return 0)
728+
let confirmed_block = robust.get_latest_confirmed(100).await?;
729+
assert_eq!(confirmed_block, 0);
730+
731+
// confirmations = latest + 1 (saturates at zero)
732+
let confirmed_block = robust.get_latest_confirmed(101).await?;
733+
assert_eq!(confirmed_block, 0);
734+
735+
// Saturates at zero when confirmations > latest
736+
let confirmed_block = robust.get_latest_confirmed(200).await?;
737+
assert_eq!(confirmed_block, 0);
738+
739+
Ok(())
740+
}
741+
742+
#[tokio::test]
743+
async fn test_get_block_by_hash_succeeds() -> anyhow::Result<()> {
744+
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(100).await?;
745+
746+
let block = alloy_provider
747+
.get_block_by_number(BlockNumberOrTag::Number(50))
748+
.await?
749+
.expect("block should exist");
750+
let block_hash = block.header.hash;
751+
752+
let robust_block = robust.get_block_by_hash(block_hash).await?;
753+
let alloy_block =
754+
alloy_provider.get_block_by_hash(block_hash).await?.expect("block should exist");
755+
assert_eq!(robust_block.header.hash, alloy_block.header.hash);
756+
assert_eq!(robust_block.header.number, alloy_block.header.number);
757+
758+
let genesis = alloy_provider
759+
.get_block_by_number(BlockNumberOrTag::Earliest)
760+
.await?
761+
.expect("genesis should exist");
762+
let genesis_hash = genesis.header.hash;
763+
let robust_block = robust.get_block_by_hash(genesis_hash).await?;
764+
assert_eq!(robust_block.header.number, 0);
765+
assert_eq!(robust_block.header.hash, genesis_hash);
766+
767+
Ok(())
768+
}
769+
770+
#[tokio::test]
771+
async fn test_get_block_by_hash_fails() -> anyhow::Result<()> {
772+
let (_anvil, robust, _alloy_provider) = setup_anvil().await?;
773+
774+
let result = robust.get_block_by_hash(BlockHash::ZERO).await;
775+
assert!(matches!(result, Err(Error::BlockNotFound(_))));
776+
777+
Ok(())
778+
}
508779
}

0 commit comments

Comments
 (0)