-
Notifications
You must be signed in to change notification settings - Fork 46
Handle parsing and serialization of etherscan api version #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3105649
ade7e4f
106c98f
8c03122
367ca67
533edd7
9950c42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,13 +50,36 @@ pub const ETHERSCAN_V2_API_BASE_URL: &str = "https://api.etherscan.io/v2/api"; | |
|
|
||
| /// The Etherscan.io API version 1 - classic verifier, one API per chain, 2 - new multichain | ||
| /// verifier | ||
| #[derive(Clone, Default, Debug, PartialEq, Copy)] | ||
| #[derive(Clone, Default, Debug, PartialEq, Copy, Eq, Deserialize, Serialize)] | ||
| #[serde(rename_all = "lowercase")] | ||
| pub enum EtherscanApiVersion { | ||
| #[default] | ||
| V1, | ||
| #[default] | ||
| V2, | ||
| } | ||
|
|
||
| impl std::fmt::Display for EtherscanApiVersion { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| match self { | ||
| EtherscanApiVersion::V1 => write!(f, "v1"), | ||
| EtherscanApiVersion::V2 => write!(f, "v2"), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl TryFrom<String> for EtherscanApiVersion { | ||
| type Error = EtherscanError; | ||
|
|
||
| #[inline] | ||
| fn try_from(value: String) -> Result<Self, Self::Error> { | ||
| match value.as_str() { | ||
| "v1" => Ok(EtherscanApiVersion::V1), | ||
| "v2" => Ok(EtherscanApiVersion::V2), | ||
| _ => Err(EtherscanError::InvalidApiVersion), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// The Etherscan.io API client. | ||
| #[derive(Clone, Debug)] | ||
| pub struct Client { | ||
|
|
@@ -114,50 +137,32 @@ impl Client { | |
| Client::builder().with_api_key(api_key).chain(chain)?.build() | ||
| } | ||
|
|
||
| /// Create a new client with the correct endpoint with the chain and chosen API v2 version | ||
| pub fn new_v2_from_env(chain: Chain) -> Result<Self> { | ||
|
Comment on lines
-117
to
-118
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we keep this so that this doesn't break?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure — it only makes sense imo if the default is V1 and maybe it should still be. Not sure about other tools using this package. |
||
| let api_key = std::env::var("ETHERSCAN_API_KEY")?; | ||
| Client::builder() | ||
| .with_api_version(EtherscanApiVersion::V2) | ||
| .with_api_key(api_key) | ||
| .chain(chain)? | ||
| .build() | ||
| /// Create a new client for the given [`EtherscanApiVersion`]. | ||
| pub fn new_with_api_version( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this needs a one liner doc |
||
| chain: Chain, | ||
| api_key: impl Into<String>, | ||
| api_version: EtherscanApiVersion, | ||
| ) -> Result<Self> { | ||
| Client::builder().with_api_key(api_key).with_api_version(api_version).chain(chain)?.build() | ||
| } | ||
|
|
||
| /// Create a new client with the correct endpoints based on the chain and API key | ||
| /// from the default environment variable defined in [`Chain`]. | ||
| /// Create a new client with the correct endpoint with the chain | ||
| pub fn new_from_env(chain: Chain) -> Result<Self> { | ||
| let api_key = match chain.kind() { | ||
| ChainKind::Named(named) => match named { | ||
| // Extra aliases | ||
| NamedChain::Fantom | NamedChain::FantomTestnet => std::env::var("FMTSCAN_API_KEY") | ||
| .or_else(|_| std::env::var("FANTOMSCAN_API_KEY")) | ||
| .map_err(Into::into), | ||
|
|
||
| // Backwards compatibility, ideally these should return an error. | ||
| NamedChain::Gnosis | ||
| | NamedChain::Chiado | ||
| | NamedChain::Sepolia | ||
| | NamedChain::Rsk | ||
| | NamedChain::Sokol | ||
| | NamedChain::Poa | ||
| | NamedChain::Oasis | ||
| | NamedChain::Emerald | ||
| | NamedChain::EmeraldTestnet | ||
| | NamedChain::Evmos | ||
| | NamedChain::EvmosTestnet => Ok(String::new()), | ||
| NamedChain::AnvilHardhat | NamedChain::Dev => { | ||
| Err(EtherscanError::LocalNetworksNotSupported) | ||
| } | ||
| Self::new_with_api_version( | ||
| chain, | ||
| get_api_key_from_chain(chain, EtherscanApiVersion::V2)?, | ||
| EtherscanApiVersion::V2, | ||
| ) | ||
| } | ||
|
|
||
| _ => named | ||
| .etherscan_api_key_name() | ||
| .ok_or_else(|| EtherscanError::ChainNotSupported(chain)) | ||
| .and_then(|key_name| std::env::var(key_name).map_err(Into::into)), | ||
| }, | ||
| ChainKind::Id(_) => Err(EtherscanError::ChainNotSupported(chain)), | ||
| }?; | ||
| Self::new(chain, api_key) | ||
| /// Create a new client with the correct endpoints based on the chain and API key | ||
| /// from the default environment variable defined in [`Chain`]. | ||
| pub fn new_v1_from_env(chain: Chain) -> Result<Self> { | ||
| Self::new_with_api_version( | ||
| chain, | ||
| get_api_key_from_chain(chain, EtherscanApiVersion::V1)?, | ||
| EtherscanApiVersion::V1, | ||
| ) | ||
| } | ||
|
|
||
| /// Create a new client with the correct endpoints based on the chain and API key | ||
|
|
@@ -193,6 +198,11 @@ impl Client { | |
| &self.etherscan_url | ||
| } | ||
|
|
||
| /// Returns the configured API key, if any | ||
| pub fn api_key(&self) -> Option<&str> { | ||
| self.api_key.as_deref() | ||
| } | ||
|
|
||
| /// Return the URL for the given block number | ||
| pub fn block_url(&self, block: u64) -> String { | ||
| 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> { | |
| url.into_url() | ||
| } | ||
|
|
||
| fn get_api_key_from_chain( | ||
| chain: Chain, | ||
| api_version: EtherscanApiVersion, | ||
| ) -> Result<String, EtherscanError> { | ||
| match chain.kind() { | ||
| ChainKind::Named(named) => match named { | ||
| // Fantom is special and doesn't support etherscan api v2 | ||
| NamedChain::Fantom | NamedChain::FantomTestnet => std::env::var("FMTSCAN_API_KEY") | ||
| .or_else(|_| std::env::var("FANTOMSCAN_API_KEY")) | ||
| .map_err(Into::into), | ||
|
|
||
| // Backwards compatibility, ideally these should return an error. | ||
| NamedChain::Gnosis | ||
| | NamedChain::Chiado | ||
| | NamedChain::Sepolia | ||
| | NamedChain::Rsk | ||
| | NamedChain::Sokol | ||
| | NamedChain::Poa | ||
| | NamedChain::Oasis | ||
| | NamedChain::Emerald | ||
| | NamedChain::EmeraldTestnet | ||
| | NamedChain::Evmos | ||
| | NamedChain::EvmosTestnet => Ok(String::new()), | ||
| NamedChain::AnvilHardhat | NamedChain::Dev => { | ||
| Err(EtherscanError::LocalNetworksNotSupported) | ||
| } | ||
|
|
||
| // Rather than get special ENV vars here, normal case is to pull overall | ||
| // ETHERSCAN_API_KEY | ||
| _ => { | ||
| if api_version == EtherscanApiVersion::V1 { | ||
| named | ||
| .etherscan_api_key_name() | ||
| .ok_or_else(|| EtherscanError::ChainNotSupported(chain)) | ||
| .and_then(|key_name| std::env::var(key_name).map_err(Into::into)) | ||
| } else { | ||
| std::env::var("ETHERSCAN_API_KEY").map_err(Into::into) | ||
| } | ||
| } | ||
| }, | ||
| ChainKind::Id(_) => Err(EtherscanError::ChainNotSupported(chain)), | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::{Client, EtherscanError, ResponseData}; | ||
| use crate::{Client, EtherscanApiVersion, EtherscanError, ResponseData}; | ||
| use alloy_chains::Chain; | ||
| use alloy_primitives::{Address, B256}; | ||
|
|
||
|
|
@@ -602,13 +656,23 @@ mod tests { | |
| } | ||
|
|
||
| #[test] | ||
| fn test_api_paths() { | ||
| let client = Client::new(Chain::goerli(), "").unwrap(); | ||
| fn test_api_paths_v1() { | ||
| let client = | ||
| Client::new_with_api_version(Chain::goerli(), "", EtherscanApiVersion::V1).unwrap(); | ||
| assert_eq!(client.etherscan_api_url.as_str(), "https://api-goerli.etherscan.io/api"); | ||
|
|
||
| assert_eq!(client.block_url(100), "https://goerli.etherscan.io/block/100"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_api_paths_v2() { | ||
| let client = | ||
| Client::new_with_api_version(Chain::goerli(), "", EtherscanApiVersion::V2).unwrap(); | ||
| assert_eq!(client.etherscan_api_url.as_str(), "https://api.etherscan.io/v2/api"); | ||
|
|
||
| assert_eq!(client.block_url(100), "https://goerli.etherscan.io/block/100"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn stringifies_block_url() { | ||
| let etherscan = Client::new(Chain::mainnet(), "").unwrap(); | ||
|
|
@@ -657,4 +721,19 @@ mod tests { | |
| let resp: ResponseData<Address> = serde_json::from_value(err).unwrap(); | ||
| assert!(matches!(resp, ResponseData::Error { .. })); | ||
| } | ||
|
|
||
| #[test] | ||
| fn can_parse_api_version() { | ||
| assert_eq!( | ||
| EtherscanApiVersion::try_from("v1".to_string()).unwrap(), | ||
| EtherscanApiVersion::V1 | ||
| ); | ||
| assert_eq!( | ||
| EtherscanApiVersion::try_from("v2".to_string()).unwrap(), | ||
| EtherscanApiVersion::V2 | ||
| ); | ||
|
|
||
| let parse_err = EtherscanApiVersion::try_from("fail".to_string()).unwrap_err(); | ||
| assert!(matches!(parse_err, EtherscanError::InvalidApiVersion)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| {"expiry":1742116310,"data":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"deposit","inputs":[{"name":"pubkey","type":"bytes","internalType":"bytes"},{"name":"withdrawal_credentials","type":"bytes","internalType":"bytes"},{"name":"signature","type":"bytes","internalType":"bytes"},{"name":"deposit_data_root","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"get_deposit_count","inputs":[],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"get_deposit_root","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"pure"},{"type":"event","name":"DepositEvent","inputs":[{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"withdrawal_credentials","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"amount","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"signature","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"index","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false}]} | ||
| {"expiry":1746070810,"data":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"deposit","inputs":[{"name":"pubkey","type":"bytes","internalType":"bytes"},{"name":"withdrawal_credentials","type":"bytes","internalType":"bytes"},{"name":"signature","type":"bytes","internalType":"bytes"},{"name":"deposit_data_root","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"get_deposit_count","inputs":[],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"get_deposit_root","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"pure"},{"type":"event","name":"DepositEvent","inputs":[{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"withdrawal_credentials","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"amount","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"signature","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"index","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@grandizzy can we make this default already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option would be to not specify a default here and specify a default on the forge library.