diff --git a/Cargo.lock b/Cargo.lock index 97154acb8607a..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.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001678abc9895502532c8c4a1a225079c580655fc82a194e78b06dcf99f49b8c" +checksum = "8025385c52416bf14e5bb28d21eb5efe2490dd6fb001a49b87f1825a626b4909" dependencies = [ "alloy-chains", "alloy-json-abi", @@ -3782,6 +3783,7 @@ dependencies = [ "dotenvy", "eyre", "forge-fmt", + "foundry-block-explorers", "foundry-common", "foundry-compilers", "foundry-config", diff --git a/Cargo.toml b/Cargo.toml index 2d8aeadaa72f2..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.0", 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/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/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/run.rs b/crates/cast/src/cmd/run.rs index 947704a2df5a1..90470ef31ed97 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.to_string().into()); + } + if let Some(evm_version) = self.evm_version { map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?); } 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/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/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/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index 344efe73e8514..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, @@ -114,6 +115,16 @@ 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", + alias = "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 +165,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/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/etherscan.rs b/crates/config/src/etherscan.rs index 099dc0e344c2f..76d50b09bc934 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -9,6 +9,7 @@ use figment::{ value::{Dict, Map}, Error, Metadata, Profile, Provider, }; +use foundry_block_explorers::EtherscanApiVersion; use heck::ToKebabCase; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -83,13 +84,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(), @@ -173,6 +174,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, 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, } @@ -187,8 +191,11 @@ impl EtherscanConfig { pub fn resolve( self, alias: Option<&str>, + default_api_version: EtherscanApiVersion, ) -> Result { - let Self { chain, mut url, key } = self; + let Self { chain, mut url, key, api_version } = self; + + let api_version = api_version.unwrap_or(default_api_version); if let Some(url) = &mut url { *url = interpolate(url)?; @@ -219,17 +226,23 @@ impl EtherscanConfig { match (chain, url) { (Some(chain), Some(api_url)) => Ok(ResolvedEtherscanConfig { api_url, + api_version, browser_url: chain.etherscan_urls().map(|(_, url)| url.to_string()), key, chain: Some(chain), }), - (Some(chain), None) => ResolvedEtherscanConfig::create(key, chain).ok_or_else(|| { - let msg = alias.map(|a| format!(" `{a}`")).unwrap_or_default(); - EtherscanConfigError::UnknownChain(msg, chain) + (Some(chain), None) => ResolvedEtherscanConfig::create(key, chain, api_version) + .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, }), - (None, Some(api_url)) => { - Ok(ResolvedEtherscanConfig { api_url, browser_url: None, key, chain: None }) - } (None, None) => { let msg = alias .map(|a| format!(" for Etherscan config with unknown alias `{a}`")) @@ -251,6 +264,9 @@ pub struct ResolvedEtherscanConfig { pub browser_url: Option, /// The resolved API key. pub key: String, + /// Etherscan API Version. + #[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, @@ -258,11 +274,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: EtherscanApiVersion, + ) -> Option { let chain = chain.into(); let (api_url, browser_url) = chain.etherscan_urls()?; Some(Self { api_url: api_url.to_string(), + api_version, browser_url: Some(browser_url.to_string()), key: api_key.into(), chain: Some(chain), @@ -294,13 +315,10 @@ impl ResolvedEtherscanConfig { self, ) -> Result { - let Self { api_url, browser_url, key: api_key, chain } = self; - let (mainnet_api, mainnet_url) = NamedChain::Mainnet.etherscan_urls().expect("exist; qed"); + let Self { api_url, browser_url, key: api_key, chain, api_version } = self; - 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 @@ -314,15 +332,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() } } @@ -423,12 +441,36 @@ mod tests { chain: Some(Mainnet.into()), url: None, key: EtherscanApiKey::Key("ABCDEFG".to_string()), + api_version: None, }, ); - let mut resolved = configs.resolved(); + let mut resolved = configs.resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap().unwrap(); - let _ = config.into_client().unwrap(); + // None version = None + assert_eq!(config.api_version, EtherscanApiVersion::V2); + 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(EtherscanApiVersion::V2); + let config = resolved.remove("mainnet").unwrap().unwrap(); + assert_eq!(config.api_version, EtherscanApiVersion::V1); + let client = config.into_client().unwrap(); + assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V1); } #[test] @@ -440,10 +482,11 @@ mod tests { chain: Some(Mainnet.into()), url: Some("https://api.etherscan.io/api".to_string()), key: EtherscanApiKey::Key("ABCDEFG".to_string()), + api_version: None, }, ); - let mut resolved = configs.resolved(); + let mut resolved = configs.resolved(EtherscanApiVersion::V2); let config = resolved.remove("mainnet").unwrap().unwrap(); let _ = config.into_client().unwrap(); } @@ -457,20 +500,22 @@ mod tests { EtherscanConfig { chain: Some(Mainnet.into()), url: Some("https://api.etherscan.io/api".to_string()), + api_version: None, key: EtherscanApiKey::Env(format!("${{{env}}}")), }, ); - 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 _ = config.into_client().unwrap(); + let client = config.into_client().unwrap(); + assert_eq!(*client.etherscan_api_version(), EtherscanApiVersion::V2); std::env::remove_var(env); } @@ -484,10 +529,11 @@ mod tests { chain: None, url: Some("https://api.etherscan.io/api".to_string()), key: EtherscanApiKey::Key("ABCDEFG".to_string()), + api_version: None, }, ); - 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())); } @@ -498,11 +544,13 @@ 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(); + 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 c78bde6d8d8e7..2abb5a04cb233 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; @@ -281,6 +282,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, @@ -1377,17 +1380,23 @@ impl Config { &self, chain: Option, ) -> Result, EtherscanConfigError> { + 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) { - 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 @@ -1405,8 +1414,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.or(self.chain).unwrap_or_default(), + default_api_version, + )); } Ok(None) @@ -1421,6 +1433,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_default() + } + /// Returns the remapping for the project's _src_ directory /// /// **Note:** this will add an additional `/=` remapping here so imports that @@ -2369,6 +2392,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, @@ -3063,11 +3087,66 @@ 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(EtherscanApiVersion::V2); + 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: EtherscanApiVersion::V2, + 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: EtherscanApiVersion::V2, + 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(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(); @@ -3081,6 +3160,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: EtherscanApiVersion::V2, key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(), } ), @@ -3090,6 +3170,7 @@ mod tests { api_url: mb_urls.0.to_string(), chain: Some(Moonbeam.into()), browser_url: Some(mb_urls.1.to_string()), + api_version: EtherscanApiVersion::V1, key: "123456789".to_string(), } ), diff --git a/crates/forge/src/cmd/clone.rs b/crates/forge/src/cmd/clone.rs index aaf157637c9e7..a5bc4b2aa4676 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...")?; @@ -630,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)] @@ -709,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/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 744c38d71d977..5cffad23b9141 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, 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, + chain: Some(chain.into()), + }, rpc: Default::default(), flatten: false, force: false, diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index a1f3e64bb574f..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}, @@ -146,6 +147,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 +878,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/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/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/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/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 fbbb52398a837..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, @@ -182,6 +183,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 +501,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/test-utils/src/rpc.rs b/crates/test-utils/src/rpc.rs index 70e3c87534f95..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,16 +54,6 @@ static ETHERSCAN_MAINNET_KEYS: LazyLock> = LazyLock::new(|| { "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74", "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG", "ZUB97R31KSYX7NYVW6224Q6EYY6U56H591", - // Optimism - // "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46", - ]) -}); - -// List of etherscan keys for Optimism. -static ETHERSCAN_OPTIMISM_KEYS: LazyLock> = LazyLock::new(|| { - shuffled(vec![ - // - "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46", ]) }); @@ -145,19 +135,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_KEYS).to_string(); + eprintln!("--- next_etherscan_api_key() = {key} ---"); key } @@ -205,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] @@ -213,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() @@ -248,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"), + }; + } } diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index 66e95c7e790b3..5c84dbc64d6a0 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.to_string().into()); + } + if let Some(block) = &self.block { dict.insert("block".into(), figment::value::Value::serialize(block)?); } @@ -136,13 +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(), - &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 aad51093886d7..fd0e08e355a14 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; @@ -12,12 +13,15 @@ use foundry_block_explorers::{ errors::EtherscanError, utils::lookup_compiler_version, verify::{CodeFormat, VerifyContract}, - Client, + Client, EtherscanApiVersion, +}; +use foundry_cli::{ + opts::EtherscanOpts, + utils::{get_provider, read_constructor_args_file, LoadConfig}, }; -use foundry_cli::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,13 +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(), - &config, - )?; + let etherscan = self.client(&args.etherscan, &args.verifier, &config)?; args.retry .into_retry() .run_async_until_break(|| async { @@ -219,13 +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(), - &config, - )?; + let etherscan = self.client(&args.etherscan, &args.verifier, &config)?; let verify_args = self.create_verify_request(args, context).await?; Ok((etherscan, verify_args)) @@ -252,16 +244,37 @@ impl EtherscanVerificationProvider { /// Create an Etherscan client. pub(crate) fn client( &self, - chain: Chain, - verifier_type: &VerificationProviderType, - verifier_url: Option<&str>, - etherscan_key: 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_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 = 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(|| etherscan_config.as_ref().map(|c| c.api_url.as_str())) + .or_else(|| { + if api_version == EtherscanApiVersion::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(); @@ -269,20 +282,14 @@ 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())); + etherscan_key.or_else(|| etherscan_config.as_ref().map(|c| c.key.clone())); - let mut builder = Client::builder(); + 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) @@ -392,13 +399,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(), - &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 @@ -465,6 +466,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}; @@ -498,15 +500,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(), - &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")); @@ -526,16 +520,69 @@ 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(), - &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")); + } + + #[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", + "--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, &args.verifier, &config).unwrap(); + + 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([ + "foundry-cli", + "0xd8509bee9c9bf012282ad33aba0d87241baf5064", + "src/Counter.sol:Counter", + "--verifier", + "etherscan", + "--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::Etherscan); + + let etherscan = EtherscanVerificationProvider::default(); + 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 71bfc60cc1673..8fd9c98236a5d 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, }; @@ -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}, @@ -23,8 +24,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 +38,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 +50,7 @@ impl Default for VerifierArgs { verifier: VerificationProviderType::Sourcify, verifier_api_key: None, verifier_url: None, + verifier_api_version: None, } } } @@ -180,6 +184,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.to_string().into()); + } + Ok(figment::value::Map::from([(Config::selected_profile(), dict)])) } } @@ -444,7 +452,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.to_string().into()); + } + + Ok(figment::value::Map::from([(Config::selected_profile(), dict)])) } }