soroban_cli/commands/contract/alias/
ls.rs1use clap::Parser;
2use std::collections::HashMap;
3use std::ffi::OsStr;
4use std::fmt::Debug;
5use std::fs;
6use std::path::Path;
7
8use crate::commands::config::network;
9use crate::config::locator::{print_deprecation_warning, Location};
10use crate::config::{alias, locator};
11
12#[derive(Parser, Debug, Clone)]
13#[group(skip)]
14pub struct Cmd {
15 #[command(flatten)]
16 pub config_locator: locator::Args,
17}
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error(transparent)]
22 Locator(#[from] locator::Error),
23
24 #[error(transparent)]
25 Network(#[from] network::Error),
26
27 #[error(transparent)]
28 IoError(#[from] std::io::Error),
29}
30
31#[derive(Debug, Clone)]
32struct AliasEntry {
33 alias: String,
34 contract: String,
35 builtin: bool,
36}
37
38impl Cmd {
39 pub fn run(&self) -> Result<(), Error> {
40 let config_dirs = self.config_locator.local_and_global()?;
41
42 for cfg in config_dirs {
43 match cfg {
44 Location::Local(config_dir) => {
45 if config_dir.exists() {
46 print_deprecation_warning(&config_dir);
47 }
48 }
49 Location::Global(config_dir) => self.read_from_config_dir(&config_dir)?,
50 }
51 }
52
53 Ok(())
54 }
55
56 fn collect_aliases(config_dir: &Path) -> Result<HashMap<String, Vec<AliasEntry>>, Error> {
57 let contract_ids_dir = config_dir.join("contract-ids");
58 let mut map: HashMap<String, Vec<AliasEntry>> = HashMap::new();
59
60 if !contract_ids_dir.is_dir() {
61 return Ok(map);
62 }
63
64 for entry in fs::read_dir(&contract_ids_dir)? {
65 let path = entry?.path();
66
67 if path.extension() != Some(OsStr::new("json")) {
68 continue;
69 }
70
71 if let Some(alias) = path.file_stem() {
72 let alias = alias.to_string_lossy().into_owned();
73 let content = fs::read_to_string(&path)?;
74 let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
75
76 for (network_passphrase, contract_id) in &data.ids {
77 let entry = AliasEntry {
78 alias: alias.clone(),
79 contract: contract_id.clone(),
80 builtin: false,
81 };
82
83 map.entry(network_passphrase.clone())
84 .or_default()
85 .push(entry);
86 }
87 }
88 }
89
90 Ok(map)
91 }
92
93 fn read_from_config_dir(&self, config_dir: &Path) -> Result<(), Error> {
94 let mut map = Self::collect_aliases(config_dir)?;
95
96 for (_, network, _) in self.config_locator.list_networks_long()? {
100 map.entry(network.network_passphrase).or_default();
101 }
102
103 for (network_passphrase, list) in &mut map {
104 if let Some(contract) =
105 alias::resolve_reserved(alias::NATIVE, &self.config_locator, network_passphrase)
106 {
107 list.push(AliasEntry {
108 alias: alias::NATIVE.to_string(),
109 contract: format!("{contract}"),
110 builtin: true,
111 });
112 }
113
114 println!("ℹ️ Aliases available for network '{network_passphrase}'");
115
116 list.sort_by(|a, b| a.alias.cmp(&b.alias));
117
118 for entry in list.iter() {
119 let note = if entry.builtin {
120 " (built-in)"
121 } else if alias::is_reserved(&entry.alias) {
122 " (disabled)"
125 } else {
126 ""
127 };
128 println!("{}: {}{note}", entry.alias, entry.contract);
129 }
130
131 println!();
132 }
133
134 Ok(())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::fs;
142
143 fn write_alias(dir: &Path, name: &str, network: &str, contract: &str) {
144 let contract_ids_dir = dir.join("contract-ids");
145 fs::create_dir_all(&contract_ids_dir).unwrap();
146 let content = format!(r#"{{"ids":{{"{network}":"{contract}"}}}}"#);
147 fs::write(contract_ids_dir.join(format!("{name}.json")), content).unwrap();
148 }
149
150 #[test]
151 fn glob_metacharacters_in_config_dir_are_treated_as_literal() {
152 let tmp = tempfile::tempdir().unwrap();
153 let base = tmp.path();
154
155 write_alias(&base.join("cfg1"), "alpha", "testnet", "CAAAA");
157 write_alias(&base.join("cfg2"), "beta", "testnet", "CBBBB");
158
159 write_alias(&base.join("cfg[12]"), "gamma", "testnet", "CCCCC");
161
162 let map = Cmd::collect_aliases(&base.join("cfg[12]")).unwrap();
163
164 let aliases: Vec<&str> = map
165 .values()
166 .flat_map(|entries| entries.iter().map(|e| e.alias.as_str()))
167 .collect();
168
169 assert!(
170 aliases.contains(&"gamma"),
171 "should read alias from the literal directory"
172 );
173 assert!(
174 !aliases.contains(&"alpha"),
175 "should not read from sibling cfg1"
176 );
177 assert!(
178 !aliases.contains(&"beta"),
179 "should not read from sibling cfg2"
180 );
181 }
182}