-
Notifications
You must be signed in to change notification settings - Fork 131
Make --network take precedence over rpc-url/passphrase env vars
#2579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,10 +6,12 @@ use serde::{Deserialize, Serialize}; | |
| use serde_json::Value; | ||
| use std::collections::HashMap; | ||
| use std::str::FromStr; | ||
| use std::sync::Once; | ||
| use stellar_strkey::ed25519::PublicKey; | ||
| use url::Url; | ||
|
|
||
| use super::locator; | ||
| use crate::print::{self, Print}; | ||
| use crate::utils::http; | ||
| use crate::{ | ||
| commands::HEADING_RPC, | ||
|
|
@@ -38,8 +40,6 @@ STELLAR_NETWORK, STELLAR_RPC_URL and STELLAR_NETWORK_PASSPHRASE"# | |
| "network passphrase is used but rpc-url is missing, use `--rpc-url` or `STELLAR_RPC_URL`" | ||
| )] | ||
| MissingRpcUrl, | ||
| #[error("cannot use both `--rpc-url` and `--network`")] | ||
| CannotUseBothRpcAndNetwork, | ||
| #[error(transparent)] | ||
| Rpc(#[from] rpc::Error), | ||
| #[error(transparent)] | ||
|
|
@@ -96,21 +96,49 @@ pub struct Args { | |
| pub network: Option<String>, | ||
| } | ||
|
|
||
| // Emits the network-precedence warning at most once per process so commands | ||
| // that call `Args::get` multiple times (e.g. sign + sign_fee_bump) don't spam | ||
| // the user. Honors the global `--quiet` flag recorded in `cli::main`. | ||
| fn warn_if_overridden(network: &str, rpc_url: Option<&str>, network_passphrase: Option<&str>) { | ||
| static WARN_ONCE: Once = Once::new(); | ||
| let ignored = match (rpc_url.is_some(), network_passphrase.is_some()) { | ||
| (true, true) => { | ||
| "--rpc-url / STELLAR_RPC_URL and --network-passphrase / STELLAR_NETWORK_PASSPHRASE" | ||
| } | ||
| (true, false) => "--rpc-url / STELLAR_RPC_URL", | ||
| (false, true) => "--network-passphrase / STELLAR_NETWORK_PASSPHRASE", | ||
| (false, false) => return, | ||
| }; | ||
| WARN_ONCE.call_once(|| { | ||
| Print::new(print::is_quiet()).warnln(format!( | ||
| "--network={network} takes precedence; ignoring {ignored}" | ||
| )); | ||
| }); | ||
| } | ||
|
|
||
| impl Args { | ||
| pub fn get(&self, locator: &locator::Args) -> Result<Network, Error> { | ||
| // A named network always resolves via the on-disk config, ignoring any | ||
| // rpc-url / passphrase that may have been picked up from env vars. This | ||
| // is what lets `--network foo` win over a stray STELLAR_RPC_URL in | ||
| // `.env`. Conflicts typed entirely on the CLI fall through here too; | ||
| // the explicit name takes precedence. | ||
| match ( | ||
| self.network.as_deref(), | ||
| self.rpc_url.clone(), | ||
| self.network_passphrase.clone(), | ||
| ) { | ||
| (Some(network), rpc_url, network_passphrase) => { | ||
| warn_if_overridden(network, rpc_url.as_deref(), network_passphrase.as_deref()); | ||
| Ok(locator.read_network(network)?) | ||
| } | ||
|
Comment on lines
+131
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Precedence violation when
So a user who runs: stellar network use testnet # sets the on-disk default
stellar tx ... --rpc-url=http://custom.example.com --network-passphrase=foowill have their explicit If you want to honor the documented precedence rigorously, clap exposes At minimum, this trade-off ( |
||
| (None, None, None) => { | ||
| // Fall back to testnet as the default network if no config default is set | ||
| Ok(DEFAULTS.get(DEFAULT_NETWORK_KEY).unwrap().into()) | ||
| } | ||
| (_, Some(_), None) => Err(Error::MissingNetworkPassphrase), | ||
| (_, None, Some(_)) => Err(Error::MissingRpcUrl), | ||
| (Some(network), None, None) => Ok(locator.read_network(network)?), | ||
| (_, Some(rpc_url), Some(network_passphrase)) => { | ||
| (None, Some(_), None) => Err(Error::MissingNetworkPassphrase), | ||
| (None, None, Some(_)) => Err(Error::MissingRpcUrl), | ||
| (None, Some(rpc_url), Some(network_passphrase)) => { | ||
| let rpc_headers = self | ||
| .rpc_headers | ||
| .iter() | ||
|
|
@@ -725,4 +753,24 @@ mod tests { | |
| let bad = "not a url"; | ||
| assert_eq!(redact_rpc_url(bad), bad); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
|
Comment on lines
+756
to
+757
|
||
| async fn network_name_wins_over_rpc_tuple() { | ||
| use crate::test_utils::with_env_set; | ||
|
|
||
| let tmp = tempfile::tempdir().unwrap(); | ||
| with_env_set("STELLAR_CONFIG_HOME", tmp.path(), || { | ||
| let args = Args { | ||
| network: Some("testnet".to_string()), | ||
| rpc_url: Some("http://other.example.com:59999".to_string()), | ||
| network_passphrase: Some("Some Other Passphrase".to_string()), | ||
| rpc_headers: Vec::new(), | ||
| }; | ||
|
|
||
| let result = args.get(&locator::Args::default()).unwrap(); | ||
|
|
||
| assert_eq!(result.rpc_url, "https://soroban-testnet.stellar.org"); | ||
| assert_eq!(result.network_passphrase, passphrase::TESTNET); | ||
| }); | ||
|
Comment on lines
+756
to
+774
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is a good smoke test of the precedence flip, but note that To exercise the actual on-disk path (the case from the PR description: ".env file sets STELLAR_RPC_URL + STELLAR_NETWORK_PASSPHRASE, user has saved a custom network"), you'd need to either:
Also worth a unit-level assertion that |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| use std::io::{self, Write}; | ||
| use std::sync::OnceLock; | ||
| use std::{env, fmt::Display}; | ||
|
|
||
| use crate::xdr::{Error as XdrError, Transaction}; | ||
|
|
@@ -7,6 +8,21 @@ use crate::{ | |
| config::network::Network, utils::explorer_url_for_transaction, utils::transaction_hash, | ||
| }; | ||
|
|
||
| static QUIET: OnceLock<bool> = OnceLock::new(); | ||
|
|
||
| /// Record whether `--quiet` was passed on the command line. Called once from | ||
| /// `cli::main` after parsing so resolvers running deep in the call stack can | ||
| /// honor the global flag without it being threaded through every signature. | ||
| pub fn set_quiet(quiet: bool) { | ||
| let _ = QUIET.set(quiet); | ||
| } | ||
|
|
||
| /// Read the recorded `--quiet` flag, defaulting to `false` if [`set_quiet`] | ||
| /// hasn't run yet (e.g. in unit tests that don't go through `cli::main`). | ||
| pub fn is_quiet() -> bool { | ||
| *QUIET.get().unwrap_or(&false) | ||
| } | ||
|
Comment on lines
+11
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A process-global
|
||
|
|
||
| #[derive(Clone)] | ||
| pub struct Print { | ||
| pub quiet: bool, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few thoughts on this helper:
1. The
--network={network}wording is misleading when the source is env / use.The third integration test (
network_flag_warning_fires_for_env_only_network) intentionally exercises a setup where the user typed no--networkflag — onlySTELLAR_NETWORK=local. The user then sees--network=local takes precedenceand may legitimately wonder which--networkflag the warning refers to. Consider phrasing the warning in a source-agnostic way, e.g.network "local" takes precedence(or surfacing the actual source:network "local" (from STELLAR_NETWORK) takes precedence). This is also whereSTELLAR_NETWORK_SOURCE, already set byset_env_value_from_config, could be consulted to disambiguateenvvsuse.2.
--rpc-url / STELLAR_RPC_URLreads as one combined thing.Slash-joined names can scan as "the rpc-url/stellar-rpc-url option". Minor, but
oris unambiguous:ignoring --rpc-url or STELLAR_RPC_URL.3. The
Onceis process-global.That's fine for the typical CLI invocation (one process, one warning). It's worth knowing that in a long-lived test/library context, a second
Args::getwith overrides would be silenced even if the first call had none —Oncedoesn't reset. Not a bug given current usage, just a caveat for future use.4. The
Print::new(...)allocation only matters when not quiet.You can short-circuit before formatting the message:
if print::is_quiet() { return; }at the top of thecall_once. Saves aformat!allocation when--quietis set. Trivial.