Skip to content

Adding support to etherscan-v2 #10298

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ jiff = "0.2"
idna_adapter = "=1.1.0"

[patch.crates-io]
foundry-block-explorers = { git = "https://github.com/foundry-rs/block-explorers", rev = "8c03122" }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this package is released, the tag should be removed.

## alloy-core
# alloy-dyn-abi = { path = "../../alloy-rs/core/crates/dyn-abi" }
# alloy-json-abi = { path = "../../alloy-rs/core/crates/json-abi" }
Expand Down
73 changes: 62 additions & 11 deletions crates/config/src/etherscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use figment::{
value::{Dict, Map},
Error, Metadata, Profile, Provider,
};
use foundry_block_explorers::EtherscanApiVersion;
use heck::ToKebabCase;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
Expand Down Expand Up @@ -173,6 +174,9 @@ pub struct EtherscanConfig {
/// Etherscan API URL
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Etherscan API Version. Defaults to v2
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_version: Option<EtherscanApiVersion>,
/// The etherscan API KEY that's required to make requests
pub key: EtherscanApiKey,
}
Expand All @@ -188,7 +192,9 @@ impl EtherscanConfig {
self,
alias: Option<&str>,
) -> Result<ResolvedEtherscanConfig, EtherscanConfigError> {
let Self { chain, mut url, key } = self;
let Self { chain, mut url, key, api_version } = self;

let api_version_string = api_version.map(|v| v.to_string());

if let Some(url) = &mut url {
*url = interpolate(url)?;
Expand Down Expand Up @@ -219,17 +225,23 @@ impl EtherscanConfig {
match (chain, url) {
(Some(chain), Some(api_url)) => Ok(ResolvedEtherscanConfig {
api_url,
api_version: api_version_string,
browser_url: chain.etherscan_urls().map(|(_, url)| url.to_string()),
key,
chain: Some(chain),
}),
(Some(chain), None) => ResolvedEtherscanConfig::create(key, chain).ok_or_else(|| {
let msg = alias.map(|a| format!(" `{a}`")).unwrap_or_default();
EtherscanConfigError::UnknownChain(msg, chain)
(Some(chain), None) => ResolvedEtherscanConfig::create(key, chain, api_version_string)
.ok_or_else(|| {
let msg = alias.map(|a| format!(" `{a}`")).unwrap_or_default();
EtherscanConfigError::UnknownChain(msg, chain)
}),
(None, Some(api_url)) => Ok(ResolvedEtherscanConfig {
api_url,
browser_url: None,
key,
chain: None,
api_version: api_version_string,
}),
(None, Some(api_url)) => {
Ok(ResolvedEtherscanConfig { api_url, browser_url: None, key, chain: None })
}
(None, None) => {
let msg = alias
.map(|a| format!(" for Etherscan config with unknown alias `{a}`"))
Expand All @@ -251,18 +263,26 @@ pub struct ResolvedEtherscanConfig {
pub browser_url: Option<String>,
/// The resolved API key.
pub key: String,
/// Etherscan API Version.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_version: Option<String>,
Comment on lines +266 to +268
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also be just etherscanapiversion here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn’t too sure what the pattern was for the __Resolved struct if it was recommended to be a string since the other fields were and it’d be parsed later. If this is the case I can switch it :). Went back and forth.

/// The chain name or EIP-155 chain ID.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chain: Option<Chain>,
}

impl ResolvedEtherscanConfig {
/// Creates a new instance using the api key and chain
pub fn create(api_key: impl Into<String>, chain: impl Into<Chain>) -> Option<Self> {
pub fn create(
api_key: impl Into<String>,
chain: impl Into<Chain>,
api_version: Option<impl Into<String>>,
) -> Option<Self> {
let chain = chain.into();
let (api_url, browser_url) = chain.etherscan_urls()?;
Some(Self {
api_url: api_url.to_string(),
api_version: api_version.map(|v| v.into()),
browser_url: Some(browser_url.to_string()),
key: api_key.into(),
chain: Some(chain),
Expand Down Expand Up @@ -294,7 +314,7 @@ impl ResolvedEtherscanConfig {
self,
) -> Result<foundry_block_explorers::Client, foundry_block_explorers::errors::EtherscanError>
{
let Self { api_url, browser_url, key: api_key, chain } = self;
let Self { api_url, browser_url, key: api_key, chain, api_version } = self;
let (mainnet_api, mainnet_url) = NamedChain::Mainnet.etherscan_urls().expect("exist; qed");

let cache = chain
Expand All @@ -310,12 +330,14 @@ impl ResolvedEtherscanConfig {
}

let api_url = into_url(&api_url)?;
let parsed_api_version = api_version.map(EtherscanApiVersion::try_from).transpose()?;
let client = reqwest::Client::builder()
.user_agent(ETHERSCAN_USER_AGENT)
.tls_built_in_root_certs(api_url.scheme() == "https")
.build()?;
foundry_block_explorers::Client::builder()
.with_client(client)
.with_api_version(parsed_api_version.unwrap_or_default())
.with_api_key(api_key)
.with_api_url(api_url)?
// the browser url is not used/required by the client so we can simply set the
Expand Down Expand Up @@ -423,12 +445,36 @@ mod tests {
chain: Some(Mainnet.into()),
url: None,
key: EtherscanApiKey::Key("ABCDEFG".to_string()),
api_version: None,
},
);

let mut resolved = configs.resolved();
let config = resolved.remove("mainnet").unwrap().unwrap();
let _ = config.into_client().unwrap();
// None version = None
assert_eq!(config.api_version, None);
let client = config.into_client().unwrap();
assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2);
}

#[test]
fn can_create_v1_client_via_chain() {
let mut configs = EtherscanConfigs::default();
configs.insert(
"mainnet".to_string(),
EtherscanConfig {
chain: Some(Mainnet.into()),
url: None,
api_version: Some(EtherscanApiVersion::V1),
key: EtherscanApiKey::Key("ABCDEG".to_string()),
},
);

let mut resolved = configs.resolved();
let config = resolved.remove("mainnet").unwrap().unwrap();
assert_eq!(config.api_version, Some("v1".to_string()));
let client = config.into_client().unwrap();
assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V1);
}

#[test]
Expand All @@ -440,6 +486,7 @@ mod tests {
chain: Some(Mainnet.into()),
url: Some("https://api.etherscan.io/api".to_string()),
key: EtherscanApiKey::Key("ABCDEFG".to_string()),
api_version: None,
},
);

Expand All @@ -457,6 +504,7 @@ mod tests {
EtherscanConfig {
chain: Some(Mainnet.into()),
url: Some("https://api.etherscan.io/api".to_string()),
api_version: None,
key: EtherscanApiKey::Env(format!("${{{env}}}")),
},
);
Expand All @@ -470,7 +518,8 @@ mod tests {
let mut resolved = configs.resolved();
let config = resolved.remove("mainnet").unwrap().unwrap();
assert_eq!(config.key, "ABCDEFG");
let _ = config.into_client().unwrap();
let client = config.into_client().unwrap();
assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2);

std::env::remove_var(env);
}
Expand All @@ -484,6 +533,7 @@ mod tests {
chain: None,
url: Some("https://api.etherscan.io/api".to_string()),
key: EtherscanApiKey::Key("ABCDEFG".to_string()),
api_version: None,
},
);

Expand All @@ -498,6 +548,7 @@ mod tests {
chain: None,
url: Some("https://api.etherscan.io/api".to_string()),
key: EtherscanApiKey::Key("ABCDEFG".to_string()),
api_version: None,
};
let resolved = config.clone().resolve(Some("base_sepolia")).unwrap();
assert_eq!(resolved.chain, Some(Chain::base_sepolia()));
Expand Down
59 changes: 58 additions & 1 deletion crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,7 @@ impl Config {
// etherscan fallback via API key
if let Some(key) = self.etherscan_api_key.as_ref() {
let chain = chain.or(self.chain).unwrap_or_default();
return Ok(ResolvedEtherscanConfig::create(key, chain));
return Ok(ResolvedEtherscanConfig::create(key, chain, None::<String>));
}

Ok(None)
Expand Down Expand Up @@ -3081,6 +3081,7 @@ mod tests {
api_url: mainnet_urls.0.to_string(),
chain: Some(NamedChain::Mainnet.into()),
browser_url: Some(mainnet_urls.1.to_string()),
api_version: None,
key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
}
),
Expand All @@ -3090,6 +3091,62 @@ mod tests {
api_url: mb_urls.0.to_string(),
chain: Some(Moonbeam.into()),
browser_url: Some(mb_urls.1.to_string()),
api_version: None,
key: "123456789".to_string(),
}
),
])
);

Ok(())
});
}

#[test]
fn test_resolve_etherscan_with_versions() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"foundry.toml",
r#"
[profile.default]

[etherscan]
mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
"#,
)?;

let config = Config::load().unwrap();

assert!(config.etherscan.clone().resolved().has_unresolved());

jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");

let configs = config.etherscan.resolved();
assert!(!configs.has_unresolved());

let mb_urls = Moonbeam.etherscan_urls().unwrap();
let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
assert_eq!(
configs,
ResolvedEtherscanConfigs::new([
(
"mainnet",
ResolvedEtherscanConfig {
api_url: mainnet_urls.0.to_string(),
chain: Some(NamedChain::Mainnet.into()),
browser_url: Some(mainnet_urls.1.to_string()),
api_version: Some("v2".to_string()),
key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
}
),
(
"moonbeam",
ResolvedEtherscanConfig {
api_url: mb_urls.0.to_string(),
chain: Some(Moonbeam.into()),
browser_url: Some(mb_urls.1.to_string()),
api_version: Some("v1".to_string()),
key: "123456789".to_string(),
}
),
Expand Down
Loading
Loading