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