@@ -50,13 +50,36 @@ pub const ETHERSCAN_V2_API_BASE_URL: &str = "https://api.etherscan.io/v2/api";
5050
5151/// The Etherscan.io API version 1 - classic verifier, one API per chain, 2 - new multichain
5252/// verifier
53- #[ derive( Clone , Default , Debug , PartialEq , Copy ) ]
53+ #[ derive( Clone , Default , Debug , PartialEq , Copy , Eq , Deserialize , Serialize ) ]
54+ #[ serde( rename_all = "lowercase" ) ]
5455pub enum EtherscanApiVersion {
55- #[ default]
5656 V1 ,
57+ #[ default]
5758 V2 ,
5859}
5960
61+ impl std:: fmt:: Display for EtherscanApiVersion {
62+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
63+ match self {
64+ EtherscanApiVersion :: V1 => write ! ( f, "v1" ) ,
65+ EtherscanApiVersion :: V2 => write ! ( f, "v2" ) ,
66+ }
67+ }
68+ }
69+
70+ impl TryFrom < String > for EtherscanApiVersion {
71+ type Error = EtherscanError ;
72+
73+ #[ inline]
74+ fn try_from ( value : String ) -> Result < Self , Self :: Error > {
75+ match value. as_str ( ) {
76+ "v1" => Ok ( EtherscanApiVersion :: V1 ) ,
77+ "v2" => Ok ( EtherscanApiVersion :: V2 ) ,
78+ _ => Err ( EtherscanError :: InvalidApiVersion ) ,
79+ }
80+ }
81+ }
82+
6083/// The Etherscan.io API client.
6184#[ derive( Clone , Debug ) ]
6285pub struct Client {
@@ -114,50 +137,32 @@ impl Client {
114137 Client :: builder ( ) . with_api_key ( api_key) . chain ( chain) ?. build ( )
115138 }
116139
117- /// Create a new client with the correct endpoint with the chain and chosen API v2 version
118- pub fn new_v2_from_env ( chain : Chain ) -> Result < Self > {
119- let api_key = std:: env:: var ( "ETHERSCAN_API_KEY" ) ?;
120- Client :: builder ( )
121- . with_api_version ( EtherscanApiVersion :: V2 )
122- . with_api_key ( api_key)
123- . chain ( chain) ?
124- . build ( )
140+ /// Create a new client for the given [`EtherscanApiVersion`].
141+ pub fn new_with_api_version (
142+ chain : Chain ,
143+ api_key : impl Into < String > ,
144+ api_version : EtherscanApiVersion ,
145+ ) -> Result < Self > {
146+ Client :: builder ( ) . with_api_key ( api_key) . with_api_version ( api_version) . chain ( chain) ?. build ( )
125147 }
126148
127- /// Create a new client with the correct endpoints based on the chain and API key
128- /// from the default environment variable defined in [`Chain`].
149+ /// Create a new client with the correct endpoint with the chain
129150 pub fn new_from_env ( chain : Chain ) -> Result < Self > {
130- let api_key = match chain. kind ( ) {
131- ChainKind :: Named ( named) => match named {
132- // Extra aliases
133- NamedChain :: Fantom | NamedChain :: FantomTestnet => std:: env:: var ( "FMTSCAN_API_KEY" )
134- . or_else ( |_| std:: env:: var ( "FANTOMSCAN_API_KEY" ) )
135- . map_err ( Into :: into) ,
136-
137- // Backwards compatibility, ideally these should return an error.
138- NamedChain :: Gnosis
139- | NamedChain :: Chiado
140- | NamedChain :: Sepolia
141- | NamedChain :: Rsk
142- | NamedChain :: Sokol
143- | NamedChain :: Poa
144- | NamedChain :: Oasis
145- | NamedChain :: Emerald
146- | NamedChain :: EmeraldTestnet
147- | NamedChain :: Evmos
148- | NamedChain :: EvmosTestnet => Ok ( String :: new ( ) ) ,
149- NamedChain :: AnvilHardhat | NamedChain :: Dev => {
150- Err ( EtherscanError :: LocalNetworksNotSupported )
151- }
151+ Self :: new_with_api_version (
152+ chain,
153+ get_api_key_from_chain ( chain, EtherscanApiVersion :: V2 ) ?,
154+ EtherscanApiVersion :: V2 ,
155+ )
156+ }
152157
153- _ => named
154- . etherscan_api_key_name ( )
155- . ok_or_else ( || EtherscanError :: ChainNotSupported ( chain ) )
156- . and_then ( |key_name| std :: env :: var ( key_name ) . map_err ( Into :: into ) ) ,
157- } ,
158- ChainKind :: Id ( _ ) => Err ( EtherscanError :: ChainNotSupported ( chain ) ) ,
159- } ? ;
160- Self :: new ( chain , api_key )
158+ /// Create a new client with the correct endpoints based on the chain and API key
159+ /// from the default environment variable defined in [`Chain`].
160+ pub fn new_v1_from_env ( chain : Chain ) -> Result < Self > {
161+ Self :: new_with_api_version (
162+ chain ,
163+ get_api_key_from_chain ( chain , EtherscanApiVersion :: V1 ) ? ,
164+ EtherscanApiVersion :: V1 ,
165+ )
161166 }
162167
163168 /// Create a new client with the correct endpoints based on the chain and API key
@@ -193,6 +198,11 @@ impl Client {
193198 & self . etherscan_url
194199 }
195200
201+ /// Returns the configured API key, if any
202+ pub fn api_key ( & self ) -> Option < & str > {
203+ self . api_key . as_deref ( )
204+ }
205+
196206 /// Return the URL for the given block number
197207 pub fn block_url ( & self , block : u64 ) -> String {
198208 self . etherscan_url . join ( & format ! ( "block/{block}" ) ) . unwrap ( ) . to_string ( )
@@ -587,9 +597,53 @@ fn into_url(url: impl IntoUrl) -> std::result::Result<Url, reqwest::Error> {
587597 url. into_url ( )
588598}
589599
600+ fn get_api_key_from_chain (
601+ chain : Chain ,
602+ api_version : EtherscanApiVersion ,
603+ ) -> Result < String , EtherscanError > {
604+ match chain. kind ( ) {
605+ ChainKind :: Named ( named) => match named {
606+ // Fantom is special and doesn't support etherscan api v2
607+ NamedChain :: Fantom | NamedChain :: FantomTestnet => std:: env:: var ( "FMTSCAN_API_KEY" )
608+ . or_else ( |_| std:: env:: var ( "FANTOMSCAN_API_KEY" ) )
609+ . map_err ( Into :: into) ,
610+
611+ // Backwards compatibility, ideally these should return an error.
612+ NamedChain :: Gnosis
613+ | NamedChain :: Chiado
614+ | NamedChain :: Sepolia
615+ | NamedChain :: Rsk
616+ | NamedChain :: Sokol
617+ | NamedChain :: Poa
618+ | NamedChain :: Oasis
619+ | NamedChain :: Emerald
620+ | NamedChain :: EmeraldTestnet
621+ | NamedChain :: Evmos
622+ | NamedChain :: EvmosTestnet => Ok ( String :: new ( ) ) ,
623+ NamedChain :: AnvilHardhat | NamedChain :: Dev => {
624+ Err ( EtherscanError :: LocalNetworksNotSupported )
625+ }
626+
627+ // Rather than get special ENV vars here, normal case is to pull overall
628+ // ETHERSCAN_API_KEY
629+ _ => {
630+ if api_version == EtherscanApiVersion :: V1 {
631+ named
632+ . etherscan_api_key_name ( )
633+ . ok_or_else ( || EtherscanError :: ChainNotSupported ( chain) )
634+ . and_then ( |key_name| std:: env:: var ( key_name) . map_err ( Into :: into) )
635+ } else {
636+ std:: env:: var ( "ETHERSCAN_API_KEY" ) . map_err ( Into :: into)
637+ }
638+ }
639+ } ,
640+ ChainKind :: Id ( _) => Err ( EtherscanError :: ChainNotSupported ( chain) ) ,
641+ }
642+ }
643+
590644#[ cfg( test) ]
591645mod tests {
592- use crate :: { Client , EtherscanError , ResponseData } ;
646+ use crate :: { Client , EtherscanApiVersion , EtherscanError , ResponseData } ;
593647 use alloy_chains:: Chain ;
594648 use alloy_primitives:: { Address , B256 } ;
595649
@@ -602,13 +656,23 @@ mod tests {
602656 }
603657
604658 #[ test]
605- fn test_api_paths ( ) {
606- let client = Client :: new ( Chain :: goerli ( ) , "" ) . unwrap ( ) ;
659+ fn test_api_paths_v1 ( ) {
660+ let client =
661+ Client :: new_with_api_version ( Chain :: goerli ( ) , "" , EtherscanApiVersion :: V1 ) . unwrap ( ) ;
607662 assert_eq ! ( client. etherscan_api_url. as_str( ) , "https://api-goerli.etherscan.io/api" ) ;
608663
609664 assert_eq ! ( client. block_url( 100 ) , "https://goerli.etherscan.io/block/100" ) ;
610665 }
611666
667+ #[ test]
668+ fn test_api_paths_v2 ( ) {
669+ let client =
670+ Client :: new_with_api_version ( Chain :: goerli ( ) , "" , EtherscanApiVersion :: V2 ) . unwrap ( ) ;
671+ assert_eq ! ( client. etherscan_api_url. as_str( ) , "https://api.etherscan.io/v2/api" ) ;
672+
673+ assert_eq ! ( client. block_url( 100 ) , "https://goerli.etherscan.io/block/100" ) ;
674+ }
675+
612676 #[ test]
613677 fn stringifies_block_url ( ) {
614678 let etherscan = Client :: new ( Chain :: mainnet ( ) , "" ) . unwrap ( ) ;
@@ -657,4 +721,19 @@ mod tests {
657721 let resp: ResponseData < Address > = serde_json:: from_value ( err) . unwrap ( ) ;
658722 assert ! ( matches!( resp, ResponseData :: Error { .. } ) ) ;
659723 }
724+
725+ #[ test]
726+ fn can_parse_api_version ( ) {
727+ assert_eq ! (
728+ EtherscanApiVersion :: try_from( "v1" . to_string( ) ) . unwrap( ) ,
729+ EtherscanApiVersion :: V1
730+ ) ;
731+ assert_eq ! (
732+ EtherscanApiVersion :: try_from( "v2" . to_string( ) ) . unwrap( ) ,
733+ EtherscanApiVersion :: V2
734+ ) ;
735+
736+ let parse_err = EtherscanApiVersion :: try_from ( "fail" . to_string ( ) ) . unwrap_err ( ) ;
737+ assert ! ( matches!( parse_err, EtherscanError :: InvalidApiVersion ) ) ;
738+ }
660739}
0 commit comments