diff --git a/Cargo.lock b/Cargo.lock index f1cf124250..d6b5e0ae66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,6 +571,7 @@ dependencies = [ "ethers", "futures", "hex", + "home", "humantime", "ibc-types", "rand 0.8.5", @@ -580,6 +581,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..721f565070 100644 --- a/crates/astria-cli/Cargo.toml +++ b/crates/astria-cli/Cargo.toml @@ -13,11 +13,13 @@ name = "astria-cli" [dependencies] color-eyre = "0.6" +home = "0.5" +toml = "0.7" astria-bridge-contracts = { path = "../astria-bridge-contracts" } astria-core = { path = "../astria-core", features = ["serde"] } -clap = { workspace = true, features = ["derive", "env"] } +clap = { workspace = true, features = ["derive", "env", "string"] } ethers = { workspace = true, features = ["ws"] } hex = { workspace = true } ibc-types = { workspace = true } diff --git a/crates/astria-cli/src/config.rs b/crates/astria-cli/src/config.rs new file mode 100644 index 0000000000..e7fcd391a9 --- /dev/null +++ b/crates/astria-cli/src/config.rs @@ -0,0 +1,50 @@ +use std::{ + collections::HashMap, + fs, + path::Path, +}; + +use color_eyre::eyre; +use serde::{ + Deserialize, + Serialize, +}; +use toml; + +pub const DEFAULT_SEQUENCER_URL: &str = "https://rpc.sequencer.dawn-0.astria.org"; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NetworkConfig { + pub sequencer_chain_id: String, + pub sequencer_url: String, + pub asset: String, + pub fee_asset: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SequencerNetworksConfig { + networks: HashMap, +} + +impl SequencerNetworksConfig { + /// Load the config from a file + pub fn load>(path: P) -> eyre::Result { + let toml_str = fs::read_to_string(path)?; + let config: SequencerNetworksConfig = toml::from_str(&toml_str)?; + return Ok(config); + } + + /// Get the network config for the selected network + pub fn get_network(&self, network: &String) -> eyre::Result<&NetworkConfig> { + if let Some(network_config) = self.networks.get(network) { + Ok(network_config) + } else { + let keys = self.networks.keys().collect::>(); + Err(eyre::eyre!( + "'{}' not found: Expected one of the following: {:?}", + network, + keys + )) + } + } +} diff --git a/crates/astria-cli/src/lib.rs b/crates/astria-cli/src/lib.rs index 18a4aecb56..3a441e45bd 100644 --- a/crates/astria-cli/src/lib.rs +++ b/crates/astria-cli/src/lib.rs @@ -1,2 +1,3 @@ pub mod cli; pub mod commands; +pub mod config; diff --git a/crates/astria-cli/src/main.rs b/crates/astria-cli/src/main.rs index c587580795..e43f5bcb33 100644 --- a/crates/astria-cli/src/main.rs +++ b/crates/astria-cli/src/main.rs @@ -4,8 +4,11 @@ use astria_cli::{ cli::Cli, commands, }; +use clap::Command; use color_eyre::eyre; +mod query; + #[tokio::main] async fn main() -> ExitCode { tracing_subscriber::fmt() @@ -13,11 +16,24 @@ async fn main() -> ExitCode { .with_writer(std::io::stderr) .init(); - if let Err(err) = run().await { - eprintln!("{err:?}"); - return ExitCode::FAILURE; + let matches = Command::new("astria-cli") + .subcommand(query::command()) + // .subcommand(command2::command(&config)) + .get_matches(); + + match matches.subcommand() { + Some(("query", args)) => query::run(args).await.expect("Could not run query command"), + // Some(("command2", sub_matches)) => command2::run(sub_matches, &config), + _ => { + return ExitCode::FAILURE; + } } + // if let Err(err) = run().await { + // eprintln!("{err:?}"); + // return ExitCode::FAILURE; + // } + ExitCode::SUCCESS } diff --git a/crates/astria-cli/src/query/blockheight.rs b/crates/astria-cli/src/query/blockheight.rs new file mode 100644 index 0000000000..243167e7c5 --- /dev/null +++ b/crates/astria-cli/src/query/blockheight.rs @@ -0,0 +1,109 @@ +use astria_cli::config::{ + SequencerNetworksConfig, + DEFAULT_SEQUENCER_URL, +}; +use astria_sequencer_client::{ + Client, + HttpClient, +}; +use clap::{ + builder::Str, + Arg, + ArgAction, + ArgMatches, + Command, +}; +use color_eyre::{ + eyre, + eyre::Context, +}; +use home::home_dir; + +pub(crate) fn command() -> Command { + let mut path = home_dir().expect("Could not determine the home directory."); + path.push(".astria"); + path.push("sequencer-networks-config.toml"); + + Command::new("blockheight") + .about("Get the current blockheight from the sequencer") + .arg( + // flag input + Arg::new("sequencer-url") + .long("sequencer-url") + .help("URL of the sequencer") + .action(ArgAction::Set) + .default_value(DEFAULT_SEQUENCER_URL) + .env("SEQUENCER_URL"), + ) + .arg( + // count bool flag + Arg::new("verbose") + .short('v') + .long("verbose") + .action(ArgAction::Count) + .help("Print debug information verbosely"), + ) + .arg( + // flag input + Arg::new("network") + .long("network") + .action(ArgAction::Set) + .help("Select a network config preset"), + ) + .arg( + // flag input + Arg::new("config") + .long("config") + .action(ArgAction::Set) + .help("Specify a network config file") + .default_value(Str::from(path.display().to_string())), + ) +} + +pub(crate) async fn run(matches: &ArgMatches) -> eyre::Result<()> { + // load and parse the config file + let config: SequencerNetworksConfig = { + let config_path = matches.get_one::("config"); + if let Some(path) = config_path { + SequencerNetworksConfig::load(path).expect("Could not load config file") + } else { + let mut path = home_dir().expect("Could not determine the home directory."); + path.push(".astria"); + path.push("sequencer-networks-config.toml"); + SequencerNetworksConfig::load(path).expect("Could not load config file") + } + }; + + // get verbosity cound (currently unused) + let verbose = matches.get_count("verbose"); + println!("verbose count: {:?}", verbose); + + // get the chosen network config + let network = matches.get_one::("network"); + println!("network: {:?}", network); + + // get the correct sequencer_url based on all inputs + let sequenecer_url = if let Some(chosen_network) = network { + let net_config = config + .get_network(chosen_network) + .expect("network not found"); + net_config.sequencer_url.clone() + } else { + let seq_url = matches.get_one::("sequencer-url"); + seq_url.unwrap().clone() + }; + + // submit the query to the sequencer + let sequencer_client = HttpClient::new(sequenecer_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/query/mod.rs b/crates/astria-cli/src/query/mod.rs new file mode 100644 index 0000000000..772f44ea4e --- /dev/null +++ b/crates/astria-cli/src/query/mod.rs @@ -0,0 +1,24 @@ +mod blockheight; +mod nonce; + +use clap::{ + ArgMatches, + Command, +}; +use color_eyre::eyre; + +pub(crate) fn command() -> Command { + Command::new("query") + .about("Query the sequencer") + .subcommand(blockheight::command()) + .subcommand(nonce::command()) +} + +pub(crate) async fn run(matches: &ArgMatches) -> eyre::Result<()> { + match matches.subcommand() { + Some(("blockheight", args)) => blockheight::run(args).await, + Some(("nonce", args)) => nonce::run(args).await, + // Some(("subcommand2", sub_matches)) => subcommand2::run(sub_matches, config), + _ => Err(eyre::eyre!("Unknown subcommand")), + } +} diff --git a/crates/astria-cli/src/query/nonce.rs b/crates/astria-cli/src/query/nonce.rs new file mode 100644 index 0000000000..79d1ece7b0 --- /dev/null +++ b/crates/astria-cli/src/query/nonce.rs @@ -0,0 +1,125 @@ +use astria_cli::config::{ + SequencerNetworksConfig, + DEFAULT_SEQUENCER_URL, +}; +use astria_core::primitive::v1::Address; +use astria_sequencer_client::{ + HttpClient, + SequencerClientExt, +}; +use clap::{ + builder::Str, + Arg, + ArgAction, + ArgMatches, + Command, +}; +use color_eyre::{ + eyre, + eyre::Context, +}; +use home::home_dir; + +pub(crate) fn command() -> Command { + // Create default 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"); + + Command::new("nonce") + .about("Get the nonce of an account") + .arg( + // flag input + Arg::new("sequencer-url") + .long("sequencer-url") + .help("URL of the sequencer") + .action(ArgAction::Set) + .default_value(DEFAULT_SEQUENCER_URL) + .env("SEQUENCER_URL"), + ) + .arg( + // count bool flag + Arg::new("verbose") + .short('v') + .long("verbose") + .action(ArgAction::Count) + .help("Print debug information verbosely"), + ) + .arg( + // flag input + Arg::new("network") + .long("network") + .action(ArgAction::Set) + .help("Select a network config preset"), + ) + .arg( + // flag input + Arg::new("config") + .long("config") + .action(ArgAction::Set) + .help("Specify a network config file") + .default_value(Str::from(path.display().to_string())), + ) + .arg( + // postional argument + Arg::new("address") + .action(ArgAction::Set) + .help("Specify a network config file") + .required(true), + ) +} + +pub(crate) async fn run(matches: &ArgMatches) -> eyre::Result<()> { + // load and parse the config file + let config: SequencerNetworksConfig = { + let config_path = matches.get_one::("config"); + if let Some(path) = config_path { + SequencerNetworksConfig::load(path).expect("Could not load config file") + } else { + let mut path = home_dir().expect("Could not determine the home directory."); + path.push(".astria"); + path.push("sequencer-networks-config.toml"); + SequencerNetworksConfig::load(path).expect("Could not load config file") + } + }; + + // get verbosity cound (currently unused) + let verbose = matches.get_count("verbose"); + println!("verbose count: {:?}", verbose); + + // get the chosen network config + let network = matches.get_one::("network"); + println!("network: {:?}", network); + + // parse the input address + let address = matches + .get_one::("address") + .expect("could not unwrap address"); + println!("network: {:?}", address); + let address: Address = address.as_str().parse()?; + + // get the correct sequencer_url based on all inputs + let sequenecer_url = if let Some(chosen_network) = network { + let net_config = config + .get_network(chosen_network) + .expect("network not found"); + net_config.sequencer_url.clone() + } else { + let seq_url = matches.get_one::("sequencer-url"); + seq_url.unwrap().clone() + }; + + // submit the query to the sequencer + let sequencer_client = HttpClient::new(sequenecer_url.as_str()) + .wrap_err("failed constructing http sequencer client")?; + + let res = sequencer_client + .get_latest_nonce(address) + .await + .wrap_err("failed to get nonce")?; + + println!("Nonce for address {}", address); + println!(" {} at height {}", res.nonce, res.height); + + Ok(()) +} 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}}