sui_replay/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    // Assume local node
141    "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
152/// If rpc_url is provided, use it. Otherwise, load the network config from the config file.
153pub 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    // Read from file
192    let data = ReplayableNetworkConfigSet::load_config(path_str).unwrap();
193    set.path = Some(final_path);
194    assert!(set == data);
195}