saito_rust/
config_handler.rs

1use figment::providers::{Format, Json};
2use figment::Figment;
3use log::{debug, error, info};
4use saito_core::core::util::configuration::{
5    get_default_issuance_writing_block_interval, BlockchainConfig, Configuration, ConsensusConfig,
6    Endpoint, PeerConfig, Server,
7};
8use serde::{Deserialize, Serialize};
9use std::io::{Error, ErrorKind};
10use std::path::Path;
11
12fn get_default_consensus() -> Option<ConsensusConfig> {
13    Some(ConsensusConfig::default())
14}
15
16#[derive(Deserialize, Debug, Serialize)]
17pub struct NodeConfigurations {
18    server: Server,
19    peers: Vec<PeerConfig>,
20    #[serde(skip)]
21    lite: bool,
22    spv_mode: Option<bool>,
23    #[serde(default = "get_default_consensus")]
24    consensus: Option<ConsensusConfig>,
25    blockchain: BlockchainConfig,
26}
27
28impl NodeConfigurations {
29    pub fn write_to_file(&self, config_file_path: String) -> Result<(), Error> {
30        let file = std::fs::File::create(config_file_path)?;
31        serde_json::to_writer_pretty(&file, &self)?;
32        Ok(())
33    }
34}
35impl Default for NodeConfigurations {
36    fn default() -> Self {
37        NodeConfigurations {
38            server: Server {
39                host: "127.0.0.1".to_string(),
40                port: 12101,
41                protocol: "http".to_string(),
42                endpoint: Endpoint {
43                    host: "127.0.0.1".to_string(),
44                    port: 12101,
45                    protocol: "http".to_string(),
46                },
47                verification_threads: 4,
48                channel_size: 1000,
49                stat_timer_in_ms: 5000,
50                thread_sleep_time_in_ms: 10,
51                block_fetch_batch_size: 10,
52                reconnection_wait_time: 10,
53            },
54            peers: vec![],
55            lite: false,
56            spv_mode: Some(false),
57            consensus: Some(ConsensusConfig {
58                genesis_period: 100_000,
59                heartbeat_interval: 5_000,
60                prune_after_blocks: 8,
61                max_staker_recursions: 3,
62                default_social_stake: 0,
63                default_social_stake_period: 60,
64            }),
65            blockchain: BlockchainConfig {
66                last_block_hash: "0000000000000000000000000000000000000000000000000000000000000000"
67                    .to_string(),
68                last_block_id: 0,
69                last_timestamp: 0,
70                genesis_block_id: 0,
71                genesis_timestamp: 0,
72                lowest_acceptable_timestamp: 0,
73                lowest_acceptable_block_hash:
74                    "0000000000000000000000000000000000000000000000000000000000000000".to_string(),
75                lowest_acceptable_block_id: 0,
76                fork_id: "0000000000000000000000000000000000000000000000000000000000000000"
77                    .to_string(),
78                initial_loading_completed: false,
79                issuance_writing_block_interval: get_default_issuance_writing_block_interval(),
80            },
81        }
82    }
83}
84
85impl Configuration for NodeConfigurations {
86    fn get_server_configs(&self) -> Option<&Server> {
87        Some(&self.server)
88    }
89
90    fn get_peer_configs(&self) -> &Vec<PeerConfig> {
91        &self.peers
92    }
93
94    fn get_blockchain_configs(&self) -> &BlockchainConfig {
95        &self.blockchain
96    }
97
98    fn get_block_fetch_url(&self) -> String {
99        let endpoint = &self.get_server_configs().unwrap().endpoint;
100        endpoint.protocol.to_string()
101            + "://"
102            + endpoint.host.as_str()
103            + ":"
104            + endpoint.port.to_string().as_str()
105    }
106
107    fn is_spv_mode(&self) -> bool {
108        self.spv_mode.is_some() && self.spv_mode.unwrap()
109    }
110
111    fn is_browser(&self) -> bool {
112        false
113    }
114
115    fn replace(&mut self, config: &dyn Configuration) {
116        self.server = config.get_server_configs().cloned().unwrap();
117        self.peers = config.get_peer_configs().clone();
118        self.spv_mode = Some(config.is_spv_mode());
119        self.lite = config.is_spv_mode();
120        self.consensus = config.get_consensus_config().cloned();
121    }
122
123    fn get_consensus_config(&self) -> Option<&ConsensusConfig> {
124        self.consensus.as_ref()
125    }
126}
127
128pub struct ConfigHandler {}
129
130impl ConfigHandler {
131    pub fn load_configs(config_file_path: String) -> Result<NodeConfigurations, Error> {
132        debug!(
133            "loading configurations from path : {:?} current_dir = {:?}",
134            config_file_path,
135            std::env::current_dir()
136        );
137        let path = Path::new(config_file_path.as_str());
138        if !path.exists() {
139            info!("writing default config file to : {:?}", config_file_path);
140            if path.parent().is_some() {
141                std::fs::create_dir_all(path.parent().unwrap())?;
142            }
143            let configs = NodeConfigurations::default();
144            configs.write_to_file(config_file_path.to_string())?;
145        }
146        // TODO : add prompt with user friendly format
147        let configs = Figment::new()
148            .merge(Json::file(config_file_path))
149            .extract::<NodeConfigurations>();
150
151        if configs.is_err() {
152            error!("failed loading configs. {:?}", configs.err().unwrap());
153            return Err(std::io::Error::from(ErrorKind::InvalidInput));
154        }
155
156        Ok(configs.unwrap())
157    }
158}
159
160#[cfg(test)]
161mod test {
162    use std::io::ErrorKind;
163
164    use saito_core::core::util::configuration::Configuration;
165
166    use crate::config_handler::ConfigHandler;
167
168    #[test]
169    fn load_config_from_existing_file() {
170        let path = String::from("src/test/data/config_handler_tests.json");
171        let result = ConfigHandler::load_configs(path);
172        assert!(result.is_ok());
173        let configs = result.unwrap();
174        assert_eq!(
175            configs.get_server_configs().unwrap().host,
176            String::from("localhost")
177        );
178        assert_eq!(configs.get_server_configs().unwrap().port, 12101);
179        assert_eq!(
180            configs.get_server_configs().unwrap().protocol,
181            String::from("http")
182        );
183        assert_eq!(
184            configs.get_server_configs().unwrap().endpoint.host,
185            String::from("localhost")
186        );
187        assert_eq!(configs.get_server_configs().unwrap().endpoint.port, 12101);
188        assert_eq!(
189            configs.get_server_configs().unwrap().endpoint.protocol,
190            String::from("http")
191        );
192    }
193
194    #[test]
195    fn load_config_from_bad_file_format() {
196        let path = String::from("src/test/data/config_handler_tests_bad_format.xml");
197        let result = ConfigHandler::load_configs(path);
198        assert!(result.is_err());
199        assert_eq!(result.err().unwrap().kind(), ErrorKind::InvalidInput);
200    }
201
202    // FIX : this test is creating a new config file. so it should be deleted after the test since this test will fail if run again
203    #[ignore]
204    #[test]
205    fn load_config_from_non_existing_file() {
206        // pretty_env_logger::init();
207        let path = String::from("config/new_file_to_write.json");
208        let result = ConfigHandler::load_configs(path);
209        assert!(result.is_ok());
210    }
211}