Skip to content

Commit ba8e5a3

Browse files
authored
sourcify support, etherscan v1 deprecation (#110)
1 parent 9874be3 commit ba8e5a3

File tree

104 files changed

+893
-599
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+893
-599
lines changed

script/cli/src/libs/explorer.rs

Lines changed: 54 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ use alloy::{
77
#[derive(Clone, PartialEq, Eq, Default)]
88
pub enum SupportedExplorerType {
99
#[default]
10-
Manual,
10+
Unknown, // Ambiguous - requires user selection
1111
EtherscanV2,
12-
Etherscan,
1312
Blockscout,
13+
Sourcify,
1414
}
1515

1616
#[derive(Default, Clone)]
@@ -19,6 +19,7 @@ pub struct Explorer {
1919
pub url: String,
2020
pub standard: String,
2121
pub explorer_type: SupportedExplorerType,
22+
pub sourcify_api_url: Option<String>,
2223
}
2324

2425
#[derive(Default, Clone)]
@@ -30,74 +31,46 @@ pub struct ExplorerApiLib {
3031

3132
impl ExplorerApiLib {
3233
pub fn new(explorer: Explorer, api_key: String) -> Result<Self, Box<dyn std::error::Error>> {
33-
if explorer.explorer_type == SupportedExplorerType::Blockscout {
34-
// blockscout just appends /api to their explorer url
35-
let api_url = format!("{}/api?", explorer.url);
36-
Ok(ExplorerApiLib {
37-
explorer,
38-
api_key: api_key.to_string(),
39-
api_url,
40-
})
41-
} else if explorer.explorer_type == SupportedExplorerType::EtherscanV2 {
42-
let chain_id = STATE_MANAGER
43-
.workflow_state
44-
.lock()
45-
.unwrap()
46-
.chain_id
47-
.clone();
48-
if let Some(chain_id) = chain_id {
49-
return Ok(ExplorerApiLib {
50-
explorer,
51-
api_key: api_key.to_string(),
52-
api_url: format!("https://api.etherscan.io/v2/api?chainid={}", chain_id),
53-
});
54-
} else {
55-
return Err(format!(
56-
"Chain id not found for explorer: {} ({})",
57-
explorer.name, explorer.url,
58-
)
59-
.into());
34+
let api_url = match explorer.explorer_type {
35+
SupportedExplorerType::EtherscanV2 => {
36+
let chain_id = STATE_MANAGER
37+
.workflow_state
38+
.lock()
39+
.unwrap()
40+
.chain_id
41+
.clone()
42+
.ok_or("Chain ID not found")?;
43+
format!("https://api.etherscan.io/v2/api?chainid={}", chain_id)
44+
}
45+
SupportedExplorerType::Blockscout => {
46+
format!("{}/api?", explorer.url)
6047
}
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: format!("{}/api?", 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: format!("{}/api?", api_url),
80-
});
81-
} else {
48+
SupportedExplorerType::Sourcify => {
49+
explorer
50+
.sourcify_api_url
51+
.clone()
52+
.ok_or("Sourcify API URL required")?
53+
}
54+
SupportedExplorerType::Unknown => {
8255
return Err(format!(
83-
"Invalid etherscan url: {} ({})",
84-
explorer.name, explorer.url,
85-
)
86-
.into());
56+
"Explorer type must be selected before creating API lib: {} ({})",
57+
explorer.name, explorer.url
58+
).into());
8759
}
88-
} else {
89-
return Err(
90-
format!("Unsupported explorer: {} ({})", explorer.name, explorer.url,).into(),
91-
);
92-
}
60+
};
61+
62+
Ok(ExplorerApiLib {
63+
explorer,
64+
api_key,
65+
api_url,
66+
})
9367
}
9468

9569
pub async fn get_contract_data(
9670
&self,
9771
contract_address: Address,
9872
) -> Result<(String, String, Option<Constructor>), Box<dyn std::error::Error>> {
99-
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
100-
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
73+
if self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
10174
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
10275
{
10376
let url = format!(
@@ -139,8 +112,7 @@ impl ExplorerApiLib {
139112
&self,
140113
contract_address: Address,
141114
) -> Result<String, Box<dyn std::error::Error>> {
142-
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
143-
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
115+
if self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
144116
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
145117
{
146118
let url = format!(
@@ -162,21 +134,33 @@ impl ExplorerApiLib {
162134
}
163135

164136
impl SupportedExplorerType {
165-
pub fn to_env_var_name(&self) -> String {
137+
pub fn to_env_var_name(&self, chain_id: &str) -> String {
166138
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(),
139+
SupportedExplorerType::Unknown => {
140+
// Unknown type - fallback to generic key
141+
format!("VERIFIER_API_KEY_{}", chain_id)
142+
}
143+
SupportedExplorerType::EtherscanV2 => {
144+
// Etherscan v2 API key is shared across all chains
145+
"ETHERSCAN_API_KEY".to_string()
146+
}
147+
SupportedExplorerType::Blockscout => {
148+
// Chain-specific Blockscout keys to avoid cross-chain reuse
149+
format!("BLOCKSCOUT_API_KEY_{}", chain_id)
150+
}
151+
SupportedExplorerType::Sourcify => {
152+
// Chain-specific Sourcify keys to avoid cross-chain reuse
153+
format!("SOURCIFY_API_KEY_{}", chain_id)
154+
}
171155
}
172156
}
173157

174158
pub fn name(&self) -> String {
175159
match self {
176-
SupportedExplorerType::Etherscan => "Etherscan".to_string(),
160+
SupportedExplorerType::Unknown => "Unknown".to_string(),
177161
SupportedExplorerType::EtherscanV2 => "Etherscan v2".to_string(),
178162
SupportedExplorerType::Blockscout => "Blockscout".to_string(),
179-
SupportedExplorerType::Manual => "".to_string(),
163+
SupportedExplorerType::Sourcify => "Sourcify".to_string(),
180164
}
181165
}
182166
}

script/cli/src/screens/deploy_contracts/execute_deploy_script.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,26 @@ impl ExecuteDeployScriptScreen {
9696
let explorer_api =
9797
ExplorerApiLib::new(explorer.clone().unwrap(), explorer_api_key.unwrap())
9898
.unwrap();
99+
100+
let verifier = match explorer_api.explorer.explorer_type {
101+
SupportedExplorerType::EtherscanV2 => "etherscan",
102+
SupportedExplorerType::Blockscout => "blockscout",
103+
SupportedExplorerType::Sourcify => "sourcify",
104+
SupportedExplorerType::Unknown => {
105+
// This should never happen if the workflow is correct
106+
panic!("Unknown explorer type should have been resolved by workflow");
107+
}
108+
};
109+
99110
command = command
100111
.arg("--verify")
101-
.arg(format!(
102-
"--verifier={}",
103-
if explorer_api.explorer.explorer_type == SupportedExplorerType::Blockscout
104-
{
105-
"blockscout"
106-
} else {
107-
// custom also works for etherscan
108-
"custom"
109-
}
110-
))
111-
.arg(format!("--verifier-url={}", explorer_api.api_url))
112-
.arg(format!("--verifier-api-key={}", explorer_api.api_key));
112+
.arg(format!("--verifier={}", verifier))
113+
.arg(format!("--verifier-url={}", explorer_api.api_url));
114+
115+
// Only add API key if it's not empty
116+
if !explorer_api.api_key.is_empty() {
117+
command = command.arg(format!("--verifier-api-key={}", explorer_api.api_key));
118+
}
113119
}
114120

115121
match execute_command(command.arg("--broadcast").arg("--skip-simulation")) {

script/cli/src/screens/shared/block_explorer.rs

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
11
use crate::libs::explorer::{Explorer, SupportedExplorerType};
22
use crate::screens::screen_manager::{Screen, ScreenResult};
3-
use crate::screens::types::select::SelectComponent;
3+
use crate::screens::types::select_or_enter::SelectOrEnterComponent;
44
use crate::state_manager::STATE_MANAGER;
55
use crate::ui::Buffer;
66
use crossterm::event::Event;
77

88
// Sets the block explorer for further operations
99
pub struct BlockExplorerScreen {
10-
explorer_select: SelectComponent,
10+
select_or_enter: SelectOrEnterComponent,
1111
explorers: Vec<Explorer>,
1212
}
1313

1414
impl BlockExplorerScreen {
1515
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
16-
let chain_id = STATE_MANAGER.workflow_state.lock()?.chain_id.clone();
16+
let state = STATE_MANAGER.workflow_state.lock()?;
17+
let chain_id = state.chain_id.clone().ok_or("Chain ID not found")?;
18+
drop(state);
19+
1720
let mut pre_selected_explorers = vec![];
1821
let mut explorers = vec![];
19-
if chain_id.is_some()
20-
&& STATE_MANAGER
21-
.chains
22-
.contains_key(&chain_id.clone().unwrap())
23-
{
22+
23+
if STATE_MANAGER.chains.contains_key(&chain_id) {
2424
explorers = STATE_MANAGER
25-
.get_chain(chain_id.unwrap().clone())
25+
.get_chain(chain_id.clone())
2626
.unwrap()
2727
.explorers
2828
.clone()
2929
.into_iter()
3030
.filter(|explorer| {
31-
explorer.explorer_type == SupportedExplorerType::Blockscout
32-
|| explorer.explorer_type == SupportedExplorerType::EtherscanV2
33-
|| explorer.explorer_type == SupportedExplorerType::Etherscan
31+
// Show all explorers - Unknown types will require user selection
32+
!explorer.url.is_empty()
3433
})
3534
.collect();
3635

@@ -40,29 +39,51 @@ impl BlockExplorerScreen {
4039
.map(|explorer| format!("{} ({})", explorer.url, explorer.explorer_type.name()))
4140
.collect();
4241
}
43-
if explorers.is_empty() {
44-
return Err("No block explorer found".into());
45-
}
42+
43+
// Always add custom URL options at the end
44+
pre_selected_explorers.push(format!("${{EXPLORER_URL_{}}}", chain_id));
45+
4646
Ok(BlockExplorerScreen {
47-
explorer_select: SelectComponent::new(pre_selected_explorers),
47+
select_or_enter: SelectOrEnterComponent::new(
48+
"Select a block explorer or enter the explorer URL".to_string(),
49+
false,
50+
pre_selected_explorers,
51+
|input, _| input,
52+
|input, _| input,
53+
),
4854
explorers,
4955
})
5056
}
5157
}
5258

5359
impl Screen for BlockExplorerScreen {
5460
fn render_content(&self, buffer: &mut Buffer) -> Result<(), Box<dyn std::error::Error>> {
55-
buffer.append_row_text("Select a block explorer\n");
56-
self.explorer_select.render(buffer);
57-
self.explorer_select.render_default_instructions(buffer);
61+
self.select_or_enter.render(buffer);
5862
Ok(())
5963
}
6064

6165
fn handle_input(&mut self, event: Event) -> Result<ScreenResult, Box<dyn std::error::Error>> {
62-
let result = self.explorer_select.handle_input(event);
63-
if result.is_some() {
64-
STATE_MANAGER.workflow_state.lock()?.block_explorer =
65-
Some(self.explorers[result.unwrap()].clone());
66+
let result = self.select_or_enter.handle_input(event);
67+
if let Some(result) = result {
68+
// Check if user entered a custom URL (not from the explorer list)
69+
let selected_index = self.select_or_enter.get_selected_index();
70+
71+
if selected_index < self.explorers.len() {
72+
// User selected an existing explorer from the list
73+
STATE_MANAGER.workflow_state.lock()?.block_explorer =
74+
Some(self.explorers[selected_index].clone());
75+
} else {
76+
// User selected custom URL or entered manually
77+
// Create a new Explorer with Unknown type - will be resolved later
78+
let custom_explorer = Explorer {
79+
name: "Custom".to_string(),
80+
url: result,
81+
standard: "custom".to_string(),
82+
explorer_type: SupportedExplorerType::Unknown,
83+
sourcify_api_url: None,
84+
};
85+
STATE_MANAGER.workflow_state.lock()?.block_explorer = Some(custom_explorer);
86+
}
6687
return Ok(ScreenResult::NextScreen(None));
6788
}
6889
Ok(ScreenResult::Continue)

script/cli/src/screens/shared/enter_explorer_api_key.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,65 @@
1+
use crate::libs::explorer::SupportedExplorerType;
12
use crate::screens::screen_manager::{Screen, ScreenResult};
23
use crate::screens::types::enter_env_var::EnterEnvVarComponent;
34
use crate::state_manager::STATE_MANAGER;
45
use crate::ui::Buffer;
56
use crossterm::event::Event;
67

7-
// Sets the rpc url for further operations
88
pub struct EnterExplorerApiKeyScreen {
99
env_var_name: String,
10+
explorer_type: SupportedExplorerType,
1011
enter_env_var: EnterEnvVarComponent,
1112
}
1213

1314
impl EnterExplorerApiKeyScreen {
1415
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
15-
let explorer = STATE_MANAGER.workflow_state.lock()?.block_explorer.clone();
16-
let mut env_var_name = "".to_string();
17-
if let Some(explorer) = explorer {
18-
env_var_name = explorer.explorer_type.to_env_var_name();
16+
let state = STATE_MANAGER.workflow_state.lock()?;
17+
let explorer = state.block_explorer.clone().ok_or("No explorer selected")?;
18+
let chain_id = state.chain_id.clone().ok_or("Chain ID not found")?;
19+
20+
// Explorer type must be resolved before reaching this screen
21+
if explorer.explorer_type == SupportedExplorerType::Unknown {
22+
return Err("Explorer type must be selected before entering API key. This is a workflow error.".into());
1923
}
24+
25+
let env_var_name = explorer.explorer_type.to_env_var_name(&chain_id);
26+
let explorer_type = explorer.explorer_type.clone();
27+
2028
Ok(EnterExplorerApiKeyScreen {
2129
env_var_name: env_var_name.clone(),
30+
explorer_type,
2231
enter_env_var: EnterEnvVarComponent::new(env_var_name, |input, _| input),
2332
})
2433
}
2534
}
2635

2736
impl Screen for EnterExplorerApiKeyScreen {
2837
fn render_content(&self, buffer: &mut Buffer) -> Result<(), Box<dyn std::error::Error>> {
38+
let hint = match self.explorer_type {
39+
SupportedExplorerType::EtherscanV2 => "(Required for Etherscan verification)",
40+
SupportedExplorerType::Blockscout => "(Keep empty if no API key required)",
41+
SupportedExplorerType::Sourcify => "(Keep empty if no API key required)",
42+
SupportedExplorerType::Unknown => {
43+
// This should never happen due to validation in new()
44+
return Err("Invalid explorer type: Unknown".into());
45+
}
46+
};
47+
48+
buffer.append_row_text(&format!("{}\n\n", hint));
2949
self.enter_env_var.render(buffer);
3050
Ok(())
3151
}
3252

3353
fn handle_input(&mut self, event: Event) -> Result<ScreenResult, Box<dyn std::error::Error>> {
3454
let result = self.enter_env_var.handle_input(event);
3555
if result.is_some() {
56+
let api_key = result.clone().unwrap_or_default();
57+
58+
// Validate: Etherscan requires key, others optional
59+
if self.explorer_type == SupportedExplorerType::EtherscanV2 && api_key.is_empty() {
60+
return Err("Etherscan API key is required".into());
61+
}
62+
3663
STATE_MANAGER.workflow_state.lock()?.explorer_api_key = result;
3764
return Ok(ScreenResult::NextScreen(None));
3865
}

0 commit comments

Comments
 (0)