Skip to main content

soroban_cli/commands/contract/alias/
ls.rs

1use clap::Parser;
2use std::collections::HashMap;
3use std::ffi::OsStr;
4use std::fmt::Debug;
5use std::path::Path;
6use std::{fs, process};
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}
36
37impl Cmd {
38    pub fn run(&self) -> Result<(), Error> {
39        let config_dirs = self.config_locator.local_and_global()?;
40
41        for cfg in config_dirs {
42            match cfg {
43                Location::Local(config_dir) => {
44                    if config_dir.exists() {
45                        print_deprecation_warning(&config_dir);
46                    }
47                }
48                Location::Global(config_dir) => Self::read_from_config_dir(&config_dir)?,
49            }
50        }
51
52        Ok(())
53    }
54
55    fn collect_aliases(config_dir: &Path) -> Result<HashMap<String, Vec<AliasEntry>>, Error> {
56        let contract_ids_dir = config_dir.join("contract-ids");
57        let mut map: HashMap<String, Vec<AliasEntry>> = HashMap::new();
58
59        if !contract_ids_dir.is_dir() {
60            return Ok(map);
61        }
62
63        for entry in fs::read_dir(&contract_ids_dir)? {
64            let path = entry?.path();
65
66            if path.extension() != Some(OsStr::new("json")) {
67                continue;
68            }
69
70            if let Some(alias) = path.file_stem() {
71                let alias = alias.to_string_lossy().into_owned();
72                let content = fs::read_to_string(&path)?;
73                let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
74
75                for (network_passphrase, contract_id) in &data.ids {
76                    let entry = AliasEntry {
77                        alias: alias.clone(),
78                        contract: contract_id.clone(),
79                    };
80
81                    map.entry(network_passphrase.clone())
82                        .or_default()
83                        .push(entry);
84                }
85            }
86        }
87
88        Ok(map)
89    }
90
91    fn read_from_config_dir(config_dir: &Path) -> Result<(), Error> {
92        let mut map = Self::collect_aliases(config_dir)?;
93        let mut found = false;
94
95        for (network_passphrase, list) in &mut map {
96            println!("ℹ️ Aliases available for network '{network_passphrase}'");
97
98            list.sort_by(|a, b| a.alias.cmp(&b.alias));
99
100            for entry in list.iter() {
101                found = true;
102                println!("{}: {}", entry.alias, entry.contract);
103            }
104
105            println!();
106        }
107
108        if !found {
109            eprintln!("⚠️ No aliases defined for network");
110
111            process::exit(1);
112        }
113
114        Ok(())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::fs;
122
123    fn write_alias(dir: &Path, name: &str, network: &str, contract: &str) {
124        let contract_ids_dir = dir.join("contract-ids");
125        fs::create_dir_all(&contract_ids_dir).unwrap();
126        let content = format!(r#"{{"ids":{{"{network}":"{contract}"}}}}"#);
127        fs::write(contract_ids_dir.join(format!("{name}.json")), content).unwrap();
128    }
129
130    #[test]
131    fn glob_metacharacters_in_config_dir_are_treated_as_literal() {
132        let tmp = tempfile::tempdir().unwrap();
133        let base = tmp.path();
134
135        // Sibling directories that would match the glob `[12]` if unescaped.
136        write_alias(&base.join("cfg1"), "alpha", "testnet", "CAAAA");
137        write_alias(&base.join("cfg2"), "beta", "testnet", "CBBBB");
138
139        // The literal directory whose name contains bracket metacharacters.
140        write_alias(&base.join("cfg[12]"), "gamma", "testnet", "CCCCC");
141
142        let map = Cmd::collect_aliases(&base.join("cfg[12]")).unwrap();
143
144        let aliases: Vec<&str> = map
145            .values()
146            .flat_map(|entries| entries.iter().map(|e| e.alias.as_str()))
147            .collect();
148
149        assert!(
150            aliases.contains(&"gamma"),
151            "should read alias from the literal directory"
152        );
153        assert!(
154            !aliases.contains(&"alpha"),
155            "should not read from sibling cfg1"
156        );
157        assert!(
158            !aliases.contains(&"beta"),
159            "should not read from sibling cfg2"
160        );
161    }
162}