use super::VerifyArgs;
use crate::deployment::verifier::{
explorer_api_key_env, explorer_api_url, explorer_base_url, forge_available, rpc_url_for_chain,
ContractVerifier,
};
use anyhow::Result;
use colored::*;
pub fn run(args: &VerifyArgs) -> Result<()> {
eprintln!("{}\n", "🔍 Forge Guard — Contract Verification".bold());
if !forge_available() {
anyhow::bail!(
"{} `forge` CLI not found. Install Foundry: https://book.getfoundry.sh/getting-started/installation",
"❌".red()
);
}
if args.all {
return verify_all_deployments(args);
}
let address = args
.address
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Provide --address or use --all"))?;
let name = args
.name
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Provide --name or use --all"))?;
let verifier = ContractVerifier::new(&args.shared.chain, args.api_key.clone());
let api_url = explorer_api_url(&args.shared.chain);
let base_url = explorer_base_url(&args.shared.chain);
println!(" 📄 Contract: {}", name.bold());
println!(" 📍 Address: {}", address);
println!(" ⛓️ Chain: {}", args.shared.chain);
if let Some(url) = api_url {
println!(" 🔗 Explorer: {}", url.dimmed());
}
if let Some(url) = base_url {
println!(" 🔍 Address: {}/address/{}", url, address);
}
println!();
eprintln!(" 🔄 Verifying via block explorer...");
let result = verifier.forge_verify(
address,
name,
&args.shared.chain,
args.constructor_args.as_deref(),
);
if result.verified {
println!(" {} {}", "✅ VERIFIED".green().bold(), result.details);
if let Some(url) = base_url {
println!(" 🔍 {}/address/{}#code", url, address);
}
return Ok(());
}
eprintln!(" ⚠️ Explorer verification: {}", result.details);
eprintln!(" 🔄 Checking bytecode match via RPC...");
let rpc = rpc_url_for_chain(&args.shared.chain);
let bytecode_result = verifier.verify_bytecode_match(address, name, &rpc);
if bytecode_result.verified {
println!(
" {} {}",
"✅ BYTECODE MATCH".green().bold(),
bytecode_result.details
);
println!(
" {} Try: forge verify-contract {} {} --chain {}",
"💡".dimmed(),
address,
name,
args.shared.chain
);
let env = explorer_api_key_env(&args.shared.chain);
println!(
" {} Set {} to enable explorer verification",
"💡".dimmed(),
env
);
return Ok(());
}
eprintln!(" ❌ {}", bytecode_result.details);
eprintln!();
if let Some(env) = Some(explorer_api_key_env(&args.shared.chain)) {
eprintln!(
" {} Set the {} environment variable or pass --api-key",
"💡".dimmed(),
env
);
}
anyhow::bail!("Verification failed for {} on {}", name, args.shared.chain);
}
fn verify_all_deployments(args: &VerifyArgs) -> Result<()> {
eprintln!(" 🔍 Checking all deployments...\n");
let broadcast_dir = args.shared.project.join("broadcast");
if !broadcast_dir.exists() {
anyhow::bail!(
"No broadcast directory found at {}. Run `forge script` first.",
broadcast_dir.display()
);
}
let verifier = ContractVerifier::new(&args.shared.chain, args.api_key.clone());
let mut verified = 0u32;
let mut failed = 0u32;
for entry in walkdir::WalkDir::new(&broadcast_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name().to_string_lossy().ends_with(".json")
&& !e.file_name().to_string_lossy().contains("run-latest")
})
{
let content = match std::fs::read_to_string(entry.path()) {
Ok(c) => c,
Err(_) => continue,
};
let parsed: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(_) => continue,
};
let transactions = parsed["transactions"].as_array();
let receipts = parsed["receipts"].as_array();
if let Some(txs) = transactions {
for tx in txs {
let contract_address = tx["contractAddress"].as_str();
let contract_name = tx["contractName"].as_str();
if let (Some(addr), Some(name)) = (contract_address, contract_name) {
if addr.is_empty() || addr == "0x0000000000000000000000000000000000000000" {
continue;
}
print!(" 📄 {} at {} ... ", name, addr);
std::io::Write::flush(&mut std::io::stdout()).ok();
let result = verifier.forge_verify(addr, name, &args.shared.chain, None);
if result.verified {
println!("{}", "✅".green());
verified += 1;
} else {
println!("{} {}", "❌".red(), result.details);
failed += 1;
}
}
}
}
if let Some(recs) = receipts {
for rec in recs {
let contract_address = rec["contractAddress"].as_str();
if let Some(addr) = contract_address {
if addr.is_empty() || addr == "0x0000000000000000000000000000000000000000" {
continue;
}
let already_verified = transactions
.map(|txs| {
txs.iter()
.any(|tx| tx["contractAddress"].as_str() == Some(addr))
})
.unwrap_or(false);
if !already_verified {
print!(" 📄 Contract at {} ... ", addr);
std::io::Write::flush(&mut std::io::stdout()).ok();
let result =
verifier.forge_verify(addr, "unknown", &args.shared.chain, None);
if result.verified {
println!("{}", "✅".green());
verified += 1;
} else {
println!("{} {}", "❌".red(), result.details);
failed += 1;
}
}
}
}
}
}
if verified == 0 && failed == 0 {
eprintln!(" ⚠️ No deployments found in broadcast directory.");
} else {
println!(
"\n {} {} verified, {} failed",
if failed == 0 {
"✅".green()
} else {
"⚠️".yellow()
},
verified,
failed
);
}
if failed > 0 {
anyhow::bail!("{failed} contract(s) failed verification");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_arg_defaults() {
let args = VerifyArgs {
shared: super::super::SharedFlags {
chain: "ethereum".into(),
project: std::path::PathBuf::from("."),
json: false,
markdown: false,
strict: false,
offline: false,
production: false,
report: false,
parallelism: 4,
},
address: None,
name: None,
api_key: None,
constructor_args: None,
all: false,
};
assert!(args.address.is_none());
assert!(args.name.is_none());
assert!(args.api_key.is_none());
assert!(args.constructor_args.is_none());
assert!(!args.all);
}
#[test]
fn test_verify_with_address_and_name() {
let args = VerifyArgs {
shared: super::super::SharedFlags::default(),
address: Some("0x1234".into()),
name: Some("MyContract".into()),
api_key: Some("test-api-key".into()),
constructor_args: Some("0x0001".into()),
all: false,
};
assert_eq!(args.address.as_deref(), Some("0x1234"));
assert_eq!(args.name.as_deref(), Some("MyContract"));
assert_eq!(args.api_key.as_deref(), Some("test-api-key"));
assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
}
#[test]
fn test_verify_all_flag() {
let args = VerifyArgs {
shared: super::super::SharedFlags::default(),
address: None,
name: None,
api_key: None,
constructor_args: None,
all: true,
};
assert!(args.all);
}
#[test]
fn test_explorer_api_url_known_chain() {
let url = explorer_api_url("ethereum");
assert!(url.is_some());
let url_str = url.unwrap();
assert!(url_str.contains("etherscan"));
}
#[test]
fn test_explorer_api_url_unknown_chain_is_none() {
let url = explorer_api_url("nonexistent-chain");
assert!(url.is_none());
}
#[test]
fn test_explorer_base_url_known_chain() {
let url = explorer_base_url("base");
assert!(url.is_some());
let url_str = url.unwrap();
assert!(url_str.contains("basescan"));
}
#[test]
fn test_rpc_url_for_chain_returns_url() {
let url = rpc_url_for_chain("ethereum");
assert!(url.contains("rpc"));
}
#[test]
fn test_explorer_api_key_env_known_chain() {
let env = explorer_api_key_env("ethereum");
assert!(env.contains("API_KEY"));
}
#[test]
fn test_forge_available_returns_false_in_test() {
let available = forge_available();
assert!(available == true || available == false);
}
#[test]
fn test_contract_verifier_creation() {
let verifier = ContractVerifier::new("ethereum", None);
assert!(verifier.forge_available() == true || verifier.forge_available() == false);
}
#[test]
fn test_contract_verifier_with_api_key() {
let verifier = ContractVerifier::new("base", Some("test-key".into()));
assert!(verifier.forge_available() == true || verifier.forge_available() == false);
}
#[test]
fn test_verify_address_empty_check() {
let empty_addr = "0x0000000000000000000000000000000000000000";
assert!(empty_addr.is_empty() || empty_addr == "0x0000000000000000000000000000000000000000");
let valid_addr = "0x1234";
assert!(!valid_addr.is_empty() && valid_addr != "0x0000000000000000000000000000000000000000");
}
#[test]
fn test_explorer_base_url_arbitrum() {
let url = explorer_base_url("arbitrum");
assert!(url.is_some());
let url_str = url.unwrap();
assert!(url_str.contains("arbiscan"));
}
#[test]
fn test_rpc_url_unknown_chain_fallback() {
let url = rpc_url_for_chain("unknown-chain");
assert!(!url.is_empty());
}
}