diff --git a/Cargo.lock b/Cargo.lock index f1cf124250..4a260d3d86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,11 +568,14 @@ dependencies = [ "astria-sequencer-client", "clap", "color-eyre", + "env_logger", "ethers", "futures", "hex", + "home", "humantime", "ibc-types", + "log", "rand 0.8.5", "serde", "serde_json", @@ -580,6 +583,7 @@ dependencies = [ "sha2 0.10.8", "tendermint", "tokio", + "toml 0.7.8", "tracing", "tracing-subscriber 0.3.18", "tryhard", diff --git a/crates/astria-cli/Cargo.toml b/crates/astria-cli/Cargo.toml index 107b3f576e..a88e4d10ba 100644 --- a/crates/astria-cli/Cargo.toml +++ b/crates/astria-cli/Cargo.toml @@ -12,6 +12,10 @@ homepage = "https://astria.org" name = "astria-cli" [dependencies] +home = "0.5" +toml = "0.7" +env_logger = "0.10" +log = "0.4" color-eyre = "0.6" astria-bridge-contracts = { path = "../astria-bridge-contracts" } diff --git a/crates/astria-cli/src/cli/config.rs b/crates/astria-cli/src/cli/config.rs new file mode 100644 index 0000000000..2d7164b581 --- /dev/null +++ b/crates/astria-cli/src/cli/config.rs @@ -0,0 +1,86 @@ +use std::{ + collections::HashMap, + fs, +}; + +use color_eyre::eyre; +use home::home_dir; +use serde::{ + Deserialize, + Serialize, +}; +use toml; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NetworkConfig { + pub sequencer_chain_id: String, + pub sequencer_url: String, + pub asset: String, + pub fee_asset: String, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + sequencer_chain_id: "astria-dusk-10".to_string(), + sequencer_url: "https://rpc.sequencer.dusk-10.devnet.astria.org".to_string(), + asset: "TIA".to_string(), + fee_asset: "TIA".to_string(), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct Config { + networks: HashMap, +} +impl Config { + /// Validate if the parsed input for choosing a network config exists and is valid + pub(crate) fn validate_network(&self, network: String) -> bool { + for (network_name, _) in &self.networks { + if *network_name == network { + return true; + } + } + false + } + + /// Get a list of valid network names from the config + pub(crate) fn get_valid_networks(&self) -> Vec { + let mut valid_networks = Vec::new(); + for (network_name, _) in &self.networks { + valid_networks.push(network_name.clone()); + } + valid_networks + } + + /// Get the network config for the selected network + pub(crate) fn get_network(&self, network: String) -> Option<&NetworkConfig> { + self.networks.get(&network) + } +} + +pub(crate) fn get_networks_config() -> eyre::Result { + // try to get the home directory and build the path to the config file + let mut path = home_dir().expect("Could not determine the home directory."); + path.push(".astria"); + path.push("sequencer-networks-config.toml"); + + // Read the TOML file + let toml_str = fs::read_to_string(path)?; + let config: Config = toml::from_str(&toml_str)?; + + // for (network_name, network_config) in &config.networks { + // println!("Network: {}", network_name); + // println!( + // " Sequencer Chain ID: {}", + // network_config.sequencer_chain_id + // ); + // println!(" Sequencer URL: {}", network_config.sequencer_url); + // println!(" Asset: {}", network_config.asset); + // println!(" Fee Asset: {}", network_config.fee_asset); + // println!(); + // } + + Ok(config) +} diff --git a/crates/astria-cli/src/cli/mod.rs b/crates/astria-cli/src/cli/mod.rs index 9b6af0119a..a4466291d1 100644 --- a/crates/astria-cli/src/cli/mod.rs +++ b/crates/astria-cli/src/cli/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod bridge; +pub(crate) mod config; pub(crate) mod sequencer; use clap::{ @@ -7,17 +8,32 @@ use clap::{ }; 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"; +use super::*; +use crate::cli::{ + config::{ + Config, + NetworkConfig, + }, + sequencer::Command as SequencerCommand, +}; /// A CLI for deploying and managing Astria services and related infrastructure. #[derive(Debug, Parser)] #[command(name = "astria-cli", version)] pub struct Cli { + /// Sets the log level (e.g. error, warn, info, debug, trace) + #[arg(short, long, default_value = "info")] + pub(crate) log_level: String, + + /// Select the network you want to use + #[arg(short, long, default_value = "dawn")] + pub network: String, + #[command(subcommand)] pub(crate) command: Option, + + #[clap(skip)] + pub(crate) network_config: Option, } impl Cli { @@ -27,9 +43,31 @@ impl Cli { /// /// * If the arguments cannot be parsed pub fn get_args() -> eyre::Result { - let args = Self::parse(); + let mut args = Self::parse(); + + let config: Config = config::get_networks_config()?; + + // Validate the selected network name + if config.validate_network(args.network.clone()) { + println!("network: {:?}", args.network); + if let Some(network_config) = config.get_network(args.network.clone()) { + args.set_network_config(network_config.clone()); + } else { + println!("Network config not found"); + } + } else { + println!( + "Network is not valid. Expected one of: {:?}", + config.get_valid_networks() + ); + } + Ok(args) } + + pub fn set_network_config(&mut self, network_config: NetworkConfig) { + self.network_config = Some(network_config); + } } /// Commands that can be run diff --git a/crates/astria-cli/src/commands/mod.rs b/crates/astria-cli/src/commands/mod.rs index 436d5c74fc..8955b6c7db 100644 --- a/crates/astria-cli/src/commands/mod.rs +++ b/crates/astria-cli/src/commands/mod.rs @@ -7,6 +7,7 @@ use color_eyre::{ }; use crate::cli::{ + config::NetworkConfig, sequencer::{ AccountCommand, AddressCommand, @@ -48,7 +49,14 @@ pub async fn run(cli: Cli) -> eyre::Result<()> { } => match command { AccountCommand::Create => sequencer::create_account(), AccountCommand::Balance(args) => sequencer::get_balance(&args).await?, - AccountCommand::Nonce(args) => sequencer::get_nonce(&args).await?, + AccountCommand::Nonce(args) => { + if let Some(network) = &cli.network_config { + sequencer::get_nonce(&args, network.clone()).await? + } else { + let network = NetworkConfig::default(); + sequencer::get_nonce(&args, network).await? + } + } }, SequencerCommand::Address { command, diff --git a/crates/astria-cli/src/commands/sequencer.rs b/crates/astria-cli/src/commands/sequencer.rs index 46259e4763..54d8d1775f 100644 --- a/crates/astria-cli/src/commands/sequencer.rs +++ b/crates/astria-cli/src/commands/sequencer.rs @@ -1,3 +1,5 @@ +use std::env; + use astria_core::{ crypto::SigningKey, primitive::v1::{ @@ -36,17 +38,24 @@ use color_eyre::{ }; use rand::rngs::OsRng; -use crate::cli::sequencer::{ - BasicAccountArgs, - Bech32mAddressArgs, - BlockHeightGetArgs, - BridgeLockArgs, - FeeAssetChangeArgs, - IbcRelayerChangeArgs, - InitBridgeAccountArgs, - SudoAddressChangeArgs, - TransferArgs, - ValidatorUpdateArgs, +// use serde::de; +use crate::{ + cli::{ + config::NetworkConfig, + sequencer::{ + BasicAccountArgs, + Bech32mAddressArgs, + BlockHeightGetArgs, + BridgeLockArgs, + FeeAssetChangeArgs, + IbcRelayerChangeArgs, + InitBridgeAccountArgs, + SudoAddressChangeArgs, + TransferArgs, + ValidatorUpdateArgs, + }, + }, + DEFAULT_SEQUENCER_RPC, }; /// Generate a new signing key (this is also called a secret key by other implementations) @@ -123,9 +132,43 @@ pub(crate) async fn get_balance(args: &BasicAccountArgs) -> eyre::Result<()> { /// /// * 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")?; +pub(crate) async fn get_nonce(args: &BasicAccountArgs, network: NetworkConfig) -> eyre::Result<()> { + println!("Default value: {:?}", DEFAULT_SEQUENCER_RPC); + println!("Argument value: {:?}", network.sequencer_url); + + let mut seq_url = String::from(DEFAULT_SEQUENCER_RPC); + + let default = String::from(DEFAULT_SEQUENCER_RPC); + + // Check if the environment variable is set + let env_var = env::var("SEQUENCER_URL") + .ok() + .unwrap_or_else(|| default.clone()); + println!("Environment variable: {:?}", env_var); + + // Compare the input value with the default and environment variable + // this will fail if the user uses the default value as a flag argument + if args.sequencer_url != DEFAULT_SEQUENCER_RPC { + println!("Using command line value: {}", DEFAULT_SEQUENCER_RPC); + seq_url = args.sequencer_url.clone(); + } else if env_var != default.as_str() { + println!( + "Argument is using the value from the environment variable: {}", + args.sequencer_url + ); + seq_url = env_var; + } else if network.sequencer_url != DEFAULT_SEQUENCER_RPC { + println!("Using network config value: {}", network.sequencer_url); + seq_url = network.sequencer_url.clone(); + } else { + println!( + "Argument was explicitly set by the user: {}", + args.sequencer_url + ); + } + + let sequencer_client = + HttpClient::new(seq_url.as_str()).wrap_err("failed constructing http sequencer client")?; let res = sequencer_client .get_latest_nonce(args.address) diff --git a/crates/astria-cli/src/lib.rs b/crates/astria-cli/src/lib.rs index 18a4aecb56..1872248a94 100644 --- a/crates/astria-cli/src/lib.rs +++ b/crates/astria-cli/src/lib.rs @@ -1,2 +1,5 @@ pub mod cli; pub mod commands; + +pub const DEFAULT_SEQUENCER_RPC: &str = "https://rpc.sequencer.dusk-10.devnet.astria.org"; +pub const DEFAULT_SEQUENCER_CHAIN_ID: &str = "astria-dusk-10"; diff --git a/crates/astria-cli/src/main.rs b/crates/astria-cli/src/main.rs index c587580795..20119777dd 100644 --- a/crates/astria-cli/src/main.rs +++ b/crates/astria-cli/src/main.rs @@ -22,6 +22,8 @@ async fn main() -> ExitCode { } async fn run() -> eyre::Result<()> { + // Parse the TOML string into our Config struct let args = Cli::get_args()?; + commands::run(args).await } diff --git a/justfile b/justfile index ceeff571c7..c41bcd9957 100644 --- a/justfile +++ b/justfile @@ -87,3 +87,7 @@ _lint-proto: buf breaking proto/sequencerblockapis --against 'buf.build/astria/sequencerblock-apis' buf breaking proto/protocolapis --against 'buf.build/astria/protocol-apis' buf breaking proto/composerapis --against 'buf.build/astria/composer-apis' + +defaultargs := '' +cli *args=defaultargs: + cargo run -p astria-cli -- {{args}}