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