1use directories::UserDirs;
2use itertools::Itertools;
3use serde::de::DeserializeOwned;
4use std::{
5 ffi::OsStr,
6 fmt::Display,
7 fs::{self, create_dir_all, OpenOptions},
8 io::{self, Write},
9 path::{Path, PathBuf},
10 str::FromStr,
11};
12use stellar_strkey::{Contract, DecodeError};
13
14use crate::{
15 commands::{global, HEADING_GLOBAL},
16 print::Print,
17 signer::secure_store,
18 utils::find_config_dir,
19 xdr, Pwd,
20};
21
22use super::{
23 alias,
24 key::{self, Key},
25 network::{self, Network},
26 secret::Secret,
27 Config,
28};
29
30#[derive(thiserror::Error, Debug)]
31pub enum Error {
32 #[error(transparent)]
33 TomlSerialize(#[from] toml::ser::Error),
34 #[error("Failed to find home directory")]
35 HomeDirNotFound,
36 #[error("Failed read current directory")]
37 CurrentDirNotFound,
38 #[error("Failed read current directory and no STELLAR_CONFIG_HOME is set")]
39 NoConfigEnvVar,
40 #[error("Failed to create directory: {path:?}")]
41 DirCreationFailed { path: PathBuf },
42 #[error("Failed to read secret's file: {path}.\nProbably need to use `stellar keys add`")]
43 SecretFileRead { path: PathBuf },
44 #[error("Failed to read network file: {path};\nProbably need to use `stellar network add`")]
45 NetworkFileRead { path: PathBuf },
46 #[error("Failed to read file: {path}")]
47 FileRead { path: PathBuf },
48 #[error(transparent)]
49 Toml(#[from] toml::de::Error),
50 #[error("Secret file failed to deserialize")]
51 Deserialization,
52 #[error("Failed to write identity file:{filepath}: {error}")]
53 IdCreationFailed { filepath: PathBuf, error: io::Error },
54 #[error("Secret file failed to deserialize")]
55 NetworkDeserialization,
56 #[error("Failed to write network file: {0}")]
57 NetworkCreationFailed(std::io::Error),
58 #[error("Error Identity directory is invalid: {name}")]
59 IdentityList { name: String },
60 #[error("Config file failed to serialize")]
63 ConfigSerialization,
64 #[error("STELLAR_CONFIG_HOME env variable is not a valid path. Got {0}")]
67 StellarConfigDir(String),
68 #[error("XDG_CONFIG_HOME env variable is not a valid path. Got {0}")]
69 XdgConfigHome(String),
70 #[error(transparent)]
71 Io(#[from] std::io::Error),
72 #[error("Failed to remove {0}: {1}")]
73 ConfigRemoval(String, String),
74 #[error("Failed to find config {0} for {1}")]
75 ConfigMissing(String, String),
76 #[error(transparent)]
77 String(#[from] std::string::FromUtf8Error),
78 #[error(transparent)]
79 Secret(#[from] crate::config::secret::Error),
80 #[error(transparent)]
81 Json(#[from] serde_json::Error),
82 #[error("cannot access config dir for alias file")]
83 CannotAccessConfigDir,
84 #[error("cannot access alias config file (no permission or doesn't exist)")]
85 CannotAccessAliasConfigFile,
86 #[error("cannot parse contract ID {0}: {1}")]
87 CannotParseContractId(String, DecodeError),
88 #[error("contract not found: {0}")]
89 ContractNotFound(String),
90 #[error("Failed to read upgrade check file: {path}: {error}")]
91 UpgradeCheckReadFailed { path: PathBuf, error: io::Error },
92 #[error("Failed to write upgrade check file: {path}: {error}")]
93 UpgradeCheckWriteFailed { path: PathBuf, error: io::Error },
94 #[error("Contract alias {0}, cannot overlap with key")]
95 ContractAliasCannotOverlapWithKey(String),
96 #[error("Key cannot {0} cannot overlap with contract alias")]
97 KeyCannotOverlapWithContractAlias(String),
98 #[error(transparent)]
99 SecureStore(#[from] secure_store::Error),
100 #[error("Only private keys and seed phrases are supported for getting private keys {0}")]
101 SecretKeyOnly(String),
102 #[error(transparent)]
103 Key(#[from] key::Error),
104 #[error("Unable to get project directory")]
105 ProjectDirsError(),
106}
107
108#[derive(Debug, clap::Args, Default, Clone)]
109#[group(skip)]
110pub struct Args {
111 #[arg(long, global = true, help_heading = HEADING_GLOBAL)]
113 pub global: bool,
114
115 #[arg(long, global = true, help_heading = HEADING_GLOBAL)]
118 pub config_dir: Option<PathBuf>,
119}
120
121pub enum Location {
122 Local(PathBuf),
123 Global(PathBuf),
124}
125
126impl Display for Location {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(
129 f,
130 "{} {:?}",
131 match self {
132 Location::Local(_) => "Local",
133 Location::Global(_) => "Global",
134 },
135 self.as_ref()
136 )
137 }
138}
139
140impl AsRef<Path> for Location {
141 fn as_ref(&self) -> &Path {
142 match self {
143 Location::Local(p) | Location::Global(p) => p.as_path(),
144 }
145 }
146}
147
148impl Location {
149 #[must_use]
150 pub fn wrap(&self, p: PathBuf) -> Self {
151 match self {
152 Location::Local(_) => Location::Local(p),
153 Location::Global(_) => Location::Global(p),
154 }
155 }
156}
157
158impl Args {
159 pub fn config_dir(&self) -> Result<PathBuf, Error> {
160 if self.global {
161 let print = Print::new(false);
162 print.warnln("Flag --global is deprecated: global config is always used");
163 }
164
165 self.global_config_path()
166 }
167
168 pub fn local_and_global(&self) -> Result<[Location; 2], Error> {
169 Ok([
170 Location::Local(self.local_config()?),
171 Location::Global(self.global_config_path()?),
172 ])
173 }
174
175 pub fn local_config(&self) -> Result<PathBuf, Error> {
176 let pwd = self.current_dir()?;
177 Ok(find_config_dir(pwd.clone()).unwrap_or_else(|_| pwd.join(".stellar")))
178 }
179
180 pub fn current_dir(&self) -> Result<PathBuf, Error> {
181 self.config_dir.as_ref().map_or_else(
182 || std::env::current_dir().map_err(|_| Error::CurrentDirNotFound),
183 |pwd| Ok(pwd.clone()),
184 )
185 }
186
187 pub fn write_identity(&self, name: &str, secret: &Secret) -> Result<PathBuf, Error> {
188 if let Ok(Some(_)) = self.load_contract_from_alias(name) {
189 return Err(Error::KeyCannotOverlapWithContractAlias(name.to_owned()));
190 }
191 KeyType::Identity.write(name, secret, &self.config_dir()?)
192 }
193
194 pub fn write_public_key(
195 &self,
196 name: &str,
197 public_key: &stellar_strkey::ed25519::PublicKey,
198 ) -> Result<PathBuf, Error> {
199 self.write_key(name, &public_key.into())
200 }
201
202 pub fn write_key(&self, name: &str, key: &Key) -> Result<PathBuf, Error> {
203 KeyType::Identity.write(name, key, &self.config_dir()?)
204 }
205
206 pub fn write_network(&self, name: &str, network: &Network) -> Result<PathBuf, Error> {
207 KeyType::Network.write(name, network, &self.config_dir()?)
208 }
209
210 pub fn write_default_network(&self, name: &str) -> Result<(), Error> {
211 Config::new()?.set_network(name).save()
212 }
213
214 pub fn write_default_identity(&self, name: &str) -> Result<(), Error> {
215 Config::new()?.set_identity(name).save()
216 }
217
218 pub fn write_default_inclusion_fee(&self, inclusion_fee: u32) -> Result<(), Error> {
219 Config::new()?.set_inclusion_fee(inclusion_fee).save()
220 }
221
222 pub fn unset_default_identity(&self) -> Result<(), Error> {
223 Config::new()?.unset_identity().save()
224 }
225
226 pub fn unset_default_network(&self) -> Result<(), Error> {
227 Config::new()?.unset_network().save()
228 }
229
230 pub fn unset_default_inclusion_fee(&self) -> Result<(), Error> {
231 Config::new()?.unset_inclusion_fee().save()
232 }
233
234 pub fn list_identities(&self) -> Result<Vec<String>, Error> {
235 Ok(KeyType::Identity
236 .list_paths(&self.local_and_global()?)?
237 .into_iter()
238 .map(|(name, _)| name)
239 .collect())
240 }
241
242 pub fn list_identities_long(&self) -> Result<Vec<(String, String)>, Error> {
243 Ok(KeyType::Identity
244 .list_paths(&self.local_and_global()?)
245 .into_iter()
246 .flatten()
247 .map(|(name, location)| {
248 let path = match location {
249 Location::Local(path) | Location::Global(path) => path,
250 };
251 (name, format!("{}", path.display()))
252 })
253 .collect())
254 }
255
256 pub fn list_networks(&self) -> Result<Vec<String>, Error> {
257 let saved_networks = KeyType::Network
258 .list_paths(&self.local_and_global()?)
259 .into_iter()
260 .flatten()
261 .map(|x| x.0);
262 let default_networks = network::DEFAULTS.keys().map(ToString::to_string);
263 Ok(saved_networks.chain(default_networks).unique().collect())
264 }
265
266 pub fn list_networks_long(&self) -> Result<Vec<(String, Network, String)>, Error> {
267 let saved_networks = KeyType::Network
268 .list_paths(&self.local_and_global()?)
269 .into_iter()
270 .flatten()
271 .filter_map(|(name, location)| {
272 Some((
273 name,
274 KeyType::read_from_path::<Network>(location.as_ref()).ok()?,
275 location.to_string(),
276 ))
277 });
278 let default_networks = network::DEFAULTS
279 .into_iter()
280 .map(|(name, network)| ((*name).to_string(), network.into(), "Default".to_owned()));
281 Ok(saved_networks.chain(default_networks).collect())
282 }
283
284 pub fn read_identity(&self, name: &str) -> Result<Key, Error> {
285 KeyType::Identity.read_with_global(name, self)
286 }
287
288 pub fn read_key(&self, key_or_name: &str) -> Result<Key, Error> {
289 key_or_name
290 .parse()
291 .or_else(|_| self.read_identity(key_or_name))
292 }
293
294 pub fn get_secret_key(&self, key_or_name: &str) -> Result<Secret, Error> {
295 match self.read_key(key_or_name)? {
296 Key::Secret(s) => Ok(s),
297 _ => Err(Error::SecretKeyOnly(key_or_name.to_string())),
298 }
299 }
300
301 pub fn get_public_key(
302 &self,
303 key_or_name: &str,
304 hd_path: Option<usize>,
305 ) -> Result<xdr::MuxedAccount, Error> {
306 Ok(self.read_key(key_or_name)?.muxed_account(hd_path)?)
307 }
308
309 pub fn read_network(&self, name: &str) -> Result<Network, Error> {
310 let res = KeyType::Network.read_with_global(name, self);
311 if let Err(Error::ConfigMissing(_, _)) = &res {
312 let Some(network) = network::DEFAULTS.get(name) else {
313 return res;
314 };
315 return Ok(network.into());
316 }
317 res
318 }
319
320 pub fn remove_identity(&self, name: &str, global_args: &global::Args) -> Result<(), Error> {
321 let print = Print::new(global_args.quiet);
322 let identity = self.read_identity(name)?;
323
324 if let Key::Secret(Secret::SecureStore { entry_name }) = identity {
325 secure_store::delete_secret(&print, &entry_name)?;
326 }
327
328 print.infoln("Removing the key's cli config file");
329 KeyType::Identity.remove(name, &self.config_dir()?)
330 }
331
332 pub fn remove_network(&self, name: &str) -> Result<(), Error> {
333 KeyType::Network.remove(name, &self.config_dir()?)
334 }
335
336 fn load_contract_from_alias(&self, alias: &str) -> Result<Option<alias::Data>, Error> {
337 let file_name = format!("{alias}.json");
338 let config_dirs = self.local_and_global()?;
339 let local = &config_dirs[0];
340 let global = &config_dirs[1];
341
342 match local {
343 Location::Local(config_dir) => {
344 let path = config_dir.join("contract-ids").join(&file_name);
345 if path.exists() {
346 print_deprecation_warning(config_dir);
347
348 let content = fs::read_to_string(path)?;
349 let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
350
351 return Ok(Some(data));
352 }
353 }
354 Location::Global(_) => unreachable!(),
355 }
356
357 match global {
358 Location::Global(config_dir) => {
359 let path = config_dir.join("contract-ids").join(&file_name);
360 if !path.exists() {
361 return Ok(None);
362 }
363
364 let content = fs::read_to_string(path)?;
365 let data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
366
367 Ok(Some(data))
368 }
369 Location::Local(_) => unreachable!(),
370 }
371 }
372
373 fn alias_path(&self, alias: &str) -> Result<PathBuf, Error> {
374 let file_name = format!("{alias}.json");
375 let config_dir = self.config_dir()?;
376 Ok(config_dir.join("contract-ids").join(file_name))
377 }
378
379 pub fn save_contract_id(
380 &self,
381 network_passphrase: &str,
382 contract_id: &stellar_strkey::Contract,
383 alias: &str,
384 ) -> Result<(), Error> {
385 if self.read_identity(alias).is_ok() {
386 return Err(Error::ContractAliasCannotOverlapWithKey(alias.to_owned()));
387 }
388 let path = self.alias_path(alias)?;
389 let dir = path.parent().ok_or(Error::CannotAccessConfigDir)?;
390
391 create_dir_all(dir).map_err(|_| Error::CannotAccessConfigDir)?;
392
393 let content = fs::read_to_string(&path).unwrap_or_default();
394 let mut data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
395
396 let mut to_file = OpenOptions::new()
397 .create(true)
398 .truncate(true)
399 .write(true)
400 .open(path)?;
401
402 data.ids
403 .insert(network_passphrase.into(), contract_id.to_string());
404
405 let content = serde_json::to_string(&data)?;
406
407 Ok(to_file.write_all(content.as_bytes())?)
408 }
409
410 pub fn remove_contract_id(&self, network_passphrase: &str, alias: &str) -> Result<(), Error> {
411 let path = self.alias_path(alias)?;
412
413 if !path.is_file() {
414 return Err(Error::CannotAccessAliasConfigFile);
415 }
416
417 let content = fs::read_to_string(&path).unwrap_or_default();
418 let mut data: alias::Data = serde_json::from_str(&content).unwrap_or_default();
419
420 let mut to_file = OpenOptions::new()
421 .create(true)
422 .truncate(true)
423 .write(true)
424 .open(path)?;
425
426 data.ids.remove::<str>(network_passphrase);
427
428 let content = serde_json::to_string(&data)?;
429
430 Ok(to_file.write_all(content.as_bytes())?)
431 }
432
433 pub fn get_contract_id(
434 &self,
435 alias: &str,
436 network_passphrase: &str,
437 ) -> Result<Option<Contract>, Error> {
438 let Some(alias_data) = self.load_contract_from_alias(alias)? else {
439 return Ok(None);
440 };
441
442 alias_data
443 .ids
444 .get(network_passphrase)
445 .map(|id| id.parse())
446 .transpose()
447 .map_err(|e| Error::CannotParseContractId(alias.to_owned(), e))
448 }
449
450 pub fn resolve_contract_id(
451 &self,
452 alias_or_contract_id: &str,
453 network_passphrase: &str,
454 ) -> Result<Contract, Error> {
455 let Some(contract) = self.get_contract_id(alias_or_contract_id, network_passphrase)? else {
456 return alias_or_contract_id
457 .parse()
458 .map_err(|e| Error::CannotParseContractId(alias_or_contract_id.to_owned(), e));
459 };
460 Ok(contract)
461 }
462
463 pub fn global_config_path(&self) -> Result<PathBuf, Error> {
464 if let Some(config_dir) = &self.config_dir {
465 return Ok(config_dir.clone());
466 }
467
468 global_config_path()
469 }
470}
471
472pub fn print_deprecation_warning(dir: &Path) {
473 let print = Print::new(false);
474 let global_dir = global_config_path().expect("Couldn't retrieve global directory.");
475 let global_dir = fs::canonicalize(&global_dir).expect("Couldn't expand global directory.");
476
477 if dir == global_dir {
479 return;
480 }
481
482 print.warnln(format!("A local config was found at {dir:?}."));
483 print.blankln(" Local config is deprecated and will be removed in the future.".to_string());
484 print.blankln(format!(
485 " Run `stellar config migrate` to move the local config into the global config ({global_dir:?})."
486 ));
487}
488
489impl Pwd for Args {
490 fn set_pwd(&mut self, pwd: &Path) {
491 self.config_dir = Some(pwd.to_path_buf());
492 }
493}
494
495pub fn ensure_directory(dir: PathBuf) -> Result<PathBuf, Error> {
496 let parent = dir.parent().ok_or(Error::HomeDirNotFound)?;
497 std::fs::create_dir_all(parent).map_err(|_| dir_creation_failed(parent))?;
498 Ok(dir)
499}
500
501fn dir_creation_failed(p: &Path) -> Error {
502 Error::DirCreationFailed {
503 path: p.to_path_buf(),
504 }
505}
506
507pub enum KeyType {
508 Identity,
509 Network,
510 ContractIds,
511}
512
513impl Display for KeyType {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 write!(
516 f,
517 "{}",
518 match self {
519 KeyType::Identity => "identity",
520 KeyType::Network => "network",
521 KeyType::ContractIds => "contract-ids",
522 }
523 )
524 }
525}
526
527impl KeyType {
528 pub fn read_from_path<T: DeserializeOwned>(path: &Path) -> Result<T, Error> {
529 let data = fs::read_to_string(path).map_err(|_| Error::NetworkFileRead {
530 path: path.to_path_buf(),
531 })?;
532 Ok(toml::from_str(&data)?)
533 }
534
535 pub fn read_with_global<T: DeserializeOwned>(
536 &self,
537 key: &str,
538 locator: &Args,
539 ) -> Result<T, Error> {
540 for location in locator.local_and_global()? {
541 let path = self.path(location.as_ref(), key);
542
543 if let Ok(t) = Self::read_from_path(&path) {
544 if let Location::Local(config_dir) = location {
545 print_deprecation_warning(&config_dir);
546 }
547
548 return Ok(t);
549 }
550 }
551 Err(Error::ConfigMissing(self.to_string(), key.to_string()))
552 }
553
554 pub fn write<T: serde::Serialize>(
555 &self,
556 key: &str,
557 value: &T,
558 pwd: &Path,
559 ) -> Result<PathBuf, Error> {
560 let filepath = ensure_directory(self.path(pwd, key))?;
561 let data = toml::to_string(value).map_err(|_| Error::ConfigSerialization)?;
562 std::fs::write(&filepath, data).map_err(|error| Error::IdCreationFailed {
563 filepath: filepath.clone(),
564 error,
565 })?;
566 Ok(filepath)
567 }
568
569 fn root(&self, pwd: &Path) -> PathBuf {
570 pwd.join(self.to_string())
571 }
572
573 fn path(&self, pwd: &Path, key: &str) -> PathBuf {
574 let mut path = self.root(pwd).join(key);
575 path.set_extension("toml");
576 path
577 }
578
579 pub fn list_paths(&self, paths: &[Location]) -> Result<Vec<(String, Location)>, Error> {
580 Ok(paths
581 .iter()
582 .unique_by(|p| location_to_string(p))
583 .flat_map(|p| self.list(p, true).unwrap_or_default())
584 .collect())
585 }
586
587 pub fn list_paths_silent(&self, paths: &[Location]) -> Result<Vec<(String, Location)>, Error> {
588 Ok(paths
589 .iter()
590 .flat_map(|p| self.list(p, false).unwrap_or_default())
591 .collect())
592 }
593
594 #[allow(unused_variables)]
595 pub fn list(
596 &self,
597 pwd: &Location,
598 print_warning: bool,
599 ) -> Result<Vec<(String, Location)>, Error> {
600 let path = self.root(pwd.as_ref());
601 if path.exists() {
602 let mut files = self.read_dir(&path)?;
603 files.sort();
604
605 if let Location::Local(config_dir) = pwd {
606 if files.len() > 1 && print_warning {
607 print_deprecation_warning(config_dir);
608 }
609 }
610
611 Ok(files
612 .into_iter()
613 .map(|(name, p)| (name, pwd.wrap(p)))
614 .collect())
615 } else {
616 Ok(vec![])
617 }
618 }
619
620 fn read_dir(&self, dir: &Path) -> Result<Vec<(String, PathBuf)>, Error> {
621 let contents = std::fs::read_dir(dir)?;
622 let mut res = vec![];
623 for entry in contents.filter_map(Result::ok) {
624 let path = entry.path();
625 let extension = match self {
626 KeyType::Identity | KeyType::Network => "toml",
627 KeyType::ContractIds => "json",
628 };
629 if let Some(ext) = path.extension().and_then(OsStr::to_str) {
630 if ext == extension {
631 if let Some(os_str) = path.file_stem() {
632 res.push((os_str.to_string_lossy().trim().to_string(), path));
633 }
634 }
635 }
636 }
637 res.sort();
638 Ok(res)
639 }
640
641 pub fn remove(&self, key: &str, pwd: &Path) -> Result<(), Error> {
642 let path = self.path(pwd, key);
643
644 if path.exists() {
645 std::fs::remove_file(&path)
646 .map_err(|_| Error::ConfigRemoval(self.to_string(), key.to_string()))
647 } else {
648 Ok(())
649 }
650 }
651}
652
653fn global_config_path() -> Result<PathBuf, Error> {
654 if let Ok(config_home) = std::env::var("STELLAR_CONFIG_HOME") {
655 return PathBuf::from_str(&config_home).map_err(|_| Error::StellarConfigDir(config_home));
656 }
657
658 let config_dir = if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
659 PathBuf::from_str(&config_home).map_err(|_| Error::XdgConfigHome(config_home))?
660 } else {
661 UserDirs::new()
662 .ok_or(Error::HomeDirNotFound)?
663 .home_dir()
664 .join(".config")
665 };
666
667 let soroban_dir = config_dir.join("soroban");
668 let stellar_dir = config_dir.join("stellar");
669 let soroban_exists = soroban_dir.exists();
670 let stellar_exists = stellar_dir.exists();
671
672 if stellar_exists && soroban_exists {
673 tracing::warn!("the .stellar and .soroban config directories exist at path {config_dir:?}, using the .stellar");
674 }
675
676 if stellar_exists {
677 return Ok(stellar_dir);
678 }
679
680 if soroban_exists {
681 return Ok(soroban_dir);
682 }
683
684 Ok(stellar_dir)
685}
686
687fn location_to_string(location: &Location) -> String {
688 match location {
689 Location::Local(p) | Location::Global(p) => fs::canonicalize(AsRef::<Path>::as_ref(p))
690 .unwrap_or(p.clone())
691 .display()
692 .to_string(),
693 }
694}
695
696pub fn cli_config_file() -> Result<PathBuf, Error> {
699 Ok(global_config_path()?.join("config.toml"))
700}