iota_replay/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fs::File, io::BufReader, path::PathBuf, str::FromStr};
6
7use http::Uri;
8use serde::{Deserialize, Serialize};
9use serde_with::serde_as;
10use tracing::info;
11
12use crate::types::ReplayEngineError;
13
14pub const DEFAULT_CONFIG_PATH: &str = "~/.iota-replay/network-config.yaml";
15
16#[serde_as]
17#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
18#[serde(rename_all = "kebab-case")]
19pub struct ReplayableNetworkConfigSet {
20    #[serde(skip)]
21    path: Option<PathBuf>,
22    #[serde(default)]
23    pub base_network_configs: Vec<ReplayableNetworkBaseConfig>,
24}
25
26#[serde_as]
27#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
28#[serde(rename_all = "kebab-case")]
29pub struct ReplayableNetworkBaseConfig {
30    pub name: String,
31    #[serde(default)]
32    pub epoch_zero_start_timestamp: u64,
33    #[serde(default)]
34    pub epoch_zero_rgp: u64,
35    #[serde(default = "default_full_node_address")]
36    pub public_full_node: String,
37}
38
39impl ReplayableNetworkConfigSet {
40    pub fn load_config(path: String) -> Result<Self, ReplayEngineError> {
41        let path = shellexpand::tilde(&path).to_string();
42        let path = PathBuf::from_str(&path).unwrap();
43        ReplayableNetworkConfigSet::from_file(path.clone()).map_err(|err| {
44            ReplayEngineError::UnableToOpenYamlFile {
45                path: path.as_os_str().to_string_lossy().to_string(),
46                err: err.to_string(),
47            }
48        })
49    }
50
51    pub fn save_config(&self, override_path: Option<String>) -> Result<PathBuf, ReplayEngineError> {
52        let path = override_path.unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string());
53        let path = shellexpand::tilde(&path).to_string();
54        let path = PathBuf::from_str(&path).unwrap();
55        self.to_file(path.clone())
56            .map_err(|err| ReplayEngineError::UnableToOpenYamlFile {
57                path: path.as_os_str().to_string_lossy().to_string(),
58                err: err.to_string(),
59            })?;
60        Ok(path)
61    }
62
63    pub fn from_file(path: PathBuf) -> Result<Self, ReplayEngineError> {
64        let file =
65            File::open(path.clone()).map_err(|err| ReplayEngineError::UnableToOpenYamlFile {
66                path: path.as_os_str().to_string_lossy().to_string(),
67                err: err.to_string(),
68            })?;
69        let reader = BufReader::new(file);
70        let mut config: ReplayableNetworkConfigSet =
71            serde_yaml::from_reader(reader).map_err(|err| {
72                ReplayEngineError::UnableToOpenYamlFile {
73                    path: path.as_os_str().to_string_lossy().to_string(),
74                    err: err.to_string(),
75                }
76            })?;
77        config.path = Some(path);
78        Ok(config)
79    }
80
81    pub fn to_file(&self, path: PathBuf) -> Result<(), ReplayEngineError> {
82        let prefix = path.parent().unwrap();
83        std::fs::create_dir_all(prefix).unwrap();
84        let file =
85            File::create(path.clone()).map_err(|err| ReplayEngineError::UnableToOpenYamlFile {
86                path: path.as_os_str().to_string_lossy().to_string(),
87                err: err.to_string(),
88            })?;
89        serde_yaml::to_writer(file, self).map_err(|err| {
90            ReplayEngineError::UnableToWriteYamlFile {
91                path: path.as_os_str().to_string_lossy().to_string(),
92                err: err.to_string(),
93            }
94        })?;
95        Ok(())
96    }
97
98    pub fn get_base_config(&self, chain: &str) -> Option<&ReplayableNetworkBaseConfig> {
99        self.base_network_configs.iter().find(|c| c.name == chain)
100    }
101}
102
103impl Default for ReplayableNetworkConfigSet {
104    fn default() -> Self {
105        let testnet = ReplayableNetworkBaseConfig {
106            name: "testnet".to_string(),
107            epoch_zero_start_timestamp: 0,
108            epoch_zero_rgp: 0,
109            public_full_node: url_from_str("https://api.testnet.iota.cafe")
110                .expect("invalid socket address")
111                .to_string(),
112        };
113        let devnet = ReplayableNetworkBaseConfig {
114            name: "devnet".to_string(),
115            epoch_zero_start_timestamp: 0,
116            epoch_zero_rgp: 0,
117            public_full_node: url_from_str("https://api.devnet.iota.cafe")
118                .expect("invalid socket address")
119                .to_string(),
120        };
121        let mainnet = ReplayableNetworkBaseConfig {
122            name: "mainnet".to_string(),
123            epoch_zero_start_timestamp: 0,
124            epoch_zero_rgp: 0,
125            public_full_node: url_from_str("https://api.mainnet.iota.cafe")
126                .expect("invalid socket address")
127                .to_string(),
128        };
129
130        Self {
131            path: None,
132            base_network_configs: vec![testnet, devnet, mainnet],
133        }
134    }
135}
136
137pub fn default_full_node_address() -> String {
138    // Assume local node
139    "0.0.0.0:9000".to_string()
140}
141
142pub fn url_from_str(s: &str) -> Result<Uri, ReplayEngineError> {
143    Uri::from_str(s).map_err(|e| ReplayEngineError::InvalidUrl {
144        err: e.to_string(),
145        url: s.to_string(),
146    })
147}
148
149/// If rpc_url is provided, use it. Otherwise, load the network config from the
150/// config file.
151pub fn get_rpc_url(
152    rpc_url: Option<String>,
153    config_path: Option<PathBuf>,
154    chain: Option<String>,
155) -> anyhow::Result<String> {
156    if let Some(url) = rpc_url {
157        return Ok(url);
158    }
159
160    let config_path = config_path
161        .map(|p| p.to_str().unwrap().to_string())
162        .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string());
163    let chain = chain.unwrap_or_else(|| "mainnet".to_string());
164    info!(
165        "RPC URL not provided. Loading network config for {:?} from config file {:?}. \
166                    If a different chain is desired, please provide the chain name.",
167        chain, config_path
168    );
169    let url = ReplayableNetworkConfigSet::load_config(config_path)?
170        .get_base_config(&chain)
171        .ok_or(anyhow::anyhow!(format!(
172            "Unable to find network config for {:?}",
173            chain
174        )))?
175        .public_full_node
176        .clone();
177    Ok(url)
178}
179
180#[test]
181fn test_yaml() {
182    let mut set = ReplayableNetworkConfigSet::default();
183
184    let path = tempfile::tempdir().unwrap().path().to_path_buf();
185    let path_str = path.to_str().unwrap().to_owned();
186
187    let final_path = set.save_config(Some(path_str.clone())).unwrap();
188
189    // Read from file
190    let data = ReplayableNetworkConfigSet::load_config(path_str).unwrap();
191    set.path = Some(final_path);
192    assert!(set == data);
193}