Skip to main content

iota_config/
persisted_config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Filesystem-backed configuration helpers.
6
7use std::{
8    fs,
9    io::BufWriter,
10    path::{Path, PathBuf},
11};
12
13use anyhow::{Context, Result};
14use iota_types::multiaddr::Multiaddr;
15use serde::{Serialize, de::DeserializeOwned};
16use tracing::trace;
17
18use crate::{IOTA_CONFIG_DIR, IOTA_GENESIS_FILENAME};
19
20const IOTA_DIR: &str = ".iota";
21
22/// Return the IOTA config directory, creating it if it doesn't exist.
23pub fn iota_config_dir() -> Result<PathBuf, anyhow::Error> {
24    match std::env::var_os("IOTA_CONFIG_DIR") {
25        Some(config_env) => Ok(config_env.into()),
26        None => match dirs::home_dir() {
27            Some(v) => Ok(v.join(IOTA_DIR).join(IOTA_CONFIG_DIR)),
28            None => anyhow::bail!("cannot obtain home directory path"),
29        },
30    }
31    .and_then(|dir| {
32        if !dir.exists() {
33            fs::create_dir_all(dir.clone())?;
34        }
35        Ok(dir)
36    })
37}
38
39/// Check if the genesis blob exists in the given directory or the default
40/// directory.
41pub fn genesis_blob_exists(config_dir: Option<PathBuf>) -> bool {
42    if let Some(dir) = config_dir {
43        dir.join(IOTA_GENESIS_FILENAME).exists()
44    } else if let Some(config_env) = std::env::var_os("IOTA_CONFIG_DIR") {
45        Path::new(&config_env).join(IOTA_GENESIS_FILENAME).exists()
46    } else if let Some(home) = dirs::home_dir() {
47        let mut config = PathBuf::new();
48        config.push(&home);
49        config.extend([IOTA_DIR, IOTA_CONFIG_DIR, IOTA_GENESIS_FILENAME]);
50        config.exists()
51    } else {
52        false
53    }
54}
55
56/// Config file name for the validator at the given address (or index).
57pub fn validator_config_file(address: Multiaddr, i: usize) -> String {
58    multiaddr_to_filename(address).unwrap_or(format!("validator-config-{i}.yaml"))
59}
60
61/// Config file name for the State Sync Full Node at the given address (or
62/// index).
63pub fn ssfn_config_file(address: Multiaddr, i: usize) -> String {
64    multiaddr_to_filename(address).unwrap_or(format!("ssfn-config-{i}.yaml"))
65}
66
67/// Derive a `<hostname>-<port>.yaml` file name from a multiaddr, if possible.
68fn multiaddr_to_filename(address: Multiaddr) -> Option<String> {
69    if let Some(hostname) = address.hostname() {
70        if let Some(port) = address.port() {
71            return Some(format!("{hostname}-{port}.yaml"));
72        }
73    }
74    None
75}
76
77/// A config type that can be loaded from and saved to a YAML file on disk.
78pub trait Config
79where
80    Self: DeserializeOwned + Serialize,
81{
82    fn persisted(self, path: &Path) -> PersistedConfig<Self> {
83        PersistedConfig {
84            inner: self,
85            path: path.to_path_buf(),
86        }
87    }
88
89    fn load<P: AsRef<Path>>(path: P) -> Result<Self, anyhow::Error> {
90        let path = path.as_ref();
91        trace!("Reading config from {}", path.display());
92        let reader = fs::File::open(path)
93            .with_context(|| format!("unable to load config from {}", path.display()))?;
94        Ok(serde_yaml::from_reader(reader)?)
95    }
96
97    fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), anyhow::Error> {
98        let path = path.as_ref();
99        trace!("Writing config to {}", path.display());
100        let mut write = BufWriter::new(fs::File::create(path)?);
101        serde_yaml::to_writer(&mut write, &self)
102            .with_context(|| format!("unable to save config to {}", path.display()))?;
103        Ok(())
104    }
105}
106
107/// A [`Config`] paired with the on-disk path it is persisted to.
108pub struct PersistedConfig<C> {
109    inner: C,
110    path: PathBuf,
111}
112
113impl<C> PersistedConfig<C>
114where
115    C: Config,
116{
117    pub fn read(path: &Path) -> Result<C, anyhow::Error> {
118        Config::load(path)
119    }
120
121    pub fn save(&self) -> Result<(), anyhow::Error> {
122        self.inner.save(&self.path)
123    }
124
125    pub fn into_inner(self) -> C {
126        self.inner
127    }
128
129    pub fn path(&self) -> &Path {
130        &self.path
131    }
132}
133
134impl<C> std::ops::Deref for PersistedConfig<C> {
135    type Target = C;
136
137    fn deref(&self) -> &Self::Target {
138        &self.inner
139    }
140}
141
142impl<C> std::ops::DerefMut for PersistedConfig<C> {
143    fn deref_mut(&mut self) -> &mut Self::Target {
144        &mut self.inner
145    }
146}