Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 54 additions & 70 deletions script/cli/src/libs/explorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use alloy::{
#[derive(Clone, PartialEq, Eq, Default)]
pub enum SupportedExplorerType {
#[default]
Manual,
Unknown, // Ambiguous - requires user selection
EtherscanV2,
Etherscan,
Blockscout,
Sourcify,
}

#[derive(Default, Clone)]
Expand All @@ -19,6 +19,7 @@ pub struct Explorer {
pub url: String,
pub standard: String,
pub explorer_type: SupportedExplorerType,
pub sourcify_api_url: Option<String>,
}

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

impl ExplorerApiLib {
pub fn new(explorer: Explorer, api_key: String) -> Result<Self, Box<dyn std::error::Error>> {
if explorer.explorer_type == SupportedExplorerType::Blockscout {
// blockscout just appends /api to their explorer url
let api_url = format!("{}/api?", explorer.url);
Ok(ExplorerApiLib {
explorer,
api_key: api_key.to_string(),
api_url,
})
} else if explorer.explorer_type == SupportedExplorerType::EtherscanV2 {
let chain_id = STATE_MANAGER
.workflow_state
.lock()
.unwrap()
.chain_id
.clone();
if let Some(chain_id) = chain_id {
return Ok(ExplorerApiLib {
explorer,
api_key: api_key.to_string(),
api_url: format!("https://api.etherscan.io/v2/api?chainid={}", chain_id),
});
} else {
return Err(format!(
"Chain id not found for explorer: {} ({})",
explorer.name, explorer.url,
)
.into());
let api_url = match explorer.explorer_type {
SupportedExplorerType::EtherscanV2 => {
let chain_id = STATE_MANAGER
.workflow_state
.lock()
.unwrap()
.chain_id
.clone()
.ok_or("Chain ID not found")?;
format!("https://api.etherscan.io/v2/api?chainid={}", chain_id)
}
SupportedExplorerType::Blockscout => {
format!("{}/api?", explorer.url)
}
} 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 {
SupportedExplorerType::Sourcify => {
explorer
.sourcify_api_url
.clone()
.ok_or("Sourcify API URL required")?
}
SupportedExplorerType::Unknown => {
return Err(format!(
"Invalid etherscan url: {} ({})",
explorer.name, explorer.url,
)
.into());
"Explorer type must be selected before creating API lib: {} ({})",
explorer.name, explorer.url
).into());
}
} else {
return Err(
format!("Unsupported explorer: {} ({})", explorer.name, explorer.url,).into(),
);
}
};

Ok(ExplorerApiLib {
explorer,
api_key,
api_url,
})
}

pub async fn get_contract_data(
&self,
contract_address: Address,
) -> Result<(String, String, Option<Constructor>), Box<dyn std::error::Error>> {
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
if self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
{
let url = format!(
Expand Down Expand Up @@ -139,8 +112,7 @@ impl ExplorerApiLib {
&self,
contract_address: Address,
) -> Result<String, Box<dyn std::error::Error>> {
if self.explorer.explorer_type == SupportedExplorerType::Etherscan
|| self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
if self.explorer.explorer_type == SupportedExplorerType::EtherscanV2
|| self.explorer.explorer_type == SupportedExplorerType::Blockscout
{
let url = format!(
Expand All @@ -162,21 +134,33 @@ impl ExplorerApiLib {
}

impl SupportedExplorerType {
pub fn to_env_var_name(&self) -> String {
pub fn to_env_var_name(&self, chain_id: &str) -> 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(),
SupportedExplorerType::Unknown => {
// Unknown type - fallback to generic key
format!("VERIFIER_API_KEY_{}", chain_id)
}
SupportedExplorerType::EtherscanV2 => {
// Etherscan v2 API key is shared across all chains
"ETHERSCAN_API_KEY".to_string()
}
SupportedExplorerType::Blockscout => {
// Chain-specific Blockscout keys to avoid cross-chain reuse
format!("BLOCKSCOUT_API_KEY_{}", chain_id)
}
SupportedExplorerType::Sourcify => {
// Chain-specific Sourcify keys to avoid cross-chain reuse
format!("SOURCIFY_API_KEY_{}", chain_id)
}
}
}

pub fn name(&self) -> String {
match self {
SupportedExplorerType::Etherscan => "Etherscan".to_string(),
SupportedExplorerType::Unknown => "Unknown".to_string(),
SupportedExplorerType::EtherscanV2 => "Etherscan v2".to_string(),
SupportedExplorerType::Blockscout => "Blockscout".to_string(),
SupportedExplorerType::Manual => "".to_string(),
SupportedExplorerType::Sourcify => "Sourcify".to_string(),
}
}
}
Expand Down
30 changes: 18 additions & 12 deletions script/cli/src/screens/deploy_contracts/execute_deploy_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,26 @@ impl ExecuteDeployScriptScreen {
let explorer_api =
ExplorerApiLib::new(explorer.clone().unwrap(), explorer_api_key.unwrap())
.unwrap();

let verifier = match explorer_api.explorer.explorer_type {
SupportedExplorerType::EtherscanV2 => "etherscan",
SupportedExplorerType::Blockscout => "blockscout",
SupportedExplorerType::Sourcify => "sourcify",
SupportedExplorerType::Unknown => {
// This should never happen if the workflow is correct
panic!("Unknown explorer type should have been resolved by workflow");
}
};

command = command
.arg("--verify")
.arg(format!(
"--verifier={}",
if explorer_api.explorer.explorer_type == SupportedExplorerType::Blockscout
{
"blockscout"
} else {
// custom also works for etherscan
"custom"
}
))
.arg(format!("--verifier-url={}", explorer_api.api_url))
.arg(format!("--verifier-api-key={}", explorer_api.api_key));
.arg(format!("--verifier={}", verifier))
.arg(format!("--verifier-url={}", explorer_api.api_url));

// Only add API key if it's not empty
if !explorer_api.api_key.is_empty() {
command = command.arg(format!("--verifier-api-key={}", explorer_api.api_key));
}
}

match execute_command(command.arg("--broadcast").arg("--skip-simulation")) {
Expand Down
67 changes: 44 additions & 23 deletions script/cli/src/screens/shared/block_explorer.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
use crate::libs::explorer::{Explorer, SupportedExplorerType};
use crate::screens::screen_manager::{Screen, ScreenResult};
use crate::screens::types::select::SelectComponent;
use crate::screens::types::select_or_enter::SelectOrEnterComponent;
use crate::state_manager::STATE_MANAGER;
use crate::ui::Buffer;
use crossterm::event::Event;

// Sets the block explorer for further operations
pub struct BlockExplorerScreen {
explorer_select: SelectComponent,
select_or_enter: SelectOrEnterComponent,
explorers: Vec<Explorer>,
}

impl BlockExplorerScreen {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let chain_id = STATE_MANAGER.workflow_state.lock()?.chain_id.clone();
let state = STATE_MANAGER.workflow_state.lock()?;
let chain_id = state.chain_id.clone().ok_or("Chain ID not found")?;
drop(state);

let mut pre_selected_explorers = vec![];
let mut explorers = vec![];
if chain_id.is_some()
&& STATE_MANAGER
.chains
.contains_key(&chain_id.clone().unwrap())
{

if STATE_MANAGER.chains.contains_key(&chain_id) {
explorers = STATE_MANAGER
.get_chain(chain_id.unwrap().clone())
.get_chain(chain_id.clone())
.unwrap()
.explorers
.clone()
.into_iter()
.filter(|explorer| {
explorer.explorer_type == SupportedExplorerType::Blockscout
|| explorer.explorer_type == SupportedExplorerType::EtherscanV2
|| explorer.explorer_type == SupportedExplorerType::Etherscan
// Show all explorers - Unknown types will require user selection
!explorer.url.is_empty()
})
.collect();

Expand All @@ -40,29 +39,51 @@ impl BlockExplorerScreen {
.map(|explorer| format!("{} ({})", explorer.url, explorer.explorer_type.name()))
.collect();
}
if explorers.is_empty() {
return Err("No block explorer found".into());
}

// Always add custom URL options at the end
pre_selected_explorers.push(format!("${{EXPLORER_URL_{}}}", chain_id));

Ok(BlockExplorerScreen {
explorer_select: SelectComponent::new(pre_selected_explorers),
select_or_enter: SelectOrEnterComponent::new(
"Select a block explorer or enter the explorer URL".to_string(),
false,
pre_selected_explorers,
|input, _| input,
|input, _| input,
),
explorers,
})
}
}

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

fn handle_input(&mut self, event: Event) -> Result<ScreenResult, Box<dyn std::error::Error>> {
let result = self.explorer_select.handle_input(event);
if result.is_some() {
STATE_MANAGER.workflow_state.lock()?.block_explorer =
Some(self.explorers[result.unwrap()].clone());
let result = self.select_or_enter.handle_input(event);
if let Some(result) = result {
// Check if user entered a custom URL (not from the explorer list)
let selected_index = self.select_or_enter.get_selected_index();

if selected_index < self.explorers.len() {
// User selected an existing explorer from the list
STATE_MANAGER.workflow_state.lock()?.block_explorer =
Some(self.explorers[selected_index].clone());
} else {
// User selected custom URL or entered manually
// Create a new Explorer with Unknown type - will be resolved later
let custom_explorer = Explorer {
name: "Custom".to_string(),
url: result,
standard: "custom".to_string(),
explorer_type: SupportedExplorerType::Unknown,
sourcify_api_url: None,
};
STATE_MANAGER.workflow_state.lock()?.block_explorer = Some(custom_explorer);
}
return Ok(ScreenResult::NextScreen(None));
}
Ok(ScreenResult::Continue)
Expand Down
37 changes: 32 additions & 5 deletions script/cli/src/screens/shared/enter_explorer_api_key.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,65 @@
use crate::libs::explorer::SupportedExplorerType;
use crate::screens::screen_manager::{Screen, ScreenResult};
use crate::screens::types::enter_env_var::EnterEnvVarComponent;
use crate::state_manager::STATE_MANAGER;
use crate::ui::Buffer;
use crossterm::event::Event;

// Sets the rpc url for further operations
pub struct EnterExplorerApiKeyScreen {
env_var_name: String,
explorer_type: SupportedExplorerType,
enter_env_var: EnterEnvVarComponent,
}

impl EnterExplorerApiKeyScreen {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let explorer = STATE_MANAGER.workflow_state.lock()?.block_explorer.clone();
let mut env_var_name = "".to_string();
if let Some(explorer) = explorer {
env_var_name = explorer.explorer_type.to_env_var_name();
let state = STATE_MANAGER.workflow_state.lock()?;
let explorer = state.block_explorer.clone().ok_or("No explorer selected")?;
let chain_id = state.chain_id.clone().ok_or("Chain ID not found")?;

// Explorer type must be resolved before reaching this screen
if explorer.explorer_type == SupportedExplorerType::Unknown {
return Err("Explorer type must be selected before entering API key. This is a workflow error.".into());
}

let env_var_name = explorer.explorer_type.to_env_var_name(&chain_id);
let explorer_type = explorer.explorer_type.clone();

Ok(EnterExplorerApiKeyScreen {
env_var_name: env_var_name.clone(),
explorer_type,
enter_env_var: EnterEnvVarComponent::new(env_var_name, |input, _| input),
})
}
}

impl Screen for EnterExplorerApiKeyScreen {
fn render_content(&self, buffer: &mut Buffer) -> Result<(), Box<dyn std::error::Error>> {
let hint = match self.explorer_type {
SupportedExplorerType::EtherscanV2 => "(Required for Etherscan verification)",
SupportedExplorerType::Blockscout => "(Keep empty if no API key required)",
SupportedExplorerType::Sourcify => "(Keep empty if no API key required)",
SupportedExplorerType::Unknown => {
// This should never happen due to validation in new()
return Err("Invalid explorer type: Unknown".into());
}
};

buffer.append_row_text(&format!("{}\n\n", hint));
self.enter_env_var.render(buffer);
Ok(())
}

fn handle_input(&mut self, event: Event) -> Result<ScreenResult, Box<dyn std::error::Error>> {
let result = self.enter_env_var.handle_input(event);
if result.is_some() {
let api_key = result.clone().unwrap_or_default();

// Validate: Etherscan requires key, others optional
if self.explorer_type == SupportedExplorerType::EtherscanV2 && api_key.is_empty() {
return Err("Etherscan API key is required".into());
}

STATE_MANAGER.workflow_state.lock()?.explorer_api_key = result;
return Ok(ScreenResult::NextScreen(None));
}
Expand Down
Loading
Loading