diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 8c151cb84..aa2c8e865 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -45,6 +45,7 @@ Anything after the `--` double dash (the "slop") is parsed as arguments to the c * `contract` — Tools for smart contract developers * `events` — Watch the network for contract events +* `env` — Prints the current environment variables or defaults to the stdout, in a format that can be used as .env file. Environment variables have precedency over defaults * `keys` — Create and manage identities including keys and addresses * `network` — Start and configure networks * `snapshot` — Download a snapshot of a ledger from an archive @@ -897,6 +898,19 @@ Watch the network for contract events +## `stellar env` + +Prints the current environment variables or defaults to the stdout, in a format that can be used as .env file. Environment variables have precedency over defaults + +**Usage:** `stellar env [OPTIONS]` + +###### **Options:** + +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." + + + ## `stellar keys` Create and manage identities including keys and addresses @@ -912,6 +926,7 @@ Create and manage identities including keys and addresses * `ls` — List identities * `rm` — Remove an identity * `show` — Given an identity return its private key +* `use` — Set the default identity that will be used on all commands. This allows you to skip `--source-account` or setting a environment variable, while reusing this value in all commands that require it @@ -1052,6 +1067,23 @@ Given an identity return its private key +## `stellar keys use` + +Set the default identity that will be used on all commands. This allows you to skip `--source-account` or setting a environment variable, while reusing this value in all commands that require it + +**Usage:** `stellar keys use [OPTIONS] ` + +###### **Arguments:** + +* `` — Set the default network name + +###### **Options:** + +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." + + + ## `stellar network` Start and configure networks @@ -1065,6 +1097,7 @@ Start and configure networks * `ls` — List networks * `start` — ⚠️ Deprecated: use `stellar container start` instead * `stop` — ⚠️ Deprecated: use `stellar container stop` instead +* `use` — Set the default network that will be used on all commands. This allows you to skip `--network` or setting a environment variable, while reusing this value in all commands that require it * `container` — Commands to start, stop and get logs for a quickstart container @@ -1174,6 +1207,23 @@ Stop a network started with `network start`. For example, if you ran `stellar ne +## `stellar network use` + +Set the default network that will be used on all commands. This allows you to skip `--network` or setting a environment variable, while reusing this value in all commands that require it + +**Usage:** `stellar network use [OPTIONS] ` + +###### **Arguments:** + +* `` — Set the default network name + +###### **Options:** + +* `--global` — Use global config +* `--config-dir ` — Location of config directory, default is "." + + + ## `stellar network container` Commands to start, stop and get logs for a quickstart container diff --git a/cmd/crates/soroban-test/tests/it/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index 31d60e116..b796910a8 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -340,3 +340,56 @@ fn config_dirs_precedence() { )) .stdout("SAQMV6P3OWM2SKCK3OEWNXSRYWK5RNNUL5CPHQGIJF2WVT4EI2BZ63GG\n"); } + +#[test] +fn set_default_identity() { + let sandbox = TestEnv::default(); + + sandbox + .new_assert_cmd("keys") + .env( + "SOROBAN_SECRET_KEY", + "SC4ZPYELVR7S7EE7KZDZN3ETFTNQHHLTUL34NUAAWZG5OK2RGJ4V2U3Z", + ) + .arg("add") + .arg("alice") + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("use") + .arg("alice") + .assert() + .stderr(predicate::str::contains( + "The default source account is set to `alice`", + )) + .success(); + + sandbox + .new_assert_cmd("env") + .assert() + .stdout(predicate::str::contains("STELLAR_ACCOUNT=alice")) + .success(); +} + +#[test] +fn set_default_network() { + let sandbox = TestEnv::default(); + + sandbox + .new_assert_cmd("network") + .arg("use") + .arg("testnet") + .assert() + .stderr(predicate::str::contains( + "The default network is set to `testnet`", + )) + .success(); + + sandbox + .new_assert_cmd("env") + .assert() + .stdout(predicate::str::contains("STELLAR_NETWORK=testnet")) + .success(); +} diff --git a/cmd/soroban-cli/src/commands/env/mod.rs b/cmd/soroban-cli/src/commands/env/mod.rs new file mode 100644 index 000000000..4d745400c --- /dev/null +++ b/cmd/soroban-cli/src/commands/env/mod.rs @@ -0,0 +1,64 @@ +use crate::{ + commands::global, + config::{ + locator::{self}, + Config, + }, + print::Print, +}; +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Cmd { + #[command(flatten)] + pub config_locator: locator::Args, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + let config = Config::new()?; + let mut lines: Vec<(String, String)> = Vec::new(); + + if let Some(data) = get("STELLAR_NETWORK", config.defaults.network) { + lines.push(data); + } + + if let Some(data) = get("STELLAR_ACCOUNT", config.defaults.identity) { + lines.push(data); + } + + if lines.is_empty() { + print.warnln("No defaults or environment variables set".to_string()); + return Ok(()); + } + + let max_len = lines.iter().map(|l| l.0.len()).max().unwrap_or(0); + + lines.sort(); + + for (value, source) in lines { + println!("{value:max_len$} # {source}"); + } + + Ok(()) + } +} + +fn get(env_var: &str, default_value: Option) -> Option<(String, String)> { + if let Ok(value) = std::env::var(env_var) { + return Some((format!("{env_var}={value}"), "env".to_string())); + } + + if let Some(value) = default_value { + return Some((format!("{env_var}={value}"), "default".to_string())); + } + + None +} diff --git a/cmd/soroban-cli/src/commands/keys/default.rs b/cmd/soroban-cli/src/commands/keys/default.rs new file mode 100644 index 000000000..9aa180b6d --- /dev/null +++ b/cmd/soroban-cli/src/commands/keys/default.rs @@ -0,0 +1,35 @@ +use clap::command; + +use crate::{commands::global, config::locator, print::Print}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// Set the default network name. + pub name: String, + + #[command(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let printer = Print::new(global_args.quiet); + let _ = self.config_locator.read_identity(&self.name)?; + + self.config_locator.write_default_identity(&self.name)?; + + printer.infoln(format!( + "The default source account is set to `{}`", + self.name, + )); + + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/keys/mod.rs b/cmd/soroban-cli/src/commands/keys/mod.rs index 30df5ccee..8729ee9af 100644 --- a/cmd/soroban-cli/src/commands/keys/mod.rs +++ b/cmd/soroban-cli/src/commands/keys/mod.rs @@ -3,6 +3,7 @@ use clap::Parser; pub mod add; pub mod address; +pub mod default; pub mod fund; pub mod generate; pub mod ls; @@ -13,18 +14,30 @@ pub mod show; pub enum Cmd { /// Add a new identity (keypair, ledger, macOS keychain) Add(add::Cmd), + /// Given an identity return its address (public key) Address(address::Cmd), + /// Fund an identity on a test network Fund(fund::Cmd), + /// Generate a new identity with a seed phrase, currently 12 words Generate(generate::Cmd), + /// List identities Ls(ls::Cmd), + /// Remove an identity Rm(rm::Cmd), + /// Given an identity return its private key Show(show::Cmd), + + /// Set the default identity that will be used on all commands. + /// This allows you to skip `--source-account` or setting a environment + /// variable, while reusing this value in all commands that require it. + #[command(name = "use")] + Default(default::Cmd), } #[derive(thiserror::Error, Debug)] @@ -34,18 +47,24 @@ pub enum Error { #[error(transparent)] Address(#[from] address::Error), + #[error(transparent)] Fund(#[from] fund::Error), #[error(transparent)] Generate(#[from] generate::Error), + #[error(transparent)] Rm(#[from] rm::Error), + #[error(transparent)] Ls(#[from] ls::Error), #[error(transparent)] Show(#[from] show::Error), + + #[error(transparent)] + Default(#[from] default::Error), } impl Cmd { @@ -58,6 +77,7 @@ impl Cmd { Cmd::Ls(cmd) => cmd.run()?, Cmd::Rm(cmd) => cmd.run()?, Cmd::Show(cmd) => cmd.run()?, + Cmd::Default(cmd) => cmd.run(global_args)?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index a6a85e816..f1febae27 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -8,6 +8,7 @@ use crate::config; pub mod cache; pub mod completion; pub mod contract; +pub mod env; pub mod events; pub mod global; pub mod keys; @@ -116,7 +117,8 @@ impl Root { Cmd::Version(version) => version.run(), Cmd::Keys(id) => id.run(&self.global_args).await?, Cmd::Tx(tx) => tx.run(&self.global_args).await?, - Cmd::Cache(data) => data.run()?, + Cmd::Cache(cache) => cache.run()?, + Cmd::Env(env) => env.run(&self.global_args)?, }; Ok(()) } @@ -135,9 +137,15 @@ pub enum Cmd { /// Tools for smart contract developers #[command(subcommand)] Contract(contract::Cmd), + /// Watch the network for contract events Events(events::Cmd), + /// Prints the current environment variables or defaults to the stdout, in + /// a format that can be used as .env file. Environment variables have + /// precedency over defaults. + Env(env::Cmd), + /// Create and manage identities including keys and addresses #[command(subcommand)] Keys(keys::Cmd), @@ -160,9 +168,11 @@ pub enum Cmd { /// Print shell completion code for the specified shell. #[command(long_about = completion::LONG_ABOUT)] Completion(completion::Cmd), + /// Cache for transactions and contract specs #[command(subcommand)] Cache(cache::Cmd), + /// Print version information Version(version::Cmd), } @@ -172,24 +182,36 @@ pub enum Error { // TODO: stop using Debug for displaying errors #[error(transparent)] Contract(#[from] contract::Error), + #[error(transparent)] Events(#[from] events::Error), + #[error(transparent)] Keys(#[from] keys::Error), + #[error(transparent)] Xdr(#[from] stellar_xdr::cli::Error), + #[error(transparent)] Clap(#[from] clap::error::Error), + #[error(transparent)] Plugin(#[from] plugin::Error), + #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] Snapshot(#[from] snapshot::Error), + #[error(transparent)] Tx(#[from] tx::Error), + #[error(transparent)] Cache(#[from] cache::Error), + + #[error(transparent)] + Env(#[from] env::Error), } #[async_trait] diff --git a/cmd/soroban-cli/src/commands/network/default.rs b/cmd/soroban-cli/src/commands/network/default.rs new file mode 100644 index 000000000..337e08a69 --- /dev/null +++ b/cmd/soroban-cli/src/commands/network/default.rs @@ -0,0 +1,34 @@ +use clap::command; + +use crate::{commands::global, print::Print}; + +use super::locator; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), +} + +#[derive(Debug, clap::Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// Set the default network name. + pub name: String, + + #[command(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let printer = Print::new(global_args.quiet); + let _ = self.config_locator.read_network(&self.name)?; + + self.config_locator.write_default_network(&self.name)?; + + printer.infoln(format!("The default network is set to `{}`", self.name)); + + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 8dd61b394..a7519bc83 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -6,6 +6,7 @@ use super::{config::locator, global}; pub mod add; pub mod container; +pub mod default; pub mod ls; pub mod rm; @@ -13,10 +14,13 @@ pub mod rm; pub enum Cmd { /// Add a new network Add(add::Cmd), + /// Remove a network Rm(rm::Cmd), + /// List networks Ls(ls::Cmd), + /// ⚠️ Deprecated: use `stellar container start` instead /// /// Start network @@ -29,11 +33,18 @@ pub enum Cmd { /// /// `docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable rpc,horizon` Start(container::StartCmd), + /// ⚠️ Deprecated: use `stellar container stop` instead /// /// Stop a network started with `network start`. For example, if you ran `stellar network start local`, you can use `stellar network stop local` to stop it. Stop(container::StopCmd), + /// Set the default network that will be used on all commands. + /// This allows you to skip `--network` or setting a environment variable, + /// while reusing this value in all commands that require it. + #[command(name = "use")] + Default(default::Cmd), + /// Commands to start, stop and get logs for a quickstart container #[command(subcommand)] Container(container::Cmd), @@ -41,6 +52,9 @@ pub enum Cmd { #[derive(thiserror::Error, Debug)] pub enum Error { + #[error(transparent)] + Default(#[from] default::Error), + #[error(transparent)] Add(#[from] add::Error), @@ -81,6 +95,7 @@ pub enum Error { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { + Cmd::Default(cmd) => cmd.run(global_args)?, Cmd::Add(cmd) => cmd.run()?, Cmd::Rm(new) => new.run()?, Cmd::Ls(cmd) => cmd.run()?, diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index 0a13c4cc3..60b7856a6 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -5,8 +5,7 @@ use std::{ ffi::OsStr, fmt::Display, fs::{self, create_dir_all, OpenOptions}, - io, - io::Write, + io::{self, Write}, path::{Path, PathBuf}, str::FromStr, }; @@ -18,10 +17,13 @@ use super::{ alias, network::{self, Network}, secret::Secret, + Config, }; #[derive(thiserror::Error, Debug)] pub enum Error { + #[error(transparent)] + TomlSerialize(#[from] toml::ser::Error), #[error("Failed to find home directory")] HomeDirNotFound, #[error("Failed read current directory")] @@ -34,6 +36,8 @@ pub enum Error { SecretFileRead { path: PathBuf }, #[error("Failed to read network file: {path};\nProbably need to use `stellar network add`")] NetworkFileRead { path: PathBuf }, + #[error("Failed to read file: {path}")] + FileRead { path: PathBuf }, #[error(transparent)] Toml(#[from] toml::de::Error), #[error("Secret file failed to deserialize")] @@ -163,6 +167,14 @@ impl Args { KeyType::Network.write(name, network, &self.config_dir()?) } + pub fn write_default_network(&self, name: &str) -> Result<(), Error> { + Config::new()?.set_network(name).save() + } + + pub fn write_default_identity(&self, name: &str) -> Result<(), Error> { + Config::new()?.set_identity(name).save() + } + pub fn list_identities(&self) -> Result, Error> { Ok(KeyType::Identity .list_paths(&self.local_and_global()?)? @@ -344,6 +356,12 @@ impl Args { } } +impl Pwd for Args { + fn set_pwd(&mut self, pwd: &Path) { + self.config_dir = Some(pwd.to_path_buf()); + } +} + pub fn ensure_directory(dir: PathBuf) -> Result { let parent = dir.parent().ok_or(Error::HomeDirNotFound)?; std::fs::create_dir_all(parent).map_err(|_| dir_creation_failed(parent))?; @@ -496,8 +514,6 @@ pub fn global_config_path() -> Result { Ok(stellar_dir) } -impl Pwd for Args { - fn set_pwd(&mut self, pwd: &Path) { - self.config_dir = Some(pwd.to_path_buf()); - } +pub fn config_file() -> Result { + Ok(global_config_path()?.join("config.toml")) } diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index b961f0f67..21ebb1fe9 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -1,6 +1,10 @@ use address::Address; use clap::{arg, command}; use serde::{Deserialize, Serialize}; +use std::{ + fs::{self, File}, + io::Write, +}; use crate::{ print::Print, @@ -8,7 +12,6 @@ use crate::{ xdr::{self, SequenceNumber, Transaction, TransactionEnvelope}, Pwd, }; - use network::Network; pub mod address; @@ -125,9 +128,6 @@ impl Pwd for Args { } } -#[derive(Default, Serialize, Deserialize)] -pub struct Config {} - #[derive(Debug, clap::Args, Clone, Default)] #[group(skip)] pub struct ArgsLocatorAndNetwork { @@ -143,3 +143,48 @@ impl ArgsLocatorAndNetwork { Ok(self.network.get(&self.locator)?) } } + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Config { + pub defaults: Defaults, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Defaults { + pub network: Option, + pub identity: Option, +} + +impl Config { + pub fn new() -> Result { + let path = locator::config_file()?; + + if path.exists() { + let data = fs::read(&path).map_err(|_| locator::Error::FileRead { path })?; + + Ok(toml::from_slice(&data)?) + } else { + Ok(Config::default()) + } + } + + #[must_use] + pub fn set_network(mut self, s: &str) -> Self { + self.defaults.network = Some(s.to_string()); + self + } + + #[must_use] + pub fn set_identity(mut self, s: &str) -> Self { + self.defaults.identity = Some(s.to_string()); + self + } + + pub fn save(&self) -> Result<(), locator::Error> { + let toml_string = toml::to_string(&self)?; + let mut file = File::create(locator::config_file()?)?; + file.write_all(toml_string.as_bytes())?; + + Ok(()) + } +}