diff --git a/foundry.toml b/foundry.toml index e448cd8e..5526db96 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 }, diff --git a/script/cli/.gitignore b/script/cli/.gitignore index 619beff2..a19df8d2 100644 --- a/script/cli/.gitignore +++ b/script/cli/.gitignore @@ -1 +1 @@ -src/assets/chains.json \ No newline at end of file +src/assets/ \ No newline at end of file diff --git a/script/cli/Cargo.toml b/script/cli/Cargo.toml index 26aa8231..eb1fa4f9 100644 --- a/script/cli/Cargo.toml +++ b/script/cli/Cargo.toml @@ -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] \ No newline at end of file diff --git a/script/cli/build.rs b/script/cli/build.rs index 2e7a6883..0511bec2 100644 --- a/script/cli/build.rs +++ b/script/cli/build.rs @@ -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"); + 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"); + } + } } diff --git a/script/cli/justfile b/script/cli/justfile index a2cb1e62..363de820 100644 --- a/script/cli/justfile +++ b/script/cli/justfile @@ -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 @@ -25,7 +25,7 @@ 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: @@ -33,4 +33,4 @@ watch: # Install the project install: - cargo install --path . + CLEAN=true cargo install --path . diff --git a/script/cli/src/libs/explorer.rs b/script/cli/src/libs/explorer.rs index 59da0f1e..2ccca181 100644 --- a/script/cli/src/libs/explorer.rs +++ b/script/cli/src/libs/explorer.rs @@ -1,4 +1,3 @@ -use crate::util::chain_config::Explorer; use crate::{errors::log, state_manager::STATE_MANAGER}; use alloy::{ json_abi::{Constructor, JsonAbi}, @@ -8,93 +7,88 @@ 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, +} + +#[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> { - if explorer.name.to_lowercase().contains("blockscout") { + if explorer.explorer_type == SupportedExplorerType::Blockscout { // blockscout just appends /api to their explorer url + 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::>().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::>().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(), + ); } } @@ -102,8 +96,9 @@ impl ExplorerApiLib { &self, contract_address: Address, ) -> Result<(String, String, Option), Box> { - 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={}", @@ -135,8 +130,7 @@ impl ExplorerApiLib { } Err(format!( "Unsupported explorer: {} ({})", - self.name, - self.url, + self.explorer.name, self.explorer.url, ) .into()) } @@ -145,8 +139,9 @@ impl ExplorerApiLib { &self, contract_address: Address, ) -> Result> { - 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={}", @@ -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> { match reqwest::get(url).await { Ok(response) => { @@ -184,9 +198,7 @@ async fn get_etherscan_result(url: &str) -> Result { - Err(format!("Explorer Request Error: {}", e).into()) - } + Err(e) => Err(format!("Explorer Request Error: {}", e).into()), } } diff --git a/script/cli/src/screens/deploy_contracts/execute_deploy_script.rs b/script/cli/src/screens/deploy_contracts/execute_deploy_script.rs index 41d3ce2b..26ca78fa 100644 --- a/script/cli/src/screens/deploy_contracts/execute_deploy_script.rs +++ b/script/cli/src/screens/deploy_contracts/execute_deploy_script.rs @@ -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")) { @@ -208,9 +208,7 @@ fn execute_command(command: &mut Command) -> Result, Box { - Err(e.to_string().into()) - } + Err(e) => Err(e.to_string().into()), } } @@ -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", diff --git a/script/cli/src/screens/home.rs b/script/cli/src/screens/home.rs index d9ea8e9c..8bc589ed 100644 --- a/script/cli/src/screens/home.rs +++ b/script/cli/src/screens/home.rs @@ -4,10 +4,10 @@ use crate::screens::types::select::SelectComponent; use crate::state_manager::STATE_MANAGER; use crate::ui::Buffer; use crate::workflows::deploy::deploy_contracts::DeployContractsWorkflow; -use crate::workflows::error_workflow::ErrorWorkflow; use crate::workflows::{ config::create_config::CreateConfigWorkflow, register::register_contract::RegisterContractWorkflow, + verify::verify_contract::VerifyContractWorkflow, }; use crossterm::event::Event; @@ -64,7 +64,7 @@ impl Screen for HomeScreen { DeployContractsWorkflow::new(), )]))), 2 | 3 => Ok(ScreenResult::NextScreen(Some(vec![Box::new( - ErrorWorkflow::new("Coming soon!".to_string()), + VerifyContractWorkflow::new(), )]))), 4 => Ok(ScreenResult::NextScreen(Some(vec![Box::new( RegisterContractWorkflow::new(), diff --git a/script/cli/src/screens/mod.rs b/script/cli/src/screens/mod.rs index 6491fa0c..cd11e146 100644 --- a/script/cli/src/screens/mod.rs +++ b/script/cli/src/screens/mod.rs @@ -6,6 +6,8 @@ pub mod shared; pub mod register_contract; // screens used by the deploy contracts workflow pub mod deploy_contracts; +// screens used by the verify contract workflow +pub mod verify_contract; // home screen pub mod home; diff --git a/script/cli/src/screens/register_contract/get_contract_info.rs b/script/cli/src/screens/register_contract/get_contract_info.rs index f36f9c60..ea0edd3a 100644 --- a/script/cli/src/screens/register_contract/get_contract_info.rs +++ b/script/cli/src/screens/register_contract/get_contract_info.rs @@ -8,8 +8,6 @@ use alloy::primitives::Address; use crossterm::event::Event; use std::sync::{Arc, Mutex}; -// Tests the connection to the rpc url, fails if the connection is not successful or the chain id doesn't match the expected chain id -// chain id and rpc url MUST be set before this screen is rendered pub struct GetContractInfoScreen { execution_status: Arc>, execution_message: Arc>, diff --git a/script/cli/src/screens/register_contract/mod.rs b/script/cli/src/screens/register_contract/mod.rs index 03fb9fb1..f605a436 100644 --- a/script/cli/src/screens/register_contract/mod.rs +++ b/script/cli/src/screens/register_contract/mod.rs @@ -1,2 +1 @@ -pub mod enter_address; pub mod get_contract_info; diff --git a/script/cli/src/screens/shared/block_explorer.rs b/script/cli/src/screens/shared/block_explorer.rs index 02ebf00b..52070530 100644 --- a/script/cli/src/screens/shared/block_explorer.rs +++ b/script/cli/src/screens/shared/block_explorer.rs @@ -1,8 +1,8 @@ +use crate::libs::explorer::{Explorer, SupportedExplorerType}; use crate::screens::screen_manager::{Screen, ScreenResult}; use crate::screens::types::select::SelectComponent; use crate::state_manager::STATE_MANAGER; use crate::ui::Buffer; -use crate::util::chain_config::Explorer; use crossterm::event::Event; // Sets the block explorer for further operations @@ -28,15 +28,16 @@ impl BlockExplorerScreen { .clone() .into_iter() .filter(|explorer| { - explorer.name.to_lowercase().contains("blockscout") - || explorer.name.to_lowercase().contains("scan") + explorer.explorer_type == SupportedExplorerType::Blockscout + || explorer.explorer_type == SupportedExplorerType::EtherscanV2 + || explorer.explorer_type == SupportedExplorerType::Etherscan }) .collect(); pre_selected_explorers = explorers .clone() .into_iter() - .map(|explorer| explorer.url) + .map(|explorer| format!("{} ({})", explorer.url, explorer.explorer_type.name())) .collect(); } if explorers.is_empty() { diff --git a/script/cli/src/screens/register_contract/enter_address.rs b/script/cli/src/screens/shared/enter_address.rs similarity index 69% rename from script/cli/src/screens/register_contract/enter_address.rs rename to script/cli/src/screens/shared/enter_address.rs index d8c0f579..ab7aaa0b 100644 --- a/script/cli/src/screens/register_contract/enter_address.rs +++ b/script/cli/src/screens/shared/enter_address.rs @@ -1,23 +1,28 @@ use crate::screens::screen_manager::{Screen, ScreenResult}; use crate::screens::types::text_input::TextInputComponent; -use crate::state_manager::STATE_MANAGER; use crate::ui::Buffer; use crate::util::screen_util::validate_address; use crossterm::event::Event; +type Hook = Box Result> + Send>; + pub struct EnterAddressScreen { text_input: TextInputComponent, + title: String, + hook: Option, } impl EnterAddressScreen { - pub fn new() -> Self { + pub fn new(title: String, hook: Option) -> Self { EnterAddressScreen { text_input: TextInputComponent::new(false, "".to_string(), validate_address), + title, + hook, } } fn render_title(&self, buffer: &mut Buffer) { - buffer.append_row_text("Please enter the contract address to register\n"); + buffer.append_row_text(&self.title); } fn render_instructions(&self, buffer: &mut Buffer) { @@ -35,14 +40,13 @@ impl Screen for EnterAddressScreen { fn handle_input(&mut self, event: Event) -> Result> { let address = self.text_input.handle_input(event); - if address.is_some() && address.clone().unwrap().len() == 42 { - STATE_MANAGER - .workflow_state - .lock() - .unwrap() - .register_contract_data - .address = address; - return Ok(ScreenResult::NextScreen(None)); + if let Some(address) = address { + if address.len() == 42 { + if let Some(hook) = &self.hook { + return hook(address); + } + return Ok(ScreenResult::NextScreen(None)); + } } Ok(ScreenResult::Continue) } diff --git a/script/cli/src/screens/shared/enter_explorer_api_key.rs b/script/cli/src/screens/shared/enter_explorer_api_key.rs index b82513a1..a6d8b0af 100644 --- a/script/cli/src/screens/shared/enter_explorer_api_key.rs +++ b/script/cli/src/screens/shared/enter_explorer_api_key.rs @@ -14,18 +14,8 @@ impl EnterExplorerApiKeyScreen { pub fn new() -> Result> { let explorer = STATE_MANAGER.workflow_state.lock()?.block_explorer.clone(); let mut env_var_name = "".to_string(); - if explorer.is_some() { - if explorer - .clone() - .unwrap() - .name - .to_lowercase() - .contains("scan") - { - env_var_name = "ETHERSCAN_API_KEY".to_string(); - } else { - env_var_name = explorer.unwrap().name.clone().to_uppercase() + "_API_KEY"; - } + if let Some(explorer) = explorer { + env_var_name = explorer.explorer_type.to_env_var_name(); } Ok(EnterExplorerApiKeyScreen { env_var_name: env_var_name.clone(), diff --git a/script/cli/src/screens/shared/mod.rs b/script/cli/src/screens/shared/mod.rs index e6912244..c2f871be 100644 --- a/script/cli/src/screens/shared/mod.rs +++ b/script/cli/src/screens/shared/mod.rs @@ -1,5 +1,6 @@ pub mod block_explorer; pub mod chain_id; +pub mod enter_address; pub mod enter_explorer_api_key; pub mod error_screen; pub mod generic_multi_select; diff --git a/script/cli/src/screens/verify_contract/mod.rs b/script/cli/src/screens/verify_contract/mod.rs new file mode 100644 index 00000000..61b3c6d9 --- /dev/null +++ b/script/cli/src/screens/verify_contract/mod.rs @@ -0,0 +1 @@ +pub mod verify_contract_screen; diff --git a/script/cli/src/screens/verify_contract/verify_contract_screen.rs b/script/cli/src/screens/verify_contract/verify_contract_screen.rs new file mode 100644 index 00000000..46e5102b --- /dev/null +++ b/script/cli/src/screens/verify_contract/verify_contract_screen.rs @@ -0,0 +1,197 @@ +use crate::libs::explorer::{ExplorerApiLib, SupportedExplorerType}; +use crate::screens::screen_manager::{Screen, ScreenResult}; +use crate::screens::types::select::SelectComponent; +use crate::state_manager::STATE_MANAGER; +use crate::ui::{get_spinner_frame, Buffer}; +use alloy::primitives::Address; +use crossterm::event::Event; +use regex::Regex; +use std::io::BufRead; +use std::process::Command; +use std::sync::{Arc, Mutex}; + +pub struct VerifyContractData { + pub address: Option, +} + +pub struct VerifyContractScreen { + execution_status: Arc>, + execution_message: Arc>, + select: SelectComponent, +} + +#[derive(PartialEq, Debug)] +enum ExecutionStatus { + Pending, + Success, + Failed, +} + +impl VerifyContractScreen { + pub fn new() -> Result> { + let screen = VerifyContractScreen { + select: SelectComponent::new(vec![ + "Enter another address".to_string(), + "Exit".to_string(), + ]), + execution_status: Arc::new(Mutex::new(ExecutionStatus::Pending)), + execution_message: Arc::new(Mutex::new(String::new())), + }; + + let chain_id = STATE_MANAGER + .workflow_state + .lock()? + .chain_id + .clone() + .unwrap(); + + let rpc_url = STATE_MANAGER + .workflow_state + .lock()? + .web3 + .clone() + .unwrap() + .rpc_url; + + let execution_status = Arc::clone(&screen.execution_status); + let execution_message = Arc::clone(&screen.execution_message); + + let explorer = STATE_MANAGER + .workflow_state + .lock()? + .block_explorer + .clone() + .unwrap(); + + let explorer_api_key = STATE_MANAGER + .workflow_state + .lock()? + .explorer_api_key + .clone() + .unwrap(); + + let explorer_api = ExplorerApiLib::new(explorer, explorer_api_key)?; + + let contract_address = STATE_MANAGER + .workflow_state + .lock()? + .verify_contract_data + .address + .clone() + .unwrap() + .parse::
()?; + + tokio::spawn(async move { + let mut command = &mut Command::new("forge"); + command = command + .arg("verify-contract") + .arg(format!("--chain={}", chain_id)) + .arg(format!("--rpc-url={}", rpc_url)) + .arg("-vvvv") + .arg("--watch") + .arg(format!( + "--verifier={}", + if explorer_api.explorer.explorer_type == SupportedExplorerType::Blockscout { + "blockscout" + } else { + // custom also works for etherscan + "custom" + } + )) + .arg(format!("--verifier-url={}", explorer_api.api_url)) + .arg(format!("--verifier-api-key={}", explorer_api.api_key)) + .arg("--guess-constructor-args") + .arg(format!("{}", contract_address)); + + match execute_command(&mut command) { + Ok(_) => { + *execution_status.lock().unwrap() = ExecutionStatus::Success; + *execution_message.lock().unwrap() = "".to_string(); + } + Err(e) => { + *execution_status.lock().unwrap() = ExecutionStatus::Failed; + *execution_message.lock().unwrap() = e.to_string(); + } + } + }); + + Ok(screen) + } +} + +impl Screen for VerifyContractScreen { + fn render_content(&self, buffer: &mut Buffer) -> Result<(), Box> { + if *self.execution_status.lock().unwrap() == ExecutionStatus::Pending { + buffer.append_row_text(&format!("{} Verifying contract\n", get_spinner_frame())); + } else if *self.execution_status.lock().unwrap() == ExecutionStatus::Success { + buffer.append_row_text(&format!("Contract verified successfully\n")); + self.select.render(buffer); + } else if *self.execution_status.lock().unwrap() == ExecutionStatus::Failed { + buffer.append_row_text(&format!( + "Error verifying contract: {}\n", + self.execution_message.lock().unwrap() + )); + self.select.render(buffer); + } + Ok(()) + } + + fn handle_input(&mut self, e: Event) -> Result> { + if *self.execution_status.lock().unwrap() != ExecutionStatus::Pending { + let result = self.select.handle_input(e); + if let Some(result) = result { + if result == 0 { + return Ok(ScreenResult::PreviousScreen); + } else if result == 1 { + return Ok(ScreenResult::NextScreen(None)); + } + } + } + + Ok(ScreenResult::Continue) + } + + fn execute(&mut self) -> Result<(), Box> { + Ok(()) + } +} + +fn execute_command(command: &mut Command) -> Result, Box> { + let cmd_str = format!("{:?}", command); + let re = Regex::new(r"--verifier-api-key=\S*").unwrap(); + let masked_cmd = re.replace_all(&cmd_str, "--verifier-api-key=***"); + let masked_cmd = masked_cmd.replace("\"", ""); + crate::errors::log(format!("Executing command: {}", masked_cmd)); + let mut result = command + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + + // Handle stdout + let stdout = result.stdout.take().expect("Failed to capture stdout"); + let stdout_reader = std::io::BufReader::new(stdout); + for line in stdout_reader.lines() { + let line = line?; + crate::errors::log(line); + } + + // Handle stderr + let stderr = result.stderr.take().expect("Failed to capture stderr"); + let stderr_reader = std::io::BufReader::new(stderr); + let mut error_message = String::new(); + for line in stderr_reader.lines() { + let line = line?; + crate::errors::log(line.clone()); + error_message.push_str(&line); + error_message.push('\n'); + } + match result.wait() { + Ok(status) => { + if !status.success() { + return Err(error_message.into()); + } + Ok(None) + } + Err(e) => Err(e.to_string().into()), + } +} diff --git a/script/cli/src/state_manager.rs b/script/cli/src/state_manager.rs index 7481805b..2bfe72a2 100644 --- a/script/cli/src/state_manager.rs +++ b/script/cli/src/state_manager.rs @@ -1,5 +1,7 @@ +use crate::libs::explorer::{Explorer, SupportedExplorerType}; use crate::libs::web3::Web3Lib; -use crate::util::chain_config::{parse_chain_config, Chain, Explorer}; +use crate::screens::verify_contract::verify_contract_screen::VerifyContractData; +use crate::util::chain_config::{parse_chain_config, Chain}; use crate::util::deployment_log::RegisterContractData; use crossterm::{ cursor::Show, @@ -25,6 +27,7 @@ pub struct WorkflowState { pub explorer_api_key: Option, pub block_explorer: Option, pub register_contract_data: RegisterContractData, + pub verify_contract_data: VerifyContractData, pub task: Value, pub private_key: Option, } @@ -38,6 +41,7 @@ impl WorkflowState { explorer_api_key: None, block_explorer: None, register_contract_data: RegisterContractData { address: None }, + verify_contract_data: VerifyContractData { address: None }, task: serde_json::json!({}), private_key: None, } @@ -70,7 +74,7 @@ impl StateManager { if chain_id == "7777777" || chain_id == "57073" { if let Some(ref mut chain_data) = chain { if chain_data.explorers.len() == 1 { - chain_data.explorers[0].name = "blockscout".to_string(); + chain_data.explorers[0].explorer_type = SupportedExplorerType::Blockscout; } } } diff --git a/script/cli/src/util/chain_config.rs b/script/cli/src/util/chain_config.rs index ee4b7174..e71fce14 100644 --- a/script/cli/src/util/chain_config.rs +++ b/script/cli/src/util/chain_config.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use crate::libs::explorer::{Explorer, SupportedExplorerType}; + #[derive(Default, Clone)] pub struct Currency { pub name: String, @@ -7,19 +9,6 @@ pub struct Currency { pub decimals: u8, } -#[derive(Clone)] -pub enum SupportedExplorerType { - Etherscan, - Blockscout, -} - -#[derive(Default, Clone)] -pub struct Explorer { - pub name: String, - pub url: String, - pub standard: String, -} - #[derive(Default, Clone)] pub struct Chain { pub name: String, @@ -31,14 +20,15 @@ pub struct Chain { // The chains.json file is downloaded from here: https://chainid.network/chains.json // The build.sh script automatically downloads the latest file and includes it in the binary. // this json in the file is automatically parsed and populated into the state manager to provide chain information such as rpcs, explorers, etc. -const JSON_DATA: &str = include_str!("../assets/chains.json"); +const CHAINS_DATA: &str = include_str!("../assets/chains.json"); +const ETHERSCAN_CHAINLIST_DATA: &str = include_str!("../assets/etherscan_chainlist.json"); pub fn parse_chain_config() -> HashMap { let mut chains = HashMap::new(); - let json_data: serde_json::Value = - serde_json::from_str(JSON_DATA).expect("Failed to parse JSON"); + let chains_data: serde_json::Value = + serde_json::from_str(CHAINS_DATA).expect("Failed to parse JSON"); - if let serde_json::Value::Array(chains_arr) = json_data { + if let serde_json::Value::Array(chains_arr) = chains_data { for chain_obj in chains_arr { if let serde_json::Value::Object(chain_data) = chain_obj { let chain_id = chain_data["chainId"] @@ -64,7 +54,7 @@ pub fn parse_chain_config() -> HashMap { arr.iter() .filter_map(|v| { if let serde_json::Value::Object(explorer) = v { - Some(Explorer { + let mut explorer = Explorer { name: explorer .get("name") .and_then(|n| n.as_str()) @@ -80,7 +70,10 @@ pub fn parse_chain_config() -> HashMap { .and_then(|s| s.as_str()) .unwrap_or("") .to_string(), - }) + explorer_type: SupportedExplorerType::Manual, + }; + guess_explorer_type(&mut explorer); + Some(explorer) } else { None } @@ -115,5 +108,43 @@ pub fn parse_chain_config() -> HashMap { } } } + + let etherscan_chainlist: serde_json::Value = + serde_json::from_str(ETHERSCAN_CHAINLIST_DATA).expect("Failed to parse JSON"); + + if let Some(result_array) = etherscan_chainlist.get("result").and_then(|v| v.as_array()) { + for entry in result_array { + if let Some(entry_obj) = entry.as_object() { + let chain_id = entry_obj + .get("chainid") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + if chains.contains_key(&chain_id) { + let mut explorers = chains.get_mut(&chain_id).unwrap().explorers.clone(); + if let Some(etherscan_explorer) = explorers + .iter() + .find(|e| e.explorer_type == SupportedExplorerType::Etherscan) + { + let mut v2_explorer = etherscan_explorer.clone(); + v2_explorer.explorer_type = SupportedExplorerType::EtherscanV2; + explorers.push(v2_explorer); + chains.get_mut(&chain_id).unwrap().explorers = explorers; + } + } + } + } + } + chains } + +fn guess_explorer_type(explorer: &mut Explorer) { + if explorer.url.contains("scan") || explorer.name.contains("scan") { + explorer.explorer_type = SupportedExplorerType::Etherscan; + } else if explorer.url.contains("blockscout") || explorer.name.contains("blockscout") { + explorer.explorer_type = SupportedExplorerType::Blockscout; + } else { + explorer.explorer_type = SupportedExplorerType::Manual; + } +} diff --git a/script/cli/src/util/deployment_log.rs b/script/cli/src/util/deployment_log.rs index 39f5b20a..d0e1e350 100644 --- a/script/cli/src/util/deployment_log.rs +++ b/script/cli/src/util/deployment_log.rs @@ -112,7 +112,9 @@ pub async fn generate_deployment_log( // } let proxy = contract_name == "TransparentUpgradeableProxy"; let mut contract_data = if proxy { - crate::errors::log("Proxy contract detected. Getting admin and implementation addresses".to_string()); + crate::errors::log( + "Proxy contract detected. Getting admin and implementation addresses".to_string(), + ); let admin: U256 = web3 .provider .get_storage_at( @@ -163,7 +165,8 @@ pub async fn generate_deployment_log( } let mut implementation_args = None; - if implementation_constructor.is_some() && !implementation_constructor_arguments.is_empty() { + if implementation_constructor.is_some() && !implementation_constructor_arguments.is_empty() + { implementation_args = Some( implementation_constructor .clone() @@ -251,7 +254,7 @@ pub async fn generate_deployment_log( .arg("--rpc-url") .arg(web3.rpc_url) .arg("-e") - .arg(explorer_api.url) + .arg(explorer_api.explorer.url) .arg("-s") .output() .expect("Failed to execute markdown generation script"); diff --git a/script/cli/src/workflows/mod.rs b/script/cli/src/workflows/mod.rs index 26f936b5..c1f5610b 100644 --- a/script/cli/src/workflows/mod.rs +++ b/script/cli/src/workflows/mod.rs @@ -1,6 +1,7 @@ pub mod config; pub mod deploy; pub mod register; +pub mod verify; pub mod default_workflow; pub mod error_workflow; diff --git a/script/cli/src/workflows/register/register_contract.rs b/script/cli/src/workflows/register/register_contract.rs index 0c5fb275..b8ee0322 100644 --- a/script/cli/src/workflows/register/register_contract.rs +++ b/script/cli/src/workflows/register/register_contract.rs @@ -1,8 +1,9 @@ use crate::errors; -use crate::screens::register_contract::enter_address::EnterAddressScreen; use crate::screens::register_contract::get_contract_info::GetContractInfoScreen; +use crate::screens::screen_manager::ScreenResult; use crate::screens::shared::block_explorer::BlockExplorerScreen; use crate::screens::shared::chain_id::ChainIdScreen; +use crate::screens::shared::enter_address::EnterAddressScreen; use crate::screens::shared::enter_explorer_api_key::EnterExplorerApiKeyScreen; use crate::screens::shared::rpc_url::get_rpc_url_screen; use crate::screens::shared::test_connection::TestConnectionScreen; @@ -87,7 +88,18 @@ impl RegisterContractWorkflow { EnterExplorerApiKeyScreen::new()?, ))), 6 => Ok(WorkflowResult::NextScreen(Box::new( - EnterAddressScreen::new(), + EnterAddressScreen::new( + "Please enter the contract address to register\n".to_string(), + Some(Box::new(|address| { + STATE_MANAGER + .workflow_state + .lock() + .unwrap() + .register_contract_data + .address = Some(address); + Ok(ScreenResult::NextScreen(None)) + })), + ), ))), 7 => Ok(WorkflowResult::NextScreen(Box::new( GetContractInfoScreen::new()?, diff --git a/script/cli/src/workflows/verify/mod.rs b/script/cli/src/workflows/verify/mod.rs new file mode 100644 index 00000000..f3a52c23 --- /dev/null +++ b/script/cli/src/workflows/verify/mod.rs @@ -0,0 +1 @@ +pub mod verify_contract; diff --git a/script/cli/src/workflows/verify/verify_contract.rs b/script/cli/src/workflows/verify/verify_contract.rs new file mode 100644 index 00000000..ab4a75c6 --- /dev/null +++ b/script/cli/src/workflows/verify/verify_contract.rs @@ -0,0 +1,119 @@ +use crate::errors; +use crate::screens::screen_manager::ScreenResult; +use crate::screens::shared::block_explorer::BlockExplorerScreen; +use crate::screens::shared::chain_id::ChainIdScreen; +use crate::screens::shared::enter_address::EnterAddressScreen; +use crate::screens::shared::enter_explorer_api_key::EnterExplorerApiKeyScreen; +use crate::screens::shared::rpc_url::get_rpc_url_screen; +use crate::screens::shared::test_connection::TestConnectionScreen; +use crate::screens::verify_contract::verify_contract_screen::VerifyContractScreen; +use crate::state_manager::STATE_MANAGER; +use crate::workflows::error_workflow::ErrorWorkflow; +use crate::workflows::workflow_manager::{process_nested_workflows, Workflow, WorkflowResult}; + +pub struct VerifyContractWorkflow { + current_screen: usize, + child_workflows: Vec>, +} + +impl VerifyContractWorkflow { + pub fn new() -> Self { + VerifyContractWorkflow { + current_screen: 0, + child_workflows: vec![], + } + } +} + +impl Workflow for VerifyContractWorkflow { + fn next_screen( + &mut self, + new_workflows: Option>>, + ) -> Result> { + match process_nested_workflows(&mut self.child_workflows, new_workflows)? { + WorkflowResult::NextScreen(screen) => Ok(WorkflowResult::NextScreen(screen)), + WorkflowResult::Finished => { + self.current_screen += 1; + self.get_screen() + } + } + } + + fn previous_screen(&mut self) -> Result> { + if !self.child_workflows.is_empty() { + return self.child_workflows[0].previous_screen(); + } + if self.current_screen > 1 { + self.current_screen -= 1; + } + // if current screen is the test connection screen, go back to the rpc url screen + if self.current_screen == 3 { + self.current_screen = 2; + } + self.get_screen() + } + + fn handle_error( + &mut self, + error: Box, + ) -> Result> { + match self.current_screen { + 3 => { + if error.downcast_ref::().is_some() { + STATE_MANAGER.workflow_state.lock()?.web3 = None; + self.current_screen = 2; + return self.get_screen(); + } + self.display_error(error.to_string()) + } + _ => self.display_error(error.to_string()), + } + } +} + +impl VerifyContractWorkflow { + fn get_screen(&self) -> Result> { + match self.current_screen { + 1 => Ok(WorkflowResult::NextScreen(Box::new(ChainIdScreen::new( + None, + )))), + 2 => get_rpc_url_screen(), + 3 => Ok(WorkflowResult::NextScreen(Box::new( + TestConnectionScreen::new()?, + ))), + 4 => Ok(WorkflowResult::NextScreen(Box::new( + BlockExplorerScreen::new()?, + ))), + 5 => Ok(WorkflowResult::NextScreen(Box::new( + EnterExplorerApiKeyScreen::new()?, + ))), + 6 => Ok(WorkflowResult::NextScreen(Box::new( + EnterAddressScreen::new( + "Please enter the contract address to verify\n".to_string(), + Some(Box::new(|address| { + STATE_MANAGER + .workflow_state + .lock() + .unwrap() + .verify_contract_data + .address = Some(address); + Ok(ScreenResult::NextScreen(None)) + })), + ), + ))), + 7 => Ok(WorkflowResult::NextScreen(Box::new( + VerifyContractScreen::new()?, + ))), + _ => Ok(WorkflowResult::Finished), + } + } + + fn display_error( + &mut self, + error_message: String, + ) -> Result> { + self.child_workflows = vec![Box::new(ErrorWorkflow::new(error_message))]; + self.current_screen = 1000000; + self.child_workflows[0].next_screen(None) + } +}