use clap::{Args, Subcommand};
use crate::config::{
load_config_for_edit, system_config_path, write_config, GatewayProfile, SystemConfigEnv,
};
use crate::secret::{SecretEnv, SystemSecretEnv};
use crate::util::CliError;
#[derive(Debug, Args)]
pub struct GatewayArgs {
#[command(subcommand)]
pub verb: GatewayVerb,
}
#[derive(Debug, Subcommand)]
pub enum GatewayVerb {
Add(GatewayAddArgs),
Use(GatewayUseArgs),
List(GatewayListArgs),
Show(GatewayShowArgs),
Remove(GatewayRemoveArgs),
}
impl GatewayArgs {
#[must_use]
pub fn json_mode(&self) -> bool {
match &self.verb {
GatewayVerb::Add(a) => a.json,
GatewayVerb::Use(a) => a.json,
GatewayVerb::List(a) => a.json,
GatewayVerb::Show(a) => a.json,
GatewayVerb::Remove(a) => a.json,
}
}
}
pub fn run(args: GatewayArgs) -> Result<(), CliError> {
run_with_env(args, &SystemConfigEnv, &SystemSecretEnv)
}
pub fn run_with_env(
args: GatewayArgs,
config_env: &dyn crate::config::ConfigEnv,
secret_env: &dyn SecretEnv,
) -> Result<(), CliError> {
match args.verb {
GatewayVerb::Add(a) => run_add(a, config_env, secret_env),
GatewayVerb::Use(a) => run_use(a, config_env),
GatewayVerb::List(a) => run_list(a, config_env),
GatewayVerb::Show(a) => run_show(a, config_env),
GatewayVerb::Remove(a) => run_remove(a, config_env),
}
}
#[derive(Debug, Args)]
pub struct GatewayAddArgs {
pub name: String,
#[arg(long = "base-url")]
pub base_url: String,
#[arg(long = "api-key-stdin")]
pub api_key_stdin: bool,
#[arg(long)]
pub json: bool,
}
fn run_add(
args: GatewayAddArgs,
config_env: &dyn crate::config::ConfigEnv,
secret_env: &dyn SecretEnv,
) -> Result<(), CliError> {
validate_name(&args.name)?;
let base_url = args.base_url.trim().to_string();
if base_url.is_empty() {
return Err(CliError::input("gateway add: --base-url must not be empty"));
}
let api_key = if args.api_key_stdin {
let raw = secret_env.read_stdin()?;
let trimmed = raw.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
} else if secret_env.stdin_is_terminal() {
let entered = secret_env.prompt_hidden(&format!(
"Enter API key for gateway '{}' (blank = none): ",
args.name
))?;
let trimmed = entered.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
} else {
None
};
let mut config = load_config_for_edit(config_env)?;
config.gateways.insert(
args.name.clone(),
GatewayProfile {
base_url: base_url.clone(),
api_key,
},
);
if config.default_gateway.is_none() {
config.default_gateway = Some(args.name.clone());
}
write_config(config_env, &config)?;
let has_key = config
.gateways
.get(&args.name)
.and_then(|p| p.api_key.as_ref())
.is_some();
if args.json {
let value = serde_json::json!({
"status": "ok",
"name": args.name,
"base_url": base_url,
"has_api_key": has_key,
"is_default": config.default_gateway.as_deref() == Some(args.name.as_str()),
});
println!("{value}");
} else {
println!("ok: saved gateway profile '{}' -> {base_url}", args.name);
if config.default_gateway.as_deref() == Some(args.name.as_str()) {
println!(" (set as the default gateway)");
}
}
Ok(())
}
#[derive(Debug, Args)]
pub struct GatewayUseArgs {
pub name: String,
#[arg(long)]
pub json: bool,
}
fn run_use(
args: GatewayUseArgs,
config_env: &dyn crate::config::ConfigEnv,
) -> Result<(), CliError> {
let mut config = load_config_for_edit(config_env)?;
if !config.gateways.contains_key(&args.name) {
return Err(unknown_profile(&args.name, &config));
}
config.default_gateway = Some(args.name.clone());
write_config(config_env, &config)?;
if args.json {
println!(
"{}",
serde_json::json!({ "status": "ok", "default_gateway": args.name })
);
} else {
println!("ok: default gateway is now '{}'", args.name);
}
Ok(())
}
#[derive(Debug, Args)]
pub struct GatewayListArgs {
#[arg(long)]
pub json: bool,
}
fn run_list(
args: GatewayListArgs,
config_env: &dyn crate::config::ConfigEnv,
) -> Result<(), CliError> {
let config = load_config_for_edit(config_env)?;
let default = config.default_gateway.as_deref();
if args.json {
let profiles: Vec<serde_json::Value> = config
.gateways
.iter()
.map(|(name, p)| {
serde_json::json!({
"name": name,
"base_url": p.base_url,
"api_key": mask_key(p.api_key.as_deref()),
"has_api_key": p.api_key.is_some(),
"is_default": default == Some(name.as_str()),
})
})
.collect();
println!("{}", serde_json::json!({ "gateways": profiles }));
return Ok(());
}
if config.gateways.is_empty() {
println!(
"no gateway profiles. Add one with 'cardanowall gateway add <name> --base-url <url>'."
);
return Ok(());
}
for (name, p) in &config.gateways {
let marker = if default == Some(name.as_str()) {
"* "
} else {
" "
};
println!(
"{marker}{name}\t{}\t{}",
p.base_url,
mask_key(p.api_key.as_deref())
);
}
Ok(())
}
#[derive(Debug, Args)]
pub struct GatewayShowArgs {
pub name: String,
#[arg(long)]
pub reveal: bool,
#[arg(long)]
pub json: bool,
}
fn run_show(
args: GatewayShowArgs,
config_env: &dyn crate::config::ConfigEnv,
) -> Result<(), CliError> {
let config = load_config_for_edit(config_env)?;
let Some(profile) = config.gateways.get(&args.name) else {
return Err(unknown_profile(&args.name, &config));
};
let is_default = config.default_gateway.as_deref() == Some(args.name.as_str());
let key_display = if args.reveal {
profile.api_key.clone().unwrap_or_default()
} else {
mask_key(profile.api_key.as_deref())
};
if args.json {
let value = serde_json::json!({
"name": args.name,
"base_url": profile.base_url,
"api_key": if args.reveal { serde_json::Value::from(profile.api_key.clone()) } else { serde_json::Value::from(key_display.clone()) },
"has_api_key": profile.api_key.is_some(),
"is_default": is_default,
});
println!("{value}");
} else {
println!("name: {}", args.name);
println!("base_url: {}", profile.base_url);
println!("api_key: {key_display}");
println!("default: {}", if is_default { "yes" } else { "no" });
}
Ok(())
}
#[derive(Debug, Args)]
pub struct GatewayRemoveArgs {
pub name: String,
#[arg(long)]
pub json: bool,
}
fn run_remove(
args: GatewayRemoveArgs,
config_env: &dyn crate::config::ConfigEnv,
) -> Result<(), CliError> {
let mut config = load_config_for_edit(config_env)?;
if config.gateways.remove(&args.name).is_none() {
return Err(unknown_profile(&args.name, &config));
}
if config.default_gateway.as_deref() == Some(args.name.as_str()) {
config.default_gateway = None;
}
write_config(config_env, &config)?;
if args.json {
println!(
"{}",
serde_json::json!({ "status": "ok", "removed": args.name })
);
} else {
println!("ok: removed gateway profile '{}'", args.name);
}
Ok(())
}
fn mask_key(key: Option<&str>) -> String {
match key {
Some(k) if !k.is_empty() => "********".to_string(),
_ => "<none>".to_string(),
}
}
fn validate_name(name: &str) -> Result<(), CliError> {
if name.is_empty()
|| !name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
{
return Err(CliError::input(format!(
"gateway: profile name must be non-empty and match [A-Za-z0-9._-]; got \"{name}\""
)));
}
Ok(())
}
fn unknown_profile(name: &str, config: &crate::config::CardanoWallConfig) -> CliError {
let known: Vec<&str> = config.gateways.keys().map(String::as_str).collect();
let hint = if known.is_empty() {
"no gateway profiles are defined".to_string()
} else {
format!("known profiles: {}", known.join(", "))
};
let path = system_config_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "config.toml".to_string());
CliError::input(format!(
"gateway: no profile named \"{name}\" in {path} ({hint})"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn masks_key() {
assert_eq!(mask_key(Some("supersecret")), "********");
assert_eq!(mask_key(Some("")), "<none>");
assert_eq!(mask_key(None), "<none>");
}
#[test]
fn validates_name() {
assert!(validate_name("prod").is_ok());
assert!(validate_name("prod.eu-1_test").is_ok());
assert_eq!(validate_name("bad name").unwrap_err().code, 4);
assert_eq!(validate_name("").unwrap_err().code, 4);
assert_eq!(validate_name("a/b").unwrap_err().code, 4);
}
}