From e3e8d09f5ad70bcf396a170474709622b75737bb Mon Sep 17 00:00:00 2001 From: pselle Date: Fri, 6 Nov 2026 11:53:55 -0500 Subject: [PATCH 1/2] fix: read-only commands don't require a source account fetch-contract-id, fetch-hash, current-version, download, fetch-account-id, fetch-account-owner and create-alias only simulate reads, which work with any public key. Make --source-account optional for them, so import_contract! builds in CI without an identity. Closes #111 Co-Authored-By: Claude --- .../src/commands/account.rs | 6 ++- .../src/commands/create_alias.rs | 4 +- .../src/commands/current_version.rs | 4 +- .../src/commands/download.rs | 4 +- .../src/commands/fetch_account_id.rs | 4 +- .../src/commands/fetch_account_owner.rs | 4 +- .../src/commands/fetch_contract_id.rs | 4 +- .../src/commands/fetch_hash.rs | 4 +- .../src/commands/global.rs | 52 +++++++++++++++++++ 9 files changed, 70 insertions(+), 16 deletions(-) diff --git a/crates/stellar-registry-cli/src/commands/account.rs b/crates/stellar-registry-cli/src/commands/account.rs index 802adbd..6ebf554 100644 --- a/crates/stellar-registry-cli/src/commands/account.rs +++ b/crates/stellar-registry-cli/src/commands/account.rs @@ -447,7 +447,8 @@ mod tests { let config = registry .parse_cmd::(&["bob"]) .unwrap() - .config; + .config + .config(); let root = Registry::new(&config, None).await.unwrap(); assert_eq!( root.account_preauth_owner("bob", &bob).await.unwrap(), @@ -547,7 +548,8 @@ mod tests { let config = registry .parse_cmd::(&["bob"]) .unwrap() - .config; + .config + .config(); let root = Registry::new(&config, None).await.unwrap(); assert!(matches!( root.fetch_account_id("bob").await, diff --git a/crates/stellar-registry-cli/src/commands/create_alias.rs b/crates/stellar-registry-cli/src/commands/create_alias.rs index e445328..336e2d2 100644 --- a/crates/stellar-registry-cli/src/commands/create_alias.rs +++ b/crates/stellar-registry-cli/src/commands/create_alias.rs @@ -21,7 +21,7 @@ pub struct Cmd { pub force: bool, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -72,7 +72,7 @@ impl Cmd { } pub async fn get_contract_id(&self) -> Result { - let registry = &self.contract.registry(&self.config).await?; + let registry = &self.contract.registry(&self.config.config()).await?; eprintln!("Fetching contract ID via registry..."); if self.force { return Ok(registry diff --git a/crates/stellar-registry-cli/src/commands/current_version.rs b/crates/stellar-registry-cli/src/commands/current_version.rs index 653ccd8..600adc1 100644 --- a/crates/stellar-registry-cli/src/commands/current_version.rs +++ b/crates/stellar-registry-cli/src/commands/current_version.rs @@ -10,7 +10,7 @@ pub struct Cmd { pub wasm_name: Prefixed, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -31,7 +31,7 @@ impl Cmd { } pub async fn current_version(&self) -> Result { - let registry = self.wasm_name.registry(&self.config).await?; + let registry = self.wasm_name.registry(&self.config.config()).await?; let slop = ["current_version", "--wasm-name", self.wasm_name.name()]; let raw = registry .as_contract() diff --git a/crates/stellar-registry-cli/src/commands/download.rs b/crates/stellar-registry-cli/src/commands/download.rs index c39549e..ff55792 100644 --- a/crates/stellar-registry-cli/src/commands/download.rs +++ b/crates/stellar-registry-cli/src/commands/download.rs @@ -20,7 +20,7 @@ pub struct Cmd { pub out_file: Option, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -61,7 +61,7 @@ impl Cmd { } pub async fn download_bytes(&self) -> Result, Error> { - let registry = &self.wasm_name.registry(&self.config).await?; + let registry = &self.wasm_name.registry(&self.config.config()).await?; let mut slop = vec!["fetch_hash", "--wasm-name", &self.wasm_name.name()]; let version = self.version.clone().map(|v| format!("\"{v}\"")); if let Some(version) = version.as_deref() { diff --git a/crates/stellar-registry-cli/src/commands/fetch_account_id.rs b/crates/stellar-registry-cli/src/commands/fetch_account_id.rs index 9e4dc01..2758648 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_account_id.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_account_id.rs @@ -16,7 +16,7 @@ pub struct Cmd { pub force: bool, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -40,7 +40,7 @@ impl Cmd { } pub async fn fetch_account_id(&self) -> Result { - let registry = self.account_name.registry(&self.config).await?; + let registry = self.account_name.registry(&self.config.config()).await?; let name = self.account_name.name(); if self.force { return Ok(registry.fetch_account_id_unchecked(name).await?); diff --git a/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs b/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs index f6bcda3..99e5c8f 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs @@ -11,7 +11,7 @@ pub struct Cmd { pub account_name: Prefixed, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -31,7 +31,7 @@ impl Cmd { } pub async fn fetch_account_owner(&self) -> Result { - let registry = self.account_name.registry(&self.config).await?; + let registry = self.account_name.registry(&self.config.config()).await?; Ok(registry .fetch_account_owner(self.account_name.name()) .await?) diff --git a/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs b/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs index b3bd4d7..950af43 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs @@ -17,7 +17,7 @@ pub struct Cmd { pub force: bool, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -42,7 +42,7 @@ impl Cmd { } pub async fn fetch_contract_id(&self) -> Result { - let registry = self.contract_name.registry(&self.config).await?; + let registry = self.contract_name.registry(&self.config.config()).await?; if self.force { return Ok(registry .fetch_contract_id_unchecked(self.contract_name.name()) diff --git a/crates/stellar-registry-cli/src/commands/fetch_hash.rs b/crates/stellar-registry-cli/src/commands/fetch_hash.rs index b59aba4..4f44a15 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_hash.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_hash.rs @@ -14,7 +14,7 @@ pub struct Cmd { pub version: Option, #[command(flatten)] - pub config: global::Args, + pub config: global::ReadArgs, } #[derive(thiserror::Error, Debug)] @@ -35,7 +35,7 @@ impl Cmd { } pub async fn fetch_hash(&self) -> Result { - let registry = self.wasm_name.registry(&self.config).await?; + let registry = self.wasm_name.registry(&self.config.config()).await?; let mut slop = vec!["fetch_hash", "--wasm-name", &self.wasm_name.name()]; let version = self.version.clone().map(|v| format!("\"{v}\"")); if let Some(version) = version.as_deref() { diff --git a/crates/stellar-registry-cli/src/commands/global.rs b/crates/stellar-registry-cli/src/commands/global.rs index 5812d4b..345d51e 100644 --- a/crates/stellar-registry-cli/src/commands/global.rs +++ b/crates/stellar-registry-cli/src/commands/global.rs @@ -22,3 +22,55 @@ impl Args { Ok(rpc::Client::new(&self.config.get_network()?.rpc_url)?) } } + +/// Simulation accepts any public key as the source, even an unfunded one. +const READ_ONLY_SOURCE: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; + +/// Config for commands that only simulate reads, so need no source account. +#[derive(clap::Args, Debug, Clone)] +pub struct ReadArgs { + #[command(flatten)] + pub network: config::network::Args, + + #[command(flatten)] + pub locator: config::locator::Args, + + /// Account to simulate as. Optional: reads work with any public key. + #[arg(long, short = 's', visible_alias = "source", env = "STELLAR_ACCOUNT")] + pub source_account: Option, +} + +impl ReadArgs { + pub fn config(&self) -> config::Args { + config::Args { + network: self.network.clone(), + locator: self.locator.clone(), + source_account: self + .source_account + .clone() + .unwrap_or_else(|| READ_ONLY_SOURCE.parse().expect("valid public key")), + ..Default::default() + } + } + + pub fn get_network(&self) -> Result { + self.config().get_network() + } +} + +#[cfg(test)] +mod tests { + use clap::Parser; + + #[derive(Parser)] + struct Cmd { + #[command(flatten)] + config: super::ReadArgs, + } + + #[test] + fn read_args_need_no_source_account() { + let cmd = Cmd::try_parse_from(["cmd", "--network", "testnet"]).unwrap(); + cmd.config.config().source_account().unwrap(); + } +} From 7dc0d70b75cdc1f48522093f633dc8d96fe43af8 Mon Sep 17 00:00:00 2001 From: pselle Date: Fri, 6 Nov 2026 11:58:59 -0500 Subject: [PATCH 2/2] refactor: make the source optional with mut_arg Replace ReadArgs with a clap mut_arg on each read-only command, so they keep global::Args and call sites stay `&self.config`. Co-Authored-By: Claude --- .../src/commands/account.rs | 6 +-- .../src/commands/create_alias.rs | 5 ++- .../src/commands/current_version.rs | 5 ++- .../src/commands/download.rs | 5 ++- .../src/commands/fetch_account_id.rs | 5 ++- .../src/commands/fetch_account_owner.rs | 5 ++- .../src/commands/fetch_contract_id.rs | 5 ++- .../src/commands/fetch_hash.rs | 5 ++- .../src/commands/global.rs | 43 +++++-------------- 9 files changed, 33 insertions(+), 51 deletions(-) diff --git a/crates/stellar-registry-cli/src/commands/account.rs b/crates/stellar-registry-cli/src/commands/account.rs index 6ebf554..802adbd 100644 --- a/crates/stellar-registry-cli/src/commands/account.rs +++ b/crates/stellar-registry-cli/src/commands/account.rs @@ -447,8 +447,7 @@ mod tests { let config = registry .parse_cmd::(&["bob"]) .unwrap() - .config - .config(); + .config; let root = Registry::new(&config, None).await.unwrap(); assert_eq!( root.account_preauth_owner("bob", &bob).await.unwrap(), @@ -548,8 +547,7 @@ mod tests { let config = registry .parse_cmd::(&["bob"]) .unwrap() - .config - .config(); + .config; let root = Registry::new(&config, None).await.unwrap(); assert!(matches!( root.fetch_account_id("bob").await, diff --git a/crates/stellar-registry-cli/src/commands/create_alias.rs b/crates/stellar-registry-cli/src/commands/create_alias.rs index 336e2d2..3005e19 100644 --- a/crates/stellar-registry-cli/src/commands/create_alias.rs +++ b/crates/stellar-registry-cli/src/commands/create_alias.rs @@ -7,6 +7,7 @@ use stellar_strkey::Contract; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of deployed contract. Can use prefix if not using verified registry. /// E.g. `unverified/` @@ -21,7 +22,7 @@ pub struct Cmd { pub force: bool, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -72,7 +73,7 @@ impl Cmd { } pub async fn get_contract_id(&self) -> Result { - let registry = &self.contract.registry(&self.config.config()).await?; + let registry = &self.contract.registry(&self.config).await?; eprintln!("Fetching contract ID via registry..."); if self.force { return Ok(registry diff --git a/crates/stellar-registry-cli/src/commands/current_version.rs b/crates/stellar-registry-cli/src/commands/current_version.rs index 600adc1..091f4dc 100644 --- a/crates/stellar-registry-cli/src/commands/current_version.rs +++ b/crates/stellar-registry-cli/src/commands/current_version.rs @@ -5,12 +5,13 @@ use stellar_registry_build::name::{Prefixed, RegistryAccess}; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of published Wasm pub wasm_name: Prefixed, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -31,7 +32,7 @@ impl Cmd { } pub async fn current_version(&self) -> Result { - let registry = self.wasm_name.registry(&self.config.config()).await?; + let registry = self.wasm_name.registry(&self.config).await?; let slop = ["current_version", "--wasm-name", self.wasm_name.name()]; let raw = registry .as_contract() diff --git a/crates/stellar-registry-cli/src/commands/download.rs b/crates/stellar-registry-cli/src/commands/download.rs index ff55792..cbd82bd 100644 --- a/crates/stellar-registry-cli/src/commands/download.rs +++ b/crates/stellar-registry-cli/src/commands/download.rs @@ -7,6 +7,7 @@ use stellar_registry_build::name::{Prefixed, RegistryAccess}; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of published Wasm pub wasm_name: Prefixed, @@ -20,7 +21,7 @@ pub struct Cmd { pub out_file: Option, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -61,7 +62,7 @@ impl Cmd { } pub async fn download_bytes(&self) -> Result, Error> { - let registry = &self.wasm_name.registry(&self.config.config()).await?; + let registry = &self.wasm_name.registry(&self.config).await?; let mut slop = vec!["fetch_hash", "--wasm-name", &self.wasm_name.name()]; let version = self.version.clone().map(|v| format!("\"{v}\"")); if let Some(version) = version.as_deref() { diff --git a/crates/stellar-registry-cli/src/commands/fetch_account_id.rs b/crates/stellar-registry-cli/src/commands/fetch_account_id.rs index 2758648..38e1f69 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_account_id.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_account_id.rs @@ -5,6 +5,7 @@ use stellar_registry_build::name::{Prefixed, RegistryAccess}; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of the registered account. Can use prefix if not using verified registry. /// E.g. `unverified/` @@ -16,7 +17,7 @@ pub struct Cmd { pub force: bool, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -40,7 +41,7 @@ impl Cmd { } pub async fn fetch_account_id(&self) -> Result { - let registry = self.account_name.registry(&self.config.config()).await?; + let registry = self.account_name.registry(&self.config).await?; let name = self.account_name.name(); if self.force { return Ok(registry.fetch_account_id_unchecked(name).await?); diff --git a/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs b/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs index 99e5c8f..4b1dbc9 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_account_owner.rs @@ -5,13 +5,14 @@ use stellar_registry_build::name::{Prefixed, RegistryAccess}; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of the registered account. Can use prefix if not using verified registry. /// E.g. `unverified/` pub account_name: Prefixed, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -31,7 +32,7 @@ impl Cmd { } pub async fn fetch_account_owner(&self) -> Result { - let registry = self.account_name.registry(&self.config.config()).await?; + let registry = self.account_name.registry(&self.config).await?; Ok(registry .fetch_account_owner(self.account_name.name()) .await?) diff --git a/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs b/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs index 950af43..45d4112 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_contract_id.rs @@ -6,6 +6,7 @@ use stellar_strkey::Contract; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of deployed contract. Can use prefix if not using verified registry. /// E.g. `unverified/` @@ -17,7 +18,7 @@ pub struct Cmd { pub force: bool, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -42,7 +43,7 @@ impl Cmd { } pub async fn fetch_contract_id(&self) -> Result { - let registry = self.contract_name.registry(&self.config.config()).await?; + let registry = self.contract_name.registry(&self.config).await?; if self.force { return Ok(registry .fetch_contract_id_unchecked(self.contract_name.name()) diff --git a/crates/stellar-registry-cli/src/commands/fetch_hash.rs b/crates/stellar-registry-cli/src/commands/fetch_hash.rs index 4f44a15..f430d31 100644 --- a/crates/stellar-registry-cli/src/commands/fetch_hash.rs +++ b/crates/stellar-registry-cli/src/commands/fetch_hash.rs @@ -5,6 +5,7 @@ use stellar_registry_build::name::{Prefixed, RegistryAccess}; use crate::commands::global; #[derive(Parser, Debug, Clone)] +#[command(mut_arg("source_account", global::optional_source))] pub struct Cmd { /// Name of published Wasm pub wasm_name: Prefixed, @@ -14,7 +15,7 @@ pub struct Cmd { pub version: Option, #[command(flatten)] - pub config: global::ReadArgs, + pub config: global::Args, } #[derive(thiserror::Error, Debug)] @@ -35,7 +36,7 @@ impl Cmd { } pub async fn fetch_hash(&self) -> Result { - let registry = self.wasm_name.registry(&self.config.config()).await?; + let registry = self.wasm_name.registry(&self.config).await?; let mut slop = vec!["fetch_hash", "--wasm-name", &self.wasm_name.name()]; let version = self.version.clone().map(|v| format!("\"{v}\"")); if let Some(version) = version.as_deref() { diff --git a/crates/stellar-registry-cli/src/commands/global.rs b/crates/stellar-registry-cli/src/commands/global.rs index 345d51e..b660a3d 100644 --- a/crates/stellar-registry-cli/src/commands/global.rs +++ b/crates/stellar-registry-cli/src/commands/global.rs @@ -26,36 +26,12 @@ impl Args { /// Simulation accepts any public key as the source, even an unfunded one. const READ_ONLY_SOURCE: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; -/// Config for commands that only simulate reads, so need no source account. -#[derive(clap::Args, Debug, Clone)] -pub struct ReadArgs { - #[command(flatten)] - pub network: config::network::Args, - - #[command(flatten)] - pub locator: config::locator::Args, - - /// Account to simulate as. Optional: reads work with any public key. - #[arg(long, short = 's', visible_alias = "source", env = "STELLAR_ACCOUNT")] - pub source_account: Option, -} - -impl ReadArgs { - pub fn config(&self) -> config::Args { - config::Args { - network: self.network.clone(), - locator: self.locator.clone(), - source_account: self - .source_account - .clone() - .unwrap_or_else(|| READ_ONLY_SOURCE.parse().expect("valid public key")), - ..Default::default() - } - } - - pub fn get_network(&self) -> Result { - self.config().get_network() - } +/// For `#[command(mut_arg("source_account", global::optional_source))]` on +/// commands that only simulate reads, so they run without an identity. +pub fn optional_source(arg: clap::Arg) -> clap::Arg { + arg.required(false) + .default_value(READ_ONLY_SOURCE) + .hide_default_value(true) } #[cfg(test)] @@ -63,14 +39,15 @@ mod tests { use clap::Parser; #[derive(Parser)] + #[command(mut_arg("source_account", super::optional_source))] struct Cmd { #[command(flatten)] - config: super::ReadArgs, + config: super::Args, } #[test] - fn read_args_need_no_source_account() { + fn optional_source_needs_no_identity() { let cmd = Cmd::try_parse_from(["cmd", "--network", "testnet"]).unwrap(); - cmd.config.config().source_account().unwrap(); + cmd.config.source_account().unwrap(); } }