Skip to content

Commit 18100fb

Browse files
iainnashmattsse
andauthored
Handle parsing and serialization of etherscan api version (#83)
To add Etherscan API Version types for reasonable parsing and serialization into the toml configuration files, these functions were added to block_explorers. See: foundry-rs/foundry#10298 --------- Co-authored-by: Matthias Seitz <[email protected]>
1 parent aef15ba commit 18100fb

File tree

7 files changed

+197
-60
lines changed

7 files changed

+197
-60
lines changed

crates/block-explorers/src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub enum EtherscanError {
4444
MissingSolcVersion(String),
4545
#[error("Invalid API Key")]
4646
InvalidApiKey,
47+
#[error("Invalid API Version")]
48+
InvalidApiVersion,
4749
#[error("Sorry, you have been blocked by Cloudflare, See also https://community.cloudflare.com/t/sorry-you-have-been-blocked/110790")]
4850
BlockedByCloudflare,
4951
#[error("The Requested prompted a cloudflare captcha security challenge to review the security of your connection before proceeding.")]

crates/block-explorers/src/lib.rs

Lines changed: 124 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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")]
5455
pub 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)]
6285
pub 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)]
591645
mod 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
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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}]}
1+
{"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}]}

0 commit comments

Comments
 (0)