@@ -3,7 +3,7 @@ use std::{fmt::Debug, time::Duration};
33use 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