-
Notifications
You must be signed in to change notification settings - Fork 24
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
src/assets/chains.json | ||
src/assets/ |
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"); | ||
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"); | ||
} | ||
} | ||
} |
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}, | ||
|
@@ -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, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend a |
||
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={}", | ||
|
@@ -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<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={}", | ||
|
@@ -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) => { | ||
|
@@ -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()), | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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