From bddfae2f58fe0a2632583cb5d0e579ae4002b4b3 Mon Sep 17 00:00:00 2001 From: ilitteri Date: Fri, 6 Feb 2026 01:43:07 -0300 Subject: [PATCH 1/2] Add --verify-contract flag to rex deploy for Etherscan source verification After deploying a contract via --contract-path, the new --verify-contract flag automatically verifies the source code on Etherscan using the V2 API. This submits the Standard JSON Input (matching the exact solc flags used during compilation) and polls until verification succeeds. New flags: - --verify-contract: enable post-deploy verification - --etherscan-api-key: API key (also reads ETHERSCAN_API_KEY env var) - --contract-name: override contract name for multi-contract files - --optimizations: number of optimizer runs passed to solc and Etherscan The compilation step is refactored to return a CompilationInfo struct so that remappings and optimizer settings can be forwarded to the verification module. Artifact cleanup is deferred until after verification completes. --- Cargo.lock | 14 ++ cli/Cargo.toml | 4 + cli/README.md | 87 ++++++++++++ cli/src/cli.rs | 111 +++++++++++---- cli/src/common.rs | 28 +++- cli/src/lib.rs | 1 + cli/src/verify.rs | 345 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 566 insertions(+), 24 deletions(-) create mode 100644 cli/src/verify.rs diff --git a/Cargo.lock b/Cargo.lock index dca17fc..4ffa677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3448,6 +3448,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.13" @@ -3536,6 +3548,8 @@ dependencies = [ "keccak-hash", "rand 0.9.2", "rayon", + "regex", + "reqwest", "rex-sdk", "secp256k1", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ff05336..4017e05 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,12 +33,16 @@ secp256k1.workspace = true # Utils hex.workspace = true itertools = "0.14.0" +regex = "1" toml = "0.8.19" dirs = "6.0.0" rand = "0.9.1" rayon.workspace = true url = "2.5.7" +# HTTP +reqwest = { version = "0.12.7", features = ["json"] } + # Serde serde = "1.0.218" serde_json = "1.0.139" diff --git a/cli/README.md b/cli/README.md index 8902983..e335bb2 100644 --- a/cli/README.md +++ b/cli/README.md @@ -287,12 +287,99 @@ Options: Remove downloaded dependencies after compilation --salt Salt for deploying CREATE2 contracts. If it is provided, the contract will be deployed using CREATE2. + --optimizations + Number of optimization runs for the Solidity compiler + --verify-contract + Verify the contract on Etherscan after deployment + --etherscan-api-key + Etherscan API key for contract verification [env: ETHERSCAN_API_KEY=] + --contract-name + Contract name (defaults to filename stem). Required when file contains multiple contracts. --rpc-url [env: RPC_URL=] [default: http://localhost:8545] -h, --help Print help ``` +#### Contract verification + +The `--verify-contract` flag automatically verifies the contract source code on Etherscan after deployment. This makes the contract's source code publicly readable and its ABI available on Etherscan. + +**Requirements:** +- Must use `--contract-path` (not `--bytecode`) +- Requires an Etherscan API key via `--etherscan-api-key` or the `ETHERSCAN_API_KEY` environment variable +- Incompatible with `--cast` (verification needs to wait for the transaction receipt) +- Requires `solc` installed locally (the same version used to compile will be reported to Etherscan) + +**Supported networks:** Ethereum Mainnet, Sepolia, and Holesky (uses [Etherscan API V2](https://docs.etherscan.io/etherscan-v2)). + +**Basic example:** + +```shell +rex deploy \ + --contract-path MyContract.sol \ + --remappings "" \ + --verify-contract \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --private-key $PRIVATE_KEY \ + --rpc-url https://ethereum-sepolia-rpc.publicnode.com +``` + +**With optimizer enabled:** + +```shell +rex deploy \ + --contract-path MyContract.sol \ + --remappings "" \ + --optimizations 200 \ + --verify-contract \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --private-key $PRIVATE_KEY \ + --rpc-url https://ethereum-sepolia-rpc.publicnode.com +``` + +**With imports and remappings:** + +```shell +rex deploy \ + --contract-path MyToken.sol \ + --remappings "@openzeppelin/contracts=https://github.com/OpenZeppelin/openzeppelin-contracts.git" \ + --verify-contract \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --private-key $PRIVATE_KEY \ + --rpc-url https://ethereum-sepolia-rpc.publicnode.com +``` + +**With constructor arguments:** + +```shell +rex deploy \ + --contract-path MyToken.sol \ + --remappings "" \ + --verify-contract \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --private-key $PRIVATE_KEY \ + --rpc-url https://ethereum-sepolia-rpc.publicnode.com \ + -- "constructor(string,uint256)" "MyToken" 1000000 +``` + +**Output:** + +``` +Compiler run successful. Artifact(s) can be found in directory "./solc_out". +Contract deployed in tx: 0x1234... +Contract address: 0x5678... + +Verifying contract on Etherscan... + Compiler version: v0.8.31+commit.fd3a2265 + Contract name: MyContract + Submitting verification request... + Verification submitted (GUID: abc123...) + [1/60] Checking verification status... + Contract verified successfully! + https://sepolia.etherscan.io/address/0x5678...#code +``` + ### `rex encode-calldata` ```Shell diff --git a/cli/src/cli.rs b/cli/src/cli.rs index cfa9293..e733b35 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,6 +1,7 @@ use crate::commands::l2; use crate::common::AuthorizeArgs; use crate::utils::{parse_contract_creation, parse_func_call, parse_hex, parse_hex_string}; +use crate::verify::{VerifyParams, verify_contract_on_etherscan}; use crate::{ commands::autocomplete, common::{CallArgs, DeployArgs, SendArgs, TransferArgs}, @@ -602,23 +603,45 @@ impl Command { println!("{result}"); } - Command::Deploy { args, rpc_url } => { + Command::Deploy { mut args, rpc_url } => { if args.explorer_url { todo!("Display transaction URL in the explorer") } + if args.verify_contract { + if args.bytecode.is_some() { + return Err(eyre::eyre!( + "--verify-contract cannot be used with --bytecode. Use --contract-path instead." + )); + } + if args.etherscan_api_key.is_none() { + return Err(eyre::eyre!( + "--verify-contract requires --etherscan-api-key or ETHERSCAN_API_KEY env var." + )); + } + if args.cast { + return Err(eyre::eyre!( + "--verify-contract is incompatible with --cast (verification needs to wait for the receipt)." + )); + } + } + let deployer = Signer::Local(LocalSigner::new(args.private_key)); let client = EthClient::new(rpc_url)?; - let bytecode = if let Some(bytecode) = args.bytecode { - bytecode - } else { - compile_contract_from_path(args.clone()).await? + let (bytecode, compilation_info) = match args.bytecode.take() { + Some(bytecode) => (bytecode, None), + None => { + let info = compile_contract_from_path(&args)?; + let bytecode = info.bytecode.clone(); + (bytecode, Some(info)) + } }; - let init_args = if !args._args.is_empty() { - parse_contract_creation(args._args)? - } else { + + let init_args = if args._args.is_empty() { Bytes::new() + } else { + parse_contract_creation(std::mem::take(&mut args._args))? }; let (tx_hash, deployed_contract_address) = if let Some(salt) = args.salt { @@ -631,7 +654,7 @@ impl Command { ) .await? } else { - let init_code = [bytecode, init_args].concat().into(); + let init_code = [bytecode, init_args.clone()].concat().into(); deploy( &client, &deployer, @@ -661,6 +684,35 @@ impl Command { if !args.cast { wait_for_transaction_receipt(tx_hash, &client, 100, silent).await?; } + + if args.verify_contract { + // Unwraps are safe: validation above rejects bytecode and missing API key. + let info = compilation_info + .as_ref() + .expect("compilation_info is always set when verify_contract is true"); + + let chain_id: u64 = client + .get_chain_id() + .await? + .try_into() + .map_err(|_| eyre::eyre!("Chain ID too large to fit in u64"))?; + + verify_contract_on_etherscan(VerifyParams { + contract_address: deployed_contract_address, + contract_path: args.contract_path.unwrap(), + contract_name: args.contract_name, + constructor_args: init_args.to_vec(), + remappings: info.remappings.clone(), + optimize_runs: info.optimize_runs, + etherscan_api_key: args.etherscan_api_key.unwrap(), + chain_id, + }) + .await?; + } + + if let Some(info) = compilation_info.filter(|i| i.should_clean) { + cleanup_compilation_artifacts(info.cloned_dirs); + } } Command::ChainId { hex, rpc_url } => { let eth_client = EthClient::new(rpc_url)?; @@ -915,16 +967,24 @@ fn print_calldata(depth: usize, data: Value) { } } -async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result { +struct CompilationInfo { + bytecode: Bytes, + remappings: Vec<(String, PathBuf)>, + optimize_runs: Option, + cloned_dirs: Vec, + should_clean: bool, +} + +fn compile_contract_from_path(args: &DeployArgs) -> eyre::Result { let contract_path = args .contract_path .as_ref() .ok_or_else(|| eyre::eyre!("Contract path is required when bytecode is not provided"))?; - let clean = !args.keep_deps; + let should_clean = !args.keep_deps; let output_dir = Path::new("."); let deps_dir = Path::new("rex_deps"); - let mut solc_remappings = Vec::new(); + let mut solc_remappings: Vec<(String, PathBuf)> = Vec::new(); let mut cloned_dirs = Vec::new(); std::fs::create_dir_all(deps_dir).ok(); @@ -988,7 +1048,7 @@ async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result { false, Some(&solc_remappings_ref), &include_paths, - None, + args.optimizations, ) .map_err(|e| eyre::eyre!("Failed to compile contract: {}", e))?; @@ -1017,16 +1077,21 @@ async fn compile_contract_from_path(args: DeployArgs) -> eyre::Result { })? .into(); - if clean { - for dir in cloned_dirs { - if dir.exists() { - std::fs::remove_dir_all(&dir) - .map_err(|e| eyre::eyre!("Failed to clean up {}: {}", dir.display(), e))?; - } + Ok(CompilationInfo { + bytecode, + remappings: solc_remappings, + optimize_runs: args.optimizations, + cloned_dirs, + should_clean, + }) +} + +fn cleanup_compilation_artifacts(cloned_dirs: Vec) { + for dir in cloned_dirs { + if dir.exists() { + std::fs::remove_dir_all(&dir).ok(); } - std::fs::remove_dir_all("rex_deps").ok(); - std::fs::remove_dir_all("solc_out").ok(); } - - Ok(bytecode) + std::fs::remove_dir_all("rex_deps").ok(); + std::fs::remove_dir_all("solc_out").ok(); } diff --git a/cli/src/common.rs b/cli/src/common.rs index d8f9acd..f93a7d7 100644 --- a/cli/src/common.rs +++ b/cli/src/common.rs @@ -146,7 +146,7 @@ pub struct CallArgs { pub _args: Vec, } -#[derive(Parser, Clone)] +#[derive(Parser)] #[clap(group = clap::ArgGroup::new("source").required(true))] pub struct DeployArgs { #[clap(long, group = "source", value_parser = parse_hex, required = false)] @@ -218,6 +218,32 @@ pub struct DeployArgs { required = false )] pub salt: Option, + #[arg( + long, + required = false, + help = "Number of optimization runs for the Solidity compiler" + )] + pub optimizations: Option, + #[arg( + long, + help = "Verify the contract on Etherscan after deployment", + default_value_t = false, + requires = "source" + )] + pub verify_contract: bool, + #[arg( + long, + env = "ETHERSCAN_API_KEY", + required = false, + help = "Etherscan API key for contract verification" + )] + pub etherscan_api_key: Option, + #[arg( + long, + required = false, + help = "Contract name (defaults to filename stem). Required when file contains multiple contracts." + )] + pub contract_name: Option, #[arg(last = true, hide = true)] pub _args: Vec, } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 2bfae0f..3fa8b74 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,3 +6,4 @@ pub mod cli; mod commands; mod common; mod utils; +mod verify; diff --git a/cli/src/verify.rs b/cli/src/verify.rs new file mode 100644 index 0000000..d13f804 --- /dev/null +++ b/cli/src/verify.rs @@ -0,0 +1,345 @@ +use std::collections::{BTreeMap, HashSet}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::LazyLock; +use std::time::Duration; + +use ethrex_common::Address; +use eyre::ContextCompat; +use regex::Regex; +use serde::Deserialize; + +pub struct VerifyParams { + pub contract_address: Address, + pub contract_path: PathBuf, + pub contract_name: Option, + pub constructor_args: Vec, + pub remappings: Vec<(String, PathBuf)>, + pub optimize_runs: Option, + pub etherscan_api_key: String, + pub chain_id: u64, +} + +#[derive(Deserialize)] +struct EtherscanResponse { + status: String, + result: String, + message: String, +} + +pub async fn verify_contract_on_etherscan(params: VerifyParams) -> eyre::Result<()> { + let solc_version = get_solc_version()?; + + let source_file_key = file_name_str(¶ms.contract_path); + + let contract_name = params.contract_name.unwrap_or_else(|| { + params + .contract_path + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }); + + let standard_json = + build_standard_json_input(¶ms.contract_path, ¶ms.remappings, params.optimize_runs)?; + let standard_json_str = serde_json::to_string(&standard_json)?; + + let constructor_args_hex = hex::encode(¶ms.constructor_args); + + println!("\nVerifying contract on Etherscan..."); + println!(" Compiler version: {solc_version}"); + println!(" Contract name: {contract_name}"); + + let client = reqwest::Client::new(); + + println!(" Submitting verification request..."); + + let chain_id_str = params.chain_id.to_string(); + let contract_address_str = format!("{:#x}", params.contract_address); + let contract_name_str = format!("{source_file_key}:{contract_name}"); + + let form = [ + ("apikey", params.etherscan_api_key.as_str()), + ("module", "contract"), + ("action", "verifysourcecode"), + ("contractaddress", &contract_address_str), + ("sourceCode", &standard_json_str), + ("codeformat", "solidity-standard-json-input"), + ("contractname", &contract_name_str), + ("compilerversion", &solc_version), + ("constructorArguements", &constructor_args_hex), + ]; + + let url = format!("{ETHERSCAN_API_V2}?chainid={chain_id_str}"); + + // Etherscan may not have indexed the contract yet, retry submission + let guid = { + const MAX_SUBMIT_RETRIES: u32 = 12; + const SUBMIT_INTERVAL: Duration = Duration::from_secs(10); + let mut guid = None; + + for attempt in 1..=MAX_SUBMIT_RETRIES { + let resp = client + .post(&url) + .form(&form) + .send() + .await? + .json::() + .await?; + + if resp.status == "1" { + println!(" Verification submitted (GUID: {})", resp.result); + guid = Some(resp.result); + break; + } + + if resp.result.contains("Unable to locate") { + println!( + " [{attempt}/{MAX_SUBMIT_RETRIES}] Waiting for Etherscan to index the contract..." + ); + tokio::time::sleep(SUBMIT_INTERVAL).await; + continue; + } + + return Err(eyre::eyre!( + "Etherscan verification submission failed: {} ({})", + resp.result, + resp.message, + )); + } + + guid.ok_or_else(|| { + eyre::eyre!("Etherscan did not index the contract after {MAX_SUBMIT_RETRIES} retries") + })? + }; + + poll_verification_status(&client, ¶ms.etherscan_api_key, &chain_id_str, &guid).await?; + + let explorer_base = etherscan_explorer_url(params.chain_id); + println!( + " {explorer_base}/address/{:#x}#code", + params.contract_address + ); + + Ok(()) +} + +fn build_standard_json_input( + contract_path: &Path, + remappings: &[(String, PathBuf)], + optimize_runs: Option, +) -> eyre::Result { + let sources = resolve_sources(contract_path, remappings)?; + + let remappings_list: Vec = remappings + .iter() + .map(|(prefix, path)| format!("{prefix}={}", path.display())) + .collect(); + + let optimizer = match optimize_runs { + Some(runs) => serde_json::json!({ "enabled": true, "runs": runs }), + None => serde_json::json!({ "enabled": false }), + }; + + Ok(serde_json::json!({ + "language": "Solidity", + "sources": sources.into_iter().map(|(k, v)| { + (k, serde_json::json!({ "content": v })) + }).collect::>(), + "settings": { + "viaIR": true, + "metadata": { "appendCBOR": false }, + "optimizer": optimizer, + "remappings": remappings_list, + "outputSelection": { + "*": { "*": ["evm.bytecode"] } + } + } + })) +} + +fn resolve_sources( + contract_path: &Path, + remappings: &[(String, PathBuf)], +) -> eyre::Result> { + let mut sources = BTreeMap::new(); + let mut visited = HashSet::new(); + + let canonical = contract_path.canonicalize().map_err(|e| { + eyre::eyre!( + "Failed to resolve contract path {}: {}", + contract_path.display(), + e + ) + })?; + + resolve_sources_recursive(&canonical, remappings, &mut sources, &mut visited)?; + + Ok(sources) +} + +fn resolve_sources_recursive( + file_path: &Path, + remappings: &[(String, PathBuf)], + sources: &mut BTreeMap, + visited: &mut HashSet, +) -> eyre::Result<()> { + if !visited.insert(file_path.to_path_buf()) { + return Ok(()); + } + + let content = std::fs::read_to_string(file_path).map_err(|e| { + eyre::eyre!( + "Failed to read source file {}: {}", + file_path.display(), + e + ) + })?; + + let key = file_name_str(file_path); + + static IMPORT_RE: LazyLock = LazyLock::new(|| { + Regex::new(r#"import\s+(?:\{[^}]*\}\s+from\s+)?["']([^"']+)["']"#).expect("valid regex") + }); + + for cap in IMPORT_RE.captures_iter(&content) { + let import_path_str = &cap[1]; + let resolved = resolve_import_path(import_path_str, file_path, remappings)?; + resolve_sources_recursive(&resolved, remappings, sources, visited)?; + } + + sources.insert(key, content); + + Ok(()) +} + +fn resolve_import_path( + import_str: &str, + importing_file: &Path, + remappings: &[(String, PathBuf)], +) -> eyre::Result { + // Try remappings first + for (prefix, target_path) in remappings { + if let Some(rest) = import_str.strip_prefix(prefix.as_str()) { + let rest = rest.strip_prefix('/').unwrap_or(rest); + let resolved = target_path.join(rest); + return resolved.canonicalize().map_err(|e| { + eyre::eyre!( + "Failed to resolve remapped import '{}' -> {}: {}", + import_str, + resolved.display(), + e, + ) + }); + } + } + + // Relative import + let parent = importing_file + .parent() + .context("importing file has no parent directory")?; + let resolved = parent.join(import_str); + resolved.canonicalize().map_err(|e| { + eyre::eyre!( + "Failed to resolve import '{}' relative to {}: {}", + import_str, + parent.display(), + e, + ) + }) +} + +/// Parses `solc --version` output like "Version: 0.8.28+commit.7893614a.Linux.g++" +/// into "v0.8.28+commit.7893614a". +fn get_solc_version() -> eyre::Result { + let output = Command::new("solc").arg("--version").output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + + static SOLC_VERSION_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(\d+\.\d+\.\d+\+commit\.[0-9a-f]+)").expect("valid regex") + }); + let version = SOLC_VERSION_RE + .find(&stdout) + .context("Could not parse solc version from output")? + .as_str(); + + Ok(format!("v{version}")) +} + +fn file_name_str(path: &Path) -> String { + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string() +} + +const ETHERSCAN_API_V2: &str = "https://api.etherscan.io/v2/api"; + +fn etherscan_explorer_url(chain_id: u64) -> &'static str { + match chain_id { + 1 => "https://etherscan.io", + 11155111 => "https://sepolia.etherscan.io", + 17000 => "https://holesky.etherscan.io", + _ => "https://etherscan.io", + } +} + +async fn poll_verification_status( + client: &reqwest::Client, + api_key: &str, + chain_id: &str, + guid: &str, +) -> eyre::Result<()> { + const MAX_RETRIES: u32 = 60; + const POLL_INTERVAL: Duration = Duration::from_secs(5); + + for attempt in 1..=MAX_RETRIES { + tokio::time::sleep(POLL_INTERVAL).await; + + println!(" [{attempt}/{MAX_RETRIES}] Checking verification status..."); + + let resp = client + .get(ETHERSCAN_API_V2) + .query(&[ + ("chainid", chain_id), + ("module", "contract"), + ("action", "checkverifystatus"), + ("guid", guid), + ("apikey", api_key), + ]) + .send() + .await? + .json::() + .await?; + + if resp.status == "1" { + println!(" Contract verified successfully!"); + return Ok(()); + } + + if resp.result.contains("Already Verified") { + println!(" Contract is already verified."); + return Ok(()); + } + + // "Pending in queue" and "Unable to locate" are normal in-progress states + if resp.result.contains("Pending in queue") + || resp.result.contains("Unable to locate") + { + continue; + } + + // Any other non-pending result is a failure + return Err(eyre::eyre!( + "Etherscan verification failed: {} ({})", + resp.result, + resp.message, + )); + } + + Err(eyre::eyre!( + "Etherscan verification timed out after {} seconds", + MAX_RETRIES * POLL_INTERVAL.as_secs() as u32 + )) +} From bc379580b55697ee13799da0a0fa5235d5ed403f Mon Sep 17 00:00:00 2001 From: ilitteri Date: Fri, 6 Feb 2026 11:55:40 -0300 Subject: [PATCH 2/2] Fix cargo fmt formatting in verify.rs --- cli/src/verify.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/cli/src/verify.rs b/cli/src/verify.rs index d13f804..63f70f8 100644 --- a/cli/src/verify.rs +++ b/cli/src/verify.rs @@ -41,8 +41,11 @@ pub async fn verify_contract_on_etherscan(params: VerifyParams) -> eyre::Result< .to_string() }); - let standard_json = - build_standard_json_input(¶ms.contract_path, ¶ms.remappings, params.optimize_runs)?; + let standard_json = build_standard_json_input( + ¶ms.contract_path, + ¶ms.remappings, + params.optimize_runs, + )?; let standard_json_str = serde_json::to_string(&standard_json)?; let constructor_args_hex = hex::encode(¶ms.constructor_args); @@ -189,13 +192,8 @@ fn resolve_sources_recursive( return Ok(()); } - let content = std::fs::read_to_string(file_path).map_err(|e| { - eyre::eyre!( - "Failed to read source file {}: {}", - file_path.display(), - e - ) - })?; + let content = std::fs::read_to_string(file_path) + .map_err(|e| eyre::eyre!("Failed to read source file {}: {}", file_path.display(), e))?; let key = file_name_str(file_path); @@ -256,9 +254,8 @@ fn get_solc_version() -> eyre::Result { let output = Command::new("solc").arg("--version").output()?; let stdout = String::from_utf8_lossy(&output.stdout); - static SOLC_VERSION_RE: LazyLock = LazyLock::new(|| { - Regex::new(r"(\d+\.\d+\.\d+\+commit\.[0-9a-f]+)").expect("valid regex") - }); + static SOLC_VERSION_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+\.\d+\.\d+\+commit\.[0-9a-f]+)").expect("valid regex")); let version = SOLC_VERSION_RE .find(&stdout) .context("Could not parse solc version from output")? @@ -324,9 +321,7 @@ async fn poll_verification_status( } // "Pending in queue" and "Unable to locate" are normal in-progress states - if resp.result.contains("Pending in queue") - || resp.result.contains("Unable to locate") - { + if resp.result.contains("Pending in queue") || resp.result.contains("Unable to locate") { continue; }