diff --git a/crates/astria-cli/src/commands/bridge/collect.rs b/crates/astria-cli/src/bridge/collect.rs similarity index 99% rename from crates/astria-cli/src/commands/bridge/collect.rs rename to crates/astria-cli/src/bridge/collect.rs index 531107a235..f8255fea9b 100644 --- a/crates/astria-cli/src/commands/bridge/collect.rs +++ b/crates/astria-cli/src/bridge/collect.rs @@ -50,7 +50,7 @@ use tracing::{ }; #[derive(Args, Debug)] -pub(crate) struct WithdrawalEvents { +pub(crate) struct WithdrawalEventsArgs { /// The websocket endpoint of a geth compatible rollup. #[arg(long)] rollup_endpoint: String, @@ -88,7 +88,7 @@ pub(crate) struct WithdrawalEvents { force: bool, } -impl WithdrawalEvents { +impl WithdrawalEventsArgs { pub(crate) async fn run(self) -> eyre::Result<()> { let Self { rollup_endpoint, diff --git a/crates/astria-cli/src/bridge/mod.rs b/crates/astria-cli/src/bridge/mod.rs new file mode 100644 index 0000000000..1189d1a8a3 --- /dev/null +++ b/crates/astria-cli/src/bridge/mod.rs @@ -0,0 +1,33 @@ +pub(crate) mod collect; +pub(crate) mod submit; + +use clap::Subcommand; +use color_eyre::eyre; + +/// Interact with a Sequencer node +#[expect( + clippy::large_enum_variant, + reason = "these are fire-and-forget types that are instantiated once and consumed, so + that their size does not matter" +)] +#[derive(Debug, clap::Args)] +pub(super) struct Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + Command::CollectWithdrawals(args) => args.run().await, + Command::SubmitWithdrawals(args) => args.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Commands for interacting with Sequencer accounts + CollectWithdrawals(collect::WithdrawalEventsArgs), + SubmitWithdrawals(submit::WithdrawalEventsArgs), +} diff --git a/crates/astria-cli/src/commands/bridge/submit.rs b/crates/astria-cli/src/bridge/submit.rs similarity index 98% rename from crates/astria-cli/src/commands/bridge/submit.rs rename to crates/astria-cli/src/bridge/submit.rs index c332a5a6d1..61f184d564 100644 --- a/crates/astria-cli/src/commands/bridge/submit.rs +++ b/crates/astria-cli/src/bridge/submit.rs @@ -31,7 +31,7 @@ use tracing::{ }; #[derive(Args, Debug)] -pub(crate) struct WithdrawalEvents { +pub(crate) struct WithdrawalEventsArgs { #[arg(long, short)] input: PathBuf, #[arg(long)] @@ -44,7 +44,7 @@ pub(crate) struct WithdrawalEvents { sequencer_url: String, } -impl WithdrawalEvents { +impl WithdrawalEventsArgs { pub(crate) async fn run(self) -> eyre::Result<()> { let signing_key = read_signing_key(&self.signing_key).wrap_err_with(|| { format!( diff --git a/crates/astria-cli/src/cli/bridge.rs b/crates/astria-cli/src/cli/bridge.rs deleted file mode 100644 index efd0eccd25..0000000000 --- a/crates/astria-cli/src/cli/bridge.rs +++ /dev/null @@ -1,22 +0,0 @@ -use clap::Subcommand; -use color_eyre::eyre; - -/// Interact with a Sequencer node -// allow: these are one-shot variants. the size doesn't matter as they are -// passed around only once. -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Subcommand)] -pub(crate) enum Command { - /// Commands for interacting with Sequencer accounts - CollectWithdrawals(crate::commands::bridge::collect::WithdrawalEvents), - SubmitWithdrawals(crate::commands::bridge::submit::WithdrawalEvents), -} - -impl Command { - pub(crate) async fn run(self) -> eyre::Result<()> { - match self { - Command::CollectWithdrawals(args) => args.run().await, - Command::SubmitWithdrawals(args) => args.run().await, - } - } -} 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.rs b/crates/astria-cli/src/commands.rs new file mode 100644 index 0000000000..14b8be3600 --- /dev/null +++ b/crates/astria-cli/src/commands.rs @@ -0,0 +1,73 @@ +use astria_core::{ + crypto::SigningKey, + primitive::v1::Address, + protocol::transaction::v1alpha1::{ + Action, + TransactionParams, + 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 { + params: TransactionParams::builder() + .nonce(nonce_res.nonce) + .chain_id(chain_id) + .build(), + actions: vec![action], + } + .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) +} 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/commands/sequencer.rs b/crates/astria-cli/src/commands/sequencer.rs deleted file mode 100644 index 46259e4763..0000000000 --- a/crates/astria-cli/src/commands/sequencer.rs +++ /dev/null @@ -1,555 +0,0 @@ -use astria_core::{ - crypto::SigningKey, - primitive::v1::{ - Address, - Bech32m, - ADDRESS_LEN, - }, - protocol::transaction::v1alpha1::{ - action::{ - Action, - BridgeLockAction, - FeeAssetChangeAction, - IbcRelayerChangeAction, - InitBridgeAccountAction, - SudoAddressChangeAction, - TransferAction, - ValidatorUpdate, - }, - TransactionParams, - UnsignedTransaction, - }, -}; -use astria_sequencer_client::{ - tendermint_rpc::endpoint::tx::Response, - Client, - HttpClient, - SequencerClientExt, -}; -use color_eyre::{ - eyre, - eyre::{ - ensure, - eyre, - Context, - }, -}; -use rand::rngs::OsRng; - -use crate::cli::sequencer::{ - BasicAccountArgs, - Bech32mAddressArgs, - BlockHeightGetArgs, - BridgeLockArgs, - FeeAssetChangeArgs, - IbcRelayerChangeArgs, - InitBridgeAccountArgs, - SudoAddressChangeArgs, - TransferArgs, - ValidatorUpdateArgs, -}; - -/// Generate a new signing key (this is also called a secret key by other implementations) -fn get_new_signing_key() -> SigningKey { - SigningKey::new(OsRng) -} - -/// Get the public key from the signing key -fn get_public_key_pretty(signing_key: &SigningKey) -> String { - let verifying_key_bytes = signing_key.verification_key().to_bytes(); - hex::encode(verifying_key_bytes) -} - -/// Get the private key from the signing key -fn get_private_key_pretty(signing_key: &SigningKey) -> String { - let secret_key_bytes = signing_key.to_bytes(); - hex::encode(secret_key_bytes) -} - -/// Get the address from the signing key -fn get_address_pretty(signing_key: &SigningKey) -> String { - hex::encode(signing_key.verification_key().address_bytes()) -} - -/// Generates a new ED25519 keypair and prints the public key, private key, and address -pub(crate) fn create_account() { - let signing_key = get_new_signing_key(); - let public_key_pretty = get_public_key_pretty(&signing_key); - let private_key_pretty = get_private_key_pretty(&signing_key); - let address_pretty = get_address_pretty(&signing_key); - - 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: {private_key_pretty:?}"); - println!("Public Key: {public_key_pretty:?}"); - println!("Address: {address_pretty:?}"); -} - -/// Gets the balance of a Sequencer account -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the balance cannot be retrieved -pub(crate) async fn get_balance(args: &BasicAccountArgs) -> eyre::Result<()> { - 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(()) -} - -// Gets the balance of a Sequencer account -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the balance cannot be retrieved -pub(crate) async fn get_nonce(args: &BasicAccountArgs) -> eyre::Result<()> { - 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(()) -} - -/// Gets the latest block height of a Sequencer node -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the latest block height cannot be retrieved -pub(crate) async fn get_block_height(args: &BlockHeightGetArgs) -> eyre::Result<()> { - let sequencer_client = HttpClient::new(args.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(()) -} - -/// Returns a bech32m sequencer address given a prefix and hex-encoded byte slice -pub(crate) fn make_bech32m(args: &Bech32mAddressArgs) -> eyre::Result<()> { - use hex::FromHex as _; - let bytes = <[u8; ADDRESS_LEN]>::from_hex(&args.bytes) - .wrap_err("failed decoding provided hex bytes")?; - let address = Address::::builder() - .array(bytes) - .prefix(&args.prefix) - .try_build() - .wrap_err( - "failed constructing a valid bech32m address from the provided hex bytes and prefix", - )?; - println!("{address}"); - Ok(()) -} - -/// Gets the latest block height of a Sequencer node -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the latest block height cannot be retrieved -pub(crate) async fn send_transfer(args: &TransferArgs) -> eyre::Result<()> { - let res = submit_transaction( - args.sequencer_url.as_str(), - args.sequencer_chain_id.clone(), - &args.prefix, - args.private_key.as_str(), - Action::Transfer(TransferAction { - to: args.to_address, - amount: args.amount, - asset: args.asset.clone(), - fee_asset: args.fee_asset.clone(), - }), - ) - .await - .wrap_err("failed to submit transfer transaction")?; - - println!("Transfer completed!"); - println!("Included in block: {}", res.height); - Ok(()) -} - -/// Adds an address to the Ibc Relayer set -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be included -pub(crate) async fn ibc_relayer_add(args: &IbcRelayerChangeArgs) -> eyre::Result<()> { - 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(()) -} - -/// Removes an address to the Ibc Relayer set -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be included -pub(crate) async fn ibc_relayer_remove(args: &IbcRelayerChangeArgs) -> eyre::Result<()> { - 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(()) -} - -/// Inits a bridge account -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be included -pub(crate) async fn init_bridge_account(args: &InitBridgeAccountArgs) -> eyre::Result<()> { - use astria_core::primitive::v1::RollupId; - - let rollup_id = RollupId::from_unhashed_bytes(args.rollup_name.as_bytes()); - let res = submit_transaction( - args.sequencer_url.as_str(), - args.sequencer_chain_id.clone(), - &args.prefix, - args.private_key.as_str(), - Action::InitBridgeAccount(InitBridgeAccountAction { - rollup_id, - asset: args.asset.clone(), - fee_asset: args.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: {}", args.rollup_name); - println!("Rollup ID: {rollup_id}"); - Ok(()) -} - -/// Bridge Lock action -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be included -pub(crate) async fn bridge_lock(args: &BridgeLockArgs) -> eyre::Result<()> { - let res = submit_transaction( - args.sequencer_url.as_str(), - args.sequencer_chain_id.clone(), - &args.prefix, - args.private_key.as_str(), - Action::BridgeLock(BridgeLockAction { - to: args.to_address, - asset: args.asset.clone(), - amount: args.amount, - fee_asset: args.fee_asset.clone(), - destination_chain_address: args.destination_chain_address.clone(), - }), - ) - .await - .wrap_err("failed to submit BridgeLock transaction")?; - - println!("BridgeLock completed!"); - println!("Included in block: {}", res.height); - Ok(()) -} - -/// Adds a fee asset -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be included -pub(crate) async fn fee_asset_add(args: &FeeAssetChangeArgs) -> eyre::Result<()> { - 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(()) -} - -/// Removes a fee asset -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be included -pub(crate) async fn fee_asset_remove(args: &FeeAssetChangeArgs) -> eyre::Result<()> { - 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(()) -} - -/// Changes the Sequencer's sudo address to a new address -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the sudo address was not changed -pub(crate) async fn sudo_address_change(args: &SudoAddressChangeArgs) -> eyre::Result<()> { - let res = submit_transaction( - args.sequencer_url.as_str(), - args.sequencer_chain_id.clone(), - &args.prefix, - args.private_key.as_str(), - Action::SudoAddressChange(SudoAddressChangeAction { - new_address: args.address, - }), - ) - .await - .wrap_err("failed to submit SudoAddressChange transaction")?; - - println!("SudoAddressChange completed!"); - println!("Included in block: {}", res.height); - Ok(()) -} - -/// Updates a validator -/// -/// # Arguments -/// -/// * `args` - The arguments passed to the command -/// -/// # Errors -/// -/// * If the http client cannot be created -/// * If the transaction failed to be submitted -pub(crate) async fn validator_update(args: &ValidatorUpdateArgs) -> eyre::Result<()> { - let verification_key = astria_core::crypto::VerificationKey::try_from( - &*hex::decode(&args.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: args.power, - verification_key, - }; - - let res = submit_transaction( - args.sequencer_url.as_str(), - args.sequencer_chain_id.clone(), - &args.prefix, - args.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(()) -} - -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 { - params: TransactionParams::builder() - .nonce(nonce_res.nonce) - .chain_id(chain_id) - .build(), - actions: vec![action], - } - .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) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_get_new_signing_key() { - // generates seed of 32 bytes - let key1 = get_new_signing_key(); - assert_eq!(key1.to_bytes().len(), 32, "Signing key is not 32 bytes"); - - // generates different values - let key2 = get_new_signing_key(); - assert_ne!( - key1.to_bytes(), - key2.to_bytes(), - "Two signing key seeds are unexpectedly equal" - ); - } - - #[test] - fn test_signing_key_is_valid() { - let key = get_new_signing_key(); - let msg = "Hello, world!"; - let signature = key.sign(msg.as_bytes()); - - let verification_key = key.verification_key(); - assert!( - verification_key.verify(&signature, msg.as_bytes()).is_ok(), - "Signature verification failed" - ); - } - - #[test] - fn test_get_public_key_pretty() { - let signing_key = get_new_signing_key(); - let public_key_pretty = get_public_key_pretty(&signing_key); - assert_eq!(public_key_pretty.len(), 64); - } - - #[test] - fn test_get_private_key_pretty() { - let signing_key = get_new_signing_key(); - let private_key_pretty = get_private_key_pretty(&signing_key); - assert_eq!(private_key_pretty.len(), 64); - } - - #[test] - fn test_get_address_pretty() { - let signing_key = get_new_signing_key(); - let address_pretty = get_address_pretty(&signing_key); - assert_eq!(address_pretty.len(), 40); - } -} diff --git a/crates/astria-cli/src/lib.rs b/crates/astria-cli/src/lib.rs index 18a4aecb56..993cf2b135 100644 --- a/crates/astria-cli/src/lib.rs +++ b/crates/astria-cli/src/lib.rs @@ -1,2 +1,41 @@ -pub mod cli; -pub mod commands; +mod bridge; +mod commands; +mod sequencer; + +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, propagate_version = true)] +pub struct Cli { + #[command(subcommand)] + command: Command, +} + +impl Cli { + /// Runs the Astria CLI. + /// + /// This is the only entry point into the Astria CLI. + 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::Args), + /// Interact with Sequencer. + Sequencer(sequencer::Args), +} 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..e02f5270a4 --- /dev/null +++ b/crates/astria-cli/src/sequencer/account.rs @@ -0,0 +1,123 @@ +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 Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + Command::Create(create) => create.run().await, + Command::Balance(balance) => balance.run().await, + Command::Nonce(nonce) => nonce.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Generates a new ED25519 keypair. + Create(CreateArgs), + /// Queries the Sequencer for the balances of an account. + Balance(BalanceArgs), + /// Queries the Sequencer for the current nonce of an account. + Nonce(NonceArgs), +} + +#[derive(Debug, clap::Args)] +struct CreateArgs; + +impl CreateArgs { + async 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 BalanceArgs { + #[command(flatten)] + inner: ArgsInner, +} + +impl BalanceArgs { + 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 NonceArgs { + #[command(flatten)] + inner: ArgsInner, +} + +impl NonceArgs { + 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..69779fa65d --- /dev/null +++ b/crates/astria-cli/src/sequencer/address.rs @@ -0,0 +1,56 @@ +use astria_core::primitive::v1::{ + Address, + Bech32m, + ADDRESS_LEN, +}; +use color_eyre::eyre::{ + self, + WrapErr as _, +}; + +#[derive(Debug, clap::Args)] +pub(super) struct Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + let Command::Bech32m(bech32m) = self.command; + bech32m.run().await + } +} + +#[derive(Debug, clap::Subcommand)] +enum Command { + /// Returns a bech32m sequencer address given a prefix and hex-encoded byte slice + Bech32m(Bech32mArgs), +} + +#[derive(Debug, clap::Args)] +struct Bech32mArgs { + /// 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 Bech32mArgs { + async 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..8d99bac8c2 --- /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 Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + let Command::Get(get) = self.command; + get.run().await + } +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Get the balance of a Sequencer account + Get(GetArgs), +} + +#[derive(clap::Args, Debug)] +struct GetArgs { + /// 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 GetArgs { + 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..1435f0977e --- /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 Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + let Command::Get(get) = self.command; + get.run().await + } +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Get the current block height of the Sequencer node + Get(GetArgs), +} + +#[derive(clap::Args, Debug)] +struct GetArgs { + /// 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 GetArgs { + 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..59f53cbf06 --- /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::commands::submit_transaction; + +#[derive(clap::Args, Debug)] +pub(super) struct Args { + /// 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 Args { + 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..fbbf6eb9cf --- /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 Args { + /// 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 Args { + 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::commands::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..2db2b1e317 --- /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 Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + Command::Account(account) => account.run().await, + Command::Address(address) => address.run().await, + Command::Balance(balance) => balance.run().await, + Command::BlockHeight(block_height) => block_height.run().await, + Command::BridgeLock(bridge_lock) => bridge_lock.run().await, + Command::InitBridgeAccount(init_bridge_account) => init_bridge_account.run().await, + Command::Sudo(sudo) => sudo.run().await, + Command::Transfer(transfer) => transfer.run().await, + } + } +} + +/// Interact with a Sequencer node +#[derive(Debug, Subcommand)] +enum Command { + /// Commands for interacting with Sequencer accounts + Account(account::Args), + /// Utilities for constructing and inspecting sequencer addresses + Address(address::Args), + /// Commands for interacting with Sequencer balances + Balance(balance::Args), + /// Commands for interacting with Sequencer block heights + #[command(name = "blockheight")] + BlockHeight(block_height::Args), + /// Command for transferring to a bridge account + BridgeLock(bridge_lock::Args), + /// Command for initializing a bridge account + InitBridgeAccount(init_bridge_account::Args), + /// Commands requiring authority for Sequencer + Sudo(sudo::Args), + /// Command for sending balance between accounts + Transfer(transfer::Args), +} 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..d2d9f1d431 --- /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::commands::submit_transaction; + +#[derive(Debug, clap::Args)] +pub(super) struct Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + Command::Add(add) => add.run().await, + Command::Remove(remove) => remove.run().await, + } + } +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Add Fee Asset + Add(AddArgs), + /// Remove Fee Asset + Remove(RemoveArgs), +} + +#[derive(Clone, Debug, clap::Args)] +struct AddArgs { + #[command(flatten)] + inner: ArgsInner, +} + +impl AddArgs { + 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 RemoveArgs { + #[command(flatten)] + inner: ArgsInner, +} + +impl RemoveArgs { + 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..a5dcd9036b --- /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::commands::submit_transaction; + +#[derive(Debug, clap::Args)] +pub(super) struct Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + Command::Add(add) => add.run().await, + Command::Remove(remove) => remove.run().await, + } + } +} + +#[derive(Debug, clap::Subcommand)] +enum Command { + Add(AddArgs), + Remove(RemoveArgs), +} + +#[derive(Debug, clap::Args)] +struct AddArgs { + #[command(flatten)] + inner: ArgsInner, +} + +impl AddArgs { + 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 RemoveArgs { + #[command(flatten)] + inner: ArgsInner, +} + +impl RemoveArgs { + 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..368766bc45 --- /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 Args { + #[command(subcommand)] + command: Command, +} + +impl Args { + pub(super) async fn run(self) -> eyre::Result<()> { + match self.command { + Command::IbcRelayer(ibc_relayer) => ibc_relayer.run().await, + Command::FeeAsset(fee_asset) => fee_asset.run().await, + Command::SudoAddressChange(sudo_address_change) => sudo_address_change.run().await, + Command::ValidatorUpdate(validator_update) => validator_update.run().await, + } + } +} + +#[derive(Debug, clap::Subcommand)] +enum Command { + IbcRelayer(ibc_relayer::Args), + FeeAsset(fee_asset::Args), + SudoAddressChange(sudo_address_change::Args), + ValidatorUpdate(validator_update::Args), +} 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..79b59e2812 --- /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::commands::submit_transaction; + +#[derive(Debug, clap::Args)] +pub(super) struct Args { + /// 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 Args { + 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..4662615c23 --- /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::commands::submit_transaction; + +#[derive(clap::Args, Debug)] +pub(super) struct Args { + /// 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 Args { + 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..e38f70e912 --- /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::commands::submit_transaction; + +#[derive(clap::Args, Debug)] +pub(super) struct Args { + // 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 Args { + 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(()) + } +}