referrerpolicy=no-referrer-when-downgrade

staging_chain_spec_builder/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18#![doc = include_str!("../README.md")]
19#[cfg(feature = "generate-readme")]
20docify::compile_markdown!("README.docify.md", "README.md");
21
22use clap::{Parser, Subcommand};
23use sc_chain_spec::{
24	json_patch, set_code_substitute_in_json_chain_spec, update_code_in_json_chain_spec, ChainType,
25	GenericChainSpec, GenesisConfigBuilderRuntimeCaller, MultiaddrWithPeerId,
26};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::{
30	borrow::Cow,
31	fs,
32	path::{Path, PathBuf},
33};
34
35/// A utility to easily create a chain spec definition.
36#[derive(Debug, Parser)]
37#[command(rename_all = "kebab-case", version, about)]
38pub struct ChainSpecBuilder {
39	#[command(subcommand)]
40	pub command: ChainSpecBuilderCmd,
41	/// The path where the chain spec should be saved.
42	#[arg(long, short, default_value = "./chain_spec.json")]
43	pub chain_spec_path: PathBuf,
44}
45
46#[derive(Debug, Subcommand)]
47#[command(rename_all = "kebab-case")]
48pub enum ChainSpecBuilderCmd {
49	Create(CreateCmd),
50	Verify(VerifyCmd),
51	UpdateCode(UpdateCodeCmd),
52	ConvertToRaw(ConvertToRawCmd),
53	ListPresets(ListPresetsCmd),
54	DisplayPreset(DisplayPresetCmd),
55	AddCodeSubstitute(AddCodeSubstituteCmd),
56	/// Manages the boot nodes of an existing chain spec.
57	#[command(subcommand, alias = "bootnode")]
58	Bootnodes(BootnodesCmd),
59}
60
61/// Manages the boot nodes stored in the `bootNodes` field of an existing chain spec.
62///
63/// All these operations support both plain and raw formats.
64#[derive(Debug, Subcommand)]
65pub enum BootnodesCmd {
66	Add(AddBootnodesCmd),
67	Remove(RemoveBootnodesCmd),
68	List(ListBootnodesCmd),
69}
70
71/// Create a new chain spec by interacting with the provided runtime wasm blob.
72#[derive(Parser, Debug)]
73pub struct CreateCmd {
74	/// The name of chain.
75	#[arg(long, short = 'n', default_value = "Custom")]
76	chain_name: String,
77	/// The chain id.
78	#[arg(long, short = 'i', default_value = "custom")]
79	chain_id: String,
80	/// The chain type.
81	#[arg(value_enum, short = 't', default_value = "live")]
82	chain_type: ChainType,
83	/// The para ID for your chain.
84	#[arg(long, value_enum, short = 'p', requires = "relay_chain")]
85	pub para_id: Option<u32>,
86	/// The relay chain you wish to connect to.
87	#[arg(long, value_enum, short = 'c')]
88	pub relay_chain: Option<String>,
89	/// The path to runtime wasm blob.
90	#[arg(long, short, alias = "runtime-wasm-path")]
91	runtime: PathBuf,
92	/// Export chainspec as raw storage.
93	#[arg(long, short = 's')]
94	raw_storage: bool,
95	/// Verify the genesis config. This silently generates the raw storage from genesis config. Any
96	/// errors will be reported.
97	#[arg(long, short = 'v')]
98	verify: bool,
99	/// Chain properties in `KEY=VALUE` format.
100	///
101	/// Multiple `KEY=VALUE` entries can be specified and separated by a comma.
102	///
103	/// Example: `--properties tokenSymbol=UNIT,tokenDecimals=12,ss58Format=42,isEthereum=false`
104	/// Or: `--properties tokenSymbol=UNIT --properties tokenDecimals=12 --properties ss58Format=42
105	/// --properties=isEthereum=false`
106	///
107	/// The first uses comma as separation and the second passes the argument multiple times. Both
108	/// styles can also be mixed.
109	#[arg(long, default_value = "tokenSymbol=UNIT,tokenDecimals=12")]
110	pub properties: Vec<String>,
111	/// The boot nodes to be stored in the chain spec.
112	///
113	/// Each boot node is a multiaddress that contains the peer id of the node. Multiple addresses
114	/// can be separated by a comma, or the argument can be passed multiple times.
115	///
116	/// Example: `--bootnodes /dns/node-0.example.com/tcp/30333/p2p/12D3KooW...`
117	#[arg(long, short = 'b', value_delimiter = ',')]
118	pub bootnodes: Vec<MultiaddrWithPeerId>,
119	#[command(subcommand)]
120	action: GenesisBuildAction,
121
122	/// Allows to provide the runtime code blob, instead of reading it from the provided file path.
123	#[clap(skip)]
124	code: Option<Cow<'static, [u8]>>,
125}
126
127#[derive(Subcommand, Debug, Clone)]
128enum GenesisBuildAction {
129	Patch(PatchCmd),
130	Full(FullCmd),
131	Default(DefaultCmd),
132	NamedPreset(NamedPresetCmd),
133}
134
135/// Patches the runtime's default genesis config with provided patch.
136#[derive(Parser, Debug, Clone)]
137struct PatchCmd {
138	/// The path to the runtime genesis config patch.
139	patch_path: PathBuf,
140}
141
142/// Build the genesis config for runtime using provided json file. No defaults will be used.
143#[derive(Parser, Debug, Clone)]
144struct FullCmd {
145	/// The path to the full runtime genesis config json file.
146	config_path: PathBuf,
147}
148
149/// Gets the default genesis config for the runtime and uses it in ChainSpec. Please note that
150/// default genesis config may not be valid. For some runtimes initial values should be added there
151/// (e.g. session keys, babe epoch).
152#[derive(Parser, Debug, Clone)]
153struct DefaultCmd {}
154
155/// Uses named preset provided by runtime to build the chains spec.
156#[derive(Parser, Debug, Clone)]
157struct NamedPresetCmd {
158	preset_name: String,
159}
160
161/// Updates the code in the provided input chain spec.
162///
163/// The code field of the chain spec will be updated with the runtime provided in the
164/// command line. This operation supports both plain and raw formats.
165///
166/// This command does not update chain-spec file in-place. The result of this command will be stored
167/// in a file given as `-c/--chain-spec-path` command line argument.
168#[derive(Parser, Debug, Clone)]
169pub struct UpdateCodeCmd {
170	/// Chain spec to be updated.
171	///
172	/// Please note that the file will not be updated in-place.
173	pub input_chain_spec: PathBuf,
174	/// The path to new runtime wasm blob to be stored into chain-spec.
175	#[arg(alias = "runtime-wasm-path")]
176	pub runtime: PathBuf,
177}
178
179/// Add a code substitute in the chain spec.
180///
181/// The `codeSubstitute` object of the chain spec will be updated with the block height as key and
182/// runtime code as value. This operation supports both plain and raw formats. The `codeSubstitute`
183/// field instructs the node to use the provided runtime code at the given block height. This is
184/// useful when the chain can not progress on its own due to a bug that prevents block-building.
185///
186/// Note: For parachains, the validation function on the relaychain needs to be adjusted too,
187/// otherwise blocks built using the substituted parachain runtime will be rejected.
188#[derive(Parser, Debug, Clone)]
189pub struct AddCodeSubstituteCmd {
190	/// Chain spec to be updated.
191	pub input_chain_spec: PathBuf,
192	/// New runtime wasm blob that should replace the existing code.
193	#[arg(alias = "runtime-wasm-path")]
194	pub runtime: PathBuf,
195	/// The block height at which the code should be substituted.
196	pub block_height: u64,
197}
198
199/// Appends boot nodes to the provided input chain spec.
200///
201/// The addresses are appended to the end of the `bootNodes` field of the chain spec, in the given
202/// order. Addresses that are already present are skipped, so the command can be safely repeated.
203///
204/// This command does not update chain-spec file in-place. The result of this command will be stored
205/// in a file given as `-c/--chain-spec-path` command line argument.
206#[derive(Parser, Debug, Clone)]
207pub struct AddBootnodesCmd {
208	/// Chain spec to be updated.
209	///
210	/// Please note that the file will not be updated in-place.
211	pub input_chain_spec: PathBuf,
212	/// The boot nodes to be appended.
213	///
214	/// Each boot node is a multiaddress that contains the peer id of the node, e.g.
215	/// `/dns/node-0.example.com/tcp/30333/p2p/12D3KooW...`.
216	#[arg(required = true, num_args = 1..)]
217	pub bootnodes: Vec<MultiaddrWithPeerId>,
218}
219
220/// Removes boot nodes from the provided input chain spec.
221///
222/// The addresses given in the command line are removed from the `bootNodes` field of the chain
223/// spec. Addresses that are not present are ignored, so the command can be safely repeated.
224///
225/// This command does not update chain-spec file in-place. The result of this command will be stored
226/// in a file given as `-c/--chain-spec-path` command line argument.
227#[derive(Parser, Debug, Clone)]
228pub struct RemoveBootnodesCmd {
229	/// Chain spec to be updated.
230	///
231	/// Please note that the file will not be updated in-place.
232	pub input_chain_spec: PathBuf,
233	/// The boot nodes to be removed.
234	///
235	/// Each boot node is a multiaddress that contains the peer id of the node, e.g.
236	/// `/dns/node-0.example.com/tcp/30333/p2p/12D3KooW...`.
237	#[arg(required_unless_present = "all", conflicts_with = "all", num_args = 1..)]
238	pub bootnodes: Vec<MultiaddrWithPeerId>,
239	/// Remove all the boot nodes of the chain spec.
240	#[arg(long)]
241	pub all: bool,
242}
243
244/// Lists the boot nodes of the provided input chain spec.
245#[derive(Parser, Debug, Clone)]
246pub struct ListBootnodesCmd {
247	/// Chain spec to be inspected.
248	pub input_chain_spec: PathBuf,
249}
250
251/// Converts the given chain spec into the raw format.
252#[derive(Parser, Debug, Clone)]
253pub struct ConvertToRawCmd {
254	/// Chain spec to be converted.
255	pub input_chain_spec: PathBuf,
256}
257
258/// Lists available presets
259#[derive(Parser, Debug, Clone)]
260pub struct ListPresetsCmd {
261	/// The path to runtime wasm blob.
262	#[arg(long, short, alias = "runtime-wasm-path")]
263	pub runtime: PathBuf,
264}
265
266/// Displays given preset
267#[derive(Parser, Debug, Clone)]
268pub struct DisplayPresetCmd {
269	/// The path to runtime wasm blob.
270	#[arg(long, short, alias = "runtime-wasm-path")]
271	pub runtime: PathBuf,
272	/// Preset to be displayed. If none is given default will be displayed.
273	#[arg(long, short)]
274	pub preset_name: Option<String>,
275}
276
277/// Verifies the provided input chain spec.
278///
279/// Silently checks if given input chain spec can be converted to raw. It allows to check if all
280/// RuntimeGenesisConfig fields are properly initialized and if the json does not contain invalid
281/// fields.
282#[derive(Parser, Debug, Clone)]
283pub struct VerifyCmd {
284	/// Chain spec to be verified.
285	pub input_chain_spec: PathBuf,
286}
287
288#[derive(Deserialize, Serialize, Clone)]
289pub struct ParachainExtension {
290	/// The relay chain of the Parachain.
291	pub relay_chain: String,
292	/// The id of the Parachain.
293	pub para_id: Option<u32>,
294}
295
296type ChainSpec = GenericChainSpec<()>;
297
298impl ChainSpecBuilder {
299	/// Executes the internal command.
300	pub fn run(&self) -> Result<(), String> {
301		let chain_spec_path = self.chain_spec_path.to_path_buf();
302
303		match &self.command {
304			ChainSpecBuilderCmd::Create(cmd) => {
305				let chain_spec_json = generate_chain_spec_for_runtime(&cmd)?;
306				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
307			},
308			ChainSpecBuilderCmd::UpdateCode(UpdateCodeCmd {
309				ref input_chain_spec,
310				ref runtime,
311			}) => {
312				let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
313
314				update_code_in_json_chain_spec(
315					&mut chain_spec_json,
316					&fs::read(runtime.as_path())
317						.map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
318				);
319
320				let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
321					.map_err(|e| format!("to pretty failed: {e}"))?;
322				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
323			},
324			ChainSpecBuilderCmd::AddCodeSubstitute(AddCodeSubstituteCmd {
325				ref input_chain_spec,
326				ref runtime,
327				block_height,
328			}) => {
329				let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
330
331				set_code_substitute_in_json_chain_spec(
332					&mut chain_spec_json,
333					&fs::read(runtime.as_path())
334						.map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
335					*block_height,
336				);
337				let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
338					.map_err(|e| format!("to pretty failed: {e}"))?;
339				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
340			},
341			ChainSpecBuilderCmd::Bootnodes(BootnodesCmd::Add(AddBootnodesCmd {
342				ref input_chain_spec,
343				ref bootnodes,
344			})) => {
345				let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
346				let mut updated_bootnodes = extract_bootnodes(&chain_spec_json)?;
347				for bootnode in bootnodes {
348					if !updated_bootnodes.contains(bootnode) {
349						updated_bootnodes.push(bootnode.clone());
350					}
351				}
352
353				set_bootnodes(&mut chain_spec_json, updated_bootnodes)?;
354				write_chain_spec_json(&chain_spec_json, chain_spec_path.as_path())?;
355			},
356			ChainSpecBuilderCmd::Bootnodes(BootnodesCmd::Remove(RemoveBootnodesCmd {
357				ref input_chain_spec,
358				ref bootnodes,
359				all,
360			})) => {
361				let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
362				// When all the boot nodes are removed the existing ones are not inspected, which
363				// allows to fix a chain spec containing an invalid address.
364				let remaining_bootnodes = if *all {
365					Vec::new()
366				} else {
367					extract_bootnodes(&chain_spec_json)?
368						.into_iter()
369						.filter(|bootnode| !bootnodes.contains(bootnode))
370						.collect()
371				};
372
373				set_bootnodes(&mut chain_spec_json, remaining_bootnodes)?;
374				write_chain_spec_json(&chain_spec_json, chain_spec_path.as_path())?;
375			},
376			ChainSpecBuilderCmd::Bootnodes(BootnodesCmd::List(ListBootnodesCmd {
377				ref input_chain_spec,
378			})) => {
379				let chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
380				let bootnodes = extract_bootnodes(&chain_spec_json)?;
381				println!("{}", serde_json::json!({ "bootNodes": bootnodes }).to_string());
382			},
383			ChainSpecBuilderCmd::ConvertToRaw(ConvertToRawCmd { ref input_chain_spec }) => {
384				let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
385
386				let mut genesis_json =
387					serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
388						.map_err(|e| format!("Conversion to json failed: {e}"))?;
389
390				// We want to extract only raw genesis ("genesis::raw" key), and apply it as a patch
391				// for the original json file.
392				genesis_json.as_object_mut().map(|map| {
393					map.retain(|key, _| key == "genesis");
394				});
395
396				let mut org_chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
397
398				// The original plain genesis ("genesis::runtimeGenesis") is no longer needed, so
399				// just remove it:
400				org_chain_spec_json
401					.get_mut("genesis")
402					.and_then(|genesis| genesis.as_object_mut())
403					.and_then(|genesis| genesis.remove("runtimeGenesis"));
404				json_patch::merge(&mut org_chain_spec_json, genesis_json);
405
406				let chain_spec_json = serde_json::to_string_pretty(&org_chain_spec_json)
407					.map_err(|e| format!("Conversion to pretty failed: {e}"))?;
408				fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
409			},
410			ChainSpecBuilderCmd::Verify(VerifyCmd { ref input_chain_spec }) => {
411				let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
412				serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
413					.map_err(|e| format!("Conversion to json failed: {e}"))?;
414			},
415			ChainSpecBuilderCmd::ListPresets(ListPresetsCmd { runtime }) => {
416				let code = fs::read(runtime.as_path())
417					.map_err(|e| format!("wasm blob shall be readable {e}"))?;
418				let caller: GenesisConfigBuilderRuntimeCaller =
419					GenesisConfigBuilderRuntimeCaller::new(&code[..]);
420				let presets = caller
421					.preset_names()
422					.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
423				println!("{}", serde_json::json!({"presets":presets}).to_string());
424			},
425			ChainSpecBuilderCmd::DisplayPreset(DisplayPresetCmd { runtime, preset_name }) => {
426				let code = fs::read(runtime.as_path())
427					.map_err(|e| format!("wasm blob shall be readable {e}"))?;
428				let caller: GenesisConfigBuilderRuntimeCaller =
429					GenesisConfigBuilderRuntimeCaller::new(&code[..]);
430				let preset = caller
431					.get_named_preset(preset_name.as_ref())
432					.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
433				println!("{preset}");
434			},
435		}
436		Ok(())
437	}
438
439	/// Sets the code used by [`CreateCmd`]
440	///
441	/// The file pointed by `CreateCmd::runtime` field will not be read. Provided blob will used
442	/// instead for chain spec generation.
443	pub fn set_create_cmd_runtime_code(&mut self, code: Cow<'static, [u8]>) {
444		match &mut self.command {
445			ChainSpecBuilderCmd::Create(cmd) => {
446				cmd.code = Some(code);
447			},
448			_ => {
449				panic!("Overwriting code blob is only supported for CreateCmd");
450			},
451		};
452	}
453}
454
455fn process_action<T: Serialize + Clone + Sync + 'static>(
456	cmd: &CreateCmd,
457	code: &[u8],
458	builder: sc_chain_spec::ChainSpecBuilder<T>,
459) -> Result<String, String> {
460	let builder = match cmd.action {
461		GenesisBuildAction::NamedPreset(NamedPresetCmd { ref preset_name }) => {
462			builder.with_genesis_config_preset_name(&preset_name)
463		},
464		GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
465			let patch = fs::read(patch_path.as_path())
466				.map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
467			builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
468				|e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
469			)?)
470		},
471		GenesisBuildAction::Full(FullCmd { ref config_path }) => {
472			let config = fs::read(config_path.as_path())
473				.map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
474			builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
475				|e| format!("config file {config_path:?} shall contain a valid json: {e}"),
476			)?)
477		},
478		GenesisBuildAction::Default(DefaultCmd {}) => {
479			let caller: GenesisConfigBuilderRuntimeCaller =
480				GenesisConfigBuilderRuntimeCaller::new(&code);
481			let default_config = caller
482				.get_default_config()
483				.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
484			builder.with_genesis_config(default_config)
485		},
486	};
487
488	let chain_spec = builder.build();
489
490	match (cmd.verify, cmd.raw_storage) {
491		(_, true) => chain_spec.as_json(true),
492		(true, false) => {
493			chain_spec.as_json(true)?;
494			println!("Genesis config verification: OK");
495			chain_spec.as_json(false)
496		},
497		(false, false) => chain_spec.as_json(false),
498	}
499}
500
501impl CreateCmd {
502	/// Returns the associated runtime code.
503	///
504	/// If the code blob was previously set, returns it. Otherwise reads the file.
505	fn get_runtime_code(&self) -> Result<Cow<'static, [u8]>, String> {
506		Ok(if let Some(code) = self.code.clone() {
507			code
508		} else {
509			fs::read(self.runtime.as_path())
510				.map_err(|e| format!("wasm blob shall be readable {e}"))?
511				.into()
512		})
513	}
514}
515
516/// Parses chain properties passed as a comma-separated KEY=VALUE pairs.
517fn parse_properties(raw: &String, props: &mut sc_chain_spec::Properties) -> Result<(), String> {
518	for pair in raw.split(',') {
519		let mut iter = pair.splitn(2, '=');
520		let key = iter
521			.next()
522			.ok_or_else(|| format!("Invalid chain property key: {pair}"))?
523			.trim()
524			.to_owned();
525		let value_str = iter
526			.next()
527			.ok_or_else(|| format!("Invalid chain property value for key: {key}"))?
528			.trim();
529
530		// Try to parse as bool, number, or fallback to String
531		let value = match value_str.parse::<bool>() {
532			Ok(b) => Value::Bool(b),
533			Err(_) => match value_str.parse::<u32>() {
534				Ok(i) => Value::Number(i.into()),
535				Err(_) => Value::String(value_str.to_string()),
536			},
537		};
538
539		props.insert(key, value);
540	}
541	Ok(())
542}
543
544/// Processes `CreateCmd` and returns string representation of JSON version of `ChainSpec`.
545pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
546	let code = cmd.get_runtime_code()?;
547
548	let chain_type = &cmd.chain_type;
549
550	let mut properties = sc_chain_spec::Properties::new();
551	for raw in &cmd.properties {
552		parse_properties(raw, &mut properties)?;
553	}
554
555	let builder = ChainSpec::builder(&code[..], Default::default())
556		.with_name(&cmd.chain_name[..])
557		.with_id(&cmd.chain_id[..])
558		.with_properties(properties)
559		.with_chain_type(chain_type.clone())
560		.with_boot_nodes(cmd.bootnodes.clone());
561
562	let chain_spec_json_string = process_action(&cmd, &code[..], builder)?;
563	let parachain_properties = cmd.relay_chain.as_ref().map(|rc| {
564		cmd.para_id
565			.map(|para_id| {
566				serde_json::json!({
567					"relay_chain": rc,
568					"para_id": para_id,
569				})
570			})
571			.unwrap_or(serde_json::json!({
572				"relay_chain": rc,
573			}))
574	});
575
576	let chain_spec = parachain_properties
577		.map(|props| {
578			let chain_spec_json_blob = serde_json::from_str(chain_spec_json_string.as_str())
579				.map_err(|e| format!("deserialization a json failed {e}"));
580			chain_spec_json_blob.and_then(|mut cs| {
581				json_patch::merge(&mut cs, props);
582				serde_json::to_string_pretty(&cs).map_err(|e| format!("to pretty failed: {e}"))
583			})
584		})
585		.unwrap_or(Ok(chain_spec_json_string));
586	chain_spec
587}
588
589/// The key of the chain spec json field holding the boot nodes.
590const BOOT_NODES_KEY: &str = "bootNodes";
591
592/// Extracts the boot nodes stored in the given chain spec json.
593///
594/// An empty list is returned if the chain spec does not contain the boot nodes field.
595fn extract_bootnodes(chain_spec_json: &Value) -> Result<Vec<MultiaddrWithPeerId>, String> {
596	let Some(bootnodes) = chain_spec_json.get(BOOT_NODES_KEY) else { return Ok(Vec::new()) };
597
598	serde_json::from_value(bootnodes.clone())
599		.map_err(|e| format!("`{BOOT_NODES_KEY}` field of the chain spec is invalid: {e}"))
600}
601
602/// Stores the given boot nodes in the given chain spec json, replacing the existing ones.
603fn set_bootnodes(
604	chain_spec_json: &mut Value,
605	bootnodes: Vec<MultiaddrWithPeerId>,
606) -> Result<(), String> {
607	let bootnodes = serde_json::to_value(bootnodes)
608		.map_err(|e| format!("Conversion of the boot nodes to json failed: {e}"))?;
609
610	chain_spec_json
611		.as_object_mut()
612		.ok_or_else(|| "Provided chain spec is not a json object".to_string())?
613		.insert(BOOT_NODES_KEY.to_string(), bootnodes);
614	Ok(())
615}
616
617/// Stores the given chain spec json in the file at the given path.
618fn write_chain_spec_json(chain_spec_json: &Value, chain_spec_path: &Path) -> Result<(), String> {
619	let chain_spec_json = serde_json::to_string_pretty(chain_spec_json)
620		.map_err(|e| format!("to pretty failed: {e}"))?;
621	fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())
622}
623
624/// Extract any chain spec and convert it to JSON
625fn extract_chain_spec_json(input_chain_spec: &Path) -> Result<serde_json::Value, String> {
626	let chain_spec = &fs::read(input_chain_spec)
627		.map_err(|e| format!("Provided chain spec could not be read: {e}"))?;
628
629	serde_json::from_slice(&chain_spec).map_err(|e| format!("Conversion to json failed: {e}"))
630}