From 97e23a6c8a5f4b5d7e3d8649edd00f58e1326c24 Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Sun, 13 Apr 2025 23:25:47 -0400 Subject: [PATCH 01/15] Adding support to etherscanv2 --- crates/verify/src/etherscan/mod.rs | 96 +++++++++++++++++++++++++++++- crates/verify/src/provider.rs | 21 +++++-- 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index aad51093886d7..15233d57fc4bc 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -11,7 +11,7 @@ use eyre::{eyre, Context, OptionExt, Result}; use foundry_block_explorers::{ errors::EtherscanError, utils::lookup_compiler_version, - verify::{CodeFormat, VerifyContract}, + verify::{self, CodeFormat, VerifyContract}, Client, }; use foundry_cli::utils::{get_provider, read_constructor_args_file, LoadConfig}; @@ -261,7 +261,13 @@ impl EtherscanVerificationProvider { let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?; let etherscan_api_url = verifier_url - .or_else(|| etherscan_config.as_ref().map(|c| c.api_url.as_str())) + .or_else(|| { + if verifier_type.is_etherscan_v2() { + None + } else { + etherscan_config.as_ref().map(|c| c.api_url.as_str()) + } + }) .map(str::to_owned); let api_url = etherscan_api_url.as_deref(); @@ -272,8 +278,14 @@ impl EtherscanVerificationProvider { let etherscan_key = etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.as_str())); + + let etherscan_api_version = if verifier_type.is_etherscan_v2() { + foundry_block_explorers::EtherscanApiVersion::V2 + } else { + foundry_block_explorers::EtherscanApiVersion::V1 + }; - let mut builder = Client::builder(); + let mut builder = Client::builder().with_api_version(etherscan_api_version); builder = if let Some(api_url) = api_url { // we don't want any trailing slashes because this can cause cloudflare issues: @@ -466,6 +478,7 @@ async fn ensure_solc_build_metadata(version: Version) -> Result { mod tests { use super::*; use clap::Parser; + use foundry_block_explorers::EtherscanApiVersion; use foundry_common::fs; use foundry_test_utils::{forgetest_async, str}; use tempfile::tempdir; @@ -539,6 +552,83 @@ mod tests { assert!(format!("{client:?}").contains("dummykey")); } + #[test] + fn can_extract_etherscan_v2_verify_config() { + let temp = tempdir().unwrap(); + let root = temp.path(); + + let config = r#" + [profile.default] + + [etherscan] + mumbai = { key = "dummykey", chain = 80001, url = "https://api-testnet.polygonscan.com/" } + "#; + + let toml_file = root.join(Config::FILE_NAME); + fs::write(toml_file, config).unwrap(); + + let args: VerifyArgs = VerifyArgs::parse_from([ + "foundry-cli", + "0xd8509bee9c9bf012282ad33aba0d87241baf5064", + "src/Counter.sol:Counter", + "--verifier", + "etherscan-v2", + "--chain", + "mumbai", + "--root", + root.as_os_str().to_str().unwrap(), + ]); + + let config = args.load_config().unwrap(); + + let etherscan = EtherscanVerificationProvider::default(); + + let client = etherscan + .client( + args.etherscan.chain.unwrap_or_default(), + &args.verifier.verifier, + args.verifier.verifier_url.as_deref(), + args.etherscan.key().as_deref(), + &config, + ) + .unwrap(); + + assert_eq!(client.etherscan_api_url().as_str(), "https://api.etherscan.io/v2/api?chainid=80001"); + assert!(format!("{client:?}").contains("dummykey")); + + let args: VerifyArgs = VerifyArgs::parse_from([ + "foundry-cli", + "0xd8509bee9c9bf012282ad33aba0d87241baf5064", + "src/Counter.sol:Counter", + "--verifier", + "etherscan-v2", + "--chain", + "mumbai", + "--verifier-url", + "https://verifier-url.com/", + "--root", + root.as_os_str().to_str().unwrap(), + ]); + + let config = args.load_config().unwrap(); + + assert_eq!(args.verifier.verifier, VerificationProviderType::EtherscanV2); + + let etherscan = EtherscanVerificationProvider::default(); + let client = etherscan + .client( + args.etherscan.chain.unwrap_or_default(), + &args.verifier.verifier, + args.verifier.verifier_url.as_deref(), + args.etherscan.key().as_deref(), + &config, + ) + .unwrap(); + assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/"); + assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2); + assert!(format!("{client:?}").contains("dummykey")); + } + #[tokio::test(flavor = "multi_thread")] async fn fails_on_disabled_cache_and_missing_info() { let temp = tempdir().unwrap(); diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index 74d2094c41b83..b290916670b03 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -123,11 +123,12 @@ impl FromStr for VerificationProviderType { fn from_str(s: &str) -> Result { match s { - "e" | "etherscan" => Ok(Self::Etherscan), - "s" | "sourcify" => Ok(Self::Sourcify), - "b" | "blockscout" => Ok(Self::Blockscout), - "o" | "oklink" => Ok(Self::Oklink), - "c" | "custom" => Ok(Self::Custom), + "e" | "etherscan" => Ok(Self::Etherscan), + "ev2" | "etherscan-v2" => Ok(Self::EtherscanV2), + "s" | "sourcify" => Ok(Self::Sourcify), + "b" | "blockscout" => Ok(Self::Blockscout), + "o" | "oklink" => Ok(Self::Oklink), + "c" | "custom" => Ok(Self::Custom), _ => Err(format!("Unknown provider: {s}")), } } @@ -139,6 +140,9 @@ impl fmt::Display for VerificationProviderType { Self::Etherscan => { write!(f, "etherscan")?; } + Self::EtherscanV2 => { + write!(f, "etherscan-v2")?; + } Self::Sourcify => { write!(f, "sourcify")?; } @@ -159,6 +163,7 @@ impl fmt::Display for VerificationProviderType { #[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)] pub enum VerificationProviderType { Etherscan, + EtherscanV2, #[default] Sourcify, Blockscout, @@ -208,6 +213,10 @@ impl VerificationProviderType { } pub fn is_etherscan(&self) -> bool { - matches!(self, Self::Etherscan) + matches!(self, Self::Etherscan) || matches!(self, Self::EtherscanV2) + } + + pub fn is_etherscan_v2(&self) -> bool { + matches!(self, Self::EtherscanV2) } } From a98d6d0bd11914d7229921ace8e09495812078da Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Sun, 13 Apr 2025 23:40:35 -0400 Subject: [PATCH 02/15] clippy+fmt --- crates/verify/src/etherscan/mod.rs | 11 ++++++----- crates/verify/src/provider.rs | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 15233d57fc4bc..7a4ac9d76deaa 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -11,7 +11,7 @@ use eyre::{eyre, Context, OptionExt, Result}; use foundry_block_explorers::{ errors::EtherscanError, utils::lookup_compiler_version, - verify::{self, CodeFormat, VerifyContract}, + verify::{CodeFormat, VerifyContract}, Client, }; use foundry_cli::utils::{get_provider, read_constructor_args_file, LoadConfig}; @@ -275,10 +275,8 @@ impl EtherscanVerificationProvider { .as_ref() .and_then(|c| c.browser_url.as_deref()) .or_else(|| chain.etherscan_urls().map(|(_, url)| url)); - let etherscan_key = etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.as_str())); - let etherscan_api_version = if verifier_type.is_etherscan_v2() { foundry_block_explorers::EtherscanApiVersion::V2 } else { @@ -593,7 +591,10 @@ mod tests { ) .unwrap(); - assert_eq!(client.etherscan_api_url().as_str(), "https://api.etherscan.io/v2/api?chainid=80001"); + assert_eq!( + client.etherscan_api_url().as_str(), + "https://api.etherscan.io/v2/api?chainid=80001" + ); assert!(format!("{client:?}").contains("dummykey")); let args: VerifyArgs = VerifyArgs::parse_from([ @@ -611,7 +612,7 @@ mod tests { ]); let config = args.load_config().unwrap(); - + assert_eq!(args.verifier.verifier, VerificationProviderType::EtherscanV2); let etherscan = EtherscanVerificationProvider::default(); diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index b290916670b03..1364aac4462de 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -123,12 +123,12 @@ impl FromStr for VerificationProviderType { fn from_str(s: &str) -> Result { match s { - "e" | "etherscan" => Ok(Self::Etherscan), + "e" | "etherscan" => Ok(Self::Etherscan), "ev2" | "etherscan-v2" => Ok(Self::EtherscanV2), - "s" | "sourcify" => Ok(Self::Sourcify), - "b" | "blockscout" => Ok(Self::Blockscout), - "o" | "oklink" => Ok(Self::Oklink), - "c" | "custom" => Ok(Self::Custom), + "s" | "sourcify" => Ok(Self::Sourcify), + "b" | "blockscout" => Ok(Self::Blockscout), + "o" | "oklink" => Ok(Self::Oklink), + "c" | "custom" => Ok(Self::Custom), _ => Err(format!("Unknown provider: {s}")), } } From 395712a30cf108b21e8a50bd3c745cf3dce81260 Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Tue, 29 Apr 2025 23:04:15 -0400 Subject: [PATCH 03/15] Adding default for v2 and updating configuration to parse etherscan api version from config --- crates/config/src/etherscan.rs | 72 +++++++++++++++++++++++++---- crates/config/src/lib.rs | 73 +++++++++++++++++++++++++++--- crates/verify/src/etherscan/mod.rs | 2 +- 3 files changed, 131 insertions(+), 16 deletions(-) diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index 099dc0e344c2f..0a9f6a292aebb 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -10,6 +10,8 @@ use figment::{ Error, Metadata, Profile, Provider, }; use heck::ToKebabCase; +use foundry_block_explorers::EtherscanApiVersion; +use inflector::Inflector; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{ collections::BTreeMap, @@ -173,6 +175,9 @@ pub struct EtherscanConfig { /// Etherscan API URL #[serde(default, skip_serializing_if = "Option::is_none")] pub url: Option, + /// Etherscan API Version. Defaults to v2 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api_version: Option, /// The etherscan API KEY that's required to make requests pub key: EtherscanApiKey, } @@ -188,7 +193,7 @@ impl EtherscanConfig { self, alias: Option<&str>, ) -> Result { - let Self { chain, mut url, key } = self; + let Self { chain, mut url, key, api_version } = self; if let Some(url) = &mut url { *url = interpolate(url)?; @@ -219,17 +224,27 @@ impl EtherscanConfig { match (chain, url) { (Some(chain), Some(api_url)) => Ok(ResolvedEtherscanConfig { api_url, + api_version: api_version.map(|v| v.to_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(|| { + (Some(chain), None) => ResolvedEtherscanConfig::create( + key, + chain, + api_version.map(|v| v.to_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 }) - } + (None, Some(api_url)) => Ok(ResolvedEtherscanConfig { + api_url, + browser_url: None, + key, + chain: None, + api_version: api_version.map(|v| v.to_string()), + }), (None, None) => { let msg = alias .map(|a| format!(" for Etherscan config with unknown alias `{a}`")) @@ -251,6 +266,9 @@ pub struct ResolvedEtherscanConfig { pub browser_url: Option, /// The resolved API key. pub key: String, + /// Etherscan API Version. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api_version: Option, /// The chain name or EIP-155 chain ID. #[serde(default, skip_serializing_if = "Option::is_none")] pub chain: Option, @@ -258,11 +276,16 @@ pub struct ResolvedEtherscanConfig { impl ResolvedEtherscanConfig { /// Creates a new instance using the api key and chain - pub fn create(api_key: impl Into, chain: impl Into) -> Option { + pub fn create( + api_key: impl Into, + chain: impl Into, + api_version: Option>, + ) -> Option { 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), @@ -294,7 +317,7 @@ impl ResolvedEtherscanConfig { self, ) -> Result { - 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 @@ -310,12 +333,14 @@ impl ResolvedEtherscanConfig { } let api_url = into_url(&api_url)?; + let parsed_api_version = EtherscanApiVersion::try_from(api_version.unwrap_or_default())?; 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) .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 @@ -423,12 +448,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] @@ -440,6 +489,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, }, ); @@ -457,6 +507,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}}}")), }, ); @@ -470,7 +521,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); } @@ -484,6 +536,7 @@ mod tests { chain: None, url: Some("https://api.etherscan.io/api".to_string()), key: EtherscanApiKey::Key("ABCDEFG".to_string()), + api_version: None, }, ); @@ -498,6 +551,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())); diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b7d3f0f7e5c0d..2b8c25d362bc1 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1137,9 +1137,9 @@ impl Config { /// Whether caching should be enabled for the given chain id pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into) -> bool { - !self.no_storage_caching && - self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) && - self.rpc_storage_caching.enable_for_endpoint(endpoint) + !self.no_storage_caching + && self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) + && self.rpc_storage_caching.enable_for_endpoint(endpoint) } /// Returns the `ProjectPathsConfig` sub set of the config. @@ -1403,7 +1403,11 @@ 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::, + )); } Ok(None) @@ -2008,8 +2012,8 @@ impl Config { let file_name = block.file_name(); let filepath = if file_type.is_dir() { block.path().join("storage.json") - } else if file_type.is_file() && - file_name.to_string_lossy().chars().all(char::is_numeric) + } else if file_type.is_file() + && file_name.to_string_lossy().chars().all(char::is_numeric) { block.path() } else { @@ -3077,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(), } ), @@ -3086,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(), } ), diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 7a4ac9d76deaa..2907bcc1d0382 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -593,7 +593,7 @@ mod tests { assert_eq!( client.etherscan_api_url().as_str(), - "https://api.etherscan.io/v2/api?chainid=80001" + "https://api.etherscan.io/v2/api" ); assert!(format!("{client:?}").contains("dummykey")); From 5866805a31a9b7c0bfbb8ed860ec88f0225b56db Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Tue, 29 Apr 2025 23:52:44 -0400 Subject: [PATCH 04/15] Updating api_version to use new variable and fix merge --- crates/config/src/etherscan.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index 0a9f6a292aebb..26c2ed7587876 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -9,9 +9,8 @@ use figment::{ value::{Dict, Map}, Error, Metadata, Profile, Provider, }; -use heck::ToKebabCase; use foundry_block_explorers::EtherscanApiVersion; -use inflector::Inflector; +use heck::ToKebabCase; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{ collections::BTreeMap, @@ -195,6 +194,8 @@ impl EtherscanConfig { ) -> Result { 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)?; } @@ -224,26 +225,22 @@ impl EtherscanConfig { match (chain, url) { (Some(chain), Some(api_url)) => Ok(ResolvedEtherscanConfig { api_url, - api_version: api_version.map(|v| v.to_string()), + 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, - api_version.map(|v| v.to_string()), - ) - .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.map(|v| v.to_string()), + api_version: api_version_string, }), (None, None) => { let msg = alias From 735212e261bbd9255fae2993361644b3ad496145 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Wed, 30 Apr 2025 11:32:12 +0300 Subject: [PATCH 05/15] Use block explorer rev, fix fmt --- Cargo.lock | 3 +-- Cargo.toml | 1 + crates/config/src/lib.rs | 16 ++++++---------- crates/verify/src/etherscan/mod.rs | 5 +---- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9fec46ff7e011..55b48168a0859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3698,8 +3698,7 @@ dependencies = [ [[package]] name = "foundry-block-explorers" version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001678abc9895502532c8c4a1a225079c580655fc82a194e78b06dcf99f49b8c" +source = "git+https://github.com/foundry-rs/block-explorers?rev=8c03122#8c0312275fecf11b2258622fe1f7eba89e8f0a8e" dependencies = [ "alloy-chains", "alloy-json-abi", diff --git a/Cargo.toml b/Cargo.toml index 2d8aeadaa72f2..1fd0b89c1a59a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } ## alloy-core # alloy-dyn-abi = { path = "../../alloy-rs/core/crates/dyn-abi" } # alloy-json-abi = { path = "../../alloy-rs/core/crates/json-abi" } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 2b8c25d362bc1..67bcff5e7e037 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1137,9 +1137,9 @@ impl Config { /// Whether caching should be enabled for the given chain id pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into) -> bool { - !self.no_storage_caching - && self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) - && self.rpc_storage_caching.enable_for_endpoint(endpoint) + !self.no_storage_caching && + self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) && + self.rpc_storage_caching.enable_for_endpoint(endpoint) } /// Returns the `ProjectPathsConfig` sub set of the config. @@ -1403,11 +1403,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, - None::, - )); + return Ok(ResolvedEtherscanConfig::create(key, chain, None::)); } Ok(None) @@ -2012,8 +2008,8 @@ impl Config { let file_name = block.file_name(); let filepath = if file_type.is_dir() { block.path().join("storage.json") - } else if file_type.is_file() - && file_name.to_string_lossy().chars().all(char::is_numeric) + } else if file_type.is_file() && + file_name.to_string_lossy().chars().all(char::is_numeric) { block.path() } else { diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 2907bcc1d0382..4a1d384a9298f 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -591,10 +591,7 @@ mod tests { ) .unwrap(); - assert_eq!( - client.etherscan_api_url().as_str(), - "https://api.etherscan.io/v2/api" - ); + assert_eq!(client.etherscan_api_url().as_str(), "https://api.etherscan.io/v2/api"); assert!(format!("{client:?}").contains("dummykey")); let args: VerifyArgs = VerifyArgs::parse_from([ From e2f4f112698760bf87b2402b78aa990592ee4764 Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Wed, 30 Apr 2025 22:57:23 -0400 Subject: [PATCH 06/15] fix api version parsing --- crates/config/src/etherscan.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index 26c2ed7587876..41161db2472c4 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -330,14 +330,16 @@ impl ResolvedEtherscanConfig { } let api_url = into_url(&api_url)?; - let parsed_api_version = EtherscanApiVersion::try_from(api_version.unwrap_or_default())?; + 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) + .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 From 3d60de4aa858412e435df7b65d7ec572f5845d46 Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Wed, 30 Apr 2025 23:05:09 -0400 Subject: [PATCH 07/15] fix fmt --- crates/config/src/etherscan.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index 41161db2472c4..917946f1b2bc0 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -330,9 +330,7 @@ impl ResolvedEtherscanConfig { } let api_url = into_url(&api_url)?; - let parsed_api_version = api_version - .map(EtherscanApiVersion::try_from) - .transpose()?; + 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") From 3f82c09e2d16c45775ef3b7f696eaf618a8548d2 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Mon, 5 May 2025 09:44:01 +0300 Subject: [PATCH 08/15] Simplify Etherscan provider option, default v2 --- crates/cli/src/opts/rpc.rs | 10 ++++++++ crates/config/src/etherscan.rs | 26 ++++++++++--------- crates/config/src/lib.rs | 22 ++++++++++++---- crates/forge/src/cmd/create.rs | 7 +++++- crates/forge/tests/cli/config.rs | 2 ++ crates/verify/src/bytecode.rs | 1 + crates/verify/src/etherscan/mod.rs | 40 +++++++++++++++++++++--------- crates/verify/src/provider.rs | 11 +------- crates/verify/src/verify.rs | 9 ++++--- 9 files changed, 85 insertions(+), 43 deletions(-) diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 344efe73e8514..2d4dfff006b6b 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -114,6 +114,11 @@ pub struct EtherscanOpts { #[serde(rename = "etherscan_api_key", skip_serializing_if = "Option::is_none")] pub key: Option, + /// The Etherscan API version. + #[arg(short, long = "etherscan-api-version", env = "ETHERSCAN_API_VERSION")] + #[serde(rename = "etherscan_api_version", skip_serializing_if = "Option::is_none")] + pub api_version: Option, + /// The chain name or EIP-155 chain ID. #[arg( short, @@ -154,6 +159,11 @@ impl EtherscanOpts { if let Some(key) = self.key() { dict.insert("etherscan_api_key".into(), key.into()); } + + if let Some(api_version) = &self.api_version { + dict.insert("etherscan_api_version".into(), api_version.to_string().into()); + } + if let Some(chain) = self.chain { if let ChainKind::Id(id) = chain.kind() { dict.insert("chain_id".into(), (*id).into()); diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index 917946f1b2bc0..e49880bc7565a 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -58,6 +58,9 @@ pub enum EtherscanConfigError { #[error("At least one of `url` or `chain` must be present{0}")] MissingUrlOrChain(String), + + #[error("Invalid Etherscan API version {0}")] + InvalidApiVersion(String), } /// Container type for Etherscan API keys and URLs. @@ -194,7 +197,7 @@ impl EtherscanConfig { ) -> Result { let Self { chain, mut url, key, api_version } = self; - let api_version_string = api_version.map(|v| v.to_string()); + let api_version = api_version.unwrap_or(EtherscanApiVersion::V2); if let Some(url) = &mut url { *url = interpolate(url)?; @@ -225,12 +228,12 @@ impl EtherscanConfig { match (chain, url) { (Some(chain), Some(api_url)) => Ok(ResolvedEtherscanConfig { api_url, - api_version: api_version_string, + api_version, browser_url: chain.etherscan_urls().map(|(_, url)| url.to_string()), key, chain: Some(chain), }), - (Some(chain), None) => ResolvedEtherscanConfig::create(key, chain, api_version_string) + (Some(chain), None) => ResolvedEtherscanConfig::create(key, chain, api_version) .ok_or_else(|| { let msg = alias.map(|a| format!(" `{a}`")).unwrap_or_default(); EtherscanConfigError::UnknownChain(msg, chain) @@ -240,7 +243,7 @@ impl EtherscanConfig { browser_url: None, key, chain: None, - api_version: api_version_string, + api_version, }), (None, None) => { let msg = alias @@ -264,8 +267,8 @@ pub struct ResolvedEtherscanConfig { /// The resolved API key. pub key: String, /// Etherscan API Version. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub api_version: Option, + #[serde(default)] + pub api_version: EtherscanApiVersion, /// The chain name or EIP-155 chain ID. #[serde(default, skip_serializing_if = "Option::is_none")] pub chain: Option, @@ -276,13 +279,13 @@ impl ResolvedEtherscanConfig { pub fn create( api_key: impl Into, chain: impl Into, - api_version: Option>, + api_version: EtherscanApiVersion, ) -> Option { 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()), + api_version, browser_url: Some(browser_url.to_string()), key: api_key.into(), chain: Some(chain), @@ -330,14 +333,13 @@ 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_version(api_version) .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 @@ -452,7 +454,7 @@ mod tests { let mut resolved = configs.resolved(); let config = resolved.remove("mainnet").unwrap().unwrap(); // None version = None - assert_eq!(config.api_version, None); + assert_eq!(config.api_version, EtherscanApiVersion::V2); let client = config.into_client().unwrap(); assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2); } @@ -472,7 +474,7 @@ mod tests { let mut resolved = configs.resolved(); let config = resolved.remove("mainnet").unwrap().unwrap(); - assert_eq!(config.api_version, Some("v1".to_string())); + assert_eq!(config.api_version, EtherscanApiVersion::V1); let client = config.into_client().unwrap(); assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V1); } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 592be139824e8..befdcfac55858 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -96,6 +96,7 @@ pub mod fix; // reexport so cli types can implement `figment::Provider` to easily merge compiler arguments pub use alloy_chains::{Chain, NamedChain}; pub use figment; +use foundry_block_explorers::EtherscanApiVersion; pub mod providers; pub use providers::Remappings; @@ -120,6 +121,7 @@ mod bind_json; use bind_json::BindJsonConfig; mod compilation; +use crate::etherscan::EtherscanConfigError::InvalidApiVersion; pub use compilation::{CompilationRestrictions, SettingsOverrides}; /// Foundry configuration @@ -281,6 +283,8 @@ pub struct Config { pub eth_rpc_headers: Option>, /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table pub etherscan_api_key: Option, + /// etherscan API version + pub etherscan_api_version: Option, /// Multiple etherscan api configs and their aliases #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")] pub etherscan: EtherscanConfigs, @@ -1406,7 +1410,14 @@ 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, None::)); + + let api_version = match self.etherscan_api_version.as_ref() { + Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string()) + .map_err(|_| InvalidApiVersion(api_version.to_string()))?, + None => EtherscanApiVersion::V2, + }; + + return Ok(ResolvedEtherscanConfig::create(key, chain, api_version)); } Ok(None) @@ -2369,6 +2380,7 @@ impl Default for Config { eth_rpc_timeout: None, eth_rpc_headers: None, etherscan_api_key: None, + etherscan_api_version: None, verbosity: 0, remappings: vec![], auto_detect_remappings: true, @@ -3081,7 +3093,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, + api_version: EtherscanApiVersion::V2, key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(), } ), @@ -3091,7 +3103,7 @@ mod tests { api_url: mb_urls.0.to_string(), chain: Some(Moonbeam.into()), browser_url: Some(mb_urls.1.to_string()), - api_version: None, + api_version: EtherscanApiVersion::V2, key: "123456789".to_string(), } ), @@ -3136,7 +3148,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: Some("v2".to_string()), + api_version: EtherscanApiVersion::V2, key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(), } ), @@ -3146,7 +3158,7 @@ mod tests { 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()), + api_version: EtherscanApiVersion::V1, key: "123456789".to_string(), } ), diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 744c38d71d977..1c9ca1cbbfc7a 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -228,6 +228,7 @@ impl CreateArgs { num_of_optimizations: None, etherscan: EtherscanOpts { key: self.eth.etherscan.key.clone(), + api_version: self.eth.etherscan.api_version.clone(), chain: Some(chain.into()), }, rpc: Default::default(), @@ -416,7 +417,11 @@ impl CreateArgs { constructor_args, constructor_args_path: None, num_of_optimizations, - etherscan: EtherscanOpts { key: self.eth.etherscan.key(), chain: Some(chain.into()) }, + etherscan: EtherscanOpts { + key: self.eth.etherscan.key(), + api_version: self.eth.etherscan.api_version.clone(), + chain: Some(chain.into()), + }, rpc: Default::default(), flatten: false, force: false, diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 799365fcd1b03..41db96842d0ef 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -119,6 +119,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { eth_rpc_timeout: None, eth_rpc_headers: None, etherscan_api_key: None, + etherscan_api_version: None, etherscan: Default::default(), verbosity: 4, remappings: vec![Remapping::from_str("forge-std/=lib/forge-std/").unwrap().into()], @@ -1161,6 +1162,7 @@ exclude = [] "eth_rpc_timeout": null, "eth_rpc_headers": null, "etherscan_api_key": null, + "etherscan_api_version": null, "ignored_error_codes": [ "license", "code-size", diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index 66e95c7e790b3..f5b4b5ab77c9b 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -141,6 +141,7 @@ impl VerifyBytecodeArgs { &self.verifier.verifier, self.verifier.verifier_url.as_deref(), self.etherscan.key().as_deref(), + self.verifier.verifier_api_version.as_deref(), &config, )?; diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 4a1d384a9298f..b8e225a532fd3 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -12,7 +12,7 @@ use foundry_block_explorers::{ errors::EtherscanError, utils::lookup_compiler_version, verify::{CodeFormat, VerifyContract}, - Client, + Client, EtherscanApiVersion, }; use foundry_cli::utils::{get_provider, read_constructor_args_file, LoadConfig}; use foundry_common::{abi::encode_function_args, retry::RetryError}; @@ -155,6 +155,7 @@ impl VerificationProvider for EtherscanVerificationProvider { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key().as_deref(), + args.verifier.verifier_api_version.as_deref(), &config, )?; args.retry @@ -224,6 +225,7 @@ impl EtherscanVerificationProvider { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key().as_deref(), + args.verifier.verifier_api_version.as_deref(), &config, )?; let verify_args = self.create_verify_request(args, context).await?; @@ -256,13 +258,28 @@ impl EtherscanVerificationProvider { verifier_type: &VerificationProviderType, verifier_url: Option<&str>, etherscan_key: Option<&str>, + verifier_api_version: Option<&str>, config: &Config, ) -> Result { let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?; + let api_version = match verifier_api_version { + Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string())?, + None => { + if verifier_type.is_etherscan() { + etherscan_config + .as_ref() + .map(|c| c.api_version) + .unwrap_or(EtherscanApiVersion::V2) + } else { + EtherscanApiVersion::V1 + } + } + }; + let etherscan_api_url = verifier_url .or_else(|| { - if verifier_type.is_etherscan_v2() { + if api_version == EtherscanApiVersion::V2 { None } else { etherscan_config.as_ref().map(|c| c.api_url.as_str()) @@ -277,13 +294,8 @@ impl EtherscanVerificationProvider { .or_else(|| chain.etherscan_urls().map(|(_, url)| url)); let etherscan_key = etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.as_str())); - let etherscan_api_version = if verifier_type.is_etherscan_v2() { - foundry_block_explorers::EtherscanApiVersion::V2 - } else { - foundry_block_explorers::EtherscanApiVersion::V1 - }; - let mut builder = Client::builder().with_api_version(etherscan_api_version); + let mut builder = Client::builder().with_api_version(api_version); builder = if let Some(api_url) = api_url { // we don't want any trailing slashes because this can cause cloudflare issues: @@ -407,6 +419,7 @@ impl EtherscanVerificationProvider { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key.as_deref(), + args.verifier.verifier_api_version.as_deref(), &context.config, )?; @@ -476,7 +489,6 @@ async fn ensure_solc_build_metadata(version: Version) -> Result { mod tests { use super::*; use clap::Parser; - use foundry_block_explorers::EtherscanApiVersion; use foundry_common::fs; use foundry_test_utils::{forgetest_async, str}; use tempfile::tempdir; @@ -515,6 +527,7 @@ mod tests { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key().as_deref(), + args.verifier.verifier_api_version.as_deref(), &config, ) .unwrap(); @@ -543,6 +556,7 @@ mod tests { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key().as_deref(), + args.verifier.verifier_api_version.as_deref(), &config, ) .unwrap(); @@ -570,7 +584,7 @@ mod tests { "0xd8509bee9c9bf012282ad33aba0d87241baf5064", "src/Counter.sol:Counter", "--verifier", - "etherscan-v2", + "etherscan", "--chain", "mumbai", "--root", @@ -587,6 +601,7 @@ mod tests { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key().as_deref(), + args.verifier.verifier_api_version.as_deref(), &config, ) .unwrap(); @@ -599,7 +614,7 @@ mod tests { "0xd8509bee9c9bf012282ad33aba0d87241baf5064", "src/Counter.sol:Counter", "--verifier", - "etherscan-v2", + "etherscan", "--chain", "mumbai", "--verifier-url", @@ -610,7 +625,7 @@ mod tests { let config = args.load_config().unwrap(); - assert_eq!(args.verifier.verifier, VerificationProviderType::EtherscanV2); + assert_eq!(args.verifier.verifier, VerificationProviderType::Etherscan); let etherscan = EtherscanVerificationProvider::default(); let client = etherscan @@ -619,6 +634,7 @@ mod tests { &args.verifier.verifier, args.verifier.verifier_url.as_deref(), args.etherscan.key().as_deref(), + args.verifier.verifier_api_version.as_deref(), &config, ) .unwrap(); diff --git a/crates/verify/src/provider.rs b/crates/verify/src/provider.rs index 1364aac4462de..74d2094c41b83 100644 --- a/crates/verify/src/provider.rs +++ b/crates/verify/src/provider.rs @@ -124,7 +124,6 @@ impl FromStr for VerificationProviderType { fn from_str(s: &str) -> Result { match s { "e" | "etherscan" => Ok(Self::Etherscan), - "ev2" | "etherscan-v2" => Ok(Self::EtherscanV2), "s" | "sourcify" => Ok(Self::Sourcify), "b" | "blockscout" => Ok(Self::Blockscout), "o" | "oklink" => Ok(Self::Oklink), @@ -140,9 +139,6 @@ impl fmt::Display for VerificationProviderType { Self::Etherscan => { write!(f, "etherscan")?; } - Self::EtherscanV2 => { - write!(f, "etherscan-v2")?; - } Self::Sourcify => { write!(f, "sourcify")?; } @@ -163,7 +159,6 @@ impl fmt::Display for VerificationProviderType { #[derive(Clone, Debug, Default, PartialEq, Eq, clap::ValueEnum)] pub enum VerificationProviderType { Etherscan, - EtherscanV2, #[default] Sourcify, Blockscout, @@ -213,10 +208,6 @@ impl VerificationProviderType { } pub fn is_etherscan(&self) -> bool { - matches!(self, Self::Etherscan) || matches!(self, Self::EtherscanV2) - } - - pub fn is_etherscan_v2(&self) -> bool { - matches!(self, Self::EtherscanV2) + matches!(self, Self::Etherscan) } } diff --git a/crates/verify/src/verify.rs b/crates/verify/src/verify.rs index 71bfc60cc1673..6d8fb93ce3f6e 100644 --- a/crates/verify/src/verify.rs +++ b/crates/verify/src/verify.rs @@ -2,7 +2,7 @@ use crate::{ etherscan::EtherscanVerificationProvider, - provider::{VerificationProvider, VerificationProviderType}, + provider::{VerificationContext, VerificationProvider, VerificationProviderType}, utils::is_host_only, RetryArgs, }; @@ -23,8 +23,6 @@ use revm_primitives::HashSet; use semver::BuildMetadata; use std::path::PathBuf; -use crate::provider::VerificationContext; - /// Verification provider arguments #[derive(Clone, Debug, Parser)] pub struct VerifierArgs { @@ -39,6 +37,10 @@ pub struct VerifierArgs { /// The verifier URL, if using a custom provider. #[arg(long, help_heading = "Verifier options", env = "VERIFIER_URL")] pub verifier_url: Option, + + /// The verifier API version, if using a custom provider. + #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_VERSION")] + pub verifier_api_version: Option, } impl Default for VerifierArgs { @@ -47,6 +49,7 @@ impl Default for VerifierArgs { verifier: VerificationProviderType::Sourcify, verifier_api_key: None, verifier_url: None, + verifier_api_version: None, } } } From d8b07a67006768dcbcfe256c0457314b1c9edfc0 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Mon, 5 May 2025 13:00:42 +0300 Subject: [PATCH 09/15] Use released version --- Cargo.lock | 5 +++-- Cargo.toml | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55b48168a0859..ad917084ef535 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3697,8 +3697,9 @@ dependencies = [ [[package]] name = "foundry-block-explorers" -version = "0.13.1" -source = "git+https://github.com/foundry-rs/block-explorers?rev=8c03122#8c0312275fecf11b2258622fe1f7eba89e8f0a8e" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb9266295447e86b27be80b74cfe7b37a79e48cc70fd01c21812af7ab3d79b" dependencies = [ "alloy-chains", "alloy-json-abi", diff --git a/Cargo.toml b/Cargo.toml index 1fd0b89c1a59a..534e21f0d0893 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,7 @@ foundry-wallets = { path = "crates/wallets" } foundry-linking = { path = "crates/linking" } # solc & compilation utilities -foundry-block-explorers = { version = "0.13.0", default-features = false } +foundry-block-explorers = { version = "0.13.2", default-features = false } foundry-compilers = { version = "0.14.0", default-features = false } foundry-fork-db = "0.12" solang-parser = "=0.3.3" @@ -324,7 +324,6 @@ jiff = "0.2" idna_adapter = "=1.1.0" [patch.crates-io] -foundry-block-explorers = { git = "https://github.com/foundry-rs/block-explorers", rev = "8c03122" } ## alloy-core # alloy-dyn-abi = { path = "../../alloy-rs/core/crates/dyn-abi" } # alloy-json-abi = { path = "../../alloy-rs/core/crates/json-abi" } From b9f0864e4bab95e93911f700b726a78e7c9012aa Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 6 May 2025 08:34:00 +0300 Subject: [PATCH 10/15] Updates, fix script --verify --- crates/cast/src/cmd/run.rs | 4 ++ crates/cli/src/opts/rpc.rs | 7 +- crates/config/src/etherscan.rs | 26 +++---- crates/config/src/lib.rs | 42 ++++++----- crates/forge/src/cmd/test/mod.rs | 8 +++ crates/script/src/lib.rs | 7 ++ crates/verify/src/bytecode.rs | 14 ++-- crates/verify/src/etherscan/mod.rs | 108 ++++++++--------------------- crates/verify/src/verify.rs | 4 ++ 9 files changed, 102 insertions(+), 118 deletions(-) diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 947704a2df5a1..ffc0f42620832 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -299,6 +299,10 @@ impl figment::Provider for RunArgs { map.insert("etherscan_api_key".into(), api_key.as_str().into()); } + if let Some(api_version) = &self.etherscan.api_version { + map.insert("etherscan_api_version".into(), api_version.as_str().into()); + } + if let Some(evm_version) = self.evm_version { map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?); } diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 2d4dfff006b6b..6216a5f629922 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -115,7 +115,12 @@ pub struct EtherscanOpts { pub key: Option, /// The Etherscan API version. - #[arg(short, long = "etherscan-api-version", env = "ETHERSCAN_API_VERSION")] + #[arg( + short, + long = "etherscan-api-version", + alias = "api-version", + env = "ETHERSCAN_API_VERSION" + )] #[serde(rename = "etherscan_api_version", skip_serializing_if = "Option::is_none")] pub api_version: Option, diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index e49880bc7565a..fc37d90fab26e 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -87,13 +87,13 @@ impl EtherscanConfigs { } /// Returns all (alias -> url) pairs - pub fn resolved(self) -> ResolvedEtherscanConfigs { + pub fn resolved(self, default_api_version: EtherscanApiVersion) -> ResolvedEtherscanConfigs { ResolvedEtherscanConfigs { configs: self .configs .into_iter() .map(|(name, e)| { - let resolved = e.resolve(Some(&name)); + let resolved = e.resolve(Some(&name), default_api_version); (name, resolved) }) .collect(), @@ -178,7 +178,7 @@ pub struct EtherscanConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub url: Option, /// Etherscan API Version. Defaults to v2 - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, alias = "api-version", skip_serializing_if = "Option::is_none")] pub api_version: Option, /// The etherscan API KEY that's required to make requests pub key: EtherscanApiKey, @@ -194,10 +194,11 @@ impl EtherscanConfig { pub fn resolve( self, alias: Option<&str>, + default_api_version: EtherscanApiVersion, ) -> Result { let Self { chain, mut url, key, api_version } = self; - let api_version = api_version.unwrap_or(EtherscanApiVersion::V2); + let api_version = api_version.unwrap_or(default_api_version); if let Some(url) = &mut url { *url = interpolate(url)?; @@ -451,7 +452,7 @@ mod tests { }, ); - let mut resolved = configs.resolved(); + let mut resolved = configs.resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap().unwrap(); // None version = None assert_eq!(config.api_version, EtherscanApiVersion::V2); @@ -472,7 +473,7 @@ mod tests { }, ); - let mut resolved = configs.resolved(); + let mut resolved = configs.resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap().unwrap(); assert_eq!(config.api_version, EtherscanApiVersion::V1); let client = config.into_client().unwrap(); @@ -492,7 +493,7 @@ mod tests { }, ); - let mut resolved = configs.resolved(); + let mut resolved = configs.resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap().unwrap(); let _ = config.into_client().unwrap(); } @@ -511,13 +512,13 @@ mod tests { }, ); - let mut resolved = configs.clone().resolved(); + let mut resolved = configs.clone().resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap(); assert!(config.is_err()); std::env::set_var(env, "ABCDEFG"); - let mut resolved = configs.resolved(); + let mut resolved = configs.resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap().unwrap(); assert_eq!(config.key, "ABCDEFG"); let client = config.into_client().unwrap(); @@ -539,7 +540,7 @@ mod tests { }, ); - let mut resolved = configs.clone().resolved(); + let mut resolved = configs.clone().resolved(EtherscanApiVersion::V2); let config = resolved.remove("blast_sepolia").unwrap().unwrap(); assert_eq!(config.chain, Some(Chain::blast_sepolia())); } @@ -552,10 +553,11 @@ mod tests { key: EtherscanApiKey::Key("ABCDEFG".to_string()), api_version: None, }; - let resolved = config.clone().resolve(Some("base_sepolia")).unwrap(); + let resolved = + config.clone().resolve(Some("base_sepolia"), EtherscanApiVersion::V2).unwrap(); assert_eq!(resolved.chain, Some(Chain::base_sepolia())); - let resolved = config.resolve(Some("base-sepolia")).unwrap(); + let resolved = config.resolve(Some("base-sepolia"), EtherscanApiVersion::V2).unwrap(); assert_eq!(resolved.chain, Some(Chain::base_sepolia())); } } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index befdcfac55858..0e9c177c10786 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1381,17 +1381,27 @@ impl Config { &self, chain: Option, ) -> Result, EtherscanConfigError> { + let default_api_version = match self.etherscan_api_version.as_ref() { + Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string()) + .map_err(|_| InvalidApiVersion(api_version.to_string()))?, + None => EtherscanApiVersion::V2, + }; + if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref()) { if self.etherscan.contains_key(maybe_alias) { - return self.etherscan.clone().resolved().remove(maybe_alias).transpose(); + return self + .etherscan + .clone() + .resolved(default_api_version) + .remove(maybe_alias) + .transpose(); } } // try to find by comparing chain IDs after resolving - if let Some(res) = chain - .or(self.chain) - .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain)) - { + if let Some(res) = chain.or(self.chain).and_then(|chain| { + self.etherscan.clone().resolved(default_api_version).find_chain(chain) + }) { match (res, self.etherscan_api_key.as_ref()) { (Ok(mut config), Some(key)) => { // we update the key, because if an etherscan_api_key is set, it should take @@ -1409,15 +1419,11 @@ 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(); - - let api_version = match self.etherscan_api_version.as_ref() { - Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string()) - .map_err(|_| InvalidApiVersion(api_version.to_string()))?, - None => EtherscanApiVersion::V2, - }; - - return Ok(ResolvedEtherscanConfig::create(key, chain, api_version)); + return Ok(ResolvedEtherscanConfig::create( + key, + chain.or(self.chain).unwrap_or_default(), + default_api_version, + )); } Ok(None) @@ -3075,11 +3081,11 @@ mod tests { let config = Config::load().unwrap(); - assert!(config.etherscan.clone().resolved().has_unresolved()); + assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved()); jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789"); - let configs = config.etherscan.resolved(); + let configs = config.etherscan.resolved(EtherscanApiVersion::V2); assert!(!configs.has_unresolved()); let mb_urls = Moonbeam.etherscan_urls().unwrap(); @@ -3130,11 +3136,11 @@ mod tests { let config = Config::load().unwrap(); - assert!(config.etherscan.clone().resolved().has_unresolved()); + assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved()); jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789"); - let configs = config.etherscan.resolved(); + let configs = config.etherscan.resolved(EtherscanApiVersion::V2); assert!(!configs.has_unresolved()); let mb_urls = Moonbeam.etherscan_urls().unwrap(); diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index a1f3e64bb574f..2c7087009bea0 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -146,6 +146,10 @@ pub struct TestArgs { #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")] etherscan_api_key: Option, + /// The Etherscan API version. + #[arg(long, env = "ETHERSCAN_API_VERSION", value_name = "VERSION")] + etherscan_api_version: Option, + /// List tests instead of running them. #[arg(long, short, conflicts_with_all = ["show_progress", "decode_internal", "summary"], help_heading = "Display options")] list: bool, @@ -873,6 +877,10 @@ impl Provider for TestArgs { dict.insert("etherscan_api_key".to_string(), etherscan_api_key.to_string().into()); } + if let Some(api_version) = &self.etherscan_api_version { + dict.insert("etherscan_api_version".to_string(), api_version.to_string().into()); + } + if self.show_progress { dict.insert("show_progress".to_string(), true.into()); } diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index fbbb52398a837..8dc5c62e545fd 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -182,6 +182,10 @@ pub struct ScriptArgs { #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")] pub etherscan_api_key: Option, + /// The Etherscan API version. + #[arg(long, env = "ETHERSCAN_API_VERSION", value_name = "VERSION")] + pub etherscan_api_version: Option, + /// Verifies all the contracts found in the receipts of a script, if any. #[arg(long)] pub verify: bool, @@ -496,6 +500,9 @@ impl Provider for ScriptArgs { figment::value::Value::from(etherscan_api_key.to_string()), ); } + if let Some(api_version) = &self.etherscan_api_version { + dict.insert("etherscan_api_version".to_string(), api_version.to_string().into()); + } if let Some(timeout) = self.timeout { dict.insert("transaction_timeout".to_string(), timeout.into()); } diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index f5b4b5ab77c9b..d1528f0f30d7d 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -105,6 +105,10 @@ impl figment::Provider for VerifyBytecodeArgs { dict.insert("etherscan_api_key".into(), api_key.as_str().into()); } + if let Some(api_version) = &self.verifier.verifier_api_version { + dict.insert("etherscan_api_version".into(), api_version.as_str().into()); + } + if let Some(block) = &self.block { dict.insert("block".into(), figment::value::Value::serialize(block)?); } @@ -136,14 +140,8 @@ impl VerifyBytecodeArgs { self.etherscan.key = config.get_etherscan_config_with_chain(Some(chain))?.map(|c| c.key); // Etherscan client - let etherscan = EtherscanVerificationProvider.client( - self.etherscan.chain.unwrap_or_default(), - &self.verifier.verifier, - self.verifier.verifier_url.as_deref(), - self.etherscan.key().as_deref(), - self.verifier.verifier_api_version.as_deref(), - &config, - )?; + let etherscan = + EtherscanVerificationProvider.client(&self.etherscan, &self.verifier, &config)?; // Get the bytecode at the address, bailing if it doesn't exist. let code = provider.get_code_at(self.address).await?; diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index b8e225a532fd3..dac776bdfb8c2 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -1,7 +1,8 @@ use crate::{ - provider::{VerificationContext, VerificationProvider, VerificationProviderType}, + provider::{VerificationContext, VerificationProvider}, retry::RETRY_CHECK_ON_VERIFY, verify::{VerifyArgs, VerifyCheckArgs}, + VerifierArgs, }; use alloy_json_abi::Function; use alloy_primitives::hex; @@ -14,10 +15,13 @@ use foundry_block_explorers::{ verify::{CodeFormat, VerifyContract}, Client, EtherscanApiVersion, }; -use foundry_cli::utils::{get_provider, read_constructor_args_file, LoadConfig}; +use foundry_cli::{ + opts::EtherscanOpts, + utils::{get_provider, read_constructor_args_file, LoadConfig}, +}; use foundry_common::{abi::encode_function_args, retry::RetryError}; use foundry_compilers::{artifacts::BytecodeObject, Artifact}; -use foundry_config::{Chain, Config}; +use foundry_config::Config; use foundry_evm::constants::DEFAULT_CREATE2_DEPLOYER; use regex::Regex; use semver::{BuildMetadata, Version}; @@ -150,14 +154,7 @@ impl VerificationProvider for EtherscanVerificationProvider { /// Executes the command to check verification status on Etherscan async fn check(&self, args: VerifyCheckArgs) -> Result<()> { let config = args.load_config()?; - let etherscan = self.client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - args.verifier.verifier_api_version.as_deref(), - &config, - )?; + let etherscan = self.client(&args.etherscan, &args.verifier, &config)?; args.retry .into_retry() .run_async_until_break(|| async { @@ -220,14 +217,7 @@ impl EtherscanVerificationProvider { context: &VerificationContext, ) -> Result<(Client, VerifyContract)> { let config = args.load_config()?; - let etherscan = self.client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - args.verifier.verifier_api_version.as_deref(), - &config, - )?; + let etherscan = self.client(&args.etherscan, &args.verifier, &config)?; let verify_args = self.create_verify_request(args, context).await?; Ok((etherscan, verify_args)) @@ -254,19 +244,26 @@ impl EtherscanVerificationProvider { /// Create an Etherscan client. pub(crate) fn client( &self, - chain: Chain, - verifier_type: &VerificationProviderType, - verifier_url: Option<&str>, - etherscan_key: Option<&str>, - verifier_api_version: Option<&str>, + etherscan_opts: &EtherscanOpts, + verifier_args: &VerifierArgs, config: &Config, ) -> Result { + let chain = etherscan_opts.chain.unwrap_or_default(); + let etherscan_key = etherscan_opts.key(); + let verifier_type = &verifier_args.verifier; + let verifier_api_version = verifier_args.verifier_api_version.as_deref(); + let verifier_url = verifier_args.verifier_url.as_deref(); + + // Verifier is etherscan if explicitly set or if no verifier set (default sourcify) but + // API key passed. + let is_etherscan = verifier_type.is_etherscan() || + (verifier_type.is_sourcify() && etherscan_key.is_some()); let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?; let api_version = match verifier_api_version { Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string())?, None => { - if verifier_type.is_etherscan() { + if is_etherscan { etherscan_config .as_ref() .map(|c| c.api_version) @@ -293,18 +290,13 @@ impl EtherscanVerificationProvider { .and_then(|c| c.browser_url.as_deref()) .or_else(|| chain.etherscan_urls().map(|(_, url)| url)); let etherscan_key = - etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.as_str())); + etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.clone())); let mut builder = Client::builder().with_api_version(api_version); builder = if let Some(api_url) = api_url { // we don't want any trailing slashes because this can cause cloudflare issues: let api_url = api_url.trim_end_matches('/'); - - // Verifier is etherscan if explicitly set or if no verifier set (default sourcify) but - // API key passed. - let is_etherscan = verifier_type.is_etherscan() || - (verifier_type.is_sourcify() && etherscan_key.is_some()); let base_url = if !is_etherscan { // If verifier is not Etherscan then set base url as api url without /api suffix. api_url.strip_prefix("/api").unwrap_or(api_url) @@ -414,14 +406,7 @@ impl EtherscanVerificationProvider { context: &VerificationContext, ) -> Result { let provider = get_provider(&context.config)?; - let client = self.client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key.as_deref(), - args.verifier.verifier_api_version.as_deref(), - &context.config, - )?; + let client = self.client(&args.etherscan, &args.verifier, &context.config)?; let creation_data = client.contract_creation_data(args.address).await?; let transaction = provider @@ -488,6 +473,7 @@ async fn ensure_solc_build_metadata(version: Version) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::provider::VerificationProviderType; use clap::Parser; use foundry_common::fs; use foundry_test_utils::{forgetest_async, str}; @@ -521,16 +507,7 @@ mod tests { let config = args.load_config().unwrap(); let etherscan = EtherscanVerificationProvider::default(); - let client = etherscan - .client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - args.verifier.verifier_api_version.as_deref(), - &config, - ) - .unwrap(); + let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap(); assert_eq!(client.etherscan_api_url().as_str(), "https://api-testnet.polygonscan.com/"); assert!(format!("{client:?}").contains("dummykey")); @@ -550,16 +527,7 @@ mod tests { let config = args.load_config().unwrap(); let etherscan = EtherscanVerificationProvider::default(); - let client = etherscan - .client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - args.verifier.verifier_api_version.as_deref(), - &config, - ) - .unwrap(); + let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap(); assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/"); assert!(format!("{client:?}").contains("dummykey")); } @@ -595,16 +563,7 @@ mod tests { let etherscan = EtherscanVerificationProvider::default(); - let client = etherscan - .client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - args.verifier.verifier_api_version.as_deref(), - &config, - ) - .unwrap(); + let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap(); assert_eq!(client.etherscan_api_url().as_str(), "https://api.etherscan.io/v2/api"); assert!(format!("{client:?}").contains("dummykey")); @@ -628,16 +587,7 @@ mod tests { assert_eq!(args.verifier.verifier, VerificationProviderType::Etherscan); let etherscan = EtherscanVerificationProvider::default(); - let client = etherscan - .client( - args.etherscan.chain.unwrap_or_default(), - &args.verifier.verifier, - args.verifier.verifier_url.as_deref(), - args.etherscan.key().as_deref(), - args.verifier.verifier_api_version.as_deref(), - &config, - ) - .unwrap(); + let client = etherscan.client(&args.etherscan, &args.verifier, &config).unwrap(); assert_eq!(client.etherscan_api_url().as_str(), "https://verifier-url.com/"); assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2); assert!(format!("{client:?}").contains("dummykey")); diff --git a/crates/verify/src/verify.rs b/crates/verify/src/verify.rs index 6d8fb93ce3f6e..2317682eeb6b8 100644 --- a/crates/verify/src/verify.rs +++ b/crates/verify/src/verify.rs @@ -183,6 +183,10 @@ impl figment::Provider for VerifyArgs { dict.insert("etherscan_api_key".into(), api_key.as_str().into()); } + if let Some(api_version) = &self.verifier.verifier_api_version { + dict.insert("etherscan_api_version".into(), api_version.as_str().into()); + } + Ok(figment::value::Map::from([(Config::selected_profile(), dict)])) } } From 2dfa7c07da3498b79f6780810f3def4eb8762a4d Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 6 May 2025 14:47:10 +0300 Subject: [PATCH 11/15] Clone api version, cast --- Cargo.lock | 1 + crates/cast/src/cmd/artifact.rs | 10 +++------- crates/cast/src/cmd/constructor_args.rs | 7 ++----- crates/cast/src/cmd/creation_code.rs | 13 ++++++++----- crates/cast/src/cmd/interface.rs | 3 ++- crates/cast/src/cmd/storage.rs | 3 ++- crates/cast/src/tx.rs | 7 +++++++ crates/cli/Cargo.toml | 1 + crates/cli/src/utils/abi.rs | 4 +++- crates/common/src/abi.rs | 7 +++++-- crates/config/src/lib.rs | 11 +++++++++++ crates/forge/src/cmd/clone.rs | 4 +++- 12 files changed, 48 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dcae37462000b..0e873e35cab08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3782,6 +3782,7 @@ dependencies = [ "dotenvy", "eyre", "forge-fmt", + "foundry-block-explorers", "foundry-common", "foundry-compilers", "foundry-config", diff --git a/crates/cast/src/cmd/artifact.rs b/crates/cast/src/cmd/artifact.rs index dc83cb2aea211..79ec880d34cfd 100644 --- a/crates/cast/src/cmd/artifact.rs +++ b/crates/cast/src/cmd/artifact.rs @@ -1,12 +1,11 @@ use super::{ - creation_code::{fetch_creation_code, parse_code_output}, + creation_code::{fetch_creation_code_from_etherscan, parse_code_output}, interface::{fetch_abi_from_etherscan, load_abi_from_file}, }; use alloy_primitives::Address; use alloy_provider::Provider; use clap::{command, Parser}; use eyre::Result; -use foundry_block_explorers::Client; use foundry_cli::{ opts::{EtherscanOpts, RpcOpts}, utils::{self, LoadConfig}, @@ -46,15 +45,12 @@ pub struct ArtifactArgs { impl ArtifactArgs { pub async fn run(self) -> Result<()> { - let Self { contract, etherscan, rpc, output: output_location, abi_path } = self; + let Self { contract, mut etherscan, rpc, output: output_location, abi_path } = self; - let mut etherscan = etherscan; let config = rpc.load_config()?; let provider = utils::get_provider(&config)?; - let api_key = etherscan.key().unwrap_or_default(); let chain = provider.get_chain_id().await?; etherscan.chain = Some(chain.into()); - let client = Client::new(chain.into(), api_key)?; let abi = if let Some(ref abi_path) = abi_path { load_abi_from_file(abi_path, None)? @@ -64,7 +60,7 @@ impl ArtifactArgs { let (abi, _) = abi.first().ok_or_else(|| eyre::eyre!("No ABI found"))?; - let bytecode = fetch_creation_code(contract, client, provider).await?; + let bytecode = fetch_creation_code_from_etherscan(contract, ðerscan, provider).await?; let bytecode = parse_code_output(bytecode, contract, ðerscan, abi_path.as_deref(), true, false) .await?; diff --git a/crates/cast/src/cmd/constructor_args.rs b/crates/cast/src/cmd/constructor_args.rs index 2775e2e99ecfd..3d60673857f37 100644 --- a/crates/cast/src/cmd/constructor_args.rs +++ b/crates/cast/src/cmd/constructor_args.rs @@ -1,5 +1,5 @@ use super::{ - creation_code::fetch_creation_code, + creation_code::fetch_creation_code_from_etherscan, interface::{fetch_abi_from_etherscan, load_abi_from_file}, }; use alloy_dyn_abi::DynSolType; @@ -7,7 +7,6 @@ use alloy_primitives::{Address, Bytes}; use alloy_provider::Provider; use clap::{command, Parser}; use eyre::{eyre, OptionExt, Result}; -use foundry_block_explorers::Client; use foundry_cli::{ opts::{EtherscanOpts, RpcOpts}, utils::{self, LoadConfig}, @@ -37,12 +36,10 @@ impl ConstructorArgsArgs { let config = rpc.load_config()?; let provider = utils::get_provider(&config)?; - let api_key = etherscan.key().unwrap_or_default(); let chain = provider.get_chain_id().await?; etherscan.chain = Some(chain.into()); - let client = Client::new(chain.into(), api_key)?; - let bytecode = fetch_creation_code(contract, client, provider).await?; + let bytecode = fetch_creation_code_from_etherscan(contract, ðerscan, provider).await?; let args_arr = parse_constructor_args(bytecode, contract, ðerscan, abi_path).await?; for arg in args_arr { diff --git a/crates/cast/src/cmd/creation_code.rs b/crates/cast/src/cmd/creation_code.rs index 9967f38fde9bb..db47b1bdec523 100644 --- a/crates/cast/src/cmd/creation_code.rs +++ b/crates/cast/src/cmd/creation_code.rs @@ -50,12 +50,10 @@ impl CreationCodeArgs { let config = rpc.load_config()?; let provider = utils::get_provider(&config)?; - let api_key = etherscan.key().unwrap_or_default(); let chain = provider.get_chain_id().await?; etherscan.chain = Some(chain.into()); - let client = Client::new(chain.into(), api_key)?; - let bytecode = fetch_creation_code(contract, client, provider).await?; + let bytecode = fetch_creation_code_from_etherscan(contract, ðerscan, provider).await?; let bytecode = parse_code_output( bytecode, @@ -131,11 +129,16 @@ pub async fn parse_code_output( } /// Fetches the creation code of a contract from Etherscan and RPC. -pub async fn fetch_creation_code( +pub async fn fetch_creation_code_from_etherscan( contract: Address, - client: Client, + etherscan: &EtherscanOpts, provider: RetryProvider, ) -> Result { + let config = etherscan.load_config()?; + let chain = config.chain.unwrap_or_default(); + let api_version = config.get_etherscan_api_version(Some(chain)); + let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); + let client = Client::new_with_api_version(chain, api_key, api_version)?; let creation_data = client.contract_creation_data(contract).await?; let creation_tx_hash = creation_data.transaction_hash; let tx_data = provider.get_transaction_by_hash(creation_tx_hash).await?; diff --git a/crates/cast/src/cmd/interface.rs b/crates/cast/src/cmd/interface.rs index f37f92864e534..992f5b833eac3 100644 --- a/crates/cast/src/cmd/interface.rs +++ b/crates/cast/src/cmd/interface.rs @@ -143,8 +143,9 @@ pub async fn fetch_abi_from_etherscan( ) -> Result> { let config = etherscan.load_config()?; let chain = config.chain.unwrap_or_default(); + let api_version = config.get_etherscan_api_version(Some(chain)); let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = Client::new(chain, api_key)?; + let client = Client::new_with_api_version(chain, api_key, api_version)?; let source = client.contract_source_code(address).await?; source.items.into_iter().map(|item| Ok((item.abi()?, item.contract_name))).collect() } diff --git a/crates/cast/src/cmd/storage.rs b/crates/cast/src/cmd/storage.rs index 7f75b61fb146f..b477ebf2b2637 100644 --- a/crates/cast/src/cmd/storage.rs +++ b/crates/cast/src/cmd/storage.rs @@ -135,8 +135,9 @@ impl StorageArgs { } let chain = utils::get_chain(config.chain, &provider).await?; + let api_version = config.get_etherscan_api_version(Some(chain)); let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = Client::new(chain, api_key)?; + let client = Client::new_with_api_version(chain, api_key, api_version)?; let source = if let Some(proxy) = self.proxy { find_source(client, proxy.resolve(&provider).await?).await? } else { diff --git a/crates/cast/src/tx.rs b/crates/cast/src/tx.rs index 1097da794e15f..36b14807b98cd 100644 --- a/crates/cast/src/tx.rs +++ b/crates/cast/src/tx.rs @@ -13,6 +13,7 @@ use alloy_serde::WithOtherFields; use alloy_signer::Signer; use alloy_transport::TransportError; use eyre::Result; +use foundry_block_explorers::EtherscanApiVersion; use foundry_cli::{ opts::{CliAuthorizationList, TransactionOpts}, utils::{self, parse_function_args}, @@ -141,6 +142,7 @@ pub struct CastTxBuilder { auth: Option, chain: Chain, etherscan_api_key: Option, + etherscan_api_version: EtherscanApiVersion, access_list: Option>, state: S, } @@ -152,6 +154,7 @@ impl> CastTxBuilder { let mut tx = WithOtherFields::::default(); let chain = utils::get_chain(config.chain, &provider).await?; + let etherscan_api_version = config.get_etherscan_api_version(Some(chain)); let etherscan_api_key = config.get_etherscan_api_key(Some(chain)); let legacy = tx_opts.legacy || chain.is_legacy(); @@ -192,6 +195,7 @@ impl> CastTxBuilder { blob: tx_opts.blob, chain, etherscan_api_key, + etherscan_api_version, auth: tx_opts.auth, access_list: tx_opts.access_list, state: InitState, @@ -208,6 +212,7 @@ impl> CastTxBuilder { blob: self.blob, chain: self.chain, etherscan_api_key: self.etherscan_api_key, + etherscan_api_version: self.etherscan_api_version, auth: self.auth, access_list: self.access_list, state: ToState { to }, @@ -233,6 +238,7 @@ impl> CastTxBuilder { self.chain, &self.provider, self.etherscan_api_key.as_deref(), + self.etherscan_api_version, ) .await? } else { @@ -264,6 +270,7 @@ impl> CastTxBuilder { blob: self.blob, chain: self.chain, etherscan_api_key: self.etherscan_api_key, + etherscan_api_version: self.etherscan_api_version, auth: self.auth, access_list: self.access_list, state: InputState { kind: self.state.to.into(), input, func }, diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index a3a510f03cb1a..b9dfd5f83d67f 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -21,6 +21,7 @@ foundry-evm.workspace = true foundry-wallets.workspace = true foundry-compilers = { workspace = true, features = ["full"] } +foundry-block-explorers.workspace = true alloy-eips.workspace = true alloy-dyn-abi.workspace = true diff --git a/crates/cli/src/utils/abi.rs b/crates/cli/src/utils/abi.rs index c7f4d260416d7..c037fd1e17961 100644 --- a/crates/cli/src/utils/abi.rs +++ b/crates/cli/src/utils/abi.rs @@ -3,6 +3,7 @@ use alloy_json_abi::Function; use alloy_primitives::{hex, Address}; use alloy_provider::{network::AnyNetwork, Provider}; use eyre::{OptionExt, Result}; +use foundry_block_explorers::EtherscanApiVersion; use foundry_common::{ abi::{encode_function_args, get_func, get_func_etherscan}, ens::NameOrAddress, @@ -31,6 +32,7 @@ pub async fn parse_function_args>( chain: Chain, provider: &P, etherscan_api_key: Option<&str>, + etherscan_api_version: EtherscanApiVersion, ) -> Result<(Vec, Option)> { if sig.trim().is_empty() { eyre::bail!("Function signature or calldata must be provided.") @@ -50,7 +52,7 @@ pub async fn parse_function_args>( "If you wish to fetch function data from Etherscan, please provide an Etherscan API key.", )?; let to = to.ok_or_eyre("A 'to' address must be provided to fetch function data.")?; - get_func_etherscan(sig, to, &args, chain, etherscan_api_key).await? + get_func_etherscan(sig, to, &args, chain, etherscan_api_key, etherscan_api_version).await? }; Ok((encode_function_args(&func, &args)?, Some(func))) diff --git a/crates/common/src/abi.rs b/crates/common/src/abi.rs index fa9f241719fdb..28824925cacda 100644 --- a/crates/common/src/abi.rs +++ b/crates/common/src/abi.rs @@ -4,7 +4,9 @@ use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::{Error, Event, Function, Param}; use alloy_primitives::{hex, Address, LogData}; use eyre::{Context, ContextCompat, Result}; -use foundry_block_explorers::{contract::ContractMetadata, errors::EtherscanError, Client}; +use foundry_block_explorers::{ + contract::ContractMetadata, errors::EtherscanError, Client, EtherscanApiVersion, +}; use foundry_config::Chain; use std::{future::Future, pin::Pin}; @@ -120,8 +122,9 @@ pub async fn get_func_etherscan( args: &[String], chain: Chain, etherscan_api_key: &str, + etherscan_api_version: EtherscanApiVersion, ) -> Result { - let client = Client::new(chain, etherscan_api_key)?; + let client = Client::new_with_api_version(chain, etherscan_api_key, etherscan_api_version)?; let source = find_source(client, contract).await?; let metadata = source.items.first().wrap_err("etherscan returned empty metadata")?; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 0e9c177c10786..51fb58df56115 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1438,6 +1438,17 @@ impl Config { self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key) } + /// Helper function to get the API version. + /// + /// See also [Self::get_etherscan_config_with_chain] + pub fn get_etherscan_api_version(&self, chain: Option) -> EtherscanApiVersion { + self.get_etherscan_config_with_chain(chain) + .ok() + .flatten() + .map(|c| c.api_version) + .unwrap_or(EtherscanApiVersion::V2) + } + /// Returns the remapping for the project's _src_ directory /// /// **Note:** this will add an additional `/=` remapping here so imports that diff --git a/crates/forge/src/cmd/clone.rs b/crates/forge/src/cmd/clone.rs index aaf157637c9e7..f489c6df82549 100644 --- a/crates/forge/src/cmd/clone.rs +++ b/crates/forge/src/cmd/clone.rs @@ -101,8 +101,10 @@ impl CloneArgs { // step 0. get the chain and api key from the config let config = etherscan.load_config()?; let chain = config.chain.unwrap_or_default(); + let etherscan_api_version = config.get_etherscan_api_version(Some(chain)); let etherscan_api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default(); - let client = Client::new(chain, etherscan_api_key.clone())?; + let client = + Client::new_with_api_version(chain, etherscan_api_key.clone(), etherscan_api_version)?; // step 1. get the metadata from client sh_println!("Downloading the source code of {address} from Etherscan...")?; From 622aed41f5eb4620eb7af94d60f4439352ded2f1 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 6 May 2025 17:53:39 +0300 Subject: [PATCH 12/15] Cast fixes --- crates/anvil/tests/it/fork.rs | 1 + crates/cast/tests/cli/main.rs | 26 +++++++++++------------ crates/config/src/etherscan.rs | 20 +++++++---------- crates/forge/src/cmd/clone.rs | 4 ++-- crates/forge/tests/cli/cmd.rs | 12 +++++------ crates/forge/tests/cli/verify_bytecode.rs | 8 +++---- crates/test-utils/src/rpc.rs | 25 ++++------------------ 7 files changed, 38 insertions(+), 58 deletions(-) diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index 422bd2514cf6d..cda5fc805cf8c 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -1324,6 +1324,7 @@ async fn test_fork_execution_reverted() { // #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn test_immutable_fork_transaction_hash() { use std::str::FromStr; diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index be2e9a7ed7101..a661c432df1c5 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -9,7 +9,7 @@ use anvil::{EthereumHardfork, NodeConfig}; use foundry_test_utils::{ rpc::{ next_etherscan_api_key, next_http_archive_rpc_url, next_http_rpc_endpoint, - next_mainnet_etherscan_api_key, next_rpc_endpoint, next_ws_rpc_endpoint, + next_rpc_endpoint, next_ws_rpc_endpoint, }, str, util::OutputExt, @@ -1378,7 +1378,7 @@ casttest!(storage_layout_simple, |_prj, cmd| { "--block", "21034138", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2", ]) .assert_success() @@ -1405,7 +1405,7 @@ casttest!(storage_layout_simple_json, |_prj, cmd| { "--block", "21034138", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0x13b0D85CcB8bf860b6b79AF3029fCA081AE9beF2", "--json", ]) @@ -1422,7 +1422,7 @@ casttest!(storage_layout_complex, |_prj, cmd| { "--block", "21034138", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0xBA12222222228d8Ba445958a75a0704d566BF2C8", ]) .assert_success() @@ -1470,7 +1470,7 @@ casttest!(storage_layout_complex_proxy, |_prj, cmd| { "--block", "7857852", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0xE2588A9CAb7Ea877206E35f615a39f84a64A7A3b", "--proxy", "0x29fcb43b46531bca003ddc8fcb67ffe91900c762" @@ -1512,7 +1512,7 @@ casttest!(storage_layout_complex_json, |_prj, cmd| { "--block", "21034138", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0xBA12222222228d8Ba445958a75a0704d566BF2C8", "--json", ]) @@ -1601,7 +1601,7 @@ casttest!(fetch_weth_interface_from_etherscan, |_prj, cmd| { cmd.args([ "interface", "--etherscan-api-key", - &next_mainnet_etherscan_api_key(), + &next_etherscan_api_key(), "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", ]) .assert_success() @@ -1880,7 +1880,7 @@ casttest!(fetch_creation_code_from_etherscan, |_prj, cmd| { cmd.args([ "creation-code", "--etherscan-api-key", - &next_mainnet_etherscan_api_key(), + &next_etherscan_api_key(), "0x0923cad07f06b2d0e5e49e63b8b35738d4156b95", "--rpc-url", eth_rpc_url.as_str(), @@ -1899,7 +1899,7 @@ casttest!(fetch_creation_code_only_args_from_etherscan, |_prj, cmd| { cmd.args([ "creation-code", "--etherscan-api-key", - &next_mainnet_etherscan_api_key(), + &next_etherscan_api_key(), "0x6982508145454ce325ddbe47a25d4ec3d2311933", "--rpc-url", eth_rpc_url.as_str(), @@ -1919,7 +1919,7 @@ casttest!(fetch_constructor_args_from_etherscan, |_prj, cmd| { cmd.args([ "constructor-args", "--etherscan-api-key", - &next_mainnet_etherscan_api_key(), + &next_etherscan_api_key(), "0x6982508145454ce325ddbe47a25d4ec3d2311933", "--rpc-url", eth_rpc_url.as_str(), @@ -1940,7 +1940,7 @@ casttest!(test_non_mainnet_traces, |prj, cmd| { "--rpc-url", next_rpc_endpoint(NamedChain::Optimism).as_str(), "--etherscan-api-key", - next_etherscan_api_key(NamedChain::Optimism).as_str(), + next_etherscan_api_key().as_str(), ]) .assert_success() .stdout_eq(str![[r#" @@ -1963,7 +1963,7 @@ casttest!(fetch_artifact_from_etherscan, |_prj, cmd| { cmd.args([ "artifact", "--etherscan-api-key", - &next_mainnet_etherscan_api_key(), + &next_etherscan_api_key(), "0x0923cad07f06b2d0e5e49e63b8b35738d4156b95", "--rpc-url", eth_rpc_url.as_str(), @@ -2444,7 +2444,7 @@ contract WETH9 { casttest!(fetch_src_default, |_prj, cmd| { let weth = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); - let etherscan_api_key = next_mainnet_etherscan_api_key(); + let etherscan_api_key = next_etherscan_api_key(); cmd.args(["source", &weth.to_string(), "--flatten", "--etherscan-api-key", ðerscan_api_key]) .assert_success() diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index fc37d90fab26e..4098fd919b7ba 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -319,12 +319,9 @@ impl ResolvedEtherscanConfig { ) -> Result { 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 - // try to match against mainnet, which is usually the most common target - .or_else(|| (api_url == mainnet_api).then(Chain::mainnet)) - .and_then(Config::foundry_etherscan_chain_cache_dir); + let chain = chain.unwrap_or_default(); + let cache = Config::foundry_etherscan_chain_cache_dir(chain); if let Some(cache_path) = &cache { // we also create the `sources` sub dir here @@ -338,16 +335,15 @@ impl ResolvedEtherscanConfig { .user_agent(ETHERSCAN_USER_AGENT) .tls_built_in_root_certs(api_url.scheme() == "https") .build()?; - foundry_block_explorers::Client::builder() + let mut client_builder = foundry_block_explorers::Client::builder() .with_client(client) .with_api_version(api_version) .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 - // mainnet browser url here - .with_url(browser_url.as_deref().unwrap_or(mainnet_url))? - .with_cache(cache, Duration::from_secs(24 * 60 * 60)) - .build() + .with_cache(cache, Duration::from_secs(24 * 60 * 60)); + if let Some(browser_url) = browser_url { + client_builder = client_builder.with_url(browser_url)?; + } + client_builder.chain(chain)?.build() } } diff --git a/crates/forge/src/cmd/clone.rs b/crates/forge/src/cmd/clone.rs index f489c6df82549..a5bc4b2aa4676 100644 --- a/crates/forge/src/cmd/clone.rs +++ b/crates/forge/src/cmd/clone.rs @@ -632,7 +632,7 @@ mod tests { use super::*; use alloy_primitives::hex; use foundry_compilers::CompilerContract; - use foundry_test_utils::rpc::next_mainnet_etherscan_api_key; + use foundry_test_utils::rpc::next_etherscan_api_key; use std::collections::BTreeMap; #[expect(clippy::disallowed_macros)] @@ -711,7 +711,7 @@ mod tests { // create folder if not exists std::fs::create_dir_all(&data_folder).unwrap(); // create metadata.json and creation_data.json - let client = Client::new(Chain::mainnet(), next_mainnet_etherscan_api_key()).unwrap(); + let client = Client::new(Chain::mainnet(), next_etherscan_api_key()).unwrap(); let meta = client.contract_source_code(address).await.unwrap(); // dump json let json = serde_json::to_string_pretty(&meta).unwrap(); diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index f845981bda592..cd34f4d646237 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -7,7 +7,7 @@ use foundry_config::{ }; use foundry_test_utils::{ foundry_compilers::PathStyle, - rpc::next_mainnet_etherscan_api_key, + rpc::next_etherscan_api_key, snapbox::IntoData, util::{pretty_err, read_string, OutputExt, TestCommand}, }; @@ -619,7 +619,7 @@ forgetest!(can_clone, |prj, cmd| { cmd.args([ "clone", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0x044b75f554b886A065b9567891e45c79542d7357", ]) .arg(prj.root()) @@ -648,7 +648,7 @@ forgetest!(can_clone_quiet, |prj, cmd| { cmd.args([ "clone", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "--quiet", "0xDb53f47aC61FE54F456A4eb3E09832D08Dd7BEec", ]) @@ -666,7 +666,7 @@ forgetest!(can_clone_no_remappings_txt, |prj, cmd| { cmd.args([ "clone", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "--no-remappings-txt", "0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf", ]) @@ -701,7 +701,7 @@ forgetest!(can_clone_keep_directory_structure, |prj, cmd| { .args([ "clone", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "--keep-directory-structure", "0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf", ]) @@ -739,7 +739,7 @@ forgetest!(can_clone_with_node_modules, |prj, cmd| { cmd.args([ "clone", "--etherscan-api-key", - next_mainnet_etherscan_api_key().as_str(), + next_etherscan_api_key().as_str(), "0xA3E217869460bEf59A1CfD0637e2875F9331e823", ]) .arg(prj.root()) diff --git a/crates/forge/tests/cli/verify_bytecode.rs b/crates/forge/tests/cli/verify_bytecode.rs index 2b61e4e847cdb..9ef4d6ddf4606 100644 --- a/crates/forge/tests/cli/verify_bytecode.rs +++ b/crates/forge/tests/cli/verify_bytecode.rs @@ -2,7 +2,7 @@ use foundry_compilers::artifacts::{BytecodeHash, EvmVersion}; use foundry_config::Config; use foundry_test_utils::{ forgetest_async, - rpc::{next_http_archive_rpc_url, next_mainnet_etherscan_api_key}, + rpc::{next_etherscan_api_key, next_http_archive_rpc_url}, util::OutputExt, TestCommand, TestProject, }; @@ -19,7 +19,7 @@ fn test_verify_bytecode( verifier_url: &str, expected_matches: (&str, &str), ) { - let etherscan_key = next_mainnet_etherscan_api_key(); + let etherscan_key = next_etherscan_api_key(); let rpc_url = next_http_archive_rpc_url(); // fetch and flatten source code @@ -33,7 +33,7 @@ fn test_verify_bytecode( prj.add_source(contract_name, &source_code).unwrap(); prj.write_config(config); - let etherscan_key = next_mainnet_etherscan_api_key(); + let etherscan_key = next_etherscan_api_key(); let mut args = vec![ "verify-bytecode", addr, @@ -74,7 +74,7 @@ fn test_verify_bytecode_with_ignore( ignore: &str, chain: &str, ) { - let etherscan_key = next_mainnet_etherscan_api_key(); + let etherscan_key = next_etherscan_api_key(); let rpc_url = next_http_archive_rpc_url(); // fetch and flatten source code diff --git a/crates/test-utils/src/rpc.rs b/crates/test-utils/src/rpc.rs index 70e3c87534f95..2ef2424cc6a20 100644 --- a/crates/test-utils/src/rpc.rs +++ b/crates/test-utils/src/rpc.rs @@ -59,14 +59,6 @@ static ETHERSCAN_MAINNET_KEYS: LazyLock> = LazyLock::new(|| { ]) }); -// List of etherscan keys for Optimism. -static ETHERSCAN_OPTIMISM_KEYS: LazyLock> = LazyLock::new(|| { - shuffled(vec![ - // - "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46", - ]) -}); - /// Returns the next index to use. fn next_idx() -> usize { static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0); @@ -145,19 +137,10 @@ fn archive_urls(is_ws: bool) -> &'static [String] { } } -/// Returns the next etherscan api key -pub fn next_mainnet_etherscan_api_key() -> String { - next_etherscan_api_key(NamedChain::Mainnet) -} - -/// Returns the next etherscan api key for given chain. -pub fn next_etherscan_api_key(chain: NamedChain) -> String { - let keys = match chain { - Optimism => ÐERSCAN_OPTIMISM_KEYS, - _ => ÐERSCAN_MAINNET_KEYS, - }; - let key = next(keys).to_string(); - eprintln!("--- next_etherscan_api_key(chain={chain:?}) = {key} ---"); +/// Returns the next etherscan api key. +pub fn next_etherscan_api_key() -> String { + let key = next(ÐERSCAN_MAINNET_KEYS).to_string(); + eprintln!("--- next_etherscan_api_key() = {key} ---"); key } From a182e563e714039d52c8daad1f621369b4bab518 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 6 May 2025 19:04:11 +0300 Subject: [PATCH 13/15] Tests nits --- crates/test-utils/src/rpc.rs | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/test-utils/src/rpc.rs b/crates/test-utils/src/rpc.rs index 2ef2424cc6a20..056a2adc578bf 100644 --- a/crates/test-utils/src/rpc.rs +++ b/crates/test-utils/src/rpc.rs @@ -41,8 +41,8 @@ static DRPC_KEYS: LazyLock> = LazyLock::new(|| { ]) }); -// List of etherscan keys for mainnet -static ETHERSCAN_MAINNET_KEYS: LazyLock> = LazyLock::new(|| { +// List of etherscan keys. +static ETHERSCAN_KEYS: LazyLock> = LazyLock::new(|| { shuffled(vec![ "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6", "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1", @@ -54,8 +54,6 @@ static ETHERSCAN_MAINNET_KEYS: LazyLock> = LazyLock::new(|| { "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74", "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG", "ZUB97R31KSYX7NYVW6224Q6EYY6U56H591", - // Optimism - // "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46", ]) }); @@ -139,7 +137,7 @@ fn archive_urls(is_ws: bool) -> &'static [String] { /// Returns the next etherscan api key. pub fn next_etherscan_api_key() -> String { - let key = next(ÐERSCAN_MAINNET_KEYS).to_string(); + let key = next(ÐERSCAN_KEYS).to_string(); eprintln!("--- next_etherscan_api_key() = {key} ---"); key } @@ -188,6 +186,7 @@ fn next_url(is_ws: bool, chain: NamedChain) -> String { mod tests { use super::*; use alloy_primitives::address; + use foundry_block_explorers::EtherscanApiVersion; use foundry_config::Chain; #[tokio::test] @@ -196,7 +195,7 @@ mod tests { let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7"); let mut first_abi = None; let mut failed = Vec::new(); - for (i, &key) in ETHERSCAN_MAINNET_KEYS.iter().enumerate() { + for (i, &key) in ETHERSCAN_KEYS.iter().enumerate() { println!("trying key {i} ({key})"); let client = foundry_block_explorers::Client::builder() @@ -231,4 +230,32 @@ mod tests { panic!("failed keys: {failed:#?}"); } } + + #[tokio::test] + #[ignore = "run manually"] + async fn test_etherscan_keys_compatibility() { + let address = address!("0x111111125421cA6dc452d289314280a0f8842A65"); + let ehterscan_key = "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46"; + let client = foundry_block_explorers::Client::builder() + .with_api_key(ehterscan_key) + .chain(Chain::optimism_mainnet()) + .unwrap() + .build() + .unwrap(); + if client.contract_abi(address).await.is_ok() { + panic!("v1 Optimism key should not work with v2 version") + } + + let client = foundry_block_explorers::Client::builder() + .with_api_key(ehterscan_key) + .with_api_version(EtherscanApiVersion::V1) + .chain(Chain::optimism_mainnet()) + .unwrap() + .build() + .unwrap(); + match client.contract_abi(address).await { + Ok(_) => {} + Err(_) => panic!("v1 Optimism key should work with v1 version"), + }; + } } From 6685dca079cdfd610b83dd5394aac80f5101a74e Mon Sep 17 00:00:00 2001 From: grandizzy Date: Tue, 6 May 2025 21:43:53 +0300 Subject: [PATCH 14/15] configs for verify check --- crates/verify/src/verify.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/verify/src/verify.rs b/crates/verify/src/verify.rs index 2317682eeb6b8..b6bbb5a9e8fbd 100644 --- a/crates/verify/src/verify.rs +++ b/crates/verify/src/verify.rs @@ -451,7 +451,16 @@ impl figment::Provider for VerifyCheckArgs { fn data( &self, ) -> Result, figment::Error> { - self.etherscan.data() + let mut dict = self.etherscan.dict(); + if let Some(api_key) = &self.etherscan.key { + dict.insert("etherscan_api_key".into(), api_key.as_str().into()); + } + + if let Some(api_version) = &self.etherscan.api_version { + dict.insert("etherscan_api_version".into(), api_version.as_str().into()); + } + + Ok(figment::value::Map::from([(Config::selected_profile(), dict)])) } } From 24782b1c90150d360d10258381df60cf695d7027 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Wed, 7 May 2025 09:06:36 +0300 Subject: [PATCH 15/15] Simplify, use EtherscanApiVersion enum --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- crates/cast/src/cmd/run.rs | 2 +- crates/cli/src/opts/rpc.rs | 3 ++- crates/config/src/etherscan.rs | 3 --- crates/config/src/lib.rs | 11 +++-------- crates/forge/src/cmd/create.rs | 4 ++-- crates/forge/src/cmd/test/mod.rs | 3 ++- crates/script/Cargo.toml | 1 + crates/script/src/lib.rs | 3 ++- crates/verify/src/bytecode.rs | 2 +- crates/verify/src/etherscan/mod.rs | 19 ++++++------------- crates/verify/src/verify.rs | 7 ++++--- 13 files changed, 28 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e873e35cab08..c52b0803a0009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3593,6 +3593,7 @@ dependencies = [ "eyre", "forge-script-sequence", "forge-verify", + "foundry-block-explorers", "foundry-cheatcodes", "foundry-cli", "foundry-common", @@ -3691,9 +3692,9 @@ dependencies = [ [[package]] name = "foundry-block-explorers" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb9266295447e86b27be80b74cfe7b37a79e48cc70fd01c21812af7ab3d79b" +checksum = "8025385c52416bf14e5bb28d21eb5efe2490dd6fb001a49b87f1825a626b4909" dependencies = [ "alloy-chains", "alloy-json-abi", diff --git a/Cargo.toml b/Cargo.toml index 534e21f0d0893..e76ac670d7cf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,7 @@ foundry-wallets = { path = "crates/wallets" } foundry-linking = { path = "crates/linking" } # solc & compilation utilities -foundry-block-explorers = { version = "0.13.2", default-features = false } +foundry-block-explorers = { version = "0.13.3", default-features = false } foundry-compilers = { version = "0.14.0", default-features = false } foundry-fork-db = "0.12" solang-parser = "=0.3.3" diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index ffc0f42620832..90470ef31ed97 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -300,7 +300,7 @@ impl figment::Provider for RunArgs { } if let Some(api_version) = &self.etherscan.api_version { - map.insert("etherscan_api_version".into(), api_version.as_str().into()); + map.insert("etherscan_api_version".into(), api_version.to_string().into()); } if let Some(evm_version) = self.evm_version { diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 6216a5f629922..2b508720a81a1 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -2,6 +2,7 @@ use crate::opts::ChainValueParser; use alloy_chains::ChainKind; use clap::Parser; use eyre::Result; +use foundry_block_explorers::EtherscanApiVersion; use foundry_config::{ figment::{ self, @@ -122,7 +123,7 @@ pub struct EtherscanOpts { env = "ETHERSCAN_API_VERSION" )] #[serde(rename = "etherscan_api_version", skip_serializing_if = "Option::is_none")] - pub api_version: Option, + pub api_version: Option, /// The chain name or EIP-155 chain ID. #[arg( diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index 4098fd919b7ba..76d50b09bc934 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -58,9 +58,6 @@ pub enum EtherscanConfigError { #[error("At least one of `url` or `chain` must be present{0}")] MissingUrlOrChain(String), - - #[error("Invalid Etherscan API version {0}")] - InvalidApiVersion(String), } /// Container type for Etherscan API keys and URLs. diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 51fb58df56115..2abb5a04cb233 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -121,7 +121,6 @@ mod bind_json; use bind_json::BindJsonConfig; mod compilation; -use crate::etherscan::EtherscanConfigError::InvalidApiVersion; pub use compilation::{CompilationRestrictions, SettingsOverrides}; /// Foundry configuration @@ -284,7 +283,7 @@ pub struct Config { /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table pub etherscan_api_key: Option, /// etherscan API version - pub etherscan_api_version: Option, + pub etherscan_api_version: Option, /// Multiple etherscan api configs and their aliases #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")] pub etherscan: EtherscanConfigs, @@ -1381,11 +1380,7 @@ impl Config { &self, chain: Option, ) -> Result, EtherscanConfigError> { - let default_api_version = match self.etherscan_api_version.as_ref() { - Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string()) - .map_err(|_| InvalidApiVersion(api_version.to_string()))?, - None => EtherscanApiVersion::V2, - }; + let default_api_version = self.etherscan_api_version.unwrap_or_default(); if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref()) { if self.etherscan.contains_key(maybe_alias) { @@ -1446,7 +1441,7 @@ impl Config { .ok() .flatten() .map(|c| c.api_version) - .unwrap_or(EtherscanApiVersion::V2) + .unwrap_or_default() } /// Returns the remapping for the project's _src_ directory diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 1c9ca1cbbfc7a..5cffad23b9141 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -228,7 +228,7 @@ impl CreateArgs { num_of_optimizations: None, etherscan: EtherscanOpts { key: self.eth.etherscan.key.clone(), - api_version: self.eth.etherscan.api_version.clone(), + api_version: self.eth.etherscan.api_version, chain: Some(chain.into()), }, rpc: Default::default(), @@ -419,7 +419,7 @@ impl CreateArgs { num_of_optimizations, etherscan: EtherscanOpts { key: self.eth.etherscan.key(), - api_version: self.eth.etherscan.api_version.clone(), + api_version: self.eth.etherscan.api_version, chain: Some(chain.into()), }, rpc: Default::default(), diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 2c7087009bea0..9e8a4b2d26643 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -16,6 +16,7 @@ use alloy_primitives::U256; use chrono::Utc; use clap::{Parser, ValueHint}; use eyre::{bail, Context, OptionExt, Result}; +use foundry_block_explorers::EtherscanApiVersion; use foundry_cli::{ opts::{BuildOpts, GlobalArgs}, utils::{self, LoadConfig}, @@ -148,7 +149,7 @@ pub struct TestArgs { /// The Etherscan API version. #[arg(long, env = "ETHERSCAN_API_VERSION", value_name = "VERSION")] - etherscan_api_version: Option, + etherscan_api_version: Option, /// List tests instead of running them. #[arg(long, short, conflicts_with_all = ["show_progress", "decode_internal", "summary"], help_heading = "Display options")] diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index b3307f6b44ece..b258da28d02f4 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -23,6 +23,7 @@ foundry-debugger.workspace = true foundry-cheatcodes.workspace = true foundry-wallets.workspace = true foundry-linking.workspace = true +foundry-block-explorers.workspace = true forge-script-sequence.workspace = true serde.workspace = true diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index 8dc5c62e545fd..ee6762ed01944 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -26,6 +26,7 @@ use dialoguer::Confirm; use eyre::{ContextCompat, Result}; use forge_script_sequence::{AdditionalContract, NestedValue}; use forge_verify::{RetryArgs, VerifierArgs}; +use foundry_block_explorers::EtherscanApiVersion; use foundry_cli::{ opts::{BuildOpts, GlobalArgs}, utils::LoadConfig, @@ -184,7 +185,7 @@ pub struct ScriptArgs { /// The Etherscan API version. #[arg(long, env = "ETHERSCAN_API_VERSION", value_name = "VERSION")] - pub etherscan_api_version: Option, + pub etherscan_api_version: Option, /// Verifies all the contracts found in the receipts of a script, if any. #[arg(long)] diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index d1528f0f30d7d..5c84dbc64d6a0 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -106,7 +106,7 @@ impl figment::Provider for VerifyBytecodeArgs { } if let Some(api_version) = &self.verifier.verifier_api_version { - dict.insert("etherscan_api_version".into(), api_version.as_str().into()); + dict.insert("etherscan_api_version".into(), api_version.to_string().into()); } if let Some(block) = &self.block { diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index dac776bdfb8c2..fd0e08e355a14 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -251,7 +251,6 @@ impl EtherscanVerificationProvider { let chain = etherscan_opts.chain.unwrap_or_default(); let etherscan_key = etherscan_opts.key(); let verifier_type = &verifier_args.verifier; - let verifier_api_version = verifier_args.verifier_api_version.as_deref(); let verifier_url = verifier_args.verifier_url.as_deref(); // Verifier is etherscan if explicitly set or if no verifier set (default sourcify) but @@ -260,19 +259,13 @@ impl EtherscanVerificationProvider { (verifier_type.is_sourcify() && etherscan_key.is_some()); let etherscan_config = config.get_etherscan_config_with_chain(Some(chain))?; - let api_version = match verifier_api_version { - Some(api_version) => EtherscanApiVersion::try_from(api_version.to_string())?, - None => { - if is_etherscan { - etherscan_config - .as_ref() - .map(|c| c.api_version) - .unwrap_or(EtherscanApiVersion::V2) - } else { - EtherscanApiVersion::V1 - } + let api_version = verifier_args.verifier_api_version.unwrap_or_else(|| { + if is_etherscan { + etherscan_config.as_ref().map(|c| c.api_version).unwrap_or_default() + } else { + EtherscanApiVersion::V1 } - }; + }); let etherscan_api_url = verifier_url .or_else(|| { diff --git a/crates/verify/src/verify.rs b/crates/verify/src/verify.rs index b6bbb5a9e8fbd..8fd9c98236a5d 100644 --- a/crates/verify/src/verify.rs +++ b/crates/verify/src/verify.rs @@ -10,6 +10,7 @@ use alloy_primitives::Address; use alloy_provider::Provider; use clap::{Parser, ValueHint}; use eyre::Result; +use foundry_block_explorers::EtherscanApiVersion; use foundry_cli::{ opts::{EtherscanOpts, RpcOpts}, utils::{self, LoadConfig}, @@ -40,7 +41,7 @@ pub struct VerifierArgs { /// The verifier API version, if using a custom provider. #[arg(long, help_heading = "Verifier options", env = "VERIFIER_API_VERSION")] - pub verifier_api_version: Option, + pub verifier_api_version: Option, } impl Default for VerifierArgs { @@ -184,7 +185,7 @@ impl figment::Provider for VerifyArgs { } if let Some(api_version) = &self.verifier.verifier_api_version { - dict.insert("etherscan_api_version".into(), api_version.as_str().into()); + dict.insert("etherscan_api_version".into(), api_version.to_string().into()); } Ok(figment::value::Map::from([(Config::selected_profile(), dict)])) @@ -457,7 +458,7 @@ impl figment::Provider for VerifyCheckArgs { } if let Some(api_version) = &self.etherscan.api_version { - dict.insert("etherscan_api_version".into(), api_version.as_str().into()); + dict.insert("etherscan_api_version".into(), api_version.to_string().into()); } Ok(figment::value::Map::from([(Config::selected_profile(), dict)]))