1use base_sdk::{
7 core::Shard,
8 crypto::{
9 ed25519,
10 read_keypair,
11 },
12};
13use serde::{
14 Deserialize,
15 Serialize,
16};
17use snafu::{
18 OptionExt,
19 ResultExt,
20 Snafu,
21};
22use std::{
23 num::NonZero,
24 path::{
25 Path,
26 PathBuf,
27 },
28};
29
30#[derive(Debug, Snafu)]
32pub enum Error {
33 #[snafu(display("Could not read config: {source}"))]
35 ReadConfig {
36 source: config::ConfigError,
38 },
39
40 #[snafu(display("Could not read keypair: {source}"))]
42 ReadKey {
43 source: base_sdk::crypto::IoError,
45 },
46
47 #[snafu(display("Shard 0 is reserved and cannot be a domain"))]
49 ShardZeroReserved,
50 #[snafu(display("Error getting current directory: check permission or existence"))]
52 BadCurrentDirectory,
53}
54
55#[derive(Debug, Deserialize, Serialize, Clone)]
60pub struct ConfigFile {
61 pub shard: Shard,
63 pub keypair: PathBuf,
69 pub rpc_url: Option<String>,
72 pub storage: Option<StorageConfig>,
74}
75
76#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
78pub enum StorageConfig {
79 #[serde(rename = "rocksdb")]
81 RocksDb {
82 path: PathBuf,
84 },
85 #[serde(rename = "in_memory")]
87 InMemory,
88}
89
90#[doc = include_str!("../doc/domain.yaml")]
97#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct Config {
105 pub shard: NonZero<Shard>,
107 pub keypair: ed25519::PrivKey,
110 pub rpc_url: Option<String>,
113 pub storage: Option<StorageConfig>,
115}
116
117impl Config {
118 pub fn load() -> Result<Self, Error> {
125 let current_directory = std::env::current_dir().map_err(|_| Error::BadCurrentDirectory)?;
126 Self::read(config::File::with_name("domain"), current_directory)
127 }
128
129 pub fn load_from(path: impl AsRef<Path>) -> Result<Self, Error> {
136 let path = path.as_ref();
137 let config_dir = path
138 .parent()
139 .map(|p| p.to_path_buf())
140 .unwrap_or_else(|| PathBuf::from("."));
141 Self::read(config::File::from(path), config_dir)
142 }
143
144 fn read<S>(src: S, config_dir: PathBuf) -> Result<Self, Error>
145 where
146 S: config::Source + Send + Sync + 'static,
147 {
148 let ConfigFile {
149 shard,
150 keypair,
151 rpc_url,
152 storage: storage_path,
153 } = config::Config::builder()
154 .add_source(src)
155 .add_source(config::Environment::with_prefix("DOMAIN"))
156 .build()
157 .context(ReadConfigSnafu)?
158 .try_deserialize()
159 .context(ReadConfigSnafu)?;
160
161 let shard = NonZero::new(shard).context(ShardZeroReservedSnafu)?;
162
163 let keypair_path = if keypair.is_relative() {
165 config_dir.join(&keypair)
166 } else {
167 keypair
168 };
169 tracing::debug!("Reading keypair from {}", keypair_path.display());
170
171 let keypair = read_keypair(keypair_path).context(ReadKeySnafu)?;
172 tracing::info!("Using public key: {}", keypair.pub_key());
173
174 Ok(Self {
175 shard,
176 keypair,
177 rpc_url,
178 storage: storage_path,
179 })
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use serial_test::{
187 parallel,
188 serial,
189 };
190 use serializers::json;
191
192 fn setup_path(prefix: impl AsRef<Path>) -> (PathBuf, ed25519::PrivKey) {
193 let dir = std::env::temp_dir().join(prefix);
194 std::fs::create_dir_all(&dir).unwrap();
195 let keypair = ed25519::PrivKey::generate();
196 let keypath = dir.join("key.json");
197 json::write_path(keypath, &keypair).unwrap();
198 (dir, keypair)
199 }
200
201 #[test]
202 #[parallel]
203 fn test_from_yaml() {
204 let (path, keypair) = setup_path("yml");
205 let yaml_path = path.join("domain.yaml");
206
207 let yaml = format!(
208 r#"
209 shard: 123
210 keypair: {}/key.json
211 rpc_url: http://localhost:9000
212 storage:
213 rocksdb:
214 path: /tmp/db
215 "#,
216 path.display()
217 );
218 std::fs::write(&yaml_path, yaml).unwrap();
219
220 assert_eq!(
221 Config::load_from(&yaml_path).unwrap(),
222 Config {
223 shard: NonZero::new(123).unwrap(),
224 keypair: keypair.clone(),
225 rpc_url: Some("http://localhost:9000".to_string()),
226 storage: Some(StorageConfig::RocksDb {
227 path: "/tmp/db".into()
228 })
229 }
230 );
231
232 let yaml = format!(
233 r#"
234 shard: 123
235 keypair: {}/key.json
236 "#,
237 path.display()
238 );
239 std::fs::write(&yaml_path, yaml).unwrap();
240
241 assert_eq!(
242 Config::load_from(&yaml_path).unwrap(),
243 Config {
244 shard: NonZero::new(123).unwrap(),
245 keypair,
246 rpc_url: None,
247 storage: None,
248 }
249 );
250
251 let yaml = format!(
252 r#"
253 shard: hello
254 keypair: {}/key.json
255 "#,
256 path.display()
257 );
258 std::fs::write(&yaml_path, yaml).unwrap();
259 assert!(Config::load_from(&yaml_path).is_err());
260
261 let yaml = r#"
262 shard: 1
263 keypair: ../wrong/path/key.json
264 "#;
265 std::fs::write(&yaml_path, yaml).unwrap();
266 assert!(Config::load_from(&yaml_path).is_err());
267
268 let yaml = format!("keypair: {}/key.json", path.display());
269 std::fs::write(&yaml_path, yaml).unwrap();
270 assert!(Config::load_from(yaml_path).is_err());
271 }
272
273 #[test]
274 #[parallel]
275 fn test_from_toml() {
276 let (path, keypair) = setup_path("toml");
277 let toml = format!(
278 r#"
279 shard = 123
280 keypair = "{}/key.json"
281 "#,
282 path.display()
283 );
284 let path = path.join("domain.toml");
285 std::fs::write(&path, toml).unwrap();
286
287 assert_eq!(
288 Config::load_from(path).unwrap(),
289 Config {
290 shard: NonZero::new(123).unwrap(),
291 keypair,
292 rpc_url: None,
293 storage: None,
294 }
295 );
296 }
297
298 #[test]
299 #[parallel]
300 fn test_from_json() {
301 let (path, keypair) = setup_path("json");
302 let json = format!(
303 r#"{{
304 "shard": "123",
305 "keypair": "{}/key.json"
306 }}"#,
307 path.display()
308 );
309 let path = path.join("domain.json");
310 std::fs::write(&path, json).unwrap();
311
312 assert_eq!(
313 Config::load_from(path).unwrap(),
314 Config {
315 shard: NonZero::new(123).unwrap(),
316 keypair,
317 rpc_url: None,
318 storage: None,
319 }
320 );
321 }
322
323 #[test]
324 #[parallel]
325 fn test_relative_from_json() {
326 let (path, keypair) = setup_path("json_rel");
329
330 let json = r#"{
331 "shard": "123",
332 "keypair": "key.json"
333 }"#
334 .to_string();
335 let path1 = path.join("domain_relative_straight.json");
336 std::fs::write(&path1, json).unwrap();
337
338 assert_eq!(
339 Config::load_from(path1).unwrap(),
340 Config {
341 shard: NonZero::new(123).unwrap(),
342 keypair: keypair.clone(),
343 rpc_url: None,
344 storage: None,
345 }
346 );
347
348 let json = r#"{
349 "shard": "123",
350 "keypair": "./key.json"
351 }"#
352 .to_string();
353 let path2 = path.join("domain_relative_dot.json");
354 std::fs::write(&path2, json).unwrap();
355
356 assert_eq!(
357 Config::load_from(path2).unwrap(),
358 Config {
359 shard: NonZero::new(123).unwrap(),
360 keypair,
361 rpc_url: None,
362 storage: None,
363 }
364 );
365
366 let (path, keypair) = setup_path("some/json/subfolder");
369
370 let dirname = path
373 .components()
374 .next_back()
375 .unwrap()
377 .as_os_str()
378 .to_str()
379 .unwrap();
380
381 let json = format!(
382 r#"{{
383 "shard": "123",
384 "keypair": "../{dirname}/key.json"
385 }}"#
386 );
387 let path = path.join("domain_relative_dotdot.json");
388 std::fs::write(&path, json).unwrap();
389
390 assert_eq!(
391 Config::load_from(path).unwrap(),
392 Config {
393 shard: NonZero::new(123).unwrap(),
394 keypair,
395 rpc_url: None,
396 storage: None,
397 }
398 );
399 }
400
401 #[test]
402 #[serial]
403 fn test_env_override() {
404 let (path, keypair) = setup_path("env");
405 let json = format!(
406 r#"{{
407 "keypair": "{}/key.json",
408 "rpc_url": "https://awesome.net/rpc"
409 }}"#,
410 path.display()
411 );
412 let path = path.join("domain.json");
413 std::fs::write(&path, json).unwrap();
414
415 std::env::set_var("DOMAIN_SHARD", "666");
417 std::env::set_var("DOMAIN_RPC_URL", "http://boring.but/real");
418
419 assert_eq!(
420 Config::load_from(path).unwrap(),
421 Config {
422 shard: NonZero::new(666).unwrap(),
423 keypair,
424 rpc_url: Some("http://boring.but/real".to_string()),
425 storage: None,
426 }
427 );
428 }
429}