diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 39589e103..d13b0e5ee 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -187,6 +187,11 @@ pub enum ValidatorsSubcommand { /// The private key to sign the transactions with. #[clap(long, env = "ADMIN_PRIVATE_KEY")] admin_private_key: B256, + + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Check the status of a validator (batch). Status { @@ -242,6 +247,10 @@ pub enum EigenLayerSubcommand { /// The amount to deposit into the strategy, in units (e.g. '1ether', '10gwei'). #[clap(long, env = "EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT", value_parser = parse_ether_value)] amount: U256, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Register an operator into the bolt AVS. @@ -264,6 +273,10 @@ pub enum EigenLayerSubcommand { /// If not provided, a random value is used. #[clap(long, env = "OPERATOR_SIGNATURE_SALT")] salt: Option, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Deregister an EigenLayer operator from the bolt AVS. @@ -274,6 +287,10 @@ pub enum EigenLayerSubcommand { /// The private key of the operator. #[clap(long, env = "OPERATOR_PRIVATE_KEY")] operator_private_key: B256, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Update the operator RPC. @@ -287,6 +304,10 @@ pub enum EigenLayerSubcommand { /// The URL of the operator RPC. #[clap(long, env = "OPERATOR_RPC")] operator_rpc: Url, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Check the status of an operator in the bolt AVS. @@ -325,6 +346,10 @@ pub enum SymbioticSubcommand { /// The operator's extra data string to be stored in the registry. #[clap(long, env = "OPERATOR_EXTRA_DATA")] extra_data: String, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Deregister a Symbiotic operator from bolt. @@ -335,6 +360,10 @@ pub enum SymbioticSubcommand { /// The private key of the operator. #[clap(long, env = "OPERATOR_PRIVATE_KEY")] operator_private_key: B256, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Update the operator RPC. @@ -348,6 +377,10 @@ pub enum SymbioticSubcommand { /// The URL of the operator RPC. #[clap(long, env = "OPERATOR_RPC")] operator_rpc: Url, + /// Run the command in "dry run" mode, run all steps without broadcast. + /// Useful for testing and debugging purposes. + #[clap(short, long, env = "DRY_RUN", default_value = "false")] + dry_run: bool, }, /// Check the status of a Symbiotic operator. diff --git a/bolt-cli/src/commands/operators/eigenlayer.rs b/bolt-cli/src/commands/operators/eigenlayer.rs index a72ac5efd..909eae2d0 100644 --- a/bolt-cli/src/commands/operators/eigenlayer.rs +++ b/bolt-cli/src/commands/operators/eigenlayer.rs @@ -16,7 +16,9 @@ use crate::{ cli::{Chain, EigenLayerSubcommand}, common::{ // bolt_manager::BoltManagerContract::{self, BoltManagerContractErrors}, + handle_rpc_dry_run, request_confirmation, + shutdown_anvil, try_parse_contract_error, }, contracts::{ @@ -41,15 +43,17 @@ impl EigenLayerSubcommand { /// Run the EigenLayer subcommand. pub async fn run(self) -> eyre::Result<()> { match self { - Self::Deposit { rpc_url, strategy, amount, operator_private_key } => { + Self::Deposit { rpc_url, strategy, amount, operator_private_key, dry_run } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; let operator = signer.address(); + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer)) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -96,19 +100,30 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully deposited collateral into strategy"); + info!("Successfully deposited collateral into strategy"); + + shutdown_anvil(anvil); Ok(()) } - Self::Register { rpc_url, operator_rpc, salt, operator_private_key, extra_data } => { + Self::Register { + rpc_url, + operator_rpc, + salt, + operator_private_key, + extra_data, + dry_run, + } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer.clone())) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -169,7 +184,7 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully registered EigenLayer operator"); + info!("Successfully registered EigenLayer operator"); } Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } @@ -193,24 +208,28 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully registered EigenLayer operator"); + info!("Successfully registered EigenLayer operator"); } Err(e) => parse_eigenlayer_middleware_holesky_errors(e)?, } } + shutdown_anvil(anvil); + Ok(()) } - Self::Deregister { rpc_url, operator_private_key } => { + Self::Deregister { rpc_url, operator_private_key, dry_run } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; let address = signer.address(); + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer)) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -238,7 +257,7 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully deregistered EigenLayer operator"); + info!("Successfully deregistered EigenLayer operator"); } Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } @@ -258,24 +277,28 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully deregistered EigenLayer operator"); + info!("Successfully deregistered EigenLayer operator"); } Err(e) => parse_eigenlayer_middleware_holesky_errors(e)?, } } + shutdown_anvil(anvil); + Ok(()) } - Self::UpdateRpc { rpc_url, operator_private_key, operator_rpc } => { + Self::UpdateRpc { rpc_url, operator_private_key, operator_rpc, dry_run } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; let address = signer.address(); + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer)) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -308,7 +331,7 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully updated EigenLayer operator RPC"); + info!("Successfully updated EigenLayer operator RPC"); } Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } @@ -334,7 +357,7 @@ impl EigenLayerSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully updated EigenLayer operator RPC"); + info!("Successfully updated EigenLayer operator RPC"); } Err(e) => match try_parse_contract_error::(e)? { BoltManagerErrors::OperatorNotRegistered(_) => { @@ -350,6 +373,8 @@ impl EigenLayerSubcommand { } } + shutdown_anvil(anvil); + Ok(()) } @@ -380,7 +405,7 @@ impl EigenLayerSubcommand { info!(?address, "EigenLayer operator is registered"); } else { warn!(?address, "Operator not registered"); - return Ok(()) + return Ok(()); } } Err(e) => { @@ -428,7 +453,7 @@ impl EigenLayerSubcommand { info!(?address, "EigenLayer operator is registered"); } else { warn!(?address, "Operator not registered"); - return Ok(()) + return Ok(()); } let middleware = BoltEigenLayerMiddlewareHolesky::new( @@ -653,6 +678,7 @@ mod tests { operator_private_key: secret_key, strategy: weth_strategy_address, amount: U256::from(1), + dry_run: false, }, }, }; @@ -669,6 +695,7 @@ mod tests { extra_data: "hello world computer 🌐".to_string(), operator_rpc: None, salt: None, + dry_run: false, }, }, }; @@ -693,6 +720,7 @@ mod tests { rpc_url: anvil_url.clone(), operator_private_key: secret_key, operator_rpc: "https://boooooolt.chainbound.io/rpc".parse().expect("valid url"), + dry_run: false, }, }, }; @@ -715,6 +743,7 @@ mod tests { subcommand: EigenLayerSubcommand::Deregister { rpc_url: anvil_url.clone(), operator_private_key: secret_key, + dry_run: false, }, }, }; diff --git a/bolt-cli/src/commands/operators/symbiotic.rs b/bolt-cli/src/commands/operators/symbiotic.rs index 94cdb9f7c..bc4066154 100644 --- a/bolt-cli/src/commands/operators/symbiotic.rs +++ b/bolt-cli/src/commands/operators/symbiotic.rs @@ -1,10 +1,7 @@ use alloy::{ contract::Error as ContractError, network::EthereumWallet, - primitives::{ - utils::format_ether, - U256, - }, + primitives::{utils::format_ether, U256}, providers::ProviderBuilder, signers::local::PrivateKeySigner, sol_types::SolInterface, @@ -14,16 +11,17 @@ use tracing::{info, warn}; use crate::{ cli::{Chain, SymbioticSubcommand}, - common::{ - request_confirmation, try_parse_contract_error, - }, + common::{handle_rpc_dry_run, request_confirmation, shutdown_anvil, try_parse_contract_error}, contracts::{ bolt::{ - BoltManager::{self, BoltManagerErrors}, - BoltSymbioticMiddlewareHolesky::{self, BoltSymbioticMiddlewareHoleskyErrors}, - BoltSymbioticMiddlewareMainnet::{self, BoltSymbioticMiddlewareMainnetErrors}, - OperatorsRegistryV1::{self, OperatorsRegistryV1Errors} - }, deployments_for_chain, erc20::IERC20, symbiotic::{IOptInService, IVault} + BoltManager::{self, BoltManagerErrors}, + BoltSymbioticMiddlewareHolesky::{self, BoltSymbioticMiddlewareHoleskyErrors}, + BoltSymbioticMiddlewareMainnet::{self, BoltSymbioticMiddlewareMainnetErrors}, + OperatorsRegistryV1::{self, OperatorsRegistryV1Errors}, + }, + deployments_for_chain, + erc20::IERC20, + symbiotic::{IOptInService, IVault}, }, }; @@ -31,22 +29,26 @@ impl SymbioticSubcommand { /// Run the symbiotic subcommand. pub async fn run(self) -> eyre::Result<()> { match self { - Self::Register { operator_rpc, operator_private_key, rpc_url, extra_data } => { + Self::Register { operator_rpc, operator_private_key, rpc_url, extra_data, dry_run } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer.clone())) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; let deployments = deployments_for_chain(chain); - let operator_rpc = operator_rpc.unwrap_or_else(|| chain.bolt_rpc().unwrap_or_else(|| - panic!("The bolt RPC is not deployed on {:?}. Please use the `--operator-rpc` flag to specify one manually.", chain)) - ); + let operator_rpc = operator_rpc.unwrap_or_else(|| { + chain.bolt_rpc().unwrap_or_else(|| { + panic!( "The bolt RPC is not deployed on {:?}. Please use the `--operator-rpc` flag to specify one manually.", chain) + }) + }); info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering Symbiotic operator"); @@ -94,7 +96,7 @@ impl SymbioticSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully registered Symbiotic operator"); + info!("Successfully registered Symbiotic operator"); } Err(e) => parse_symbiotic_middleware_mainnet_errors(e)?, } @@ -116,25 +118,29 @@ impl SymbioticSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully registered Symbiotic operator"); + info!("Successfully registered Symbiotic operator"); } Err(e) => parse_symbiotic_middleware_holesky_errors(e)?, } } + shutdown_anvil(anvil); + Ok(()) } - Self::Deregister { rpc_url, operator_private_key } => { + Self::Deregister { rpc_url, operator_private_key, dry_run } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; let address = signer.address(); + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer)) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -163,7 +169,7 @@ impl SymbioticSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully deregistered Symbiotic operator"); + info!("Successfully deregistered Symbiotic operator"); } Err(e) => parse_symbiotic_middleware_mainnet_errors(e)?, } @@ -185,24 +191,28 @@ impl SymbioticSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully deregistered Symbiotic operator"); + info!("Successfully deregistered Symbiotic operator"); } Err(e) => parse_symbiotic_middleware_holesky_errors(e)?, } } + shutdown_anvil(anvil); + Ok(()) } - Self::UpdateRpc { rpc_url, operator_private_key, operator_rpc } => { + Self::UpdateRpc { rpc_url, operator_private_key, operator_rpc, dry_run } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; let address = signer.address(); + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer)) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -217,10 +227,14 @@ impl SymbioticSubcommand { info!(?address, "Symbiotic operator is registered"); } else { warn!(?address, "Operator not registered"); - return Ok(()) + return Ok(()); } - match bolt_manager.updateOperatorRPC(operator_rpc.to_string()).send().await { + let result = match bolt_manager + .updateOperatorRPC(operator_rpc.to_string()) + .send() + .await + { Ok(pending) => { info!( hash = ?pending.tx_hash(), @@ -232,7 +246,8 @@ impl SymbioticSubcommand { eyre::bail!("Transaction failed: {:?}", receipt) } - info!("Succesfully updated Symbiotic operator RPC"); + info!("Successfully updated Symbiotic operator RPC"); + Ok(()) } Err(e) => match try_parse_contract_error::(e)? { BoltManagerErrors::OperatorNotRegistered(_) => { @@ -242,8 +257,11 @@ impl SymbioticSubcommand { unreachable!("Unexpected error with selector {:?}", other.selector()) } }, - } - Ok(()) + }; + + shutdown_anvil(anvil); + + result } Self::Status { rpc_url, address } => { @@ -274,13 +292,13 @@ impl SymbioticSubcommand { info!(?address, "Symbiotic operator is registered"); } else { warn!(?address, "Operator not registered"); - return Ok(()) + return Ok(()); } } Err(e) => { let other = try_parse_contract_error::(e)?; bail!("Unexpected error with selector {:?}", other.selector()) - }, + } } match registry.isActiveOperator(address).call().await { @@ -294,7 +312,7 @@ impl SymbioticSubcommand { Err(e) => { let other = try_parse_contract_error::(e)?; bail!("Unexpected error with selector {:?}", other.selector()) - }, + } } match middleware.getOperatorCollaterals(address).call().await { @@ -322,10 +340,13 @@ impl SymbioticSubcommand { info!(?address, "Symbiotic operator is registered"); } else { warn!(?address, "Operator not registered"); - return Ok(()) + return Ok(()); } - let middleware = BoltSymbioticMiddlewareHolesky::new(deployments.bolt.symbiotic_middleware, provider.clone()); + let middleware = BoltSymbioticMiddlewareHolesky::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); match bolt_manager.getOperatorData(address).call().await { Ok(operator_data) => { @@ -585,6 +606,7 @@ mod tests { operator_private_key: secret_key, extra_data: "sudo rm -rf / --no-preserve-root".to_string(), operator_rpc: None, + dry_run: false, }, }, }; @@ -610,6 +632,7 @@ mod tests { operator_rpc: "https://boooooooooooooooolt.chainbound.io" .parse() .expect("valid url"), + dry_run: false, }, }, }; @@ -632,6 +655,7 @@ mod tests { subcommand: SymbioticSubcommand::Deregister { rpc_url: anvil_url.clone(), operator_private_key: secret_key, + dry_run: false, }, }, }; diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 465510a72..2926e2a9c 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -8,7 +8,10 @@ use tracing::{info, warn}; use crate::{ cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, - common::{hash::compress_bls_pubkey, request_confirmation, try_parse_contract_error}, + common::{ + handle_rpc_dry_run, hash::compress_bls_pubkey, request_confirmation, shutdown_anvil, + try_parse_contract_error, + }, contracts::{ bolt::BoltValidators::{self, BoltValidatorsErrors}, deployments_for_chain, @@ -24,14 +27,17 @@ impl ValidatorsCommand { admin_private_key, authorized_operator, rpc_url, + dry_run, } => { let signer = PrivateKeySigner::from_bytes(&admin_private_key) .wrap_err("valid private key")?; + let (rpc, anvil) = handle_rpc_dry_run(rpc_url, dry_run)?; + let provider = ProviderBuilder::new() .with_recommended_fillers() .wallet(EthereumWallet::from(signer)) - .on_http(rpc_url); + .on_http(rpc); let chain = Chain::try_from_provider(&provider).await?; @@ -54,7 +60,7 @@ impl ValidatorsCommand { request_confirmation(); - match bolt_validators + let result = match bolt_validators .batchRegisterValidatorsUnsafe( pubkey_hashes, max_committed_gas_limit, @@ -74,6 +80,7 @@ impl ValidatorsCommand { } info!("Successfully registered validators into bolt"); + Ok(()) } Err(e) => { let decoded = try_parse_contract_error::(e)?; @@ -94,9 +101,11 @@ impl ValidatorsCommand { ), } } - } + }; - Ok(()) + shutdown_anvil(anvil); + + result } ValidatorsSubcommand::Status { rpc_url, pubkeys_path, pubkeys } => { @@ -183,6 +192,7 @@ mod tests { authorized_operator: account, pubkeys_path: "./test_data/pubkeys.json".parse().unwrap(), rpc_url: anvil_url.clone(), + dry_run: true, }, }; diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index 78f1acd02..cb782d4cb 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -3,6 +3,7 @@ use std::{fs, path::PathBuf, str::FromStr}; use alloy::{ contract::Error as ContractError, dyn_abi::DynSolType, + node_bindings::{Anvil, AnvilInstance}, primitives::{Bytes, U256}, sol_types::SolInterface, transports::TransportError, @@ -10,6 +11,7 @@ use alloy::{ use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::{Context, ContextCompat, Result}; use inquire::{error::InquireError, Confirm}; +use reqwest::Url; use serde::Serialize; use tracing::{error, info}; @@ -137,3 +139,32 @@ pub fn request_confirmation() { } }) } + +/// Determines the RPC URL to use. If `dry_run` is enabled, it spawns an `Anvil` instance and +/// returns its endpoint. Otherwise, returns the original `rpc_url`. +pub(crate) fn handle_rpc_dry_run( + rpc_url: Url, + dry_run: bool, +) -> eyre::Result<(Url, Option)> { + if !dry_run { + return Ok((rpc_url, None)); + } + + let anvil = Anvil::new() + .fork(rpc_url) + .try_spawn() + .map_err(|e| eyre::eyre!("[dry-run] Failed to spawn Anvil: {}", e))?; + + let anvil_url = anvil.endpoint_url(); + info!("[dry-run] Anvil endpoint URL: {}", anvil_url); + + Ok((anvil_url, Some(anvil))) +} + +/// drop provided `AnvilInstance` to control resource consumption +pub fn shutdown_anvil(anvil: Option) { + if let Some(anvil_instance) = anvil { + info!("[dry-run] Shutting down Anvil instance."); + drop(anvil_instance); + } +}