From 1c10628f5a9cede80e9938a52670a6fa55c52c52 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:09:58 +0100 Subject: [PATCH 1/7] feat(cli): handle validator errors --- bolt-cli/src/commands/validators.rs | 48 +++++++++++++++++++++-------- bolt-cli/src/common/bolt_manager.rs | 21 ++++--------- bolt-cli/src/common/mod.rs | 41 +++++++++++++++++++++++- bolt-cli/src/contracts/bolt.rs | 11 +++++++ 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 7f786cfa2..fbb0c2efa 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -9,8 +9,11 @@ use tracing::{info, warn}; use crate::{ cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, - common::{hash::compress_bls_pubkey, request_confirmation}, - contracts::{bolt::BoltValidators, deployments_for_chain}, + common::{hash::compress_bls_pubkey, request_confirmation, try_parse_contract_error}, + contracts::{ + bolt::BoltValidators::{self, BoltValidatorsErrors}, + deployments_for_chain, + }, }; impl ValidatorsCommand { @@ -54,26 +57,45 @@ impl ValidatorsCommand { request_confirmation(); - let pending = bolt_validators + match bolt_validators .batchRegisterValidatorsUnsafe( pubkey_hashes, max_committed_gas_limit, authorized_operator, ) .send() - .await?; + .await + { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "batchRegisterValidatorsUnsafe transaction sent, awaiting receipt..." + ); + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } - info!( - hash = ?pending.tx_hash(), - "batchRegisterValidatorsUnsafe transaction sent, awaiting receipt..." - ); - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + info!("Successfully registered validators into bolt"); + } + Err(e) => { + let decoded = try_parse_contract_error::(e)?; + + match decoded { + BoltValidatorsErrors::ValidatorAlreadyExists(b) => { + eyre::bail!(format!( + "Validator already exists (pubkeyHash: {:?})", + b.pubkeyHash + )) + } + BoltValidatorsErrors::InvalidAuthorizedOperator(_) => { + eyre::bail!("Invalid authorized operator") + } + _ => eyre::bail!("Unknown contract error"), + } + } } - info!("Successfully registered validators into bolt"); - Ok(()) } ValidatorsSubcommand::Status { rpc_url, pubkeys_path, pubkeys } => { diff --git a/bolt-cli/src/common/bolt_manager.rs b/bolt-cli/src/common/bolt_manager.rs index 35a763a68..d775af5e6 100644 --- a/bolt-cli/src/common/bolt_manager.rs +++ b/bolt-cli/src/common/bolt_manager.rs @@ -1,20 +1,20 @@ #![allow(dead_code)] // TODO: rm this -use std::str::FromStr; - use alloy::{ - contract::{Error as ContractError, Result as ContractResult}, - primitives::{Address, Bytes, B256}, + contract::Result as ContractResult, + primitives::{Address, B256}, providers::{ProviderBuilder, RootProvider}, sol, sol_types::{Error as SolError, SolInterface}, - transports::{http::Http, TransportError}, + transports::http::Http, }; use reqwest::{Client, Url}; use serde::Serialize; use BoltManagerContract::{BoltManagerContractErrors, BoltManagerContractInstance, ProposerStatus}; +use super::try_parse_contract_error; + /// Bolt Manager contract bindings. #[derive(Debug, Clone)] pub struct BoltManager(BoltManagerContractInstance, RootProvider>>); @@ -51,16 +51,7 @@ impl BoltManager { // TODO: clean this after https://github.com/alloy-rs/alloy/issues/787 is merged let error = match returndata.map(|data| data._0) { Ok(proposer) => return Ok(Some(proposer)), - Err(error) => match error { - ContractError::TransportError(TransportError::ErrorResp(err)) => { - let data = err.data.unwrap_or_default(); - let data = data.get().trim_matches('"'); - let data = Bytes::from_str(data).unwrap_or_default(); - - BoltManagerContractErrors::abi_decode(&data, true)? - } - e => return Err(e), - }, + Err(error) => try_parse_contract_error::(error)?, }; if matches!(error, BoltManagerContractErrors::ValidatorDoesNotExist(_)) { diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index dfd67941d..6d1d932a5 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -1,5 +1,9 @@ -use std::{fs, io::Write, path::PathBuf}; +use std::{fs, io::Write, path::PathBuf, str::FromStr}; +use alloy::{ + contract::Error as ContractError, primitives::Bytes, sol_types::SolInterface, + transports::TransportError, +}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::{Context, Result}; use serde::Serialize; @@ -37,6 +41,41 @@ pub fn write_to_file(out: &str, data: &T) -> Result<()> { Ok(()) } +/// Try to decode a contract error into a specific Solidity error interface. +/// If the error cannot be decoded or it is not a contract error, return the original error. +/// +/// Example usage: +/// +/// ```rust no_run +/// sol! { +/// library ErrorLib { +/// error SomeError(uint256 code); +/// } +/// } +/// +/// // call a contract that may return an error with the SomeError interface +/// let returndata = match myContract.call().await { +/// Ok(returndata) => returndata, +/// Err(err) => { +/// let decoded_error = try_decode_contract_error::(err)?; +/// // handle the decoded error however you want; for example, return it +/// return Err(decoded_error); +/// }, +/// } +/// ``` +pub fn try_parse_contract_error(error: ContractError) -> Result { + match error { + ContractError::TransportError(TransportError::ErrorResp(resp)) => { + let data = resp.data.unwrap_or_default(); + let data = data.get().trim_matches('"'); + let data = Bytes::from_str(data).unwrap_or_default(); + + T::abi_decode(&data, true).map_err(Into::into) + } + _ => Err(error), + } +} + /// Asks whether the user wants to proceed further. If not, the process is exited. #[allow(unreachable_code)] pub fn request_confirmation() { diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs index b74f8f7a8..299a5389e 100644 --- a/bolt-cli/src/contracts/bolt.rs +++ b/bolt-cli/src/contracts/bolt.rs @@ -25,10 +25,15 @@ sol! { /// @return ValidatorInfo struct function getValidatorByPubkeyHash(bytes20 pubkeyHash) public view returns (ValidatorInfo memory); + #[derive(Debug)] error KeyNotFound(); + #[derive(Debug)] error InvalidQuery(); #[derive(Debug)] error ValidatorDoesNotExist(bytes20 pubkeyHash); + #[derive(Debug)] + error ValidatorAlreadyExists(bytes20 pubkeyHash); + #[derive(Debug)] error InvalidAuthorizedOperator(); } } @@ -55,6 +60,9 @@ sol! { /// @dev This requires calling the EigenLayer AVS Directory contract to deregister the operator. /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. function deregisterOperator() public; + + error AlreadyRegistered(); + error NotOperator(); } #[allow(missing_docs)] @@ -74,5 +82,8 @@ sol! { /// @return collaterals The collaterals staked by the operator. /// @dev Assumes that the operator is registered and enabled. function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + + error AlreadyRegistered(); + error NotOperator(); } } From d1e908c5856891557efccfeb972d53da563e8dc0 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:20:52 +0100 Subject: [PATCH 2/7] feat(cli): handle operator errors --- bolt-cli/src/commands/operators.rs | 152 +++++++++++++++++++---------- bolt-cli/src/contracts/bolt.rs | 2 + 2 files changed, 101 insertions(+), 53 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 5a11b4921..06e712f81 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -12,11 +12,11 @@ use crate::{ cli::{ Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, }, - common::{bolt_manager::BoltManagerContract, request_confirmation}, + common::{bolt_manager::BoltManagerContract, request_confirmation, try_parse_contract_error}, contracts::{ bolt::{ - BoltEigenLayerMiddleware, - BoltSymbioticMiddleware::{self}, + BoltEigenLayerMiddleware::{self, BoltEigenLayerMiddlewareErrors}, + BoltSymbioticMiddleware::{self, BoltSymbioticMiddlewareErrors}, SignatureWithSaltAndExpiry, }, deployments_for_chain, @@ -149,23 +149,37 @@ impl OperatorsCommand { Bytes::from(signer.sign_hash_sync(&signature_digest_hash)?.as_bytes()); let signature = SignatureWithSaltAndExpiry { signature, expiry, salt }; - let result = bolt_eigenlayer_middleware + match bolt_eigenlayer_middleware .registerOperator(operator_rpc.to_string(), signature) .send() - .await?; - - info!( - hash = ?result.tx_hash(), - "registerOperator transaction sent, awaiting receipt..." - ); - - let receipt = result.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + .await + { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered EigenLayer operator"); + } + Err(e) => { + match try_parse_contract_error::(e)? { + BoltEigenLayerMiddlewareErrors::AlreadyRegistered(_) => { + eyre::bail!("Operator already registered in bolt") + } + BoltEigenLayerMiddlewareErrors::NotOperator(_) => { + eyre::bail!("Operator not registered in EigenLayer") + } + _ => unreachable!(), + } + } } - info!("Succesfully registered EigenLayer operator"); - Ok(()) } EigenLayerSubcommand::Deregister { rpc_url, operator_private_key } => { @@ -192,20 +206,30 @@ impl OperatorsCommand { let bolt_eigenlayer_middleware = BoltEigenLayerMiddleware::new(bolt_avs_address, provider); - let result = bolt_eigenlayer_middleware.deregisterOperator().send().await?; - - info!( - hash = ?result.tx_hash(), - "deregisterOperator transaction sent, awaiting receipt..." - ); - - let receipt = result.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + match bolt_eigenlayer_middleware.deregisterOperator().send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "deregisterOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully deregistered EigenLayer operator"); + } + Err(e) => { + match try_parse_contract_error::(e)? { + BoltEigenLayerMiddlewareErrors::NotRegistered(_) => { + eyre::bail!("Operator not registered in bolt") + } + _ => unreachable!(), + } + } } - info!("Succesfully deregistered EigenLayer operator"); - Ok(()) } EigenLayerSubcommand::Status { rpc_url: rpc, address } => { @@ -268,21 +292,33 @@ impl OperatorsCommand { provider.clone(), ); - let pending = - middleware.registerOperator(operator_rpc.to_string()).send().await?; - - info!( - hash = ?pending.tx_hash(), - "registerOperator transaction sent, awaiting receipt..." - ); - - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + match middleware.registerOperator(operator_rpc.to_string()).send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered Symbiotic operator"); + } + Err(e) => { + match try_parse_contract_error::(e)? { + BoltSymbioticMiddlewareErrors::AlreadyRegistered(_) => { + eyre::bail!("Operator already registered in bolt") + } + BoltSymbioticMiddlewareErrors::NotOperator(_) => { + eyre::bail!("Operator not registered in Symbiotic") + } + _ => unreachable!(), + } + } } - info!("Succesfully registered Symbiotic operator"); - Ok(()) } SymbioticSubcommand::Deregister { rpc_url, operator_private_key } => { @@ -310,20 +346,30 @@ impl OperatorsCommand { provider, ); - let pending = middleware.deregisterOperator().send().await?; - - info!( - hash = ?pending.tx_hash(), - "deregisterOperator transaction sent, awaiting receipt..." - ); - - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + match middleware.deregisterOperator().send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "deregisterOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully deregistered Symbiotic operator"); + } + Err(e) => { + match try_parse_contract_error::(e)? { + BoltSymbioticMiddlewareErrors::NotRegistered(_) => { + eyre::bail!("Operator not registered in bolt") + } + _ => unreachable!(), + } + } } - info!("Succesfully deregistered Symbiotic operator"); - Ok(()) } SymbioticSubcommand::Status { rpc_url, address } => { diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs index 299a5389e..f4af64ba5 100644 --- a/bolt-cli/src/contracts/bolt.rs +++ b/bolt-cli/src/contracts/bolt.rs @@ -63,6 +63,7 @@ sol! { error AlreadyRegistered(); error NotOperator(); + error NotRegistered(); } #[allow(missing_docs)] @@ -85,5 +86,6 @@ sol! { error AlreadyRegistered(); error NotOperator(); + error NotRegistered(); } } From 5770183e63b0e423acb62d9d12a7a1ed0bb2afc3 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:21:40 +0100 Subject: [PATCH 3/7] feat(cli): version to 0.1.0 --- bolt-cli/Cargo.lock | 2 +- bolt-cli/Cargo.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bolt-cli/Cargo.lock b/bolt-cli/Cargo.lock index 6f7692d1e..b232b05a0 100644 --- a/bolt-cli/Cargo.lock +++ b/bolt-cli/Cargo.lock @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "bolt" -version = "0.3.0-alpha" +version = "0.1.0" dependencies = [ "alloy", "alloy-node-bindings", diff --git a/bolt-cli/Cargo.toml b/bolt-cli/Cargo.toml index e88d1559e..1bb0e8ea1 100644 --- a/bolt-cli/Cargo.toml +++ b/bolt-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bolt" -version = "0.3.0-alpha" +version = "0.1.0" edition = "2021" [dependencies] @@ -26,9 +26,9 @@ bls12_381 = "0.8.0" ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404" } lighthouse_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", rev = "a87f19d" } alloy = { version = "0.7.0", features = [ - "full", - "provider-anvil-api", - "provider-anvil-node", + "full", + "provider-anvil-api", + "provider-anvil-node", ] } # utils From 5f2fe1e4d1befa0230b7d9917706ff184791b2f1 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:28:28 +0100 Subject: [PATCH 4/7] docs(cli): update README with generate cmd --- bolt-cli/README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bolt-cli/README.md b/bolt-cli/README.md index 65facf531..645734fab 100644 --- a/bolt-cli/README.md +++ b/bolt-cli/README.md @@ -19,7 +19,7 @@ git clone git@github.com:chainbound/bolt.git cd bolt-cli # build and install the binary on your machine -cargo install --path . --force +cargo install --path . --force --locked # test the installation bolt --version @@ -34,6 +34,7 @@ Available commands: - [`send`](#send) - Send a preconfirmation request to a Bolt sidecar. - [`validators`](#validators) - Subcommand for bolt validators. - [`operators`](#operators) - Subcommand for bolt operators. +- [`generate`](#generate) - Subcommand for generating bolt related data. --- @@ -342,6 +343,31 @@ Options: --- +### `generate` + +The `generate` subcommand contains functionality for generating bolt related data like BLS keypairs. + +
+Usage + +```text +❯ bolt generate --help +Useful data generation commands + +Usage: bolt generate + +Commands: + bls Generate a BLS keypair + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` + +
+ +--- + ## Security The Bolt CLI is designed to be used offline. It does not require any network connections From a8556a252deae4c4e3846d3223296ecebcce903f Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:35:51 +0100 Subject: [PATCH 5/7] feat(boltup): impl bolt-cli versioning updates --- boltup/README.md | 2 +- boltup/boltup.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/boltup/README.md b/boltup/README.md index a2dea1a09..64f48c02f 100644 --- a/boltup/README.md +++ b/boltup/README.md @@ -16,7 +16,7 @@ After the installation is complete, you can run `boltup` to install or update th boltup --tag [version] # Example -boltup --tag v0.3.0-alpha.1 +boltup --tag v0.1.0 ``` After the installation is complete, you can run `bolt` to use the bolt CLI tool. diff --git a/boltup/boltup.sh b/boltup/boltup.sh index 783eb7ed8..ce7fb0901 100755 --- a/boltup/boltup.sh +++ b/boltup/boltup.sh @@ -49,6 +49,9 @@ main() { BOLTUP_TAG="v${BOLTUP_TAG}" fi + # Add `cli-` prefix to the tag, that's were the release is located + BOLTUP_TAG="cli-${BOLTUP_TAG}" + say "installing bolt (tag ${BOLTUP_TAG})" # Figure out the platform: one of "linux", "darwin" or "win32" From 2e4b4e484eb8e04df5a1b77079e160f1cd6ad460 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:40:39 +0100 Subject: [PATCH 6/7] fix(cli): log unexpected contract errors --- bolt-cli/src/commands/operators.rs | 21 +++++++++++++++++---- bolt-cli/src/commands/validators.rs | 6 +++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 06e712f81..76563ab4f 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -4,6 +4,7 @@ use alloy::{ primitives::{utils::format_ether, Bytes}, providers::{Provider, ProviderBuilder, WalletProvider}, signers::{local::PrivateKeySigner, SignerSync}, + sol_types::SolInterface, }; use eyre::Context; use tracing::{info, warn}; @@ -175,7 +176,10 @@ impl OperatorsCommand { BoltEigenLayerMiddlewareErrors::NotOperator(_) => { eyre::bail!("Operator not registered in EigenLayer") } - _ => unreachable!(), + other => unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ), } } } @@ -225,7 +229,10 @@ impl OperatorsCommand { BoltEigenLayerMiddlewareErrors::NotRegistered(_) => { eyre::bail!("Operator not registered in bolt") } - _ => unreachable!(), + other => unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ), } } } @@ -314,7 +321,10 @@ impl OperatorsCommand { BoltSymbioticMiddlewareErrors::NotOperator(_) => { eyre::bail!("Operator not registered in Symbiotic") } - _ => unreachable!(), + other => unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ), } } } @@ -365,7 +375,10 @@ impl OperatorsCommand { BoltSymbioticMiddlewareErrors::NotRegistered(_) => { eyre::bail!("Operator not registered in bolt") } - _ => unreachable!(), + other => unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ), } } } diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index fbb0c2efa..6ce825fb3 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -2,6 +2,7 @@ use alloy::{ network::EthereumWallet, providers::{Provider, ProviderBuilder}, signers::local::PrivateKeySigner, + sol_types::SolInterface, }; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::Context; @@ -91,7 +92,10 @@ impl ValidatorsCommand { BoltValidatorsErrors::InvalidAuthorizedOperator(_) => { eyre::bail!("Invalid authorized operator") } - _ => eyre::bail!("Unknown contract error"), + other => unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ), } } } From 6a49c249f2db5ed5b71348f94d9158e542d14d1e Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Fri, 6 Dec 2024 10:42:46 +0100 Subject: [PATCH 7/7] docs(boltup): fix boltup versions --- RELEASE.md | 2 +- testnets/holesky/QUICK_START.md | 2 +- testnets/holesky/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 403d940f2..fd67370c1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -45,4 +45,4 @@ Github release assets. These will then be automatically consumed by `boltup` consumers when selecting the tag they want to use for their Bolt cli installation. -e.g. `boltup --tag v0.3.0-alpha` will pick one of the tarballs in the `v0.3.0-alpha` release. +e.g. `boltup --tag v0.1.0` will pick one of the tarballs in the `cli-v0.1.0` release. diff --git a/testnets/holesky/QUICK_START.md b/testnets/holesky/QUICK_START.md index aace77b6b..abb54d830 100644 --- a/testnets/holesky/QUICK_START.md +++ b/testnets/holesky/QUICK_START.md @@ -40,7 +40,7 @@ curl -L https://raw.githubusercontent.com/chainbound/bolt/unstable/boltup/instal exec $SHELL # install the bolt-cli binary for holesky -boltup --tag v0.3.0-alpha +boltup --tag v0.1.0 # check for successful installation bolt --help diff --git a/testnets/holesky/README.md b/testnets/holesky/README.md index 833eb9b53..4056410f5 100644 --- a/testnets/holesky/README.md +++ b/testnets/holesky/README.md @@ -889,7 +889,7 @@ curl -L https://raw.githubusercontent.com/chainbound/bolt/unstable/boltup/instal exec $SHELL # install the bolt-cli binary for holesky -boltup --tag v0.3.0-alpha +boltup --tag v0.1.0 # check for successful installation bolt --help