Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: explorer rework and contract verification #64

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ compilation_restrictions = [
{ paths = "src/pkgs/v3-periphery/contracts/NonfungiblePositionManager.sol", optimizer_runs = 2000 },
{ paths = "src/pkgs/v3-periphery/contracts/NonfungibleTokenPositionDescriptor.sol", optimizer_runs = 1000 },
{ paths = "src/pkgs/v3-periphery/contracts/libraries/NFTDescriptor.sol", optimizer_runs = 1000 },
{ paths = "src/pkgs/v3-periphery/contracts/*.sol", version = "0.7.6", via_ir = false, max_optimizer_runs = 1000000 },
{ paths = "src/pkgs/v3-periphery/contracts/SwapRouter.sol", version = "0.7.6", via_ir = false, min_optimizer_runs = 1000000 },
{ paths = "src/pkgs/v3-periphery/contracts/V3Migrator.sol", version = "0.7.6", via_ir = false, min_optimizer_runs = 1000000 },
{ paths = "src/pkgs/v3-periphery/**/libraries/**", version = "<0.8.0" },
# permit2
{ paths = "src/pkgs/permit2/src/**", version = "0.8.17", via_ir = true },
Expand Down
2 changes: 1 addition & 1 deletion script/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
src/assets/chains.json
src/assets/
2 changes: 1 addition & 1 deletion script/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ regex = "1.11.0"
tokio = { version = "1.40.0", features = ["full"] }
alloy = { version = "0.4.2", features = ["default", "json-abi", "transports", "providers", "dyn-abi", "rpc-types-trace", "rpc-types-debug"] }
eyre = "0.6.12"
reqwest = "0.12.8"
reqwest = { version = "0.12.8", features = ["json", "blocking"] }
openssl = { version = "0.10.35", features = ["vendored"] }

[build-dependencies]
52 changes: 37 additions & 15 deletions script/cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
use std::fs;
use std::path::Path;
use std::process::Command;
use std::{env, fs};

fn main() {
// Create assets directory
let clean = env::var("CLEAN").unwrap_or("false".to_string()) == "true";
fs::create_dir_all("./src/assets").expect("Failed to create assets directory");
Copy link
Contributor

Choose a reason for hiding this comment

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

recommend using structopt to manage env variables / cli parameters

if clean {
fs::remove_file("./src/assets/chains.json").expect("Failed to remove chains.json");
fs::remove_file("./src/assets/etherscan_chainlist.json")
.expect("Failed to remove etherscan_chainlist.json");
}

// Download chains.json
let output = Command::new("wget")
.args([
"-O",
"./src/assets/chains.json",
"https://chainid.network/chains.json",
])
.output()
.expect("Failed to download chains.json");
// Only download chains.json if it doesn't exist
let chains_path = Path::new("./src/assets/chains.json");
if !chains_path.exists() {
let output = Command::new("wget")
.args([
"-O",
"./src/assets/chains.json",
"https://chainid.network/chains.json",
])
.output()
.expect("Failed to download chains.json");

if !output.status.success() {
panic!("Failed to download chains.json");
if !output.status.success() {
panic!("Failed to download chains.json");
}
}

// Tell cargo to re-run this if chains.json changes
println!("cargo:rerun-if-changed=src/assets/chains.json");
let etherscan_path = Path::new("./src/assets/etherscan_chainlist.json");
if !etherscan_path.exists() {
let etherscan_chainlist = Command::new("wget")
.args([
"-O",
"./src/assets/etherscan_chainlist.json",
"https://api.etherscan.io/v2/chainlist",
])
.output()
.expect("Failed to download etherscan chain list");

if !etherscan_chainlist.status.success() {
panic!("Failed to download etherscan chain list");
}
}
}
6 changes: 3 additions & 3 deletions script/cli/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ default:

# Build the project and copy the binary to the project root
build:
cargo build --release
CLEAN=true cargo build --release
cp ./target/release/deploy-cli ../../deploy-cli

# Run tests
Expand All @@ -25,12 +25,12 @@ clean:

# Build and run the project
run:
cargo run -- --dir ../..
CLEAN=true cargo run -- --dir ../..

# Watch the project and run it when the code changes
watch:
cargo watch -x 'run -- --dir ../..'

# Install the project
install:
cargo install --path .
CLEAN=true cargo install --path .
150 changes: 81 additions & 69 deletions script/cli/src/libs/explorer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::util::chain_config::Explorer;
use crate::{errors::log, state_manager::STATE_MANAGER};
use alloy::{
json_abi::{Constructor, JsonAbi},
Expand All @@ -8,102 +7,98 @@ use alloy::{
#[derive(Clone, PartialEq, Eq, Default)]
pub enum SupportedExplorerType {
#[default]
Manual,
EtherscanV2,
Etherscan,
Blockscout,
}

#[derive(Default, Clone)]
pub struct ExplorerApiLib {
pub struct Explorer {
pub name: String,
pub url: String,
pub standard: String,
pub explorer_type: SupportedExplorerType,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Recommend having the enum be the top level type with struct variant, to allow for potential of different explorer types needing different parameters / behavior. like

enum ExplorerType {
   Etherscan(ExplorerData),
   Blockscout(ExplorerData),
   ...
   NewExplorer(OtherExplorerData),
   
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh that's cool, thanks!


#[derive(Default, Clone)]
pub struct ExplorerApiLib {
pub explorer: Explorer,
pub api_key: String,
pub api_url: String,
pub explorer_type: SupportedExplorerType,
}

impl ExplorerApiLib {
pub fn new(explorer: Explorer, api_key: String) -> Result<Self, Box<dyn std::error::Error>> {
if explorer.name.to_lowercase().contains("blockscout") {
if explorer.explorer_type == SupportedExplorerType::Blockscout {
// blockscout just appends /api to their explorer url
Copy link
Contributor

Choose a reason for hiding this comment

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

Recommend a match statement rather than if tree. better type checking for case coverage

let api_url = format!("{}/api?", explorer.url);
Ok(ExplorerApiLib {
name: explorer.name.to_string(),
url: explorer.url.to_string(),
standard: explorer.standard.to_string(),
explorer,
api_key: api_key.to_string(),
api_url: format!("{}/api?", explorer.url),
explorer_type: SupportedExplorerType::Blockscout,
api_url,
})
} else if explorer.name.to_lowercase().contains("scan") {
} else if explorer.explorer_type == SupportedExplorerType::EtherscanV2 {
let chain_id = STATE_MANAGER
.workflow_state
.lock()
.unwrap()
.chain_id
.clone();
if chain_id.is_some() {
// old Etherscan v1 API code below, let's try the v2 API multichain beta when we have a chain id
// TODO: maybe check supported chain ids and fallback to v1 if the chain id is not supported?
if let Some(chain_id) = chain_id {
return Ok(ExplorerApiLib {
name: explorer.name.to_string(),
url: explorer.url.to_string(),
standard: explorer.standard.to_string(),
explorer,
api_key: api_key.to_string(),
api_url: format!(
"https://api.etherscan.io/v2/api?chainid={}",
chain_id.unwrap()
),
explorer_type: SupportedExplorerType::Etherscan,
api_url: format!("https://api.etherscan.io/v2/api?chainid={}", chain_id),
});
} else {
// etherscan prepends their api url with the api.* subdomain. So for mainnet this would be https://etherscan.io => https://api.etherscan.io. However testnets are also their own subdomain, their subdomains are then prefixed with api- and the explorer url is then used as the suffix, e.g., https://sepolia.etherscan.io => https://api-sepolia.etherscan.io. Some chains are also using a subdomain of etherscan, e.g., Optimism uses https://optimistic.etherscan.io. Here also the dash api- prefix is used. The testnet of optimism doesn't use an additional subdomain: https://sepolia-optimistic.etherscan.io => https://api-sepolia-optimistic.etherscan.io. Some explorers are using their own subdomain, e.g., arbiscan for Arbitrum: https://arbiscan.io => https://api.arbiscan.io.
// TODO: this is kinda error prone, this would catch correct etherscan instances like arbiscan for Arbitrum but there are a lot of other explorers named *something*scan that are not using an etherscan instance and thus don't share the same api endpoints. Maybe get a list of known etherscan-like explorers and their api urls and check if the explorer_url matches any of them?
let slices = explorer.url.split(".").collect::<Vec<&str>>().len();
if slices == 2 {
// we are dealing with https://somethingscan.io
return Ok(ExplorerApiLib {
name: explorer.name.to_string(),
url: explorer.url.to_string(),
standard: explorer.standard.to_string(),
api_key: api_key.to_string(),
api_url: explorer.url.replace("https://", "https://api.").to_string(),
explorer_type: SupportedExplorerType::Etherscan,
});
} else if slices == 3 {
// we are dealing with https://subdomain.somethingscan.io
return Ok(ExplorerApiLib {
name: explorer.name.to_string(),
url: explorer.url.to_string(),
standard: explorer.standard.to_string(),
api_key: api_key.to_string(),
api_url: explorer.url.replace("https://", "https://api-").to_string(),
explorer_type: SupportedExplorerType::Etherscan,
});
} else {
return Err(format!(
"Invalid etherscan url: {} ({})",
explorer.name,
explorer.url,
)
.into());
}
return Err(format!(
"Chain id not found for explorer: {} ({})",
explorer.name, explorer.url,
)
.into());
}
} else if explorer.explorer_type == SupportedExplorerType::Etherscan {
// etherscan prepends their api url with the api.* subdomain. So for mainnet this would be https://etherscan.io => https://api.etherscan.io. However testnets are also their own subdomain, their subdomains are then prefixed with api- and the explorer url is then used as the suffix, e.g., https://sepolia.etherscan.io => https://api-sepolia.etherscan.io. Some chains are also using a subdomain of etherscan, e.g., Optimism uses https://optimistic.etherscan.io. Here also the dash api- prefix is used. The testnet of optimism doesn't use an additional subdomain: https://sepolia-optimistic.etherscan.io => https://api-sepolia-optimistic.etherscan.io. Some explorers are using their own subdomain, e.g., arbiscan for Arbitrum: https://arbiscan.io => https://api.arbiscan.io.
// TODO: this is kinda error prone, this would catch correct etherscan instances like arbiscan for Arbitrum but there are a lot of other explorers named *something*scan that are not using an etherscan instance and thus don't share the same api endpoints. Maybe get a list of known etherscan-like explorers and their api urls and check if the explorer_url matches any of them?
let slices = explorer.url.split(".").collect::<Vec<&str>>().len();
if slices == 2 {
// we are dealing with https://somethingscan.io
let api_url = explorer.url.replace("https://", "https://api.");
return Ok(ExplorerApiLib {
explorer,
api_key: api_key.to_string(),
api_url: format!("{}/api?", api_url),
});
} else if slices == 3 {
// we are dealing with https://subdomain.somethingscan.io
let api_url = explorer.url.replace("https://", "https://api-");
return Ok(ExplorerApiLib {
explorer,
api_key: api_key.to_string(),
api_url: format!("{}/api?", api_url),
});
} else {
return Err(format!(
"Invalid etherscan url: {} ({})",
explorer.name, explorer.url,
)
.into());
}
} else {
return Err(format!(
"Unsupported explorer: {} ({})",
explorer.name,
explorer.url,
)
.into());
return Err(
format!("Unsupported explorer: {} ({})", explorer.name, explorer.url,).into(),
);
}
}

pub async fn get_contract_data(
&self,
contract_address: Address,
) -> Result<(String, String, Option<Constructor>), Box<dyn std::error::Error>> {
if self.explorer_type == SupportedExplorerType::Etherscan
|| self.explorer_type == SupportedExplorerType::Blockscout
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
{
let url = format!(
"{}&module=contract&action=getsourcecode&address={}&apikey={}",
Expand Down Expand Up @@ -135,8 +130,7 @@ impl ExplorerApiLib {
}
Err(format!(
"Unsupported explorer: {} ({})",
self.name,
self.url,
self.explorer.name, self.explorer.url,
)
.into())
}
Expand All @@ -145,8 +139,9 @@ impl ExplorerApiLib {
&self,
contract_address: Address,
) -> Result<String, Box<dyn std::error::Error>> {
if self.explorer_type == SupportedExplorerType::Etherscan
|| self.explorer_type == SupportedExplorerType::Blockscout
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
{
let url = format!(
"{}&module=contract&action=getcontractcreation&contractaddresses={}&apikey={}",
Expand All @@ -160,13 +155,32 @@ impl ExplorerApiLib {
}
Err(format!(
"Unsupported explorer: {} ({})",
self.name,
self.url,
self.explorer.name, self.explorer.url,
)
.into())
}
}

impl SupportedExplorerType {
pub fn to_env_var_name(&self) -> String {
match self {
SupportedExplorerType::Etherscan => "ETHERSCAN_API_KEY".to_string(),
SupportedExplorerType::EtherscanV2 => "ETHERSCAN_API_KEY".to_string(),
SupportedExplorerType::Blockscout => "BLOCKSCOUT_API_KEY".to_string(),
SupportedExplorerType::Manual => "VERIFIER_API_KEY".to_string(),
}
}

pub fn name(&self) -> String {
match self {
SupportedExplorerType::Etherscan => "Etherscan".to_string(),
SupportedExplorerType::EtherscanV2 => "Etherscan v2".to_string(),
SupportedExplorerType::Blockscout => "Blockscout".to_string(),
SupportedExplorerType::Manual => "".to_string(),
}
}
}

async fn get_etherscan_result(url: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
match reqwest::get(url).await {
Ok(response) => {
Expand All @@ -184,9 +198,7 @@ async fn get_etherscan_result(url: &str) -> Result<serde_json::Value, Box<dyn st
));
Err("Invalid response from etherscan".into())
}
Err(e) => {
Err(format!("Explorer Request Error: {}", e).into())
}
Err(e) => Err(format!("Explorer Request Error: {}", e).into()),
}
}

Expand Down
19 changes: 8 additions & 11 deletions script/cli/src/screens/deploy_contracts/execute_deploy_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,16 @@ impl ExecuteDeployScriptScreen {
.arg("--verify")
.arg(format!(
"--verifier={}",
if explorer_api.explorer_type == SupportedExplorerType::Blockscout {
if explorer_api.explorer.explorer_type == SupportedExplorerType::Blockscout
{
"blockscout"
} else {
"etherscan"
// custom also works for etherscan
"custom"
}
))
.arg(format!("--verifier-url={}", explorer_api.api_url));
if explorer_api.explorer_type == SupportedExplorerType::Etherscan {
command = command.arg(format!("--etherscan-api-key={}", explorer_api.api_key));
}
.arg(format!("--verifier-url={}", explorer_api.api_url))
.arg(format!("--verifier-api-key={}", explorer_api.api_key));
}

match execute_command(command.arg("--broadcast").arg("--skip-simulation")) {
Expand Down Expand Up @@ -208,9 +208,7 @@ fn execute_command(command: &mut Command) -> Result<Option<String>, Box<dyn std:
}
Ok(None)
}
Err(e) => {
Err(e.to_string().into())
}
Err(e) => Err(e.to_string().into()),
}
}

Expand All @@ -221,8 +219,7 @@ impl Screen for ExecuteDeployScriptScreen {
"Deployment failed: {}\n",
self.execution_error_message.lock().unwrap()
));
buffer
.append_row_text_color("> Press any key to continue", constants::SELECTION_COLOR);
buffer.append_row_text_color("> Press any key to continue", constants::SELECTION_COLOR);
} else {
buffer.append_row_text(&format!(
"{} Executing dry run\n",
Expand Down
Loading
Loading