diff --git a/Cargo.lock b/Cargo.lock index 9202ebe0b20..b7f75fdfab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3632,6 +3632,28 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "deployment-info" +version = "0.1.0" +dependencies = [ + "alloy", + "anyhow", + "clap 4.5.48", + "dotenvy", + "espresso-contract-deployer", + "espresso-types", + "hotshot-contract-adapter", + "hotshot-state-prover", + "serde", + "serde_json", + "test-log", + "tokio", + "tracing", + "tracing-subscriber 0.3.20", + "tracing-test", + "url", +] + [[package]] name = "der" version = "0.7.10" diff --git a/Cargo.toml b/Cargo.toml index 79c7e7287fc..6bb1182313f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "client", "contracts/rust/adapter", "contracts/rust/deployer", + "contracts/rust/deployment-info", "contracts/rust/diff-test", "contracts/rust/gen-vk-contract", "crates/builder", @@ -52,6 +53,7 @@ default-members = [ "client", "contracts/rust/adapter", "contracts/rust/deployer", + "contracts/rust/deployment-info", "contracts/rust/diff-test", "contracts/rust/gen-vk-contract", "crates/builder", diff --git a/contracts/rust/adapter/src/sol_types.rs b/contracts/rust/adapter/src/sol_types.rs index a65d84f043e..d9194069a56 100644 --- a/contracts/rust/adapter/src/sol_types.rs +++ b/contracts/rust/adapter/src/sol_types.rs @@ -76,6 +76,20 @@ sol! { uint256 v; uint256 u; } + + /// Safe multisig interface + #[sol(rpc)] + interface ISafe { + function VERSION() external view returns (string memory); + function getOwners() external view returns (address[] memory); + function getThreshold() external view returns (uint256); + } + + /// Versioned contract interface + #[sol(rpc)] + interface IVersioned { + function getVersion() external view returns (uint8, uint8, uint8); + } } // Due to the rust bindings contain duplicate types for our solidity types. diff --git a/contracts/rust/deployment-info/Cargo.toml b/contracts/rust/deployment-info/Cargo.toml new file mode 100644 index 00000000000..b6e3f25e584 --- /dev/null +++ b/contracts/rust/deployment-info/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "deployment-info" +description = "Tool to collect and output deployment information for Espresso Network contracts" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[[bin]] +name = "deployment-info" +path = "src/main.rs" + +[dependencies] +alloy = { workspace = true } +anyhow = { workspace = true } +clap = { workspace = true } +dotenvy = { workspace = true } +hotshot-contract-adapter = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +url = { workspace = true } + +[dev-dependencies] +espresso-contract-deployer = { path = "../deployer" } +espresso-types = { workspace = true } +hotshot-state-prover = { workspace = true } +test-log = { workspace = true } +tracing-test = { workspace = true } + +[lints] +workspace = true diff --git a/contracts/rust/deployment-info/README.md b/contracts/rust/deployment-info/README.md new file mode 100644 index 00000000000..33672e37b25 --- /dev/null +++ b/contracts/rust/deployment-info/README.md @@ -0,0 +1,108 @@ +# Deployment Info Tool + +Tool to collect and output deployment information for Espresso Network contracts. + +## Usage + +```bash +cargo run -p deployment-info -- --network \ + [--rpc-url ] \ + [--env-file ] \ + [--output ] +``` + +Examples: + +```bash +# Uses default publicnode RPC for decaf +cargo run -p deployment-info -- --network decaf --env-file decaf.env + +# Uses default publicnode RPC for mainnet +cargo run -p deployment-info -- --network mainnet --env-file mainnet.env + +# Custom RPC URL +cargo run -p deployment-info -- \ + --network mainnet \ + --rpc-url https://eth.llamarpc.com \ + --env-file mainnet.env +``` + +If no `--output` is specified, the tool prints JSON to stdout. + +## Environment Variables + +The tool reads contract addresses from environment variables (or from a specified env file): + +- `ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS` - Safe multisig wallet +- `ESPRESSO_SEQUENCER_OPS_TIMELOCK_PROXY_ADDRESS` - Operations timelock +- `ESPRESSO_SEQUENCER_SAFE_EXIT_TIMELOCK_PROXY_ADDRESS` - Safe exit timelock +- `ESPRESSO_SEQUENCER_STAKE_TABLE_PROXY_ADDRESS` +- `ESPRESSO_SEQUENCER_ESP_TOKEN_PROXY_ADDRESS` +- `ESPRESSO_SEQUENCER_LIGHT_CLIENT_PROXY_ADDRESS` +- `ESPRESSO_SEQUENCER_FEE_CONTRACT_PROXY_ADDRESS` +- `ESPRESSO_SEQUENCER_REWARD_CLAIM_PROXY_ADDRESS` + +If addresses are not set the tool assumes the contracts are not deployed yet. + +## Output + +By default, prints to stdout. If `--output` is specified, writes to that file. + +Example output: + +```json +{ + "network": "decaf", + "multisig": { + "status": "deployed", + "address": "0xB76834E371B666feEe48e5d7d9A97CA08b5a0620", + "version": "1.3.0", + "owners": ["0x1234...", "0x5678..."], + "threshold": 2 + }, + "ops_timelock": { + "status": "not-yet-deployed" + }, + "safe_exit_timelock": { + "status": "not-yet-deployed" + }, + "light_client_proxy": { + "status": "deployed", + "proxy_address": "0x303872BB82a191771321d4828888920100d0b3e4", + "owner": "0x...", + "version": "3.0.0" + }, + "stake_table_proxy": { + "status": "not-yet-deployed" + }, + "esp_token_proxy": { + "status": "not-yet-deployed" + }, + "fee_contract_proxy": { + "status": "deployed", + "proxy_address": "0x9fce21c3f7600aa63392a5f5713986b39bb98884", + "owner": "0x...", + "version": "1.0.0" + }, + "reward_claim_proxy": { + "status": "not-yet-deployed" + } +} +``` + +## Contract Information Collected + +For each contract: +- **Proxy address**: From environment variable +- **Owner**: Queried on-chain (for Ownable contracts) +- **Version**: Queried via `IVersioned.getVersion()` interface + +For the Safe multisig: +- **Address**: From environment variable +- **Version**: Queried via `VERSION()` +- **Owners**: Queried via `getOwners()` +- **Threshold**: Queried via `getThreshold()` + +For timelocks (OpsTimelock, SafeExitTimelock): +- **Address**: From environment variable +- **Min delay**: Queried via `getMinDelay()` (delay in seconds before operations can execute) diff --git a/contracts/rust/deployment-info/src/lib.rs b/contracts/rust/deployment-info/src/lib.rs new file mode 100644 index 00000000000..ea12c7d977b --- /dev/null +++ b/contracts/rust/deployment-info/src/lib.rs @@ -0,0 +1,502 @@ +use std::{collections::HashMap, path::Path}; + +use alloy::{ + primitives::Address, + providers::{Provider, ProviderBuilder}, +}; +use anyhow::{Context, Result}; +use hotshot_contract_adapter::sol_types::{ + EspTokenV2, FeeContract, ISafe, IVersioned, LightClient, OpsTimelock, SafeExitTimelock, + StakeTable, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +const STAKE_TABLE_PROXY_ADDRESS: &str = "ESPRESSO_SEQUENCER_STAKE_TABLE_PROXY_ADDRESS"; +const ESP_TOKEN_PROXY_ADDRESS: &str = "ESPRESSO_SEQUENCER_ESP_TOKEN_PROXY_ADDRESS"; +const LIGHT_CLIENT_PROXY_ADDRESS: &str = "ESPRESSO_SEQUENCER_LIGHT_CLIENT_PROXY_ADDRESS"; +const FEE_CONTRACT_PROXY_ADDRESS: &str = "ESPRESSO_SEQUENCER_FEE_CONTRACT_PROXY_ADDRESS"; +const REWARD_CLAIM_PROXY_ADDRESS: &str = "ESPRESSO_SEQUENCER_REWARD_CLAIM_PROXY_ADDRESS"; +const MULTISIG_ADDRESS: &str = "ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS"; +const OPS_TIMELOCK_PROXY_ADDRESS: &str = "ESPRESSO_SEQUENCER_OPS_TIMELOCK_PROXY_ADDRESS"; +const SAFE_EXIT_TIMELOCK_PROXY_ADDRESS: &str = + "ESPRESSO_SEQUENCER_SAFE_EXIT_TIMELOCK_PROXY_ADDRESS"; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct DeploymentAddresses { + pub stake_table_proxy: Option
, + pub esp_token_proxy: Option
, + pub light_client_proxy: Option
, + pub fee_contract_proxy: Option
, + pub reward_claim_proxy: Option
, + pub multisig: Option
, + pub ops_timelock: Option
, + pub safe_exit_timelock: Option
, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "kebab-case")] +pub enum ContractDeployment { + Deployed { + proxy_address: Address, + owner: Address, + version: String, + }, + NotYetDeployed, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "kebab-case")] +pub enum MultisigDeployment { + Deployed { + address: Address, + version: String, + owners: Vec
, + threshold: u64, + }, + NotYetDeployed, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "kebab-case")] +pub enum TimelockDeployment { + Deployed { address: Address, min_delay: u64 }, + NotYetDeployed, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DeploymentInfo { + pub network: String, + pub multisig: MultisigDeployment, + pub ops_timelock: TimelockDeployment, + pub safe_exit_timelock: TimelockDeployment, + pub stake_table_proxy: ContractDeployment, + pub esp_token_proxy: ContractDeployment, + pub light_client_proxy: ContractDeployment, + pub fee_contract_proxy: ContractDeployment, + pub reward_claim_proxy: ContractDeployment, +} + +pub fn load_addresses_from_env_file(path: Option<&Path>) -> Result { + let env_map: HashMap = if let Some(p) = path { + dotenvy::from_path_iter(p) + .with_context(|| format!("Failed to read env file: {:?}", p))? + .filter_map(|item| item.ok()) + .collect() + } else { + dotenvy::dotenv_iter() + .ok() + .map(|iter| iter.filter_map(|item| item.ok()).collect()) + .unwrap_or_default() + }; + + fn parse_address(env_map: &HashMap, key: &str) -> Option
{ + env_map.get(key).and_then(|val| { + if val.is_empty() { + tracing::warn!("{} is set but empty", key); + None + } else { + match val.parse() { + Ok(addr) => Some(addr), + Err(e) => { + tracing::warn!("Failed to parse {} with value '{}': {}", key, val, e); + None + }, + } + } + }) + } + + Ok(DeploymentAddresses { + stake_table_proxy: parse_address(&env_map, STAKE_TABLE_PROXY_ADDRESS), + esp_token_proxy: parse_address(&env_map, ESP_TOKEN_PROXY_ADDRESS), + light_client_proxy: parse_address(&env_map, LIGHT_CLIENT_PROXY_ADDRESS), + fee_contract_proxy: parse_address(&env_map, FEE_CONTRACT_PROXY_ADDRESS), + reward_claim_proxy: parse_address(&env_map, REWARD_CLAIM_PROXY_ADDRESS), + multisig: parse_address(&env_map, MULTISIG_ADDRESS), + ops_timelock: parse_address(&env_map, OPS_TIMELOCK_PROXY_ADDRESS), + safe_exit_timelock: parse_address(&env_map, SAFE_EXIT_TIMELOCK_PROXY_ADDRESS), + }) +} + +#[derive(Debug, Clone, Copy)] +enum ContractType { + LightClient, + FeeContract, + EspToken, + StakeTable, + RewardClaim, +} + +async fn get_owner( + provider: &P, + addr: Address, + contract_type: ContractType, +) -> Result
{ + match contract_type { + ContractType::LightClient => { + let contract = LightClient::new(addr, provider); + Ok(contract.owner().call().await?) + }, + ContractType::FeeContract => { + let contract = FeeContract::new(addr, provider); + Ok(contract.owner().call().await?) + }, + ContractType::EspToken => { + let contract = EspTokenV2::new(addr, provider); + Ok(contract.owner().call().await?) + }, + ContractType::StakeTable => { + let contract = StakeTable::new(addr, provider); + Ok(contract.owner().call().await?) + }, + ContractType::RewardClaim => Ok(Address::ZERO), + } +} + +async fn get_version( + provider: &P, + addr: Address, + _contract_type: ContractType, +) -> Result { + let contract = IVersioned::new(addr, provider); + let v = contract.getVersion().call().await?; + Ok(format!("{}.{}.{}", v._0, v._1, v._2)) +} + +async fn get_contract_info( + provider: &P, + proxy_addr: Address, + contract_type: ContractType, +) -> Result { + let owner = get_owner(provider, proxy_addr, contract_type).await?; + let version = get_version(provider, proxy_addr, contract_type).await?; + + Ok(ContractDeployment::Deployed { + proxy_address: proxy_addr, + owner, + version, + }) +} + +async fn collect_contract_info( + provider: &P, + addr: Option
, + contract_type: ContractType, + contract_name: &str, +) -> Result { + let Some(addr) = addr else { + return Ok(ContractDeployment::NotYetDeployed); + }; + + get_contract_info(provider, addr, contract_type) + .await + .with_context(|| format!("Failed to query {} at {}", contract_name, addr)) +} + +async fn get_multisig_info(provider: &P, addr: Address) -> Result { + let contract = ISafe::new(addr, provider); + + let version = contract + .VERSION() + .call() + .await + .context("Failed to get VERSION")?; + + let owners = contract + .getOwners() + .call() + .await + .context("Failed to get owners")?; + + let threshold = contract + .getThreshold() + .call() + .await + .context("Failed to get threshold")? + .to::(); + + Ok(MultisigDeployment::Deployed { + address: addr, + version, + owners, + threshold, + }) +} + +async fn collect_multisig_info( + provider: &P, + addr: Option
, +) -> Result { + let Some(addr) = addr else { + return Ok(MultisigDeployment::NotYetDeployed); + }; + + get_multisig_info(provider, addr) + .await + .with_context(|| format!("Failed to query multisig at {}", addr)) +} + +async fn get_timelock_info( + provider: &P, + addr: Address, + is_ops: bool, +) -> Result { + let min_delay = if is_ops { + OpsTimelock::new(addr, provider) + .getMinDelay() + .call() + .await + .context("Failed to get min delay from OpsTimelock")? + .to::() + } else { + SafeExitTimelock::new(addr, provider) + .getMinDelay() + .call() + .await + .context("Failed to get min delay from SafeExitTimelock")? + .to::() + }; + + Ok(TimelockDeployment::Deployed { + address: addr, + min_delay, + }) +} + +async fn collect_timelock_info( + provider: &P, + addr: Option
, + name: &str, + is_ops: bool, +) -> Result { + let Some(addr) = addr else { + return Ok(TimelockDeployment::NotYetDeployed); + }; + + get_timelock_info(provider, addr, is_ops) + .await + .with_context(|| format!("Failed to query {} at {}", name, addr)) +} + +pub async fn collect_deployment_info( + rpc_url: Url, + network: String, + addresses: DeploymentAddresses, +) -> Result { + let provider = ProviderBuilder::new().connect_http(rpc_url); + + Ok(DeploymentInfo { + network, + multisig: collect_multisig_info(&provider, addresses.multisig).await?, + ops_timelock: collect_timelock_info(&provider, addresses.ops_timelock, "OpsTimelock", true) + .await?, + safe_exit_timelock: collect_timelock_info( + &provider, + addresses.safe_exit_timelock, + "SafeExitTimelock", + false, + ) + .await?, + stake_table_proxy: collect_contract_info( + &provider, + addresses.stake_table_proxy, + ContractType::StakeTable, + "StakeTable", + ) + .await?, + esp_token_proxy: collect_contract_info( + &provider, + addresses.esp_token_proxy, + ContractType::EspToken, + "EspToken", + ) + .await?, + light_client_proxy: collect_contract_info( + &provider, + addresses.light_client_proxy, + ContractType::LightClient, + "LightClient", + ) + .await?, + fee_contract_proxy: collect_contract_info( + &provider, + addresses.fee_contract_proxy, + ContractType::FeeContract, + "FeeContract", + ) + .await?, + reward_claim_proxy: collect_contract_info( + &provider, + addresses.reward_claim_proxy, + ContractType::RewardClaim, + "RewardClaim", + ) + .await?, + }) +} + +pub fn write_deployment_info(info: &DeploymentInfo, output_path: &std::path::Path) -> Result<()> { + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent).context("Failed to create output directory")?; + } + + let json = serde_json::to_string_pretty(info).context("Failed to serialize deployment info")?; + + std::fs::write(output_path, json).context("Failed to write deployment info")?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use alloy::{ + node_bindings::Anvil, + providers::{ProviderBuilder, WalletProvider}, + }; + use espresso_contract_deployer::{ + builder::DeployerArgsBuilder, network_config::light_client_genesis_from_stake_table, + Contract, Contracts, + }; + use hotshot_state_prover::v3::mock_ledger::STAKE_TABLE_CAPACITY_FOR_TEST; + + use super::*; + + #[test_log::test(tokio::test)] + async fn test_collect_deployment_info_with_deployed_contracts() -> Result<()> { + let anvil = Anvil::new().spawn(); + let provider = ProviderBuilder::new() + .wallet(anvil.wallet().unwrap()) + .connect_http(anvil.endpoint_url()); + let rpc_url = anvil.endpoint_url(); + let deployer_address = provider.default_signer_address(); + + let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table( + &Default::default(), + STAKE_TABLE_CAPACITY_FOR_TEST, + ) + .unwrap(); + + let mut contracts = Contracts::new(); + let args = DeployerArgsBuilder::default() + .deployer(provider.clone()) + .rpc_url(rpc_url.clone()) + .mock_light_client(true) + .genesis_lc_state(genesis_state) + .genesis_st_state(genesis_stake) + .blocks_per_epoch(100) + .epoch_start_block(1) + .multisig_pauser(deployer_address) + .exit_escrow_period(alloy::primitives::U256::from(250)) + .token_name("Espresso".to_string()) + .token_symbol("ESP".to_string()) + .initial_token_supply(alloy::primitives::U256::from(3590000000u64)) + .ops_timelock_delay(alloy::primitives::U256::from(100)) + .ops_timelock_admin(deployer_address) + .ops_timelock_proposers(vec![deployer_address]) + .ops_timelock_executors(vec![deployer_address]) + .safe_exit_timelock_delay(alloy::primitives::U256::from(200)) + .safe_exit_timelock_admin(deployer_address) + .safe_exit_timelock_proposers(vec![deployer_address]) + .safe_exit_timelock_executors(vec![deployer_address]) + .use_timelock_owner(false) + .build() + .unwrap(); + + args.deploy_all(&mut contracts).await?; + + let stake_table_addr = contracts + .address(Contract::StakeTableProxy) + .expect("StakeTableProxy deployed"); + let esp_token_addr = contracts + .address(Contract::EspTokenProxy) + .expect("EspTokenProxy deployed"); + let light_client_addr = contracts + .address(Contract::LightClientProxy) + .expect("LightClientProxy deployed"); + let fee_contract_addr = contracts + .address(Contract::FeeContractProxy) + .expect("FeeContractProxy deployed"); + let reward_claim_addr = contracts + .address(Contract::RewardClaimProxy) + .expect("RewardClaimProxy deployed"); + let ops_timelock_addr = contracts + .address(Contract::OpsTimelock) + .expect("OpsTimelock deployed"); + let safe_exit_timelock_addr = contracts + .address(Contract::SafeExitTimelock) + .expect("SafeExitTimelock deployed"); + + let addresses = DeploymentAddresses { + stake_table_proxy: Some(stake_table_addr), + esp_token_proxy: Some(esp_token_addr), + light_client_proxy: Some(light_client_addr), + fee_contract_proxy: Some(fee_contract_addr), + reward_claim_proxy: Some(reward_claim_addr), + multisig: None, + ops_timelock: Some(ops_timelock_addr), + safe_exit_timelock: Some(safe_exit_timelock_addr), + }; + + let info = collect_deployment_info(rpc_url, "test-network".to_string(), addresses).await?; + + assert_eq!(info.network, "test-network"); + assert_eq!( + info.stake_table_proxy, + ContractDeployment::Deployed { + proxy_address: stake_table_addr, + owner: deployer_address, + version: "2.0.0".to_string(), + } + ); + assert_eq!( + info.esp_token_proxy, + ContractDeployment::Deployed { + proxy_address: esp_token_addr, + owner: deployer_address, + version: "2.0.0".to_string(), + } + ); + assert_eq!( + info.light_client_proxy, + ContractDeployment::Deployed { + proxy_address: light_client_addr, + owner: deployer_address, + version: "3.0.0".to_string(), + } + ); + assert_eq!( + info.fee_contract_proxy, + ContractDeployment::Deployed { + proxy_address: fee_contract_addr, + owner: deployer_address, + version: "1.0.0".to_string(), + } + ); + assert_eq!( + info.reward_claim_proxy, + ContractDeployment::Deployed { + proxy_address: reward_claim_addr, + owner: Address::ZERO, + version: "1.0.0".to_string(), + } + ); + assert_eq!( + info.ops_timelock, + TimelockDeployment::Deployed { + address: ops_timelock_addr, + min_delay: 100 + } + ); + assert_eq!( + info.safe_exit_timelock, + TimelockDeployment::Deployed { + address: safe_exit_timelock_addr, + min_delay: 200 + } + ); + assert_eq!(info.multisig, MultisigDeployment::NotYetDeployed); + + Ok(()) + } +} diff --git a/contracts/rust/deployment-info/src/main.rs b/contracts/rust/deployment-info/src/main.rs new file mode 100644 index 00000000000..fc50fd18ba9 --- /dev/null +++ b/contracts/rust/deployment-info/src/main.rs @@ -0,0 +1,84 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::Parser; +use deployment_info::{ + collect_deployment_info, load_addresses_from_env_file, write_deployment_info, +}; +use tracing_subscriber::EnvFilter; +use url::Url; + +#[derive(Debug, Parser)] +#[clap( + name = "deployment-info", + about = "Collect and output deployment information for Espresso Network contracts" +)] +struct Args { + #[clap( + long, + env = "ESPRESSO_SEQUENCER_L1_PROVIDER", + help = "RPC URL for L1 provider. Defaults to publicnode for decaf/mainnet networks." + )] + rpc_url: Option, + + #[clap(long)] + network: String, + + #[clap(long, help = "Path to .env file (defaults to .env)")] + env_file: Option, + + #[clap( + long, + help = "Output file path. If not provided, prints to stdout instead of writing to a file." + )] + output: Option, +} + +fn get_default_rpc_url(network: &str) -> Option { + match network { + "decaf" => "https://ethereum-sepolia-rpc.publicnode.com".parse().ok(), + "mainnet" => "https://ethereum-rpc.publicnode.com".parse().ok(), + _ => None, + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let addresses = load_addresses_from_env_file(args.env_file.as_deref()) + .context("Failed to load addresses from env file")?; + + let rpc_url = args + .rpc_url + .or_else(|| get_default_rpc_url(&args.network)) + .context( + "RPC URL not provided and no default available for this network. Provide --rpc-url or \ + set ESPRESSO_SEQUENCER_L1_PROVIDER", + )?; + + tracing::info!("Collecting deployment info for network: {}", args.network); + tracing::info!("Using RPC URL: {}", rpc_url); + + let info = collect_deployment_info(rpc_url, args.network, addresses) + .await + .context("Failed to collect deployment info")?; + + if let Some(output_path) = args.output { + write_deployment_info(&info, &output_path) + .context("Failed to write deployment info to file")?; + tracing::info!("Successfully wrote deployment info to: {:?}", output_path); + } else { + let json = serde_json::to_string_pretty(&info) + .context("Failed to serialize deployment info to JSON")?; + println!("{}", json); + } + + Ok(()) +}