Skip to content

Commit 339b70c

Browse files
committed
split etherscan into v1 and v2 api, do better guesses at what an explorer actually is
1 parent e891c43 commit 339b70c

File tree

11 files changed

+197
-140
lines changed

11 files changed

+197
-140
lines changed

Diff for: script/cli/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
src/assets/chains.json
1+
src/assets/

Diff for: script/cli/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ regex = "1.11.0"
1313
tokio = { version = "1.40.0", features = ["full"] }
1414
alloy = { version = "0.4.2", features = ["default", "json-abi", "transports", "providers", "dyn-abi", "rpc-types-trace", "rpc-types-debug"] }
1515
eyre = "0.6.12"
16-
reqwest = "0.12.8"
16+
reqwest = { version = "0.12.8", features = ["json", "blocking"] }
1717
openssl = { version = "0.10.35", features = ["vendored"] }
1818

1919
[build-dependencies]

Diff for: script/cli/build.rs

+37-15
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
1-
use std::fs;
1+
use std::path::Path;
22
use std::process::Command;
3+
use std::{env, fs};
34

45
fn main() {
5-
// Create assets directory
6+
let clean = env::var("CLEAN").unwrap_or("false".to_string()) == "true";
67
fs::create_dir_all("./src/assets").expect("Failed to create assets directory");
8+
if clean {
9+
fs::remove_file("./src/assets/chains.json").expect("Failed to remove chains.json");
10+
fs::remove_file("./src/assets/etherscan_chainlist.json")
11+
.expect("Failed to remove etherscan_chainlist.json");
12+
}
713

8-
// Download chains.json
9-
let output = Command::new("wget")
10-
.args([
11-
"-O",
12-
"./src/assets/chains.json",
13-
"https://chainid.network/chains.json",
14-
])
15-
.output()
16-
.expect("Failed to download chains.json");
14+
// Only download chains.json if it doesn't exist
15+
let chains_path = Path::new("./src/assets/chains.json");
16+
if !chains_path.exists() {
17+
let output = Command::new("wget")
18+
.args([
19+
"-O",
20+
"./src/assets/chains.json",
21+
"https://chainid.network/chains.json",
22+
])
23+
.output()
24+
.expect("Failed to download chains.json");
1725

18-
if !output.status.success() {
19-
panic!("Failed to download chains.json");
26+
if !output.status.success() {
27+
panic!("Failed to download chains.json");
28+
}
2029
}
2130

22-
// Tell cargo to re-run this if chains.json changes
23-
println!("cargo:rerun-if-changed=src/assets/chains.json");
31+
let etherscan_path = Path::new("./src/assets/etherscan_chainlist.json");
32+
if !etherscan_path.exists() {
33+
let etherscan_chainlist = Command::new("wget")
34+
.args([
35+
"-O",
36+
"./src/assets/etherscan_chainlist.json",
37+
"https://api.etherscan.io/v2/chainlist",
38+
])
39+
.output()
40+
.expect("Failed to download etherscan chain list");
41+
42+
if !etherscan_chainlist.status.success() {
43+
panic!("Failed to download etherscan chain list");
44+
}
45+
}
2446
}

Diff for: script/cli/justfile

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default:
44

55
# Build the project and copy the binary to the project root
66
build:
7-
cargo build --release
7+
CLEAN=true cargo build --release
88
cp ./target/release/deploy-cli ../../deploy-cli
99

1010
# Run tests
@@ -25,12 +25,12 @@ clean:
2525

2626
# Build and run the project
2727
run:
28-
cargo run -- --dir ../..
28+
CLEAN=true cargo run -- --dir ../..
2929

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

3434
# Install the project
3535
install:
36-
cargo install --path .
36+
CLEAN=true cargo install --path .

Diff for: script/cli/src/libs/explorer.rs

+81-69
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::util::chain_config::Explorer;
21
use crate::{errors::log, state_manager::STATE_MANAGER};
32
use alloy::{
43
json_abi::{Constructor, JsonAbi},
@@ -8,102 +7,98 @@ use alloy::{
87
#[derive(Clone, PartialEq, Eq, Default)]
98
pub enum SupportedExplorerType {
109
#[default]
10+
Manual,
11+
EtherscanV2,
1112
Etherscan,
1213
Blockscout,
1314
}
1415

1516
#[derive(Default, Clone)]
16-
pub struct ExplorerApiLib {
17+
pub struct Explorer {
1718
pub name: String,
1819
pub url: String,
1920
pub standard: String,
21+
pub explorer_type: SupportedExplorerType,
22+
}
23+
24+
#[derive(Default, Clone)]
25+
pub struct ExplorerApiLib {
26+
pub explorer: Explorer,
2027
pub api_key: String,
2128
pub api_url: String,
22-
pub explorer_type: SupportedExplorerType,
2329
}
2430

2531
impl ExplorerApiLib {
2632
pub fn new(explorer: Explorer, api_key: String) -> Result<Self, Box<dyn std::error::Error>> {
27-
if explorer.name.to_lowercase().contains("blockscout") {
33+
if explorer.explorer_type == SupportedExplorerType::Blockscout {
2834
// blockscout just appends /api to their explorer url
35+
let api_url = format!("{}/api?", explorer.url);
2936
Ok(ExplorerApiLib {
30-
name: explorer.name.to_string(),
31-
url: explorer.url.to_string(),
32-
standard: explorer.standard.to_string(),
37+
explorer,
3338
api_key: api_key.to_string(),
34-
api_url: format!("{}/api?", explorer.url),
35-
explorer_type: SupportedExplorerType::Blockscout,
39+
api_url,
3640
})
37-
} else if explorer.name.to_lowercase().contains("scan") {
41+
} else if explorer.explorer_type == SupportedExplorerType::EtherscanV2 {
3842
let chain_id = STATE_MANAGER
3943
.workflow_state
4044
.lock()
4145
.unwrap()
4246
.chain_id
4347
.clone();
44-
if chain_id.is_some() {
45-
// old Etherscan v1 API code below, let's try the v2 API multichain beta when we have a chain id
46-
// TODO: maybe check supported chain ids and fallback to v1 if the chain id is not supported?
48+
if let Some(chain_id) = chain_id {
4749
return Ok(ExplorerApiLib {
48-
name: explorer.name.to_string(),
49-
url: explorer.url.to_string(),
50-
standard: explorer.standard.to_string(),
50+
explorer,
5151
api_key: api_key.to_string(),
52-
api_url: format!(
53-
"https://api.etherscan.io/v2/api?chainid={}",
54-
chain_id.unwrap()
55-
),
56-
explorer_type: SupportedExplorerType::Etherscan,
52+
api_url: format!("https://api.etherscan.io/v2/api?chainid={}", chain_id),
5753
});
5854
} else {
59-
// 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.
60-
// 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?
61-
let slices = explorer.url.split(".").collect::<Vec<&str>>().len();
62-
if slices == 2 {
63-
// we are dealing with https://somethingscan.io
64-
return Ok(ExplorerApiLib {
65-
name: explorer.name.to_string(),
66-
url: explorer.url.to_string(),
67-
standard: explorer.standard.to_string(),
68-
api_key: api_key.to_string(),
69-
api_url: explorer.url.replace("https://", "https://api.").to_string(),
70-
explorer_type: SupportedExplorerType::Etherscan,
71-
});
72-
} else if slices == 3 {
73-
// we are dealing with https://subdomain.somethingscan.io
74-
return Ok(ExplorerApiLib {
75-
name: explorer.name.to_string(),
76-
url: explorer.url.to_string(),
77-
standard: explorer.standard.to_string(),
78-
api_key: api_key.to_string(),
79-
api_url: explorer.url.replace("https://", "https://api-").to_string(),
80-
explorer_type: SupportedExplorerType::Etherscan,
81-
});
82-
} else {
83-
return Err(format!(
84-
"Invalid etherscan url: {} ({})",
85-
explorer.name,
86-
explorer.url,
87-
)
88-
.into());
89-
}
55+
return Err(format!(
56+
"Chain id not found for explorer: {} ({})",
57+
explorer.name, explorer.url,
58+
)
59+
.into());
60+
}
61+
} else if explorer.explorer_type == SupportedExplorerType::Etherscan {
62+
// 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.
63+
// 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?
64+
let slices = explorer.url.split(".").collect::<Vec<&str>>().len();
65+
if slices == 2 {
66+
// we are dealing with https://somethingscan.io
67+
let api_url = explorer.url.replace("https://", "https://api.");
68+
return Ok(ExplorerApiLib {
69+
explorer,
70+
api_key: api_key.to_string(),
71+
api_url,
72+
});
73+
} else if slices == 3 {
74+
// we are dealing with https://subdomain.somethingscan.io
75+
let api_url = explorer.url.replace("https://", "https://api-");
76+
return Ok(ExplorerApiLib {
77+
explorer,
78+
api_key: api_key.to_string(),
79+
api_url,
80+
});
81+
} else {
82+
return Err(format!(
83+
"Invalid etherscan url: {} ({})",
84+
explorer.name, explorer.url,
85+
)
86+
.into());
9087
}
9188
} else {
92-
return Err(format!(
93-
"Unsupported explorer: {} ({})",
94-
explorer.name,
95-
explorer.url,
96-
)
97-
.into());
89+
return Err(
90+
format!("Unsupported explorer: {} ({})", explorer.name, explorer.url,).into(),
91+
);
9892
}
9993
}
10094

10195
pub async fn get_contract_data(
10296
&self,
10397
contract_address: Address,
10498
) -> Result<(String, String, Option<Constructor>), Box<dyn std::error::Error>> {
105-
if self.explorer_type == SupportedExplorerType::Etherscan
106-
|| self.explorer_type == SupportedExplorerType::Blockscout
99+
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
100+
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
101+
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
107102
{
108103
let url = format!(
109104
"{}&module=contract&action=getsourcecode&address={}&apikey={}",
@@ -135,8 +130,7 @@ impl ExplorerApiLib {
135130
}
136131
Err(format!(
137132
"Unsupported explorer: {} ({})",
138-
self.name,
139-
self.url,
133+
self.explorer.name, self.explorer.url,
140134
)
141135
.into())
142136
}
@@ -145,8 +139,9 @@ impl ExplorerApiLib {
145139
&self,
146140
contract_address: Address,
147141
) -> Result<String, Box<dyn std::error::Error>> {
148-
if self.explorer_type == SupportedExplorerType::Etherscan
149-
|| self.explorer_type == SupportedExplorerType::Blockscout
142+
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
143+
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
144+
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
150145
{
151146
let url = format!(
152147
"{}&module=contract&action=getcontractcreation&contractaddresses={}&apikey={}",
@@ -160,13 +155,32 @@ impl ExplorerApiLib {
160155
}
161156
Err(format!(
162157
"Unsupported explorer: {} ({})",
163-
self.name,
164-
self.url,
158+
self.explorer.name, self.explorer.url,
165159
)
166160
.into())
167161
}
168162
}
169163

164+
impl SupportedExplorerType {
165+
pub fn to_env_var_name(&self) -> String {
166+
match self {
167+
SupportedExplorerType::Etherscan => "ETHERSCAN_API_KEY".to_string(),
168+
SupportedExplorerType::EtherscanV2 => "ETHERSCAN_API_KEY".to_string(),
169+
SupportedExplorerType::Blockscout => "BLOCKSCOUT_API_KEY".to_string(),
170+
SupportedExplorerType::Manual => "VERIFIER_API_KEY".to_string(),
171+
}
172+
}
173+
174+
pub fn name(&self) -> String {
175+
match self {
176+
SupportedExplorerType::Etherscan => "Etherscan".to_string(),
177+
SupportedExplorerType::EtherscanV2 => "Etherscan v2".to_string(),
178+
SupportedExplorerType::Blockscout => "Blockscout".to_string(),
179+
SupportedExplorerType::Manual => "".to_string(),
180+
}
181+
}
182+
}
183+
170184
async fn get_etherscan_result(url: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
171185
match reqwest::get(url).await {
172186
Ok(response) => {
@@ -184,9 +198,7 @@ async fn get_etherscan_result(url: &str) -> Result<serde_json::Value, Box<dyn st
184198
));
185199
Err("Invalid response from etherscan".into())
186200
}
187-
Err(e) => {
188-
Err(format!("Explorer Request Error: {}", e).into())
189-
}
201+
Err(e) => Err(format!("Explorer Request Error: {}", e).into()),
190202
}
191203
}
192204

Diff for: script/cli/src/screens/deploy_contracts/execute_deploy_script.rs

+8-11
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,16 @@ impl ExecuteDeployScriptScreen {
100100
.arg("--verify")
101101
.arg(format!(
102102
"--verifier={}",
103-
if explorer_api.explorer_type == SupportedExplorerType::Blockscout {
103+
if explorer_api.explorer.explorer_type == SupportedExplorerType::Blockscout
104+
{
104105
"blockscout"
105106
} else {
106-
"etherscan"
107+
// custom also works for etherscan
108+
"custom"
107109
}
108110
))
109-
.arg(format!("--verifier-url={}", explorer_api.api_url));
110-
if explorer_api.explorer_type == SupportedExplorerType::Etherscan {
111-
command = command.arg(format!("--etherscan-api-key={}", explorer_api.api_key));
112-
}
111+
.arg(format!("--verifier-url={}", explorer_api.api_url))
112+
.arg(format!("--verifier-api-key={}", explorer_api.api_key));
113113
}
114114

115115
match execute_command(command.arg("--broadcast").arg("--skip-simulation")) {
@@ -208,9 +208,7 @@ fn execute_command(command: &mut Command) -> Result<Option<String>, Box<dyn std:
208208
}
209209
Ok(None)
210210
}
211-
Err(e) => {
212-
Err(e.to_string().into())
213-
}
211+
Err(e) => Err(e.to_string().into()),
214212
}
215213
}
216214

@@ -221,8 +219,7 @@ impl Screen for ExecuteDeployScriptScreen {
221219
"Deployment failed: {}\n",
222220
self.execution_error_message.lock().unwrap()
223221
));
224-
buffer
225-
.append_row_text_color("> Press any key to continue", constants::SELECTION_COLOR);
222+
buffer.append_row_text_color("> Press any key to continue", constants::SELECTION_COLOR);
226223
} else {
227224
buffer.append_row_text(&format!(
228225
"{} Executing dry run\n",

0 commit comments

Comments
 (0)