1use 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 "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
149pub 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 let data = ReplayableNetworkConfigSet::load_config(path_str).unwrap();
191 set.path = Some(final_path);
192 assert!(set == data);
193}