diff --git a/crates/astria-cli/src/commands/bridge/collect.rs b/crates/astria-cli/src/bridge/collect.rs similarity index 80% rename from crates/astria-cli/src/commands/bridge/collect.rs rename to crates/astria-cli/src/bridge/collect.rs index 531107a235..01fc605387 100644 --- a/crates/astria-cli/src/commands/bridge/collect.rs +++ b/crates/astria-cli/src/bridge/collect.rs @@ -1,9 +1,5 @@ use std::{ - collections::BTreeMap, - path::{ - Path, - PathBuf, - }, + path::PathBuf, sync::Arc, time::Duration, }; @@ -12,20 +8,15 @@ use astria_bridge_contracts::{ GetWithdrawalActions, GetWithdrawalActionsBuilder, }; -use astria_core::{ - primitive::v1::{ - asset::{ - self, - }, - Address, +use astria_core::primitive::v1::{ + asset::{ + self, }, - protocol::transaction::v1alpha1::Action, + Address, }; -use clap::Args; use color_eyre::eyre::{ self, bail, - ensure, eyre, OptionExt as _, WrapErr as _, @@ -49,8 +40,10 @@ use tracing::{ warn, }; -#[derive(Args, Debug)] -pub(crate) struct WithdrawalEvents { +use super::ActionsByRollupHeight; + +#[derive(clap::Args, Debug)] +pub(super) struct Command { /// The websocket endpoint of a geth compatible rollup. #[arg(long)] rollup_endpoint: String, @@ -88,8 +81,8 @@ pub(crate) struct WithdrawalEvents { force: bool, } -impl WithdrawalEvents { - pub(crate) async fn run(self) -> eyre::Result<()> { +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { let Self { rollup_endpoint, contract_address, @@ -103,7 +96,8 @@ impl WithdrawalEvents { force, } = self; - let output = open_output(&output, force).wrap_err("failed to open output for writing")?; + let output = + super::open_output(&output, force).wrap_err("failed to open output for writing")?; let block_provider = connect_to_rollup(&rollup_endpoint) .await @@ -209,36 +203,6 @@ async fn block_to_actions( actions_by_rollup_height.insert(rollup_height, actions) } -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(transparent)] -pub(crate) struct ActionsByRollupHeight(BTreeMap>); - -impl ActionsByRollupHeight { - fn new() -> Self { - Self(BTreeMap::new()) - } - - pub(crate) fn into_inner(self) -> BTreeMap> { - self.0 - } - - #[instrument(skip_all, err)] - fn insert(&mut self, rollup_height: u64, actions: Vec) -> eyre::Result<()> { - ensure!( - self.0.insert(rollup_height, actions).is_none(), - "already collected actions for block at rollup height `{rollup_height}`; no 2 blocks \ - with the same height should have been seen", - ); - Ok(()) - } - - #[instrument(skip_all, fields(target = %output.path.display()), err)] - fn write_to_output(self, output: Output) -> eyre::Result<()> { - let writer = std::io::BufWriter::new(output.handle); - serde_json::to_writer(writer, &self.0).wrap_err("failed writing actions to file") - } -} - /// Constructs a block stream from `start` until `maybe_end`, if `Some`. /// Constructs an open ended stream from `start` if `None`. #[instrument(skip_all, fields(start, end = maybe_end), err)] @@ -292,31 +256,6 @@ async fn create_stream_of_blocks( Ok(subscription) } -#[derive(Debug)] -struct Output { - handle: std::fs::File, - path: PathBuf, -} - -#[instrument(skip(target), fields(target = %target.as_ref().display()), err)] -fn open_output>(target: P, overwrite: bool) -> eyre::Result { - let handle = if overwrite { - let mut options = std::fs::File::options(); - options.write(true).create(true).truncate(true); - options - } else { - let mut options = std::fs::File::options(); - options.write(true).create_new(true); - options - } - .open(&target) - .wrap_err("failed to open specified file for writing")?; - Ok(Output { - handle, - path: target.as_ref().to_path_buf(), - }) -} - #[instrument(err)] async fn connect_to_rollup(rollup_endpoint: &str) -> eyre::Result>> { let retry_config = tryhard::RetryFutureConfig::new(10) diff --git a/crates/astria-cli/src/bridge/mod.rs b/crates/astria-cli/src/bridge/mod.rs new file mode 100644 index 0000000000..313b886f15 --- /dev/null +++ b/crates/astria-cli/src/bridge/mod.rs @@ -0,0 +1,97 @@ +mod collect; +mod submit; + +use std::{ + collections::BTreeMap, + path::{ + Path, + PathBuf, + }, +}; + +use astria_core::protocol::transaction::v1alpha1::Action; +use clap::Subcommand; +use color_eyre::eyre::{ + self, + ensure, + WrapErr as _, +}; +use tracing::instrument; + +/// Interact with a Sequencer node +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + SubCommand::CollectWithdrawals(args) => args.run().await, + SubCommand::SubmitWithdrawals(args) => args.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum SubCommand { + /// Commands for interacting with Sequencer accounts + CollectWithdrawals(collect::Command), + SubmitWithdrawals(submit::Command), +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(transparent)] +struct ActionsByRollupHeight(BTreeMap>); + +impl ActionsByRollupHeight { + fn new() -> Self { + Self(BTreeMap::new()) + } + + fn into_inner(self) -> BTreeMap> { + self.0 + } + + #[instrument(skip_all, err)] + fn insert(&mut self, rollup_height: u64, actions: Vec) -> eyre::Result<()> { + ensure!( + self.0.insert(rollup_height, actions).is_none(), + "already collected actions for block at rollup height `{rollup_height}`; no 2 blocks \ + with the same height should have been seen", + ); + Ok(()) + } + + #[instrument(skip_all, fields(target = %output.path.display()), err)] + fn write_to_output(self, output: Output) -> eyre::Result<()> { + let writer = std::io::BufWriter::new(output.handle); + serde_json::to_writer(writer, &self.0).wrap_err("failed writing actions to file") + } +} + +#[derive(Debug)] +struct Output { + handle: std::fs::File, + path: PathBuf, +} + +#[instrument(skip(target), fields(target = %target.as_ref().display()), err)] +fn open_output>(target: P, overwrite: bool) -> eyre::Result { + let handle = if overwrite { + let mut options = std::fs::File::options(); + options.write(true).create(true).truncate(true); + options + } else { + let mut options = std::fs::File::options(); + options.write(true).create_new(true); + options + } + .open(&target) + .wrap_err("failed to open specified file for writing")?; + Ok(Output { + handle, + path: target.as_ref().to_path_buf(), + }) +} diff --git a/crates/astria-cli/src/commands/bridge/submit.rs b/crates/astria-cli/src/bridge/submit.rs similarity index 97% rename from crates/astria-cli/src/commands/bridge/submit.rs rename to crates/astria-cli/src/bridge/submit.rs index c7a2a00e71..7d40ed1f87 100644 --- a/crates/astria-cli/src/commands/bridge/submit.rs +++ b/crates/astria-cli/src/bridge/submit.rs @@ -16,7 +16,6 @@ use astria_sequencer_client::{ HttpClient, SequencerClientExt as _, }; -use clap::Args; use color_eyre::eyre::{ self, ensure, @@ -29,8 +28,8 @@ use tracing::{ warn, }; -#[derive(Args, Debug)] -pub(crate) struct WithdrawalEvents { +#[derive(clap::Args, Debug)] +pub(crate) struct Command { #[arg(long, short)] input: PathBuf, #[arg(long)] @@ -43,7 +42,7 @@ pub(crate) struct WithdrawalEvents { sequencer_url: String, } -impl WithdrawalEvents { +impl Command { pub(crate) async fn run(self) -> eyre::Result<()> { let signing_key = read_signing_key(&self.signing_key).wrap_err_with(|| { format!( @@ -96,7 +95,7 @@ impl WithdrawalEvents { } } -fn read_actions>(path: P) -> eyre::Result { +fn read_actions>(path: P) -> eyre::Result { let s = std::fs::read_to_string(path).wrap_err("failed buffering file contents as string")?; serde_json::from_str(&s) .wrap_err("failed deserializing file contents height-to-sequencer-actions serde object") diff --git a/crates/astria-cli/src/cli/mod.rs b/crates/astria-cli/src/cli/mod.rs deleted file mode 100644 index 9b6af0119a..0000000000 --- a/crates/astria-cli/src/cli/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -pub(crate) mod bridge; -pub(crate) mod sequencer; - -use clap::{ - Parser, - Subcommand, -}; -use color_eyre::eyre; - -use crate::cli::sequencer::Command as SequencerCommand; - -const DEFAULT_SEQUENCER_RPC: &str = "https://rpc.sequencer.dusk-10.devnet.astria.org"; -const DEFAULT_SEQUENCER_CHAIN_ID: &str = "astria-dusk-10"; - -/// A CLI for deploying and managing Astria services and related infrastructure. -#[derive(Debug, Parser)] -#[command(name = "astria-cli", version)] -pub struct Cli { - #[command(subcommand)] - pub(crate) command: Option, -} - -impl Cli { - /// Parse the command line arguments - /// - /// # Errors - /// - /// * If the arguments cannot be parsed - pub fn get_args() -> eyre::Result { - let args = Self::parse(); - Ok(args) - } -} - -/// Commands that can be run -#[derive(Debug, Subcommand)] -pub(crate) enum Command { - Bridge { - #[command(subcommand)] - command: bridge::Command, - }, - Sequencer { - #[command(subcommand)] - command: SequencerCommand, - }, -} diff --git a/crates/astria-cli/src/cli/sequencer.rs b/crates/astria-cli/src/cli/sequencer.rs deleted file mode 100644 index 783282aedd..0000000000 --- a/crates/astria-cli/src/cli/sequencer.rs +++ /dev/null @@ -1,380 +0,0 @@ -use astria_core::primitive::v1::asset; -use astria_sequencer_client::Address; -use clap::{ - Args, - Subcommand, -}; - -/// Interact with a Sequencer node -#[derive(Debug, Subcommand)] -pub(crate) enum Command { - /// Commands for interacting with Sequencer accounts - Account { - #[command(subcommand)] - command: AccountCommand, - }, - /// Utilities for constructing and inspecting sequencer addresses - Address { - #[command(subcommand)] - command: AddressCommand, - }, - /// Commands for interacting with Sequencer balances - Balance { - #[command(subcommand)] - command: BalanceCommand, - }, - /// Commands for interacting with Sequencer block heights - #[command(name = "blockheight")] - BlockHeight { - #[command(subcommand)] - command: BlockHeightCommand, - }, - /// Commands requiring authority for Sequencer - Sudo { - #[command(subcommand)] - command: SudoCommand, - }, - /// Command for sending balance between accounts - Transfer(TransferArgs), - /// Command for initializing a bridge account - InitBridgeAccount(InitBridgeAccountArgs), - /// Command for transferring to a bridge account - BridgeLock(BridgeLockArgs), -} - -#[derive(Debug, Subcommand)] -pub(crate) enum AccountCommand { - /// Create a new Sequencer account - Create, - Balance(BasicAccountArgs), - Nonce(BasicAccountArgs), -} - -#[derive(Debug, Subcommand)] -pub(crate) enum AddressCommand { - /// Construct a bech32m Sequencer address given a public key - Bech32m(Bech32mAddressArgs), -} - -#[derive(Debug, Subcommand)] -pub(crate) enum BalanceCommand { - /// Get the balance of a Sequencer account - Get(BasicAccountArgs), -} - -#[derive(Debug, Subcommand)] -pub(crate) enum SudoCommand { - IbcRelayer { - #[command(subcommand)] - command: IbcRelayerChangeCommand, - }, - FeeAsset { - #[command(subcommand)] - command: FeeAssetChangeCommand, - }, - SudoAddressChange(SudoAddressChangeArgs), - ValidatorUpdate(ValidatorUpdateArgs), -} - -#[derive(Debug, Subcommand)] -pub(crate) enum IbcRelayerChangeCommand { - /// Add IBC Relayer - Add(IbcRelayerChangeArgs), - /// Remove IBC Relayer - Remove(IbcRelayerChangeArgs), -} - -#[derive(Debug, Subcommand)] -pub(crate) enum FeeAssetChangeCommand { - /// Add Fee Asset - Add(FeeAssetChangeArgs), - /// Remove Fee Asset - Remove(FeeAssetChangeArgs), -} - -#[derive(Args, Debug)] -pub(crate) struct BasicAccountArgs { - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The address of the Sequencer account - pub(crate) address: Address, -} - -#[derive(Args, Debug)] -pub(crate) struct Bech32mAddressArgs { - /// The hex formatted byte part of the bech32m address - #[arg(long)] - pub(crate) bytes: String, - /// The human readable prefix (Hrp) of the bech32m adress - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, -} - -#[derive(Args, Debug)] -pub(crate) struct TransferArgs { - // The address of the Sequencer account to send amount to - pub(crate) to_address: Address, - // The amount being sent - #[arg(long)] - pub(crate) amount: u128, - /// The bech32m prefix that will be used for constructing addresses using the private key - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - /// The private key of account being sent from - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - pub(crate) private_key: String, - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// The asset to transer. - #[arg(long, default_value = "nria")] - pub(crate) asset: asset::Denom, - /// The asset to pay the transfer fees with. - #[arg(long, default_value = "nria")] - pub(crate) fee_asset: asset::Denom, -} - -#[derive(Args, Debug)] -pub(crate) struct FeeAssetChangeArgs { - /// The bech32m prefix that will be used for constructing addresses using the private key - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - pub(crate) private_key: String, - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// Asset's denomination string - #[arg(long)] - pub(crate) asset: asset::Denom, -} - -#[derive(Args, Debug)] -pub(crate) struct IbcRelayerChangeArgs { - /// The prefix to construct a bech32m address given the private key. - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - pub(crate) private_key: String, - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// The address to add or remove as an IBC relayer - #[arg(long)] - pub(crate) address: Address, -} - -#[derive(Args, Debug)] -pub(crate) struct InitBridgeAccountArgs { - /// The bech32m prefix that will be used for constructing addresses using the private key - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - pub(crate) private_key: String, - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// Plaintext rollup name (to be hashed into a rollup ID) - /// to initialize the bridge account with. - #[arg(long)] - pub(crate) rollup_name: String, - /// The asset to transer. - #[arg(long, default_value = "nria")] - pub(crate) asset: asset::Denom, - /// The asset to pay the transfer fees with. - #[arg(long, default_value = "nria")] - pub(crate) fee_asset: asset::Denom, -} - -#[derive(Args, Debug)] -pub(crate) struct BridgeLockArgs { - /// The address of the Sequencer account to lock amount to - pub(crate) to_address: Address, - /// The amount being locked - #[arg(long)] - pub(crate) amount: u128, - #[arg(long)] - pub(crate) destination_chain_address: String, - /// The prefix to construct a bech32m address given the private key. - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - pub(crate) private_key: String, - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// The asset to lock. - #[arg(long, default_value = "nria")] - pub(crate) asset: asset::Denom, - /// The asset to pay the transfer fees with. - #[arg(long, default_value = "nria")] - pub(crate) fee_asset: asset::Denom, -} - -#[derive(Debug, Subcommand)] -pub(crate) enum BlockHeightCommand { - /// Get the current block height of the Sequencer node - Get(BlockHeightGetArgs), -} - -#[derive(Args, Debug)] -pub(crate) struct BlockHeightGetArgs { - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, -} - -#[derive(Args, Debug)] -pub(crate) struct SudoAddressChangeArgs { - /// The bech32m prefix that will be used for constructing addresses using the private key - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - pub(crate) private_key: String, - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// The new address to take over sudo privileges - #[arg(long)] - pub(crate) address: Address, -} - -#[derive(Args, Debug)] -pub(crate) struct ValidatorUpdateArgs { - /// The url of the Sequencer node - #[arg( - long, - env = "SEQUENCER_URL", - default_value = crate::cli::DEFAULT_SEQUENCER_RPC - )] - pub(crate) sequencer_url: String, - /// The chain id of the sequencing chain being used - #[arg( - long = "sequencer.chain-id", - env = "ROLLUP_SEQUENCER_CHAIN_ID", - default_value = crate::cli::DEFAULT_SEQUENCER_CHAIN_ID - )] - pub(crate) sequencer_chain_id: String, - /// The bech32m prefix that will be used for constructing addresses using the private key - #[arg(long, default_value = "astria")] - pub(crate) prefix: String, - /// The private key of the sudo account authorizing change - #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] - // TODO: https://github.com/astriaorg/astria/issues/594 - // Don't use a plain text private, prefer wrapper like from - // the secrecy crate with specialized `Debug` and `Drop` implementations - // that overwrite the key on drop and don't reveal it when printing. - pub(crate) private_key: String, - /// The address of the Validator being updated - #[arg(long)] - pub(crate) validator_public_key: String, - /// The power the validator is being updated to - #[arg(long)] - pub(crate) power: u32, -} diff --git a/crates/astria-cli/src/commands/bridge/mod.rs b/crates/astria-cli/src/commands/bridge/mod.rs deleted file mode 100644 index 9f12fb87d5..0000000000 --- a/crates/astria-cli/src/commands/bridge/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod collect; -pub(crate) mod submit; diff --git a/crates/astria-cli/src/commands/mod.rs b/crates/astria-cli/src/commands/mod.rs deleted file mode 100644 index 436d5c74fc..0000000000 --- a/crates/astria-cli/src/commands/mod.rs +++ /dev/null @@ -1,107 +0,0 @@ -pub(crate) mod bridge; -mod sequencer; - -use color_eyre::{ - eyre, - eyre::eyre, -}; - -use crate::cli::{ - sequencer::{ - AccountCommand, - AddressCommand, - BalanceCommand, - BlockHeightCommand, - Command as SequencerCommand, - FeeAssetChangeCommand, - IbcRelayerChangeCommand, - SudoCommand, - }, - Cli, - Command, -}; - -/// Checks what function needs to be run and calls it with the appropriate arguments -/// -/// # Arguments -/// -/// * `cli` - The arguments passed to the command -/// -/// # Errors -/// -/// * If no command is specified -/// -/// # Panics -/// -/// * If the command is not recognized -pub async fn run(cli: Cli) -> eyre::Result<()> { - if let Some(command) = cli.command { - match command { - Command::Bridge { - command, - } => command.run().await?, - Command::Sequencer { - command, - } => match command { - SequencerCommand::Account { - command, - } => match command { - AccountCommand::Create => sequencer::create_account(), - AccountCommand::Balance(args) => sequencer::get_balance(&args).await?, - AccountCommand::Nonce(args) => sequencer::get_nonce(&args).await?, - }, - SequencerCommand::Address { - command, - } => match command { - AddressCommand::Bech32m(args) => sequencer::make_bech32m(&args)?, - }, - SequencerCommand::Balance { - command, - } => match command { - BalanceCommand::Get(args) => sequencer::get_balance(&args).await?, - }, - SequencerCommand::Sudo { - command, - } => match command { - SudoCommand::IbcRelayer { - command, - } => match command { - IbcRelayerChangeCommand::Add(args) => { - sequencer::ibc_relayer_add(&args).await?; - } - IbcRelayerChangeCommand::Remove(args) => { - sequencer::ibc_relayer_remove(&args).await?; - } - }, - SudoCommand::FeeAsset { - command, - } => match command { - FeeAssetChangeCommand::Add(args) => sequencer::fee_asset_add(&args).await?, - FeeAssetChangeCommand::Remove(args) => { - sequencer::fee_asset_remove(&args).await?; - } - }, - SudoCommand::ValidatorUpdate(args) => { - sequencer::validator_update(&args).await?; - } - SudoCommand::SudoAddressChange(args) => { - sequencer::sudo_address_change(&args).await?; - } - }, - SequencerCommand::Transfer(args) => sequencer::send_transfer(&args).await?, - SequencerCommand::BlockHeight { - command, - } => match command { - BlockHeightCommand::Get(args) => sequencer::get_block_height(&args).await?, - }, - SequencerCommand::InitBridgeAccount(args) => { - sequencer::init_bridge_account(&args).await?; - } - SequencerCommand::BridgeLock(args) => sequencer::bridge_lock(&args).await?, - }, - } - } else { - return Err(eyre!("Error: No command specified")); - } - Ok(()) -} diff --git a/crates/astria-cli/src/lib.rs b/crates/astria-cli/src/lib.rs index 18a4aecb56..e4d7a90ba8 100644 --- a/crates/astria-cli/src/lib.rs +++ b/crates/astria-cli/src/lib.rs @@ -1,2 +1,53 @@ -pub mod cli; -pub mod commands; +#![allow( + clippy::large_enum_variant, + reason = "the CLI contains enums with diverging variants. These are oneshot types that + are not expected to be copied, cloned, or passed around. Therefore large differences \ + between enum variants are not expected to cause performance issues." +)] + +mod bridge; +mod sequencer; +mod utils; + +use clap::{ + Parser, + Subcommand, +}; +use color_eyre::eyre; + +const DEFAULT_SEQUENCER_RPC: &str = "https://rpc.sequencer.dusk-10.devnet.astria.org"; +const DEFAULT_SEQUENCER_CHAIN_ID: &str = "astria-dusk-10"; + +/// Run commands against the Astria network. +#[derive(Debug, Parser)] +#[command(name = "astria-cli", version, about)] +pub struct Cli { + #[command(subcommand)] + command: Command, +} + +impl Cli { + /// Runs the Astria CLI. + /// + /// This is the only entry point into the Astria CLI. + /// + /// # Errors + /// + /// Returns various errors if executing a subcommand fails. The errors are + /// not explicitly listed here. + pub async fn run() -> eyre::Result<()> { + let cli = Self::parse(); + match cli.command { + Command::Bridge(bridge) => bridge.run().await, + Command::Sequencer(sequencer) => sequencer.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Collect events from a rollup and submit to Sequencer. + Bridge(bridge::Command), + /// Interact with Sequencer. + Sequencer(sequencer::Command), +} diff --git a/crates/astria-cli/src/main.rs b/crates/astria-cli/src/main.rs index c587580795..5e241d4ce8 100644 --- a/crates/astria-cli/src/main.rs +++ b/crates/astria-cli/src/main.rs @@ -1,27 +1,11 @@ -use std::process::ExitCode; - -use astria_cli::{ - cli::Cli, - commands, -}; use color_eyre::eyre; #[tokio::main] -async fn main() -> ExitCode { +async fn main() -> eyre::Result<()> { tracing_subscriber::fmt() .pretty() .with_writer(std::io::stderr) .init(); - if let Err(err) = run().await { - eprintln!("{err:?}"); - return ExitCode::FAILURE; - } - - ExitCode::SUCCESS -} - -async fn run() -> eyre::Result<()> { - let args = Cli::get_args()?; - commands::run(args).await + astria_cli::Cli::run().await } diff --git a/crates/astria-cli/src/sequencer/account.rs b/crates/astria-cli/src/sequencer/account.rs new file mode 100644 index 0000000000..ba08428453 --- /dev/null +++ b/crates/astria-cli/src/sequencer/account.rs @@ -0,0 +1,128 @@ +use astria_core::{ + crypto::SigningKey, + primitive::v1::Address, +}; +use astria_sequencer_client::{ + HttpClient, + SequencerClientExt as _, +}; +use clap::Subcommand; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; +use rand::rngs::OsRng; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + SubCommand::Create(create) => create.run(), + SubCommand::Balance(balance) => balance.run().await, + SubCommand::Nonce(nonce) => nonce.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum SubCommand { + /// Generates a new ED25519 keypair. + Create(Create), + /// Queries the Sequencer for the balances of an account. + Balance(Balance), + /// Queries the Sequencer for the current nonce of an account. + Nonce(Nonce), +} + +#[derive(Debug, clap::Args)] +struct Create; + +impl Create { + #[expect( + clippy::unused_self, + clippy::unnecessary_wraps, + reason = "for consistency with all the other commands" + )] + fn run(self) -> eyre::Result<()> { + let signing_key = SigningKey::new(OsRng); + let pretty_signing_key = hex::encode(signing_key.as_bytes()); + let pretty_verifying_key = hex::encode(signing_key.verification_key().as_bytes()); + let pretty_address = hex::encode(signing_key.address_bytes()); + println!("Create Sequencer Account"); + println!(); + // TODO: don't print private keys to CLI, prefer writing to file: + // https://github.com/astriaorg/astria/issues/594 + println!("Private Key: {pretty_signing_key}"); + println!("Public Key: {pretty_verifying_key}"); + println!("Address: {pretty_address}"); + Ok(()) + } +} + +#[derive(Debug, clap::Args)] +struct Balance { + #[command(flatten)] + inner: ArgsInner, +} + +impl Balance { + async fn run(self) -> eyre::Result<()> { + let args = self.inner; + let sequencer_client = HttpClient::new(args.sequencer_url.as_str()) + .wrap_err("failed constructing http sequencer client")?; + + let res = sequencer_client + .get_latest_balance(args.address) + .await + .wrap_err("failed to get balance")?; + + println!("Balances for address: {}", args.address); + for balance in res.balances { + println!(" {} {}", balance.balance, balance.denom); + } + + Ok(()) + } +} + +#[derive(Debug, clap::Args)] +struct Nonce { + #[command(flatten)] + inner: ArgsInner, +} + +impl Nonce { + async fn run(self) -> eyre::Result<()> { + let args = self.inner; + let sequencer_client = HttpClient::new(args.sequencer_url.as_str()) + .wrap_err("failed constructing http sequencer client")?; + + let res = sequencer_client + .get_latest_nonce(args.address) + .await + .wrap_err("failed to get nonce")?; + + println!("Nonce for address {}", args.address); + println!(" {} at height {}", res.nonce, res.height); + + Ok(()) + } +} + +#[derive(clap::Args, Debug)] +struct ArgsInner { + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The address of the Sequencer account + address: Address, +} diff --git a/crates/astria-cli/src/sequencer/address.rs b/crates/astria-cli/src/sequencer/address.rs new file mode 100644 index 0000000000..fa7dcee7cb --- /dev/null +++ b/crates/astria-cli/src/sequencer/address.rs @@ -0,0 +1,55 @@ +use astria_core::primitive::v1::{ + Address, + ADDRESS_LEN, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) fn run(self) -> eyre::Result<()> { + let SubCommand::Bech32m(bech32m) = self.command; + bech32m.run() + } +} + +#[derive(Debug, clap::Subcommand)] +enum SubCommand { + /// Returns a bech32m sequencer address given a prefix and hex-encoded byte slice + Bech32m(Bech32m), +} + +#[derive(Debug, clap::Args)] +struct Bech32m { + /// The hex formatted byte part of the bech32m address + #[arg(long)] + bytes: String, + /// The human readable prefix (Hrp) of the bech32m adress + #[arg(long, default_value = "astria")] + prefix: String, +} + +impl Bech32m { + fn run(self) -> eyre::Result<()> { + use hex::FromHex as _; + let bytes = <[u8; ADDRESS_LEN]>::from_hex(&self.bytes) + .wrap_err("failed decoding provided hex bytes")?; + let address = Address::::builder() + .array(bytes) + .prefix(&self.prefix) + .try_build() + .wrap_err( + "failed constructing a valid bech32m address from the provided hex bytes and \ + prefix", + )?; + println!("{address}"); + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/balance.rs b/crates/astria-cli/src/sequencer/balance.rs new file mode 100644 index 0000000000..2533dcc122 --- /dev/null +++ b/crates/astria-cli/src/sequencer/balance.rs @@ -0,0 +1,61 @@ +use astria_core::primitive::v1::Address; +use astria_sequencer_client::{ + HttpClient, + SequencerClientExt as _, +}; +use clap::Subcommand; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + let SubCommand::Get(get) = self.command; + get.run().await + } +} + +#[derive(Debug, Subcommand)] +enum SubCommand { + /// Get the balance of a Sequencer account + Get(Get), +} + +#[derive(clap::Args, Debug)] +struct Get { + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The address of the Sequencer account + address: Address, +} + +impl Get { + async fn run(self) -> eyre::Result<()> { + let sequencer_client = HttpClient::new(self.sequencer_url.as_str()) + .wrap_err("failed constructing http sequencer client")?; + + let res = sequencer_client + .get_latest_balance(self.address) + .await + .wrap_err("failed to get balance")?; + + println!("Balances for address: {}", self.address); + for balance in res.balances { + println!(" {} {}", balance.balance, balance.denom); + } + + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/block_height.rs b/crates/astria-cli/src/sequencer/block_height.rs new file mode 100644 index 0000000000..6e9f1df628 --- /dev/null +++ b/crates/astria-cli/src/sequencer/block_height.rs @@ -0,0 +1,63 @@ +use astria_sequencer_client::{ + Client as _, + HttpClient, +}; +use clap::Subcommand; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + let SubCommand::Get(get) = self.command; + get.run().await + } +} + +#[derive(Debug, Subcommand)] +enum SubCommand { + /// Get the current block height of the Sequencer node + Get(Get), +} + +#[derive(clap::Args, Debug)] +struct Get { + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, +} + +impl Get { + async fn run(self) -> eyre::Result<()> { + let sequencer_client = HttpClient::new(self.sequencer_url.as_str()) + .wrap_err("failed constructing http sequencer client")?; + + let res = sequencer_client + .latest_block() + .await + .wrap_err("failed to get cometbft block")?; + + println!("Block Height:"); + println!(" {}", res.block.header.height); + + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/bridge_lock.rs b/crates/astria-cli/src/sequencer/bridge_lock.rs new file mode 100644 index 0000000000..baeabbc717 --- /dev/null +++ b/crates/astria-cli/src/sequencer/bridge_lock.rs @@ -0,0 +1,80 @@ +use astria_core::{ + primitive::v1::{ + asset, + Address, + }, + protocol::transaction::v1alpha1::{ + action::BridgeLockAction, + Action, + }, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +use crate::utils::submit_transaction; + +#[derive(clap::Args, Debug)] +pub(super) struct Command { + /// The address of the Sequencer account to lock amount to + to_address: Address, + /// The amount being locked + #[arg(long)] + amount: u128, + #[arg(long)] + destination_chain_address: String, + /// The prefix to construct a bech32m address given the private key. + #[arg(long, default_value = "astria")] + prefix: String, + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + private_key: String, + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// The asset to lock. + #[arg(long, default_value = "nria")] + asset: asset::Denom, + /// The asset to pay the transfer fees with. + #[arg(long, default_value = "nria")] + fee_asset: asset::Denom, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + let res = submit_transaction( + self.sequencer_url.as_str(), + self.sequencer_chain_id.clone(), + &self.prefix, + self.private_key.as_str(), + Action::BridgeLock(BridgeLockAction { + to: self.to_address, + asset: self.asset.clone(), + amount: self.amount, + fee_asset: self.fee_asset.clone(), + destination_chain_address: self.destination_chain_address.clone(), + }), + ) + .await + .wrap_err("failed to submit BridgeLock transaction")?; + + println!("BridgeLock completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/init_bridge_account.rs b/crates/astria-cli/src/sequencer/init_bridge_account.rs new file mode 100644 index 0000000000..1e411457eb --- /dev/null +++ b/crates/astria-cli/src/sequencer/init_bridge_account.rs @@ -0,0 +1,77 @@ +use astria_core::{ + primitive::v1::asset, + protocol::transaction::v1alpha1::{ + action::InitBridgeAccountAction, + Action, + }, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +#[derive(clap::Args, Debug)] +pub(super) struct Command { + /// The bech32m prefix that will be used for constructing addresses using the private key + #[arg(long, default_value = "astria")] + prefix: String, + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + private_key: String, + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// Plaintext rollup name (to be hashed into a rollup ID) + /// to initialize the bridge account with. + #[arg(long)] + rollup_name: String, + /// The asset to transer. + #[arg(long, default_value = "nria")] + asset: asset::Denom, + /// The asset to pay the transfer fees with. + #[arg(long, default_value = "nria")] + fee_asset: asset::Denom, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + use astria_core::primitive::v1::RollupId; + + let rollup_id = RollupId::from_unhashed_bytes(self.rollup_name.as_bytes()); + let res = crate::utils::submit_transaction( + self.sequencer_url.as_str(), + self.sequencer_chain_id.clone(), + &self.prefix, + self.private_key.as_str(), + Action::InitBridgeAccount(InitBridgeAccountAction { + rollup_id, + asset: self.asset.clone(), + fee_asset: self.fee_asset.clone(), + sudo_address: None, + withdrawer_address: None, + }), + ) + .await + .wrap_err("failed to submit InitBridgeAccount transaction")?; + + println!("InitBridgeAccount completed!"); + println!("Included in block: {}", res.height); + println!("Rollup name: {}", self.rollup_name); + println!("Rollup ID: {rollup_id}"); + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/mod.rs b/crates/astria-cli/src/sequencer/mod.rs new file mode 100644 index 0000000000..f4bb35f0c6 --- /dev/null +++ b/crates/astria-cli/src/sequencer/mod.rs @@ -0,0 +1,54 @@ +use clap::Subcommand; +use color_eyre::eyre; + +mod account; +mod address; +mod balance; +mod block_height; +mod bridge_lock; +mod init_bridge_account; +mod sudo; +mod transfer; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + SubCommand::Account(account) => account.run().await, + SubCommand::Address(address) => address.run(), + SubCommand::Balance(balance) => balance.run().await, + SubCommand::BlockHeight(block_height) => block_height.run().await, + SubCommand::BridgeLock(bridge_lock) => bridge_lock.run().await, + SubCommand::InitBridgeAccount(init_bridge_account) => init_bridge_account.run().await, + SubCommand::Sudo(sudo) => sudo.run().await, + SubCommand::Transfer(transfer) => transfer.run().await, + } + } +} + +/// Interact with a Sequencer node +#[derive(Debug, Subcommand)] +enum SubCommand { + /// Commands for interacting with Sequencer accounts + Account(account::Command), + /// Utilities for constructing and inspecting sequencer addresses + Address(address::Command), + /// Commands for interacting with Sequencer balances + Balance(balance::Command), + /// Commands for interacting with Sequencer block heights + #[command(name = "blockheight")] + BlockHeight(block_height::Command), + /// Command for transferring to a bridge account + BridgeLock(bridge_lock::Command), + /// Command for initializing a bridge account + InitBridgeAccount(init_bridge_account::Command), + /// Commands requiring authority for Sequencer + Sudo(sudo::Command), + /// Command for sending balance between accounts + Transfer(transfer::Command), +} diff --git a/crates/astria-cli/src/sequencer/sudo/fee_asset.rs b/crates/astria-cli/src/sequencer/sudo/fee_asset.rs new file mode 100644 index 0000000000..181c833cda --- /dev/null +++ b/crates/astria-cli/src/sequencer/sudo/fee_asset.rs @@ -0,0 +1,117 @@ +use astria_core::{ + primitive::v1::asset, + protocol::transaction::v1alpha1::{ + action::FeeAssetChangeAction, + Action, + }, +}; +use clap::Subcommand; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +use crate::utils::submit_transaction; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + SubCommand::Add(add) => add.run().await, + SubCommand::Remove(remove) => remove.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum SubCommand { + /// Add Fee Asset + Add(Add), + /// Remove Fee Asset + Remove(Remove), +} + +#[derive(Clone, Debug, clap::Args)] +struct Add { + #[command(flatten)] + inner: ArgsInner, +} + +impl Add { + async fn run(self) -> eyre::Result<()> { + let args = self.inner; + let res = submit_transaction( + args.sequencer_url.as_str(), + args.sequencer_chain_id.clone(), + &args.prefix, + args.private_key.as_str(), + Action::FeeAssetChange(FeeAssetChangeAction::Addition(args.asset.clone())), + ) + .await + .wrap_err("failed to submit FeeAssetChangeAction::Addition transaction")?; + + println!("FeeAssetChangeAction::Addition completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} + +#[derive(Clone, Debug, clap::Args)] +struct Remove { + #[command(flatten)] + inner: ArgsInner, +} + +impl Remove { + async fn run(self) -> eyre::Result<()> { + let args = self.inner; + let res = submit_transaction( + args.sequencer_url.as_str(), + args.sequencer_chain_id.clone(), + &args.prefix, + args.private_key.as_str(), + Action::FeeAssetChange(FeeAssetChangeAction::Removal(args.asset.clone())), + ) + .await + .wrap_err("failed to submit FeeAssetChangeAction::Removal transaction")?; + + println!("FeeAssetChangeAction::Removal completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} + +#[derive(Clone, Debug, clap::Args)] +struct ArgsInner { + /// The bech32m prefix that will be used for constructing addresses using the private key + #[arg(long, default_value = "astria")] + prefix: String, + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + private_key: String, + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// Asset's denomination string + #[arg(long)] + asset: asset::Denom, +} diff --git a/crates/astria-cli/src/sequencer/sudo/ibc_relayer.rs b/crates/astria-cli/src/sequencer/sudo/ibc_relayer.rs new file mode 100644 index 0000000000..f8c190b157 --- /dev/null +++ b/crates/astria-cli/src/sequencer/sudo/ibc_relayer.rs @@ -0,0 +1,114 @@ +use astria_core::{ + primitive::v1::Address, + protocol::transaction::v1alpha1::{ + action::IbcRelayerChangeAction, + Action, + }, +}; +use color_eyre::{ + eyre, + eyre::WrapErr as _, +}; + +use crate::utils::submit_transaction; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + SubCommand::Add(add) => add.run().await, + SubCommand::Remove(remove) => remove.run().await, + } + } +} + +#[derive(Debug, clap::Subcommand)] +enum SubCommand { + Add(Add), + Remove(Remove), +} + +#[derive(Debug, clap::Args)] +struct Add { + #[command(flatten)] + inner: ArgsInner, +} + +impl Add { + async fn run(self) -> eyre::Result<()> { + let args = self.inner; + let res = submit_transaction( + args.sequencer_url.as_str(), + args.sequencer_chain_id.clone(), + &args.prefix, + args.private_key.as_str(), + Action::IbcRelayerChange(IbcRelayerChangeAction::Addition(args.address)), + ) + .await + .wrap_err("failed to submit IbcRelayerChangeAction::Addition transaction")?; + + println!("IbcRelayerChangeAction::Addition completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} + +#[derive(Debug, clap::Args)] +struct Remove { + #[command(flatten)] + inner: ArgsInner, +} + +impl Remove { + async fn run(self) -> eyre::Result<()> { + let args = self.inner; + let res = submit_transaction( + args.sequencer_url.as_str(), + args.sequencer_chain_id.clone(), + &args.prefix, + args.private_key.as_str(), + Action::IbcRelayerChange(IbcRelayerChangeAction::Removal(args.address)), + ) + .await + .wrap_err("failed to submit IbcRelayerChangeAction::Removal transaction")?; + + println!("IbcRelayerChangeAction::Removal completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} + +#[derive(Debug, clap::Args)] +struct ArgsInner { + /// The prefix to construct a bech32m address given the private key. + #[arg(long, default_value = "astria")] + prefix: String, + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + private_key: String, + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// The address to add or remove as an IBC relayer + #[arg(long)] + address: Address, +} diff --git a/crates/astria-cli/src/sequencer/sudo/mod.rs b/crates/astria-cli/src/sequencer/sudo/mod.rs new file mode 100644 index 0000000000..b983d2eb03 --- /dev/null +++ b/crates/astria-cli/src/sequencer/sudo/mod.rs @@ -0,0 +1,31 @@ +use color_eyre::eyre; + +mod fee_asset; +mod ibc_relayer; +mod sudo_address_change; +mod validator_update; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + #[command(subcommand)] + command: SubCommand, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + SubCommand::IbcRelayer(ibc_relayer) => ibc_relayer.run().await, + SubCommand::FeeAsset(fee_asset) => fee_asset.run().await, + SubCommand::SudoAddressChange(sudo_address_change) => sudo_address_change.run().await, + SubCommand::ValidatorUpdate(validator_update) => validator_update.run().await, + } + } +} + +#[derive(Debug, clap::Subcommand)] +enum SubCommand { + IbcRelayer(ibc_relayer::Command), + FeeAsset(fee_asset::Command), + SudoAddressChange(sudo_address_change::Command), + ValidatorUpdate(validator_update::Command), +} diff --git a/crates/astria-cli/src/sequencer/sudo/sudo_address_change.rs b/crates/astria-cli/src/sequencer/sudo/sudo_address_change.rs new file mode 100644 index 0000000000..f93277440e --- /dev/null +++ b/crates/astria-cli/src/sequencer/sudo/sudo_address_change.rs @@ -0,0 +1,63 @@ +use astria_core::{ + primitive::v1::Address, + protocol::transaction::v1alpha1::{ + action::SudoAddressChangeAction, + Action, + }, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +use crate::utils::submit_transaction; + +#[derive(Debug, clap::Args)] +pub(super) struct Command { + /// The bech32m prefix that will be used for constructing addresses using the private key + #[arg(long, default_value = "astria")] + prefix: String, + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + private_key: String, + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// The new address to take over sudo privileges + #[arg(long)] + address: Address, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + let res = submit_transaction( + self.sequencer_url.as_str(), + self.sequencer_chain_id.clone(), + &self.prefix, + self.private_key.as_str(), + Action::SudoAddressChange(SudoAddressChangeAction { + new_address: self.address, + }), + ) + .await + .wrap_err("failed to submit SudoAddressChange transaction")?; + + println!("SudoAddressChange completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/sudo/validator_update.rs b/crates/astria-cli/src/sequencer/sudo/validator_update.rs new file mode 100644 index 0000000000..bcbca1832f --- /dev/null +++ b/crates/astria-cli/src/sequencer/sudo/validator_update.rs @@ -0,0 +1,72 @@ +use astria_core::protocol::transaction::v1alpha1::{ + action::ValidatorUpdate, + Action, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +use crate::utils::submit_transaction; + +#[derive(clap::Args, Debug)] +pub(super) struct Command { + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// The bech32m prefix that will be used for constructing addresses using the private key + #[arg(long, default_value = "astria")] + prefix: String, + /// The private key of the sudo account authorizing change + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + private_key: String, + /// The address of the Validator being updated + #[arg(long)] + validator_public_key: String, + /// The power the validator is being updated to + #[arg(long)] + power: u32, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + let verification_key = astria_core::crypto::VerificationKey::try_from( + &*hex::decode(&self.validator_public_key) + .wrap_err("failed to decode public key bytes from argument")?, + ) + .wrap_err("failed to construct public key from bytes")?; + let validator_update = ValidatorUpdate { + power: self.power, + verification_key, + }; + + let res = submit_transaction( + self.sequencer_url.as_str(), + self.sequencer_chain_id.clone(), + &self.prefix, + self.private_key.as_str(), + Action::ValidatorUpdate(validator_update), + ) + .await + .wrap_err("failed to submit ValidatorUpdate transaction")?; + + println!("ValidatorUpdate completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} diff --git a/crates/astria-cli/src/sequencer/transfer.rs b/crates/astria-cli/src/sequencer/transfer.rs new file mode 100644 index 0000000000..648156cd3e --- /dev/null +++ b/crates/astria-cli/src/sequencer/transfer.rs @@ -0,0 +1,78 @@ +use astria_core::{ + primitive::v1::{ + asset, + Address, + }, + protocol::transaction::v1alpha1::{ + action::TransferAction, + Action, + }, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +use crate::utils::submit_transaction; + +#[derive(clap::Args, Debug)] +pub(super) struct Command { + // The address of the Sequencer account to send amount to + to_address: Address, + // The amount being sent + #[arg(long)] + amount: u128, + /// The bech32m prefix that will be used for constructing addresses using the private key + #[arg(long, default_value = "astria")] + prefix: String, + /// The private key of account being sent from + #[arg(long, env = "SEQUENCER_PRIVATE_KEY")] + // TODO: https://github.com/astriaorg/astria/issues/594 + // Don't use a plain text private, prefer wrapper like from + // the secrecy crate with specialized `Debug` and `Drop` implementations + // that overwrite the key on drop and don't reveal it when printing. + private_key: String, + /// The url of the Sequencer node + #[arg( + long, + env = "SEQUENCER_URL", + default_value = crate::DEFAULT_SEQUENCER_RPC + )] + sequencer_url: String, + /// The chain id of the sequencing chain being used + #[arg( + long = "sequencer.chain-id", + env = "ROLLUP_SEQUENCER_CHAIN_ID", + default_value = crate::DEFAULT_SEQUENCER_CHAIN_ID + )] + sequencer_chain_id: String, + /// The asset to transer. + #[arg(long, default_value = "nria")] + asset: asset::Denom, + /// The asset to pay the transfer fees with. + #[arg(long, default_value = "nria")] + fee_asset: asset::Denom, +} + +impl Command { + pub(super) async fn run(self) -> eyre::Result<()> { + let res = submit_transaction( + self.sequencer_url.as_str(), + self.sequencer_chain_id.clone(), + &self.prefix, + self.private_key.as_str(), + Action::Transfer(TransferAction { + to: self.to_address, + amount: self.amount, + asset: self.asset.clone(), + fee_asset: self.fee_asset.clone(), + }), + ) + .await + .wrap_err("failed to submit transfer transaction")?; + + println!("Transfer completed!"); + println!("Included in block: {}", res.height); + Ok(()) + } +} diff --git a/crates/astria-cli/src/utils.rs b/crates/astria-cli/src/utils.rs new file mode 100644 index 0000000000..543b52e333 --- /dev/null +++ b/crates/astria-cli/src/utils.rs @@ -0,0 +1,71 @@ +use astria_core::{ + crypto::SigningKey, + primitive::v1::Address, + protocol::transaction::v1alpha1::{ + Action, + UnsignedTransaction, + }, +}; +use astria_sequencer_client::{ + tendermint_rpc::endpoint::tx::Response, + HttpClient, + SequencerClientExt as _, +}; +use color_eyre::eyre::{ + self, + ensure, + eyre, + WrapErr as _, +}; + +pub(crate) async fn submit_transaction( + sequencer_url: &str, + chain_id: String, + prefix: &str, + private_key: &str, + action: Action, +) -> eyre::Result { + let sequencer_client = + HttpClient::new(sequencer_url).wrap_err("failed constructing http sequencer client")?; + + let private_key_bytes: [u8; 32] = hex::decode(private_key) + .wrap_err("failed to decode private key bytes from hex string")? + .try_into() + .map_err(|_| eyre!("invalid private key length; must be 32 bytes"))?; + let sequencer_key = SigningKey::from(private_key_bytes); + + let from_address = Address::builder() + .array(*sequencer_key.verification_key().address_bytes()) + .prefix(prefix) + .try_build() + .wrap_err("failed constructing a valid from address from the provided prefix")?; + println!("sending tx from address: {from_address}"); + + let nonce_res = sequencer_client + .get_latest_nonce(from_address) + .await + .wrap_err("failed to get nonce")?; + + let tx = UnsignedTransaction::builder() + .nonce(nonce_res.nonce) + .chain_id(chain_id) + .actions(vec![action]) + .try_build() + .wrap_err("failed to construct a transaction")? + .into_signed(&sequencer_key); + let res = sequencer_client + .submit_transaction_sync(tx) + .await + .wrap_err("failed to submit transaction")?; + + let tx_response = sequencer_client.wait_for_tx_inclusion(res.hash).await; + + ensure!(res.code.is_ok(), "failed to check tx: {}", res.log); + + ensure!( + tx_response.tx_result.code.is_ok(), + "failed to execute tx: {}", + tx_response.tx_result.log + ); + Ok(tx_response) +}