partner_chains_cli/
config.rs

1use crate::config::config_fields::{
2	CARDANO_ACTIVE_SLOTS_COEFF, CARDANO_EPOCH_DURATION_MILLIS, CARDANO_FIRST_EPOCH_NUMBER,
3	CARDANO_FIRST_EPOCH_TIMESTAMP_MILLIS, CARDANO_FIRST_SLOT_NUMBER, CARDANO_SECURITY_PARAMETER,
4};
5use crate::io::IOContext;
6use clap::Parser;
7use config_fields::CARDANO_SLOT_DURATION_MILLIS;
8use serde::{Deserialize, Serialize, de::DeserializeOwned};
9use sidechain_domain::{MainchainKeyHash, UtxoId};
10use sp_core::offchain::{Duration, Timestamp};
11use std::fmt::{Display, Formatter, Write};
12use std::str::FromStr;
13use std::{marker::PhantomData, process::exit};
14
15/// Definition of a field in a config JSON with optional default value.
16pub(crate) struct ConfigFieldDefinition<'a, T> {
17	/// Config field name
18	pub(crate) name: &'a str,
19	/// Config file name
20	pub(crate) config_file: ConfigFile,
21	/// Path to config field in config file
22	pub(crate) path: &'a [&'a str],
23	/// Optional default value for config field
24	pub(crate) default: Option<&'a str>,
25	_marker: PhantomData<T>,
26}
27
28#[cfg(test)]
29impl<'a, T> ConfigFieldDefinition<'a, T> {
30	pub(crate) fn new(
31		name: &'a str,
32		config_file: ConfigFile,
33		path: &'a [&'a str],
34		default: Option<&'a str>,
35	) -> Self {
36		Self { name, config_file, path, default, _marker: Default::default() }
37	}
38}
39
40impl<'a> ConfigFieldDefinition<'a, String> {
41	/// Tries to load the config field value, as a string, from its expected location, and
42	/// if not found it prompts the user for it.
43	pub(crate) fn load_or_prompt_and_save<C: IOContext>(&self, context: &C) -> String {
44		if let Some(value) = self.load_from_file_and_print(context) {
45			value
46		} else {
47			let value = context.prompt(&format!("Enter the {}", self.name), self.default);
48			self.save_to_file(&value, context);
49			value
50		}
51	}
52
53	/// Prompts for the config field value, as a string, with a default if it is set in [ConfigFieldDefinition].
54	pub(crate) fn prompt_with_default_from_file_and_save<C: IOContext>(
55		&self,
56		context: &C,
57	) -> String {
58		let value = context.prompt(
59			&format!("Enter the {}", self.name),
60			self.load_from_file(context).as_deref().or(self.default),
61		);
62		self.save_to_file(&value, context);
63		value
64	}
65}
66
67impl<'a, T> ConfigFieldDefinition<'a, T> {
68	pub(crate) fn prompt_with_default_from_file_parse_and_save<C: IOContext>(
69		&self,
70		context: &C,
71	) -> Result<T, <T as FromStr>::Err>
72	where
73		T: DeserializeOwned + std::fmt::Display + FromStr + serde::Serialize,
74	{
75		let loaded_value = self.load_from_file(context).map(|v| v.to_string());
76		let default_value = loaded_value.as_deref().or_else(|| self.default);
77		let value = context.prompt(&format!("Enter the {}", self.name), default_value);
78		let parsed_value: T = value.parse()?;
79		self.save_to_file(&parsed_value, context);
80		Ok(parsed_value)
81	}
82
83	pub(crate) fn select_options_with_default_from_file_and_save<C: IOContext>(
84		&self,
85		prompt: &str,
86		context: &C,
87	) -> Result<T, <T as FromStr>::Err>
88	where
89		T: DeserializeOwned
90			+ std::fmt::Display
91			+ FromStr
92			+ serde::Serialize
93			+ SelectOptions<Opt = T>
94			+ Clone
95			+ std::fmt::Debug
96			+ std::panic::UnwindSafe
97			+ std::panic::RefUnwindSafe,
98	{
99		let loaded_value = self.load_from_file(context);
100		let default_value_opt: Option<T> =
101			loaded_value.or_else(|| self.default.and_then(|s| s.parse().ok()));
102		let options: Vec<T> = default_value_opt.map_or_else(
103			|| T::select_options(),
104			|default_value| T::select_options_with_default(&default_value),
105		);
106		let value_ix = context.prompt_multi_option(prompt, options.clone());
107		let value = options[value_ix].clone();
108		self.save_to_file(&value, context);
109		Ok(value)
110	}
111
112	/// Loads and parses the config field value.
113	pub(crate) fn load_from_file<C: IOContext>(&self, context: &C) -> Option<T>
114	where
115		T: DeserializeOwned,
116	{
117		self.load_file(context).and_then(|json| self.extract_from_json_object(&json))
118	}
119
120	fn load_from_file_and_print(&self, context: &impl IOContext) -> Option<T>
121	where
122		T: DeserializeOwned + std::fmt::Display,
123	{
124		let value = self.load_from_file(context)?;
125		context.eprint(&format!(
126			"🛠️ Loaded {} from config ({}): {value}",
127			self.name,
128			context.config_file_path(self.config_file)
129		));
130		Some(value)
131	}
132
133	/// Updates the config field value in the file.
134	pub(crate) fn save_to_file<C: IOContext>(&self, value: &T, context: &C)
135	where
136		T: Serialize,
137	{
138		let mut json =
139			self.load_file(context).unwrap_or(serde_json::Value::Object(Default::default()));
140		let mut head = &mut json;
141		for &field in self.path {
142			head[field] = head
143				.get(field)
144				.cloned()
145				.filter(serde_json::Value::is_object)
146				.unwrap_or(serde_json::Value::Object(Default::default()));
147			head = &mut head[field];
148		}
149		*head = serde_json::to_value(value).unwrap();
150		context.write_file(
151			&context.config_file_path(self.config_file),
152			&serde_json::to_string_pretty(&json).unwrap(),
153		);
154	}
155
156	pub(crate) fn save_if_empty<C: IOContext>(&self, value: T, context: &C) -> T
157	where
158		T: DeserializeOwned + serde::Serialize,
159	{
160		if let Some(value) = self.load_from_file(context) {
161			value
162		} else {
163			self.save_to_file(&value, context);
164			value
165		}
166	}
167
168	/// parses the config field's type from a json value
169	pub(crate) fn extract_from_json_object(&self, json: &serde_json::Value) -> Option<T>
170	where
171		T: DeserializeOwned,
172	{
173		let mut json: Option<&serde_json::Value> = Some(json);
174		for &field in self.path {
175			if let Some(json_inner) = json {
176				json = json_inner.get(field)
177			} else {
178				return None;
179			}
180		}
181		json.and_then(|json| serde_json::from_value(json.clone()).ok())
182	}
183
184	/// loads the whole content of the config fields relevant config file
185	pub(crate) fn load_file<C: IOContext>(&self, context: &C) -> Option<serde_json::Value> {
186		let path = context.config_file_path(self.config_file);
187		if !context.file_exists(&path) {
188			return None;
189		}
190
191		if let Some(file_content_string) = context.read_file(&path) {
192			if let Ok(value) = serde_json::from_str(&file_content_string) {
193				return Some(value);
194			}
195		}
196
197		context.eprint(&format!(
198			"Config file {} is broken. Delete it or fix manually and restart this wizard",
199			path
200		));
201		exit(-1)
202	}
203}
204
205#[derive(Clone, Debug)]
206pub struct ServiceConfig {
207	pub protocol: NetworkProtocol,
208	pub hostname: String,
209	pub port: u16,
210	pub timeout_seconds: u64,
211}
212
213impl ServiceConfig {
214	pub(crate) fn url(&self) -> String {
215		format!("{}://{}:{}", self.protocol, self.hostname, self.port)
216	}
217}
218
219pub(crate) trait SelectOptions {
220	type Opt: PartialEq;
221	fn select_options() -> Vec<Self::Opt>;
222	fn select_options_with_default(default_value: &Self::Opt) -> Vec<Self::Opt> {
223		let mut options = Self::select_options();
224		options.sort_by_key(|option| if option != default_value { 1 } else { 0 });
225		options
226	}
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
230/// A network protocol.
231pub enum NetworkProtocol {
232	#[serde(rename = "http")]
233	/// Http protocol
234	Http,
235	#[serde(rename = "https")]
236	/// Https protocol
237	Https,
238}
239
240impl std::fmt::Display for NetworkProtocol {
241	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
242		let str = match self {
243			NetworkProtocol::Http => "http".to_string(),
244			NetworkProtocol::Https => "https".to_string(),
245		};
246		write!(f, "{}", str)
247	}
248}
249
250impl FromStr for NetworkProtocol {
251	type Err = String;
252
253	fn from_str(s: &str) -> Result<Self, Self::Err> {
254		match s.to_lowercase().as_str() {
255			"http" => Ok(NetworkProtocol::Http),
256			"https" => Ok(NetworkProtocol::Https),
257			other => Err(format!("Invalid security protocol {other}, please provide http or http")),
258		}
259	}
260}
261
262impl SelectOptions for NetworkProtocol {
263	type Opt = NetworkProtocol;
264	fn select_options() -> Vec<NetworkProtocol> {
265		vec![NetworkProtocol::Http, NetworkProtocol::Https]
266	}
267}
268
269#[derive(Deserialize)]
270#[allow(dead_code)]
271pub(crate) struct MainChainAddresses {
272	pub(crate) committee_candidates_address: String,
273	/// unused
274	pub(crate) d_parameter_policy_id: String,
275	/// unused
276	pub(crate) permissioned_candidates_policy_id: String,
277	/// unused
278	pub(crate) bridge: NativeTokenConfig,
279}
280
281#[derive(Deserialize, PartialEq, Clone, Debug)]
282pub(crate) struct CardanoParameters {
283	pub(crate) security_parameter: u64,
284	pub(crate) active_slots_coeff: f64,
285	pub(crate) first_epoch_number: u32,
286	pub(crate) first_slot_number: u64,
287	pub(crate) epoch_duration_millis: u64,
288	pub(crate) first_epoch_timestamp_millis: u64,
289	#[serde(default = "default_slot_duration_millis")]
290	pub(crate) slot_duration_millis: u64,
291}
292
293fn default_slot_duration_millis() -> u64 {
294	1000
295}
296
297impl CardanoParameters {
298	pub(crate) fn save(&self, context: &impl IOContext) {
299		CARDANO_SECURITY_PARAMETER.save_to_file(&self.security_parameter, context);
300		CARDANO_ACTIVE_SLOTS_COEFF.save_to_file(&self.active_slots_coeff, context);
301		CARDANO_FIRST_EPOCH_NUMBER.save_to_file(&self.first_epoch_number, context);
302		CARDANO_FIRST_SLOT_NUMBER.save_to_file(&self.first_slot_number, context);
303		CARDANO_EPOCH_DURATION_MILLIS.save_to_file(&self.epoch_duration_millis, context);
304		CARDANO_FIRST_EPOCH_TIMESTAMP_MILLIS
305			.save_to_file(&self.first_epoch_timestamp_millis, context);
306		CARDANO_SLOT_DURATION_MILLIS.save_to_file(&self.slot_duration_millis, context);
307	}
308}
309
310impl From<CardanoParameters> for sidechain_domain::mainchain_epoch::MainchainEpochConfig {
311	fn from(value: CardanoParameters) -> Self {
312		Self {
313			first_epoch_timestamp_millis: Timestamp::from_unix_millis(
314				value.first_epoch_timestamp_millis,
315			),
316			epoch_duration_millis: Duration::from_millis(value.epoch_duration_millis),
317			first_epoch_number: value.first_epoch_number,
318			first_slot_number: value.first_slot_number,
319			slot_duration_millis: Duration::from_millis(value.slot_duration_millis),
320		}
321	}
322}
323
324#[derive(Deserialize, Serialize, Parser, Clone, Debug)]
325pub(crate) struct SidechainParams {
326	#[arg(long)]
327	pub(crate) genesis_utxo: UtxoId,
328}
329
330impl Display for SidechainParams {
331	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
332		let json_string = serde_json::to_string_pretty(self).unwrap();
333		write!(f, "{}", &json_string)
334	}
335}
336
337#[derive(Deserialize, Serialize, Clone, Debug)]
338pub(crate) struct AssetConfig {
339	policy_id: String,
340	asset_name: String,
341}
342
343#[derive(Deserialize, Serialize, Clone, Debug)]
344pub(crate) struct NativeTokenConfig {
345	pub(crate) asset: AssetConfig,
346	pub(crate) illiquid_circulation_supply_validator_address: String,
347}
348
349#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
350pub(crate) struct GovernanceAuthoritiesKeyHashes(pub(crate) Vec<MainchainKeyHash>);
351
352impl FromStr for GovernanceAuthoritiesKeyHashes {
353	type Err = String;
354
355	fn from_str(s: &str) -> Result<Self, Self::Err> {
356		let mut key_hashes = vec![];
357		for x in s.split_whitespace() {
358			let trimmed = x.trim();
359			let key_hash = MainchainKeyHash::decode_hex(trimmed).map_err(|e| {
360				format!("'{}' is not a valid Cardano Key Hash, reason: {}", trimmed, e)
361			})?;
362			key_hashes.push(key_hash)
363		}
364		Ok(GovernanceAuthoritiesKeyHashes(key_hashes))
365	}
366}
367
368impl Display for GovernanceAuthoritiesKeyHashes {
369	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
370		let mut it = self.0.iter();
371		// Intersperse with a single space
372		if let Some(key_hash) = it.next() {
373			f.write_str(&key_hash.to_hex_string())?;
374			for key_hash in it {
375				f.write_char(' ')?;
376				f.write_str(&key_hash.to_hex_string())?;
377			}
378		}
379		Ok(())
380	}
381}
382
383#[derive(Deserialize)]
384pub(crate) struct ChainConfig {
385	pub(crate) cardano: CardanoParameters,
386	pub(crate) chain_parameters: SidechainParams,
387	pub(crate) cardano_addresses: MainChainAddresses,
388}
389
390#[derive(Copy, Clone)]
391pub enum ConfigFile {
392	Chain,
393	Resources,
394}
395
396pub(crate) const KEYS_FILE_PATH: &str = "partner-chains-public-keys.json";
397pub(crate) const CHAIN_SPEC_PATH: &str = "chain-spec.json";
398
399pub(crate) fn load_chain_config(context: &impl IOContext) -> anyhow::Result<ChainConfig> {
400	let chain_config_file_path = context.config_file_path(ConfigFile::Chain);
401	if let Some(chain_config_file) = context.read_file(&chain_config_file_path) {
402		serde_json::from_str::<ChainConfig>(&chain_config_file)
403			.map_err(|err| anyhow::anyhow!(format!("⚠️ Chain config file {chain_config_file_path} is invalid: {err}. Run prepare-configuration wizard or fix errors manually.")))
404	} else {
405		Err(anyhow::anyhow!(format!(
406			"⚠️ Chain config file {chain_config_file_path} does not exists. Run prepare-configuration wizard first."
407		)))
408	}
409}
410
411pub(crate) mod config_fields {
412	use super::*;
413	use sidechain_domain::{AssetName, MainchainAddress, PolicyId, UtxoId};
414
415	pub(crate) const BRIDGE_TOKEN_POLICY: ConfigFieldDefinition<'static, PolicyId> =
416		ConfigFieldDefinition {
417			config_file: ConfigFile::Chain,
418			path: &["cardano_addresses", "bridge", "asset", "policy_id"],
419			name: "bridge token policy ID",
420			default: None,
421			_marker: PhantomData,
422		};
423
424	pub(crate) const BRIDGE_TOKEN_ASSET_NAME: ConfigFieldDefinition<'static, AssetName> =
425		ConfigFieldDefinition {
426			config_file: ConfigFile::Chain,
427			path: &["cardano_addresses", "bridge", "asset", "asset_name"],
428			name: "bridge token asset name in hex",
429			default: None,
430			_marker: PhantomData,
431		};
432
433	pub(crate) const ILLIQUID_SUPPLY_ADDRESS: ConfigFieldDefinition<'static, MainchainAddress> =
434		ConfigFieldDefinition {
435			config_file: ConfigFile::Chain,
436			path: &["cardano_addresses", "bridge", "illiquid_circulation_supply_validator_address"],
437			name: "bridge token illiquid circullation supply address",
438			default: None,
439			_marker: PhantomData,
440		};
441
442	pub(crate) const SUBSTRATE_NODE_DATA_BASE_PATH: ConfigFieldDefinition<'static, String> =
443		ConfigFieldDefinition {
444			config_file: ConfigFile::Resources,
445			path: &["substrate_node_base_path"],
446			name: "node base path",
447			default: Some("./data"),
448			_marker: PhantomData,
449		};
450
451	pub(crate) const CARDANO_PAYMENT_VERIFICATION_KEY_FILE: ConfigFieldDefinition<'static, String> =
452		ConfigFieldDefinition {
453			config_file: ConfigFile::Resources,
454			path: &["cardano_payment_verification_key_file"],
455			name: "path to the payment verification file",
456			default: Some("payment.vkey"),
457			_marker: PhantomData,
458		};
459
460	pub(crate) const CARDANO_PAYMENT_SIGNING_KEY_FILE: ConfigFieldDefinition<'static, String> =
461		ConfigFieldDefinition {
462			config_file: ConfigFile::Resources,
463			path: &["cardano_payment_signing_key_file"],
464			name: "path to the payment signing key file",
465			default: Some("payment.skey"),
466			_marker: PhantomData,
467		};
468
469	pub(crate) const CARDANO_COLD_VERIFICATION_KEY_FILE: ConfigFieldDefinition<'static, String> =
470		ConfigFieldDefinition {
471			config_file: ConfigFile::Resources,
472			path: &["cardano_cold_verification_key_file"],
473			name: "path to the cold verification key file",
474			default: Some("cold.vkey"),
475			_marker: PhantomData,
476		};
477
478	pub(crate) const GENESIS_UTXO: ConfigFieldDefinition<'static, UtxoId> = ConfigFieldDefinition {
479		config_file: ConfigFile::Chain,
480		path: &["chain_parameters", "genesis_utxo"],
481		name: "genesis utxo",
482		default: None,
483		_marker: PhantomData,
484	};
485
486	pub(crate) const BOOTNODES: ConfigFieldDefinition<'static, Vec<String>> =
487		ConfigFieldDefinition {
488			config_file: ConfigFile::Chain,
489			path: &["bootnodes"],
490			name: "bootnodes",
491			default: None,
492			_marker: PhantomData,
493		};
494
495	pub(crate) const INITIAL_PERMISSIONED_CANDIDATES: ConfigFieldDefinition<
496		'static,
497		Vec<crate::permissioned_candidates::PermissionedCandidateKeys>,
498	> = ConfigFieldDefinition {
499		config_file: ConfigFile::Chain,
500		path: &["initial_permissioned_candidates"],
501		name: "initial permissioned candidates",
502		default: None,
503		_marker: PhantomData,
504	};
505
506	pub(crate) const POSTGRES_CONNECTION_STRING: ConfigFieldDefinition<'static, String> =
507		ConfigFieldDefinition {
508			config_file: ConfigFile::Resources,
509			path: &["db_sync_postgres_connection_string"],
510			name: "DB-Sync Postgres connection string",
511			default: Some("postgresql://postgres-user:postgres-password@localhost:5432/cexplorer"),
512			_marker: PhantomData,
513		};
514
515	pub(crate) const CARDANO_DATA_SOURCE: ConfigFieldDefinition<'static, String> =
516		ConfigFieldDefinition {
517			config_file: ConfigFile::Resources,
518			path: &["cardano_data_source"],
519			name: "Cardano data source",
520			default: Some("db-sync"),
521			_marker: PhantomData,
522		};
523
524	pub(crate) const OGMIOS_PROTOCOL: ConfigFieldDefinition<'static, NetworkProtocol> =
525		ConfigFieldDefinition {
526			config_file: ConfigFile::Resources,
527			path: &["ogmios", "protocol"],
528			name: "Ogmios protocol (http/https)",
529			default: Some("http"),
530			_marker: PhantomData,
531		};
532
533	pub(crate) const OGMIOS_HOSTNAME: ConfigFieldDefinition<'static, String> =
534		ConfigFieldDefinition {
535			config_file: ConfigFile::Resources,
536			path: &["ogmios", "hostname"],
537			name: "Ogmios hostname",
538			default: Some("localhost"),
539			_marker: PhantomData,
540		};
541
542	pub(crate) const OGMIOS_PORT: ConfigFieldDefinition<'static, u16> = ConfigFieldDefinition {
543		config_file: ConfigFile::Resources,
544		path: &["ogmios", "port"],
545		name: "Ogmios port",
546		default: Some("1337"),
547		_marker: PhantomData,
548	};
549
550	pub(crate) const OGMIOS_REQUEST_TIMEOUT: ConfigFieldDefinition<'static, u64> =
551		ConfigFieldDefinition {
552			config_file: ConfigFile::Resources,
553			path: &["ogmios", "request_timeout"],
554			name: "Ogmios request timeout [seconds]",
555			default: Some("180"),
556			_marker: PhantomData,
557		};
558
559	pub(crate) const COMMITTEE_CANDIDATES_ADDRESS: ConfigFieldDefinition<
560		'static,
561		MainchainAddress,
562	> = ConfigFieldDefinition {
563		config_file: ConfigFile::Chain,
564		path: &["cardano_addresses", "committee_candidates_address"],
565		name: "Committee candidates address",
566		default: None,
567		_marker: PhantomData,
568	};
569
570	pub(crate) const D_PARAMETER_POLICY_ID: ConfigFieldDefinition<'static, PolicyId> =
571		ConfigFieldDefinition {
572			config_file: ConfigFile::Chain,
573			path: &["cardano_addresses", "d_parameter_policy_id"],
574			name: "D parameter policy id",
575			default: None,
576			_marker: PhantomData,
577		};
578
579	pub(crate) const PERMISSIONED_CANDIDATES_POLICY_ID: ConfigFieldDefinition<'static, PolicyId> =
580		ConfigFieldDefinition {
581			config_file: ConfigFile::Chain,
582			path: &["cardano_addresses", "permissioned_candidates_policy_id"],
583			name: "permissioned candidates policy id",
584			default: None,
585			_marker: PhantomData,
586		};
587
588	pub(crate) const GOVERNED_MAP_VALIDATOR_ADDRESS: ConfigFieldDefinition<
589		'static,
590		MainchainAddress,
591	> = ConfigFieldDefinition {
592		config_file: ConfigFile::Chain,
593		path: &["cardano_addresses", "governed_map", "validator_address"],
594		name: "Governed Map Validator Address",
595		default: None,
596		_marker: PhantomData,
597	};
598
599	pub(crate) const GOVERNED_MAP_POLICY_ID: ConfigFieldDefinition<'static, PolicyId> =
600		ConfigFieldDefinition {
601			config_file: ConfigFile::Chain,
602			path: &["cardano_addresses", "governed_map", "policy_id"],
603			name: "Governed Map Policy Id",
604			default: None,
605			_marker: PhantomData,
606		};
607
608	pub(crate) const CARDANO_SECURITY_PARAMETER: ConfigFieldDefinition<'static, u64> =
609		ConfigFieldDefinition {
610			config_file: ConfigFile::Chain,
611			path: &["cardano", "security_parameter"],
612			name: "cardano security parameter",
613			default: None,
614			_marker: PhantomData,
615		};
616
617	pub(crate) const CARDANO_ACTIVE_SLOTS_COEFF: ConfigFieldDefinition<'static, f64> =
618		ConfigFieldDefinition {
619			config_file: ConfigFile::Chain,
620			path: &["cardano", "active_slots_coeff"],
621			name: "cardano active slot coefficient",
622			default: None,
623			_marker: PhantomData,
624		};
625
626	pub(crate) const CARDANO_FIRST_EPOCH_NUMBER: ConfigFieldDefinition<'static, u32> =
627		ConfigFieldDefinition {
628			config_file: ConfigFile::Chain,
629			path: &["cardano", "first_epoch_number"],
630			name: "cardano first epoch number in shelley era",
631			default: None,
632			_marker: PhantomData,
633		};
634
635	pub(crate) const CARDANO_FIRST_SLOT_NUMBER: ConfigFieldDefinition<'static, u64> =
636		ConfigFieldDefinition {
637			config_file: ConfigFile::Chain,
638			path: &["cardano", "first_slot_number"],
639			name: "cardano first slot number in shelley era",
640			default: None,
641			_marker: PhantomData,
642		};
643
644	pub(crate) const CARDANO_EPOCH_DURATION_MILLIS: ConfigFieldDefinition<'static, u64> =
645		ConfigFieldDefinition {
646			config_file: ConfigFile::Chain,
647			path: &["cardano", "epoch_duration_millis"],
648			name: "cardano epoch duration in millis",
649			default: None,
650			_marker: PhantomData,
651		};
652
653	pub(crate) const CARDANO_FIRST_EPOCH_TIMESTAMP_MILLIS: ConfigFieldDefinition<'static, u64> =
654		ConfigFieldDefinition {
655			config_file: ConfigFile::Chain,
656			path: &["cardano", "first_epoch_timestamp_millis"],
657			name: "cardano first shelley epoch timestamp in millis",
658			default: None,
659			_marker: PhantomData,
660		};
661
662	pub(crate) const CARDANO_SLOT_DURATION_MILLIS: ConfigFieldDefinition<'static, u64> =
663		ConfigFieldDefinition {
664			config_file: ConfigFile::Chain,
665			path: &["cardano", "slot_duration_millis"],
666			name: "cardano slot duration in millis",
667			default: Some("1000"),
668			_marker: PhantomData,
669		};
670
671	pub(crate) const NODE_P2P_PORT: ConfigFieldDefinition<'static, u16> = ConfigFieldDefinition {
672		config_file: ConfigFile::Resources,
673		path: &["node_p2p_port"],
674		name: "substrate-node p2p protocol TCP port",
675		default: Some("30333"),
676		_marker: PhantomData,
677	};
678
679	pub(crate) const INITIAL_GOVERNANCE_AUTHORITIES: ConfigFieldDefinition<
680		'static,
681		GovernanceAuthoritiesKeyHashes,
682	> = ConfigFieldDefinition {
683		config_file: ConfigFile::Chain,
684		path: &["initial_governance", "authorities"],
685		name: "space separated keys hashes of the initial Multisig Governance Authorities",
686		default: Some("[]"),
687		_marker: PhantomData,
688	};
689
690	pub(crate) const INITIAL_GOVERNANCE_THRESHOLD: ConfigFieldDefinition<'static, u8> =
691		ConfigFieldDefinition {
692			config_file: ConfigFile::Chain,
693			path: &["initial_governance", "threshold"],
694			name: "Initial Multisig Governance Threshold",
695			default: Some("1"),
696			_marker: PhantomData,
697		};
698}