pop_chains/build/
mod.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::errors::{Error, handle_command_error};
4use anyhow::{Result, anyhow};
5use duct::cmd;
6use pop_common::{Profile, account_id::convert_to_evm_accounts, manifest::from_path};
7use sc_chain_spec::{GenericChainSpec, NoExtension};
8use serde_json::{Value, json};
9use sp_core::bytes::to_hex;
10use std::{
11	fs,
12	path::{Path, PathBuf},
13	str::FromStr,
14};
15
16/// Build the deterministic runtime.
17pub mod runtime;
18
19/// A builder for generating chain specifications.
20///
21/// This enum represents two different ways to build a chain specification:
22/// - Using an existing node.
23/// - Using a runtime.
24pub enum ChainSpecBuilder {
25	/// A node-based chain specification builder.
26	Node {
27		/// Path to the node directory.
28		node_path: PathBuf,
29		/// Whether to include a default bootnode in the specification.
30		default_bootnode: bool,
31		/// The build profile to use (debug, release, production, etc).
32		profile: Profile,
33	},
34	/// A runtime-based chain specification builder.
35	Runtime {
36		/// Path to the runtime directory.
37		runtime_path: PathBuf,
38		/// The build profile to use (debug, release, production, etc).
39		profile: Profile,
40	},
41}
42
43impl ChainSpecBuilder {
44	/// Builds the chain specification using the provided profile and features.
45	///
46	/// # Arguments
47	/// * `features` - A list of cargo features to enable during the build
48	///
49	/// # Returns
50	/// The path to the built artifact
51	pub fn build(&self, features: &[String]) -> Result<PathBuf> {
52		build_project(&self.path(), None, &self.profile(), features, None)?;
53		// Check the artifact is found after being built
54		self.artifact_path()
55	}
56
57	/// Gets the path associated with this chain specification builder.
58	///
59	/// # Returns
60	/// The path to either the node or runtime directory.
61	pub fn path(&self) -> PathBuf {
62		match self {
63			ChainSpecBuilder::Node { node_path, .. } => node_path,
64			ChainSpecBuilder::Runtime { runtime_path, .. } => runtime_path,
65		}
66		.clone()
67	}
68
69	/// Gets the build profile associated with this chain specification builder.
70	///
71	/// # Returns
72	/// The build profile (debug, release, production, etc.) to use when building the chain.
73	pub fn profile(&self) -> Profile {
74		match self {
75			ChainSpecBuilder::Node { profile, .. } => profile,
76			ChainSpecBuilder::Runtime { profile, .. } => profile,
77		}
78		.clone()
79	}
80
81	/// Gets the path to the built artifact.
82	///
83	/// # Returns
84	/// The path to the built artifact (node binary or runtime WASM).
85	pub fn artifact_path(&self) -> Result<PathBuf> {
86		let manifest = from_path(&self.path())?;
87		let package = manifest.package().name();
88		let root_folder = rustilities::manifest::find_workspace_manifest(self.path())
89			.ok_or(anyhow::anyhow!("Not inside a workspace"))?
90			.parent()
91			.expect("Path to Cargo.toml workspace root folder must exist")
92			.to_path_buf();
93		let path = match self {
94			ChainSpecBuilder::Node { profile, .. } =>
95				profile.target_directory(&root_folder).join(package),
96			ChainSpecBuilder::Runtime { profile, .. } => {
97				let base = profile.target_directory(&root_folder).join("wbuild").join(package);
98				let wasm_file = package.replace("-", "_");
99				let compact_compressed = base.join(format!("{wasm_file}.compact.compressed.wasm"));
100				let raw = base.join(format!("{wasm_file}.wasm"));
101				if compact_compressed.is_file() {
102					compact_compressed
103				} else if raw.is_file() {
104					raw
105				} else {
106					return Err(anyhow::anyhow!("No runtime found"));
107				}
108			},
109		};
110		Ok(path.canonicalize()?)
111	}
112
113	/// Generates a plain (human readable) chain specification file.
114	///
115	/// # Arguments
116	/// * `chain_or_preset` - The chain (when using a node) or preset (when using a runtime) name.
117	/// * `output_file` - The path where the chain spec should be written.
118	/// * `name` - The name to be used on the chain spec if specified.
119	/// * `id` - The ID to be used on the chain spec if specified.
120	pub fn generate_plain_chain_spec(
121		&self,
122		chain_or_preset: &str,
123		output_file: &Path,
124		name: Option<&str>,
125		id: Option<&str>,
126	) -> Result<(), Error> {
127		match self {
128			ChainSpecBuilder::Node { default_bootnode, .. } => generate_plain_chain_spec_with_node(
129				&self.artifact_path()?,
130				output_file,
131				*default_bootnode,
132				chain_or_preset,
133			),
134			ChainSpecBuilder::Runtime { .. } => generate_plain_chain_spec_with_runtime(
135				fs::read(self.artifact_path()?)?,
136				output_file,
137				chain_or_preset,
138				name,
139				id,
140			),
141		}
142	}
143
144	/// Generates a raw (encoded) chain specification file from a plain one.
145	///
146	/// # Arguments
147	/// * `plain_chain_spec` - The path to the plain chain spec file.
148	/// * `raw_chain_spec_name` - The name for the generated raw chain spec file.
149	///
150	/// # Returns
151	/// The path to the generated raw chain spec file.
152	pub fn generate_raw_chain_spec(
153		&self,
154		plain_chain_spec: &Path,
155		raw_chain_spec_name: &str,
156	) -> Result<PathBuf, Error> {
157		match self {
158			ChainSpecBuilder::Node { .. } => generate_raw_chain_spec_with_node(
159				&self.artifact_path()?,
160				plain_chain_spec,
161				raw_chain_spec_name,
162			),
163			ChainSpecBuilder::Runtime { .. } =>
164				generate_raw_chain_spec_with_runtime(plain_chain_spec, raw_chain_spec_name),
165		}
166	}
167
168	/// Extracts and exports the WebAssembly runtime code from a raw chain specification.
169	///
170	/// # Arguments
171	/// * `raw_chain_spec` - Path to the raw chain specification file to extract the runtime from.
172	/// * `wasm_file_name` - Name for the file where the extracted runtime will be saved.
173	///
174	/// # Returns
175	/// The path to the generated WASM runtime file.
176	///
177	/// # Errors
178	/// Returns an error if:
179	/// - The chain specification file cannot be read or parsed.
180	/// - The runtime cannot be extracted from the chain spec.
181	/// - The runtime cannot be written to the output file.
182	pub fn export_wasm_file(
183		&self,
184		raw_chain_spec: &Path,
185		wasm_file_name: &str,
186	) -> Result<PathBuf, Error> {
187		match self {
188			ChainSpecBuilder::Node { .. } =>
189				export_wasm_file_with_node(&self.artifact_path()?, raw_chain_spec, wasm_file_name),
190			ChainSpecBuilder::Runtime { .. } =>
191				export_wasm_file_with_runtime(raw_chain_spec, wasm_file_name),
192		}
193	}
194}
195
196/// Build the chain and returns the path to the binary.
197///
198/// # Arguments
199/// * `path` - The path to the chain manifest.
200/// * `package` - The optional package to be built.
201/// * `profile` - Whether the chain should be built without any debugging functionality.
202/// * `node_path` - An optional path to the node directory. Defaults to the `node` subdirectory of
203///   the project path if not provided.
204/// * `features` - A set of features the project is built with.
205pub fn build_chain(
206	path: &Path,
207	package: Option<String>,
208	profile: &Profile,
209	node_path: Option<&Path>,
210	features: &[String],
211) -> Result<PathBuf, Error> {
212	build_project(path, package, profile, features, None)?;
213	binary_path(&profile.target_directory(path), node_path.unwrap_or(&path.join("node")))
214}
215
216/// Build the Rust project.
217///
218/// # Arguments
219/// * `path` - The optional path to the project manifest, defaulting to the current directory if not
220///   specified.
221/// * `package` - The optional package to be built.
222/// * `profile` - Whether the project should be built without any debugging functionality.
223/// * `features` - A set of features the project is built with.
224/// * `target` - The optional target to be specified.
225pub fn build_project(
226	path: &Path,
227	package: Option<String>,
228	profile: &Profile,
229	features: &[String],
230	target: Option<&str>,
231) -> Result<(), Error> {
232	let mut args = vec!["build"];
233	if let Some(package) = package.as_deref() {
234		args.push("--package");
235		args.push(package)
236	}
237	if profile == &Profile::Release {
238		args.push("--release");
239	} else if profile == &Profile::Production {
240		args.push("--profile=production");
241	}
242
243	let feature_args = features.join(",");
244	if !features.is_empty() {
245		args.push("--features");
246		args.push(&feature_args);
247	}
248
249	if let Some(target) = target {
250		args.push("--target");
251		args.push(target);
252	}
253
254	cmd("cargo", args).dir(path).run()?;
255	Ok(())
256}
257
258/// Determines whether the manifest at the supplied path is a supported chain project.
259///
260/// # Arguments
261/// * `path` - The optional path to the manifest, defaulting to the current directory if not
262///   specified.
263pub fn is_supported(path: &Path) -> bool {
264	let manifest = match from_path(path) {
265		Ok(m) => m,
266		Err(_) => return false,
267	};
268	// Simply check for a chain dependency
269	const DEPENDENCIES: [&str; 4] =
270		["cumulus-client-collator", "cumulus-primitives-core", "parachains-common", "polkadot-sdk"];
271	DEPENDENCIES.into_iter().any(|d| {
272		manifest.dependencies.contains_key(d) ||
273			manifest.workspace.as_ref().is_some_and(|w| w.dependencies.contains_key(d))
274	})
275}
276
277/// Constructs the node binary path based on the target path and the node directory path.
278///
279/// # Arguments
280/// * `target_path` - The path where the binaries are expected to be found.
281/// * `node_path` - The path to the node from which the node name will be parsed.
282pub fn binary_path(target_path: &Path, node_path: &Path) -> Result<PathBuf, Error> {
283	build_binary_path(node_path, |node_name| target_path.join(node_name))
284}
285
286/// Constructs the runtime binary path based on the target path and the directory path.
287///
288/// # Arguments
289/// * `target_path` - The path where the binaries are expected to be found.
290/// * `runtime_path` - The path to the runtime from which the runtime name will be parsed.
291pub fn runtime_binary_path(target_path: &Path, runtime_path: &Path) -> Result<PathBuf, Error> {
292	build_binary_path(runtime_path, |runtime_name| {
293		target_path.join(format!("{runtime_name}/{}.wasm", runtime_name.replace("-", "_")))
294	})
295}
296
297fn build_binary_path<F>(project_path: &Path, path_builder: F) -> Result<PathBuf, Error>
298where
299	F: Fn(&str) -> PathBuf,
300{
301	let manifest = from_path(project_path)?;
302	let project_name = manifest.package().name();
303	let release = path_builder(project_name);
304	if !release.exists() {
305		return Err(Error::MissingBinary(project_name.to_string()));
306	}
307	Ok(release)
308}
309
310/// Generates a raw chain specification file from a plain chain specification for a runtime.
311///
312/// # Arguments
313/// * `plain_chain_spec` - Location of the plain chain specification file.
314/// * `raw_chain_spec_name` - The name of the raw chain specification file to be generated.
315///
316/// # Returns
317/// The path to the generated raw chain specification file.
318pub fn generate_raw_chain_spec_with_runtime(
319	plain_chain_spec: &Path,
320	raw_chain_spec_name: &str,
321) -> Result<PathBuf, Error> {
322	let chain_spec = GenericChainSpec::<Option<()>>::from_json_file(plain_chain_spec.to_path_buf())
323		.map_err(|e| anyhow::anyhow!(e))?;
324	let raw_chain_spec = chain_spec.as_json(true).map_err(|e| anyhow::anyhow!(e))?;
325	let raw_chain_spec_file = plain_chain_spec.with_file_name(raw_chain_spec_name);
326	fs::write(&raw_chain_spec_file, raw_chain_spec)?;
327	Ok(raw_chain_spec_file)
328}
329
330/// Generates a plain chain specification file for a runtime.
331///
332/// # Arguments
333/// * `wasm` - The WebAssembly runtime bytes.
334/// * `plain_chain_spec` - The path where the plain chain specification should be written.
335/// * `preset` - Preset name for genesis configuration.
336/// * `name` - The name to be used on the chain spec if specified.
337/// * `id` - The ID to be used on the chain spec if specified.
338pub fn generate_plain_chain_spec_with_runtime(
339	wasm: Vec<u8>,
340	plain_chain_spec: &Path,
341	preset: &str,
342	name: Option<&str>,
343	id: Option<&str>,
344) -> Result<(), Error> {
345	let mut chain_spec = GenericChainSpec::<NoExtension>::builder(&wasm[..], None)
346		.with_genesis_config_preset_name(preset.trim());
347
348	if let Some(name) = name {
349		chain_spec = chain_spec.with_name(name);
350	}
351
352	if let Some(id) = id {
353		chain_spec = chain_spec.with_id(id);
354	}
355
356	let chain_spec = chain_spec.build().as_json(false).map_err(|e| anyhow::anyhow!(e))?;
357	fs::write(plain_chain_spec, chain_spec)?;
358
359	Ok(())
360}
361
362/// Extracts and exports the WebAssembly runtime from a raw chain specification.
363///
364/// # Arguments
365/// * `raw_chain_spec` - The path to the raw chain specification file to extract the runtime from.
366/// * `wasm_file_name` - The name of the file where the extracted runtime will be saved.
367///
368/// # Returns
369/// The path to the generated WASM runtime file wrapped in a Result.
370///
371/// # Errors
372/// Returns an error if:
373/// - The chain specification file cannot be read or parsed.
374/// - The runtime cannot be extracted from the chain spec.
375/// - The runtime cannot be written to the output file.
376pub fn export_wasm_file_with_runtime(
377	raw_chain_spec: &Path,
378	wasm_file_name: &str,
379) -> Result<PathBuf, Error> {
380	let chain_spec = GenericChainSpec::<Option<()>>::from_json_file(raw_chain_spec.to_path_buf())
381		.map_err(|e| anyhow::anyhow!(e))?;
382	let raw_wasm_blob =
383		cumulus_client_cli::extract_genesis_wasm(&chain_spec).map_err(|e| anyhow::anyhow!(e))?;
384	let wasm_file = raw_chain_spec.parent().unwrap_or(Path::new("./")).join(wasm_file_name);
385	fs::write(&wasm_file, raw_wasm_blob)?;
386	Ok(wasm_file)
387}
388
389/// Generates the plain text chain specification for a chain with its own node.
390///
391/// # Arguments
392/// * `binary_path` - The path to the node binary executable that contains the `build-spec` command.
393/// * `plain_chain_spec` - Location of the plain_chain_spec file to be generated.
394/// * `default_bootnode` - Whether to include localhost as a bootnode.
395/// * `chain` - The chain specification. It can be one of the predefined ones (e.g. dev, local or a
396///   custom one) or the path to an existing chain spec.
397pub fn generate_plain_chain_spec_with_node(
398	binary_path: &Path,
399	plain_chain_spec: &Path,
400	default_bootnode: bool,
401	chain: &str,
402) -> Result<(), Error> {
403	check_command_exists(binary_path, "build-spec")?;
404	let mut args = vec!["build-spec", "--chain", chain];
405	if !default_bootnode {
406		args.push("--disable-default-bootnode");
407	}
408	// Create a temporary file.
409	let temp_file = tempfile::NamedTempFile::new_in(std::env::temp_dir())?;
410	// Run the command and redirect output to the temporary file.
411	let output = cmd(binary_path, args)
412		.stdout_path(temp_file.path())
413		.stderr_capture()
414		.unchecked()
415		.run()?;
416	// Check if the command failed.
417	handle_command_error(&output, Error::BuildSpecError)?;
418	// Atomically replace the chain spec file with the temporary file.
419	temp_file.persist(plain_chain_spec).map_err(|e| {
420		Error::AnyhowError(anyhow!(
421			"Failed to replace the chain spec file with the temporary file: {e}"
422		))
423	})?;
424	Ok(())
425}
426
427/// Generates a raw chain specification file for a chain.
428///
429/// # Arguments
430/// * `binary_path` - The path to the node binary executable that contains the `build-spec` command.
431/// * `plain_chain_spec` - Location of the plain chain specification file.
432/// * `chain_spec_file_name` - The name of the chain specification file to be generated.
433pub fn generate_raw_chain_spec_with_node(
434	binary_path: &Path,
435	plain_chain_spec: &Path,
436	chain_spec_file_name: &str,
437) -> Result<PathBuf, Error> {
438	if !plain_chain_spec.exists() {
439		return Err(Error::MissingChainSpec(plain_chain_spec.display().to_string()));
440	}
441	check_command_exists(binary_path, "build-spec")?;
442	let raw_chain_spec = plain_chain_spec.with_file_name(chain_spec_file_name);
443	let output = cmd(
444		binary_path,
445		vec![
446			"build-spec",
447			"--chain",
448			&plain_chain_spec.display().to_string(),
449			"--disable-default-bootnode",
450			"--raw",
451		],
452	)
453	.stdout_path(&raw_chain_spec)
454	.stderr_capture()
455	.unchecked()
456	.run()?;
457	handle_command_error(&output, Error::BuildSpecError)?;
458	Ok(raw_chain_spec)
459}
460
461/// Export the WebAssembly runtime for the chain.
462///
463/// # Arguments
464/// * `binary_path` - The path to the node binary executable that contains the `export-genesis-wasm`
465///   command.
466/// * `raw_chain_spec` - Location of the raw chain specification file.
467/// * `wasm_file_name` - The name of the wasm runtime file to be generated.
468pub fn export_wasm_file_with_node(
469	binary_path: &Path,
470	raw_chain_spec: &Path,
471	wasm_file_name: &str,
472) -> Result<PathBuf, Error> {
473	if !raw_chain_spec.exists() {
474		return Err(Error::MissingChainSpec(raw_chain_spec.display().to_string()));
475	}
476	check_command_exists(binary_path, "export-genesis-wasm")?;
477	let wasm_file = raw_chain_spec.parent().unwrap_or(Path::new("./")).join(wasm_file_name);
478	let output = cmd(
479		binary_path,
480		vec![
481			"export-genesis-wasm",
482			"--chain",
483			&raw_chain_spec.display().to_string(),
484			&wasm_file.display().to_string(),
485		],
486	)
487	.stdout_null()
488	.stderr_capture()
489	.unchecked()
490	.run()?;
491	handle_command_error(&output, Error::BuildSpecError)?;
492	Ok(wasm_file)
493}
494
495/// Generate the chain genesis state.
496///
497/// # Arguments
498/// * `binary_path` - The path to the node binary executable that contains the
499///   `export-genesis-state` command.
500/// * `raw_chain_spec` - Location of the raw chain specification file.
501/// * `genesis_file_name` - The name of the genesis state file to be generated.
502pub fn generate_genesis_state_file_with_node(
503	binary_path: &Path,
504	raw_chain_spec: &Path,
505	genesis_file_name: &str,
506) -> Result<PathBuf, Error> {
507	if !raw_chain_spec.exists() {
508		return Err(Error::MissingChainSpec(raw_chain_spec.display().to_string()));
509	}
510	check_command_exists(binary_path, "export-genesis-state")?;
511	let genesis_file = raw_chain_spec.parent().unwrap_or(Path::new("./")).join(genesis_file_name);
512	let output = cmd(
513		binary_path,
514		vec![
515			"export-genesis-state",
516			"--chain",
517			&raw_chain_spec.display().to_string(),
518			&genesis_file.display().to_string(),
519		],
520	)
521	.stdout_null()
522	.stderr_capture()
523	.unchecked()
524	.run()?;
525	handle_command_error(&output, Error::BuildSpecError)?;
526	Ok(genesis_file)
527}
528
529/// Checks if a given command exists and can be executed by running it with the "--help" argument.
530fn check_command_exists(binary_path: &Path, command: &str) -> Result<(), Error> {
531	cmd(binary_path, vec![command, "--help"]).stdout_null().run().map_err(|_err| {
532		Error::MissingCommand {
533			command: command.to_string(),
534			binary: binary_path.display().to_string(),
535		}
536	})?;
537	Ok(())
538}
539
540/// A chain specification.
541pub struct ChainSpec(Value);
542impl ChainSpec {
543	/// Parses a chain specification from a path.
544	///
545	/// # Arguments
546	/// * `path` - The path to a chain specification file.
547	pub fn from(path: &Path) -> Result<ChainSpec> {
548		Ok(ChainSpec(Value::from_str(&fs::read_to_string(path)?)?))
549	}
550
551	/// Get the chain type from the chain specification.
552	pub fn get_chain_type(&self) -> Option<&str> {
553		self.0.get("chainType").and_then(|v| v.as_str())
554	}
555
556	/// Get the name from the chain specification.
557	pub fn get_name(&self) -> Option<&str> {
558		self.0.get("name").and_then(|v| v.as_str())
559	}
560
561	/// Get the chain ID from the chain specification.
562	pub fn get_chain_id(&self) -> Option<u64> {
563		self.0.get("para_id").and_then(|v| v.as_u64())
564	}
565
566	/// Get the property `basedOn` from the chain specification.
567	pub fn get_property_based_on(&self) -> Option<&str> {
568		self.0.get("properties").and_then(|v| v.get("basedOn")).and_then(|v| v.as_str())
569	}
570
571	/// Get the protocol ID from the chain specification.
572	pub fn get_protocol_id(&self) -> Option<&str> {
573		self.0.get("protocolId").and_then(|v| v.as_str())
574	}
575
576	/// Get the relay chain from the chain specification.
577	pub fn get_relay_chain(&self) -> Option<&str> {
578		self.0.get("relay_chain").and_then(|v| v.as_str())
579	}
580
581	/// Get the sudo key from the chain specification.
582	pub fn get_sudo_key(&self) -> Option<&str> {
583		self.0
584			.get("genesis")
585			.and_then(|genesis| genesis.get("runtimeGenesis"))
586			.and_then(|runtime_genesis| runtime_genesis.get("patch"))
587			.and_then(|patch| patch.get("sudo"))
588			.and_then(|sudo| sudo.get("key"))
589			.and_then(|key| key.as_str())
590	}
591
592	/// Replaces the chain id with the provided `para_id`.
593	///
594	/// # Arguments
595	/// * `para_id` - The new value for the para_id.
596	pub fn replace_para_id(&mut self, para_id: u32) -> Result<(), Error> {
597		// Replace para_id
598		let root = self
599			.0
600			.as_object_mut()
601			.ok_or_else(|| Error::Config("expected root object".into()))?;
602		root.insert("para_id".to_string(), json!(para_id));
603
604		// Replace genesis.runtimeGenesis.patch.parachainInfo.parachainId
605		let replace = self.0.pointer_mut("/genesis/runtimeGenesis/patch/parachainInfo/parachainId");
606		// If this fails, it means it is a raw chainspec
607		if let Some(replace) = replace {
608			*replace = json!(para_id);
609		}
610		Ok(())
611	}
612
613	/// Replaces the relay chain name with the given one.
614	///
615	/// # Arguments
616	/// * `relay_name` - The new value for the relay chain field in the specification.
617	pub fn replace_relay_chain(&mut self, relay_name: &str) -> Result<(), Error> {
618		// Replace relay_chain
619		let root = self
620			.0
621			.as_object_mut()
622			.ok_or_else(|| Error::Config("expected root object".into()))?;
623		root.insert("relay_chain".to_string(), json!(relay_name));
624		Ok(())
625	}
626
627	/// Replaces the chain type with the given one.
628	///
629	/// # Arguments
630	/// * `chain_type` - The new value for the chain type.
631	pub fn replace_chain_type(&mut self, chain_type: &str) -> Result<(), Error> {
632		// Replace chainType
633		let replace = self
634			.0
635			.get_mut("chainType")
636			.ok_or_else(|| Error::Config("expected `chainType`".into()))?;
637		*replace = json!(chain_type);
638		Ok(())
639	}
640
641	/// Replaces the protocol ID with the given one.
642	///
643	/// # Arguments
644	/// * `protocol_id` - The new value for the protocolId of the given specification.
645	pub fn replace_protocol_id(&mut self, protocol_id: &str) -> Result<(), Error> {
646		// Replace protocolId
647		let replace = self
648			.0
649			.get_mut("protocolId")
650			.ok_or_else(|| Error::Config("expected `protocolId`".into()))?;
651		*replace = json!(protocol_id);
652		Ok(())
653	}
654
655	/// Replaces the properties with the given ones.
656	///
657	/// # Arguments
658	/// * `raw_properties` - Comma-separated, key-value pairs. Example: "KEY1=VALUE1,KEY2=VALUE2".
659	pub fn replace_properties(&mut self, raw_properties: &str) -> Result<(), Error> {
660		// Replace properties
661		let replace = self
662			.0
663			.get_mut("properties")
664			.ok_or_else(|| Error::Config("expected `properties`".into()))?;
665		let mut properties = serde_json::Map::new();
666		let mut iter = raw_properties
667			.split(',')
668			.flat_map(|s| s.split('=').map(|p| p.trim()).collect::<Vec<_>>())
669			.collect::<Vec<_>>()
670			.into_iter();
671		while let Some(key) = iter.next() {
672			let value = iter.next().expect("Property value expected but not found");
673			properties.insert(key.to_string(), Value::String(value.to_string()));
674		}
675		*replace = Value::Object(properties);
676		Ok(())
677	}
678
679	/// Replaces the invulnerables session keys in the chain specification with the provided
680	/// `collator_keys`.
681	///
682	/// # Arguments
683	/// * `collator_keys` - A list of new collator keys.
684	pub fn replace_collator_keys(&mut self, collator_keys: Vec<String>) -> Result<(), Error> {
685		let uses_evm_keys = self
686			.0
687			.get("properties")
688			.and_then(|p| p.get("isEthereum"))
689			.and_then(|v| v.as_bool())
690			.unwrap_or(false);
691
692		let keys = if uses_evm_keys {
693			convert_to_evm_accounts(collator_keys.clone())?
694		} else {
695			collator_keys.clone()
696		};
697
698		let invulnerables = self
699			.0
700			.get_mut("genesis")
701			.ok_or_else(|| Error::Config("expected `genesis`".into()))?
702			.get_mut("runtimeGenesis")
703			.ok_or_else(|| Error::Config("expected `runtimeGenesis`".into()))?
704			.get_mut("patch")
705			.ok_or_else(|| Error::Config("expected `patch`".into()))?
706			.get_mut("collatorSelection")
707			.ok_or_else(|| Error::Config("expected `collatorSelection`".into()))?
708			.get_mut("invulnerables")
709			.ok_or_else(|| Error::Config("expected `invulnerables`".into()))?;
710
711		*invulnerables = json!(keys);
712
713		let session_keys = keys
714			.iter()
715			.zip(collator_keys.iter())
716			.map(|(address, original_address)| {
717				json!([
718					address,
719					address,
720					{ "aura": original_address } // Always the original address
721				])
722			})
723			.collect::<Vec<_>>();
724
725		let session_keys_field = self
726			.0
727			.get_mut("genesis")
728			.ok_or_else(|| Error::Config("expected `genesis`".into()))?
729			.get_mut("runtimeGenesis")
730			.ok_or_else(|| Error::Config("expected `runtimeGenesis`".into()))?
731			.get_mut("patch")
732			.ok_or_else(|| Error::Config("expected `patch`".into()))?
733			.get_mut("session")
734			.ok_or_else(|| Error::Config("expected `session`".into()))?
735			.get_mut("keys")
736			.ok_or_else(|| Error::Config("expected `session.keys`".into()))?;
737
738		*session_keys_field = json!(session_keys);
739
740		Ok(())
741	}
742
743	/// Converts the chain specification to a string.
744	pub fn to_string(&self) -> Result<String> {
745		Ok(serde_json::to_string_pretty(&self.0)?)
746	}
747
748	/// Writes the chain specification to a file.
749	///
750	/// # Arguments
751	/// * `path` - The path to the chain specification file.
752	pub fn to_file(&self, path: &Path) -> Result<()> {
753		fs::write(path, self.to_string()?)?;
754		Ok(())
755	}
756
757	/// Updates the runtime code in the chain specification.
758	///
759	/// # Arguments
760	/// * `bytes` - The new runtime code.
761	pub fn update_runtime_code(&mut self, bytes: &[u8]) -> Result<(), Error> {
762		// Replace `genesis.runtimeGenesis.code`
763		let code = self
764			.0
765			.get_mut("genesis")
766			.ok_or_else(|| Error::Config("expected `genesis`".into()))?
767			.get_mut("runtimeGenesis")
768			.ok_or_else(|| Error::Config("expected `runtimeGenesis`".into()))?
769			.get_mut("code")
770			.ok_or_else(|| Error::Config("expected `runtimeGenesis.code`".into()))?;
771		let hex = to_hex(bytes, true);
772		*code = json!(hex);
773		Ok(())
774	}
775}
776
777#[cfg(test)]
778mod tests {
779	use super::*;
780	use crate::{
781		Config, Error, new_chain::instantiate_standard_template, templates::ChainTemplate,
782		up::Zombienet,
783	};
784	use anyhow::Result;
785	use pop_common::{
786		manifest::{Dependency, add_feature},
787		set_executable_permission,
788	};
789	use sp_core::bytes::from_hex;
790	use std::{
791		fs::{self, write},
792		io::Write,
793		path::Path,
794	};
795	use strum::VariantArray;
796	use tempfile::{Builder, TempDir, tempdir};
797
798	static MOCK_WASM: &[u8] = include_bytes!("../../../../tests/runtimes/base_parachain.wasm");
799
800	fn setup_template_and_instantiate() -> Result<TempDir> {
801		let temp_dir = tempdir().expect("Failed to create temp dir");
802		let config = Config {
803			symbol: "DOT".to_string(),
804			decimals: 18,
805			initial_endowment: "1000000".to_string(),
806		};
807		instantiate_standard_template(&ChainTemplate::Standard, temp_dir.path(), config, None)?;
808		Ok(temp_dir)
809	}
810
811	// Function that mocks the build process generating the target dir and release.
812	fn mock_build_process(temp_dir: &Path) -> Result<(), Error> {
813		// Create a target directory
814		let target_dir = temp_dir.join("target");
815		fs::create_dir(&target_dir)?;
816		fs::create_dir(target_dir.join("release"))?;
817		// Create a release file
818		fs::File::create(target_dir.join("release/parachain-template-node"))?;
819		Ok(())
820	}
821
822	// Function that mocks the build process of WASM runtime generating the target dir and release.
823	fn mock_build_runtime_process(temp_dir: &Path) -> Result<(), Error> {
824		let runtime = "parachain-template-runtime";
825		// Create a target directory
826		let target_dir = temp_dir.join("target");
827		fs::create_dir(&target_dir)?;
828		fs::create_dir(target_dir.join("release"))?;
829		fs::create_dir(target_dir.join("release/wbuild"))?;
830		fs::create_dir(target_dir.join(format!("release/wbuild/{runtime}")))?;
831		// Create a WASM binary file
832		fs::File::create(
833			target_dir.join(format!("release/wbuild/{runtime}/{}.wasm", runtime.replace("-", "_"))),
834		)?;
835		Ok(())
836	}
837
838	// Function that generates a Cargo.toml inside node directory for testing.
839	fn generate_mock_node(temp_dir: &Path, name: Option<&str>) -> Result<PathBuf, Error> {
840		// Create a node directory
841		let target_dir = temp_dir.join(name.unwrap_or("node"));
842		fs::create_dir(&target_dir)?;
843		// Create a Cargo.toml file
844		let mut toml_file = fs::File::create(target_dir.join("Cargo.toml"))?;
845		writeln!(
846			toml_file,
847			r#"
848			[package]
849			name = "parachain_template_node"
850			version = "0.1.0"
851
852			[dependencies]
853
854			"#
855		)?;
856		Ok(target_dir)
857	}
858
859	// Function that fetch a binary from pop network
860	async fn fetch_binary(cache: &Path) -> Result<String, Error> {
861		let config = Builder::new().suffix(".toml").tempfile()?;
862		writeln!(
863			config.as_file(),
864			r#"
865            [relaychain]
866            chain = "paseo-local"
867
868			[[parachains]]
869			id = 4385
870			default_command = "pop-node"
871			"#
872		)?;
873		let mut zombienet = Zombienet::new(
874			cache,
875			config.path().try_into()?,
876			None,
877			None,
878			None,
879			None,
880			Some(&vec!["https://github.com/r0gue-io/pop-node#node-v0.3.0".to_string()]),
881		)
882		.await?;
883		let mut binary_name: String = "".to_string();
884		for binary in zombienet.binaries().filter(|b| !b.exists() && b.name() == "pop-node") {
885			binary_name = format!("{}-{}", binary.name(), binary.version().unwrap());
886			binary.source(true, &(), true).await?;
887		}
888		Ok(binary_name)
889	}
890
891	// Replace the binary fetched with the mocked binary
892	fn replace_mock_with_binary(temp_dir: &Path, binary_name: String) -> Result<PathBuf, Error> {
893		let binary_path = temp_dir.join(binary_name);
894		let content = fs::read(&binary_path)?;
895		write(temp_dir.join("target/release/parachain-template-node"), content)?;
896		// Make executable
897		set_executable_permission(temp_dir.join("target/release/parachain-template-node"))?;
898		Ok(binary_path)
899	}
900
901	fn add_production_profile(project: &Path) -> Result<()> {
902		let root_toml_path = project.join("Cargo.toml");
903		let mut root_toml_content = fs::read_to_string(&root_toml_path)?;
904		root_toml_content.push_str(
905			r#"
906			[profile.production]
907			codegen-units = 1
908			inherits = "release"
909			lto = true
910			"#,
911		);
912		// Write the updated content back to the file
913		write(&root_toml_path, root_toml_content)?;
914		Ok(())
915	}
916
917	#[test]
918	fn build_chain_works() -> Result<()> {
919		let name = "parachain_template_node";
920		let temp_dir = tempdir()?;
921		cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?;
922		let project = temp_dir.path().join(name);
923		add_production_profile(&project)?;
924		add_feature(&project, ("dummy-feature".to_string(), vec![]))?;
925		for node in [None, Some("custom_node")] {
926			let node_path = generate_mock_node(&project, node)?;
927			for package in [None, Some(String::from("parachain_template_node"))] {
928				for profile in Profile::VARIANTS {
929					let node_path = node.map(|_| node_path.as_path());
930					let binary = build_chain(
931						&project,
932						package.clone(),
933						profile,
934						node_path,
935						&["dummy-feature".to_string()],
936					)?;
937					let target_directory = profile.target_directory(&project);
938					assert!(target_directory.exists());
939					assert!(target_directory.join("parachain_template_node").exists());
940					assert_eq!(
941						binary.display().to_string(),
942						target_directory.join("parachain_template_node").display().to_string()
943					);
944				}
945			}
946		}
947		Ok(())
948	}
949
950	#[test]
951	fn build_project_works() -> Result<()> {
952		let name = "example_project";
953		let temp_dir = tempdir()?;
954		cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?;
955		let project = temp_dir.path().join(name);
956		add_production_profile(&project)?;
957		add_feature(&project, ("dummy-feature".to_string(), vec![]))?;
958		for package in [None, Some(String::from(name))] {
959			for profile in Profile::VARIANTS {
960				build_project(
961					&project,
962					package.clone(),
963					profile,
964					&["dummy-feature".to_string()],
965					None,
966				)?;
967				let target_directory = profile.target_directory(&project);
968				let binary = build_binary_path(&project, |runtime_name| {
969					target_directory.join(runtime_name)
970				})?;
971				assert!(target_directory.exists());
972				assert!(target_directory.join(name).exists());
973				assert_eq!(
974					binary.display().to_string(),
975					target_directory.join(name).display().to_string()
976				);
977			}
978		}
979		Ok(())
980	}
981
982	#[test]
983	fn binary_path_of_node_works() -> Result<()> {
984		let temp_dir =
985			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
986		mock_build_process(temp_dir.path())?;
987		let release_path =
988			binary_path(&temp_dir.path().join("target/release"), &temp_dir.path().join("node"))?;
989		assert_eq!(
990			release_path.display().to_string(),
991			format!("{}/target/release/parachain-template-node", temp_dir.path().display())
992		);
993		Ok(())
994	}
995
996	#[test]
997	fn binary_path_of_runtime_works() -> Result<()> {
998		let temp_dir =
999			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1000		// Ensure binary path works for the runtime.
1001		let runtime = "parachain-template-runtime";
1002		mock_build_runtime_process(temp_dir.path())?;
1003		let release_path = runtime_binary_path(
1004			&temp_dir.path().join("target/release/wbuild"),
1005			&temp_dir.path().join("runtime"),
1006		)?;
1007		assert_eq!(
1008			release_path.display().to_string(),
1009			format!(
1010				"{}/target/release/wbuild/{runtime}/{}.wasm",
1011				temp_dir.path().display(),
1012				runtime.replace("-", "_")
1013			)
1014		);
1015
1016		Ok(())
1017	}
1018
1019	#[test]
1020	fn binary_path_fails_missing_binary() -> Result<()> {
1021		let temp_dir =
1022			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1023		assert!(matches!(
1024			binary_path(&temp_dir.path().join("target/release"), &temp_dir.path().join("node")),
1025			Err(Error::MissingBinary(error)) if error == "parachain-template-node"
1026		));
1027		Ok(())
1028	}
1029
1030	#[tokio::test]
1031	async fn generate_files_works() -> Result<()> {
1032		let temp_dir =
1033			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1034		mock_build_process(temp_dir.path())?;
1035		let binary_name = fetch_binary(temp_dir.path()).await?;
1036		let binary_path = replace_mock_with_binary(temp_dir.path(), binary_name)?;
1037		// Test generate chain spec
1038		let plain_chain_spec = &temp_dir.path().join("plain-parachain-chainspec.json");
1039		generate_plain_chain_spec_with_node(
1040			&binary_path,
1041			&temp_dir.path().join("plain-parachain-chainspec.json"),
1042			false,
1043			"local",
1044		)?;
1045		assert!(plain_chain_spec.exists());
1046		{
1047			let mut chain_spec = ChainSpec::from(plain_chain_spec)?;
1048			chain_spec.replace_para_id(2001)?;
1049			chain_spec.to_file(plain_chain_spec)?;
1050		}
1051		let raw_chain_spec = generate_raw_chain_spec_with_node(
1052			&binary_path,
1053			plain_chain_spec,
1054			"raw-parachain-chainspec.json",
1055		)?;
1056		assert!(raw_chain_spec.exists());
1057		let content = fs::read_to_string(raw_chain_spec.clone()).expect("Could not read file");
1058		assert!(content.contains("\"para_id\": 2001"));
1059		assert!(content.contains("\"bootNodes\": []"));
1060		// Test export wasm file
1061		let wasm_file =
1062			export_wasm_file_with_node(&binary_path, &raw_chain_spec, "para-2001-wasm")?;
1063		assert!(wasm_file.exists());
1064		// Test generate chain state file
1065		let genesis_file = generate_genesis_state_file_with_node(
1066			&binary_path,
1067			&raw_chain_spec,
1068			"para-2001-genesis-state",
1069		)?;
1070		assert!(genesis_file.exists());
1071		Ok(())
1072	}
1073
1074	#[tokio::test]
1075	async fn generate_plain_chain_spec_with_runtime_works_with_name_and_id_override() -> Result<()>
1076	{
1077		let temp_dir =
1078			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1079		// Test generate chain spec
1080		let plain_chain_spec = &temp_dir.path().join("plain-parachain-chainspec.json");
1081		generate_plain_chain_spec_with_runtime(
1082			Vec::from(MOCK_WASM),
1083			plain_chain_spec,
1084			"local_testnet",
1085			Some("POP Chain Spec"),
1086			Some("pop-chain-spec"),
1087		)?;
1088		assert!(plain_chain_spec.exists());
1089		let raw_chain_spec =
1090			generate_raw_chain_spec_with_runtime(plain_chain_spec, "raw-parachain-chainspec.json")?;
1091		assert!(raw_chain_spec.exists());
1092		let content = fs::read_to_string(raw_chain_spec.clone()).expect("Could not read file");
1093		assert!(content.contains("\"name\": \"POP Chain Spec\""));
1094		assert!(content.contains("\"id\": \"pop-chain-spec\""));
1095		assert!(content.contains("\"bootNodes\": []"));
1096		Ok(())
1097	}
1098
1099	#[tokio::test]
1100	async fn generate_plain_chain_spec_with_runtime_works_with_name_override() -> Result<()> {
1101		let temp_dir =
1102			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1103		// Test generate chain spec
1104		let plain_chain_spec = &temp_dir.path().join("plain-parachain-chainspec.json");
1105		generate_plain_chain_spec_with_runtime(
1106			Vec::from(MOCK_WASM),
1107			plain_chain_spec,
1108			"local_testnet",
1109			Some("POP Chain Spec"),
1110			None,
1111		)?;
1112		assert!(plain_chain_spec.exists());
1113		let raw_chain_spec =
1114			generate_raw_chain_spec_with_runtime(plain_chain_spec, "raw-parachain-chainspec.json")?;
1115		assert!(raw_chain_spec.exists());
1116		let content = fs::read_to_string(raw_chain_spec.clone()).expect("Could not read file");
1117		assert!(content.contains("\"name\": \"POP Chain Spec\""));
1118		assert!(content.contains("\"id\": \"dev\""));
1119		assert!(content.contains("\"bootNodes\": []"));
1120		Ok(())
1121	}
1122
1123	#[tokio::test]
1124	async fn generate_plain_chain_spec_with_runtime_works_with_id_override() -> Result<()> {
1125		let temp_dir =
1126			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1127		// Test generate chain spec
1128		let plain_chain_spec = &temp_dir.path().join("plain-parachain-chainspec.json");
1129		generate_plain_chain_spec_with_runtime(
1130			Vec::from(MOCK_WASM),
1131			plain_chain_spec,
1132			"local_testnet",
1133			None,
1134			Some("pop-chain-spec"),
1135		)?;
1136		assert!(plain_chain_spec.exists());
1137		let raw_chain_spec =
1138			generate_raw_chain_spec_with_runtime(plain_chain_spec, "raw-parachain-chainspec.json")?;
1139		assert!(raw_chain_spec.exists());
1140		let content = fs::read_to_string(raw_chain_spec.clone()).expect("Could not read file");
1141		assert!(content.contains("\"name\": \"Development\""));
1142		assert!(content.contains("\"id\": \"pop-chain-spec\""));
1143		assert!(content.contains("\"bootNodes\": []"));
1144		Ok(())
1145	}
1146
1147	#[tokio::test]
1148	async fn generate_plain_chain_spec_with_runtime_works_without_name_and_id_override()
1149	-> Result<()> {
1150		let temp_dir =
1151			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1152		// Test generate chain spec
1153		let plain_chain_spec = &temp_dir.path().join("plain-parachain-chainspec.json");
1154		generate_plain_chain_spec_with_runtime(
1155			Vec::from(MOCK_WASM),
1156			plain_chain_spec,
1157			"local_testnet",
1158			None,
1159			None,
1160		)?;
1161		assert!(plain_chain_spec.exists());
1162		let raw_chain_spec =
1163			generate_raw_chain_spec_with_runtime(plain_chain_spec, "raw-parachain-chainspec.json")?;
1164		assert!(raw_chain_spec.exists());
1165		let content = fs::read_to_string(raw_chain_spec.clone()).expect("Could not read file");
1166		assert!(content.contains("\"name\": \"Development\""));
1167		assert!(content.contains("\"id\": \"dev\""));
1168		assert!(content.contains("\"bootNodes\": []"));
1169		Ok(())
1170	}
1171
1172	#[tokio::test]
1173	async fn fails_to_generate_plain_chain_spec_when_file_missing() -> Result<()> {
1174		let temp_dir =
1175			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1176		mock_build_process(temp_dir.path())?;
1177		let binary_name = fetch_binary(temp_dir.path()).await?;
1178		let binary_path = replace_mock_with_binary(temp_dir.path(), binary_name)?;
1179		assert!(matches!(
1180			generate_plain_chain_spec_with_node(
1181				&binary_path,
1182				&temp_dir.path().join("plain-parachain-chainspec.json"),
1183				false,
1184				&temp_dir.path().join("plain-parachain-chainspec.json").display().to_string(),
1185			),
1186			Err(Error::BuildSpecError(message)) if message.contains("No such file or directory")
1187		));
1188		assert!(!temp_dir.path().join("plain-parachain-chainspec.json").exists());
1189		Ok(())
1190	}
1191
1192	#[test]
1193	fn raw_chain_spec_fails_wrong_chain_spec() -> Result<()> {
1194		assert!(matches!(
1195			generate_raw_chain_spec_with_node(
1196				Path::new("./binary"),
1197				Path::new("./plain-parachain-chainspec.json"),
1198				"plain-parachain-chainspec.json"
1199			),
1200			Err(Error::MissingChainSpec(..))
1201		));
1202		Ok(())
1203	}
1204
1205	#[test]
1206	fn export_wasm_file_fails_wrong_chain_spec() -> Result<()> {
1207		assert!(matches!(
1208			export_wasm_file_with_node(
1209				Path::new("./binary"),
1210				Path::new("./raw-parachain-chainspec"),
1211				"para-2001-wasm"
1212			),
1213			Err(Error::MissingChainSpec(..))
1214		));
1215		Ok(())
1216	}
1217
1218	#[test]
1219	fn generate_genesis_state_file_wrong_chain_spec() -> Result<()> {
1220		assert!(matches!(
1221			generate_genesis_state_file_with_node(
1222				Path::new("./binary"),
1223				Path::new("./raw-parachain-chainspec"),
1224				"para-2001-genesis-state",
1225			),
1226			Err(Error::MissingChainSpec(..))
1227		));
1228		Ok(())
1229	}
1230
1231	#[test]
1232	fn get_chain_type_works() -> Result<()> {
1233		let chain_spec = ChainSpec(json!({
1234			"chainType": "test",
1235		}));
1236		assert_eq!(chain_spec.get_chain_type(), Some("test"));
1237		Ok(())
1238	}
1239
1240	#[test]
1241	fn get_chain_name_works() -> Result<()> {
1242		assert_eq!(ChainSpec(json!({})).get_name(), None);
1243		let chain_spec = ChainSpec(json!({
1244			"name": "test",
1245		}));
1246		assert_eq!(chain_spec.get_name(), Some("test"));
1247		Ok(())
1248	}
1249
1250	#[test]
1251	fn get_chain_id_works() -> Result<()> {
1252		let chain_spec = ChainSpec(json!({
1253			"para_id": 2002,
1254		}));
1255		assert_eq!(chain_spec.get_chain_id(), Some(2002));
1256		Ok(())
1257	}
1258
1259	#[test]
1260	fn get_property_based_on_works() -> Result<()> {
1261		assert_eq!(ChainSpec(json!({})).get_property_based_on(), None);
1262		let chain_spec = ChainSpec(json!({
1263			"properties": {
1264				"basedOn": "test",
1265			}
1266		}));
1267		assert_eq!(chain_spec.get_property_based_on(), Some("test"));
1268		Ok(())
1269	}
1270
1271	#[test]
1272	fn get_protocol_id_works() -> Result<()> {
1273		let chain_spec = ChainSpec(json!({
1274			"protocolId": "test",
1275		}));
1276		assert_eq!(chain_spec.get_protocol_id(), Some("test"));
1277		Ok(())
1278	}
1279
1280	#[test]
1281	fn get_relay_chain_works() -> Result<()> {
1282		let chain_spec = ChainSpec(json!({
1283			"relay_chain": "test",
1284		}));
1285		assert_eq!(chain_spec.get_relay_chain(), Some("test"));
1286		Ok(())
1287	}
1288
1289	#[test]
1290	fn get_sudo_key_works() -> Result<()> {
1291		assert_eq!(ChainSpec(json!({})).get_sudo_key(), None);
1292		let chain_spec = ChainSpec(json!({
1293			"para_id": 1000,
1294			"genesis": {
1295				"runtimeGenesis": {
1296					"patch": {
1297						"sudo": {
1298							"key": "sudo-key"
1299						}
1300					}
1301				}
1302			},
1303		}));
1304		assert_eq!(chain_spec.get_sudo_key(), Some("sudo-key"));
1305		Ok(())
1306	}
1307
1308	#[test]
1309	fn replace_para_id_works() -> Result<()> {
1310		let mut chain_spec = ChainSpec(json!({
1311			"para_id": 1000,
1312			"genesis": {
1313				"runtimeGenesis": {
1314					"patch": {
1315						"parachainInfo": {
1316							"parachainId": 1000
1317						}
1318					}
1319				}
1320			},
1321		}));
1322		chain_spec.replace_para_id(2001)?;
1323		assert_eq!(
1324			chain_spec.0,
1325			json!({
1326				"para_id": 2001,
1327				"genesis": {
1328					"runtimeGenesis": {
1329						"patch": {
1330							"parachainInfo": {
1331								"parachainId": 2001
1332							}
1333						}
1334					}
1335				},
1336			})
1337		);
1338		Ok(())
1339	}
1340
1341	#[test]
1342	fn replace_para_id_fails() -> Result<()> {
1343		let mut chain_spec = ChainSpec(json!({
1344			"para_id": 2001,
1345			"": {
1346				"runtimeGenesis": {
1347					"patch": {
1348						"parachainInfo": {
1349							"parachainId": 1000
1350						}
1351					}
1352				}
1353			},
1354		}));
1355		assert!(chain_spec.replace_para_id(2001).is_ok());
1356		chain_spec = ChainSpec(json!({
1357			"para_id": 2001,
1358			"genesis": {
1359				"": {
1360					"patch": {
1361						"parachainInfo": {
1362							"parachainId": 1000
1363						}
1364					}
1365				}
1366			},
1367		}));
1368		assert!(chain_spec.replace_para_id(2001).is_ok());
1369		chain_spec = ChainSpec(json!({
1370			"para_id": 2001,
1371			"genesis": {
1372				"runtimeGenesis": {
1373					"": {
1374						"parachainInfo": {
1375							"parachainId": 1000
1376						}
1377					}
1378				}
1379			},
1380		}));
1381		assert!(chain_spec.replace_para_id(2001).is_ok());
1382		chain_spec = ChainSpec(json!({
1383			"para_id": 2001,
1384			"genesis": {
1385				"runtimeGenesis": {
1386					"patch": {
1387						"": {
1388							"parachainId": 1000
1389						}
1390					}
1391				}
1392			},
1393		}));
1394		assert!(chain_spec.replace_para_id(2001).is_ok());
1395		chain_spec = ChainSpec(json!({
1396			"para_id": 2001,
1397			"genesis": {
1398				"runtimeGenesis": {
1399					"patch": {
1400						"parachainInfo": {
1401						}
1402					}
1403				}
1404			},
1405		}));
1406		assert!(chain_spec.replace_para_id(2001).is_ok());
1407		Ok(())
1408	}
1409
1410	#[test]
1411	fn replace_relay_chain_works() -> Result<()> {
1412		let mut chain_spec = ChainSpec(json!({"relay_chain": "old-relay"}));
1413		chain_spec.replace_relay_chain("new-relay")?;
1414		assert_eq!(chain_spec.0, json!({"relay_chain": "new-relay"}));
1415		Ok(())
1416	}
1417
1418	#[test]
1419	fn replace_chain_type_works() -> Result<()> {
1420		let mut chain_spec = ChainSpec(json!({"chainType": "old-chainType"}));
1421		chain_spec.replace_chain_type("new-chainType")?;
1422		assert_eq!(chain_spec.0, json!({"chainType": "new-chainType"}));
1423		Ok(())
1424	}
1425
1426	#[test]
1427	fn replace_chain_type_fails() -> Result<()> {
1428		let mut chain_spec = ChainSpec(json!({"": "old-chainType"}));
1429		assert!(
1430			matches!(chain_spec.replace_chain_type("new-chainType"), Err(Error::Config(error)) if error == "expected `chainType`")
1431		);
1432		Ok(())
1433	}
1434
1435	#[test]
1436	fn replace_protocol_id_works() -> Result<()> {
1437		let mut chain_spec = ChainSpec(json!({"protocolId": "old-protocolId"}));
1438		chain_spec.replace_protocol_id("new-protocolId")?;
1439		assert_eq!(chain_spec.0, json!({"protocolId": "new-protocolId"}));
1440		Ok(())
1441	}
1442
1443	#[test]
1444	fn replace_protocol_id_fails() -> Result<()> {
1445		let mut chain_spec = ChainSpec(json!({"": "old-protocolId"}));
1446		assert!(
1447			matches!(chain_spec.replace_protocol_id("new-protocolId"), Err(Error::Config(error)) if error == "expected `protocolId`")
1448		);
1449		Ok(())
1450	}
1451
1452	#[test]
1453	fn replace_collator_keys_works() -> Result<()> {
1454		let mut chain_spec = ChainSpec(json!({
1455			"para_id": 1000,
1456			"genesis": {
1457				"runtimeGenesis": {
1458					"patch": {
1459						"collatorSelection": {
1460							"invulnerables": [
1461							  "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
1462							  "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
1463							]
1464						  },
1465						  "session": {
1466							"keys": [
1467							  [
1468								"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
1469								"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
1470								{
1471								  "aura": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
1472								}
1473							  ],
1474							  [
1475								"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
1476								"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
1477								{
1478								  "aura": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
1479								}
1480							  ]
1481							]
1482						  },
1483					}
1484				}
1485			},
1486		}));
1487		chain_spec.replace_collator_keys(vec![
1488			"5Gw3s7q4QLkSWwknsi8jj5P1K79e5N4b6pfsNUzS97H1DXYF".to_string(),
1489		])?;
1490		assert_eq!(
1491			chain_spec.0,
1492			json!({
1493				"para_id": 1000,
1494				"genesis": {
1495				"runtimeGenesis": {
1496					"patch": {
1497						"collatorSelection": {
1498							"invulnerables": [
1499							  "5Gw3s7q4QLkSWwknsi8jj5P1K79e5N4b6pfsNUzS97H1DXYF",
1500							]
1501						  },
1502						  "session": {
1503							"keys": [
1504							  [
1505								"5Gw3s7q4QLkSWwknsi8jj5P1K79e5N4b6pfsNUzS97H1DXYF",
1506								"5Gw3s7q4QLkSWwknsi8jj5P1K79e5N4b6pfsNUzS97H1DXYF",
1507								{
1508								  "aura": "5Gw3s7q4QLkSWwknsi8jj5P1K79e5N4b6pfsNUzS97H1DXYF"
1509								}
1510							  ],
1511							]
1512						  },
1513					}
1514				}
1515			},
1516			})
1517		);
1518		Ok(())
1519	}
1520
1521	#[test]
1522	fn replace_use_evm_collator_keys_works() -> Result<()> {
1523		let mut chain_spec = ChainSpec(json!({
1524			"para_id": 1000,
1525			"properties": {
1526				"isEthereum": true
1527			},
1528			"genesis": {
1529				"runtimeGenesis": {
1530					"patch": {
1531						"collatorSelection": {
1532							"invulnerables": [
1533							  "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
1534							]
1535						  },
1536						  "session": {
1537							"keys": [
1538							  [
1539								"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
1540								"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
1541								{
1542								  "aura": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
1543								}
1544							  ]
1545							]
1546						  },
1547					}
1548				}
1549			},
1550		}));
1551		chain_spec.replace_collator_keys(vec![
1552			"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".to_string(),
1553		])?;
1554		assert_eq!(
1555			chain_spec.0,
1556			json!({
1557				"para_id": 1000,
1558				"properties": {
1559					"isEthereum": true
1560				},
1561				"genesis": {
1562				"runtimeGenesis": {
1563					"patch": {
1564						"collatorSelection": {
1565							"invulnerables": [
1566							  "0x9621dde636de098b43efb0fa9b61facfe328f99d",
1567							]
1568						  },
1569						  "session": {
1570							"keys": [
1571							  [
1572								"0x9621dde636de098b43efb0fa9b61facfe328f99d",
1573								"0x9621dde636de098b43efb0fa9b61facfe328f99d",
1574								{
1575								  "aura": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
1576								}
1577							  ],
1578							]
1579						  },
1580					}
1581				}
1582			},
1583			})
1584		);
1585		Ok(())
1586	}
1587
1588	#[test]
1589	fn update_runtime_code_works() -> Result<()> {
1590		let mut chain_spec =
1591			ChainSpec(json!({"genesis": {"runtimeGenesis" : {  "code": "0x00" }}}));
1592
1593		chain_spec.update_runtime_code(&from_hex("0x1234")?)?;
1594		assert_eq!(chain_spec.0, json!({"genesis": {"runtimeGenesis" : {  "code": "0x1234" }}}));
1595		Ok(())
1596	}
1597
1598	#[test]
1599	fn update_runtime_code_fails() -> Result<()> {
1600		let mut chain_spec =
1601			ChainSpec(json!({"invalidKey": {"runtimeGenesis" : {  "code": "0x00" }}}));
1602		assert!(
1603			matches!(chain_spec.update_runtime_code(&from_hex("0x1234")?), Err(Error::Config(error)) if error == "expected `genesis`")
1604		);
1605
1606		chain_spec = ChainSpec(json!({"genesis": {"invalidKey" : {  "code": "0x00" }}}));
1607		assert!(
1608			matches!(chain_spec.update_runtime_code(&from_hex("0x1234")?), Err(Error::Config(error)) if error == "expected `runtimeGenesis`")
1609		);
1610
1611		chain_spec = ChainSpec(json!({"genesis": {"runtimeGenesis" : {  "invalidKey": "0x00" }}}));
1612		assert!(
1613			matches!(chain_spec.update_runtime_code(&from_hex("0x1234")?), Err(Error::Config(error)) if error == "expected `runtimeGenesis.code`")
1614		);
1615		Ok(())
1616	}
1617
1618	#[test]
1619	fn check_command_exists_fails() -> Result<()> {
1620		let binary_path = PathBuf::from("/bin");
1621		let cmd = "nonexistent_command";
1622		assert!(matches!(
1623			check_command_exists(&binary_path, cmd),
1624			Err(Error::MissingCommand {command, binary })
1625			if command == cmd && binary == binary_path.display().to_string()
1626		));
1627		Ok(())
1628	}
1629
1630	#[test]
1631	fn is_supported_works() -> Result<()> {
1632		let temp_dir = tempdir()?;
1633		let path = temp_dir.path();
1634
1635		// Standard rust project
1636		let name = "hello_world";
1637		cmd("cargo", ["new", name]).dir(path).run()?;
1638		assert!(!is_supported(&path.join(name)));
1639
1640		// Chain
1641		let mut manifest = from_path(&path.join(name))?;
1642		manifest
1643			.dependencies
1644			.insert("cumulus-client-collator".into(), Dependency::Simple("^0.14.0".into()));
1645		let manifest = toml_edit::ser::to_string_pretty(&manifest)?;
1646		write(path.join(name).join("Cargo.toml"), manifest)?;
1647		assert!(is_supported(&path.join(name)));
1648		Ok(())
1649	}
1650
1651	#[test]
1652	fn chain_spec_builder_node_path_works() -> Result<()> {
1653		let node_path = PathBuf::from("/test/node");
1654		let builder = ChainSpecBuilder::Node {
1655			node_path: node_path.clone(),
1656			default_bootnode: true,
1657			profile: Profile::Release,
1658		};
1659		assert_eq!(builder.path(), node_path);
1660		Ok(())
1661	}
1662
1663	#[test]
1664	fn chain_spec_builder_runtime_path_works() -> Result<()> {
1665		let runtime_path = PathBuf::from("/test/runtime");
1666		let builder = ChainSpecBuilder::Runtime {
1667			runtime_path: runtime_path.clone(),
1668			profile: Profile::Release,
1669		};
1670		assert_eq!(builder.path(), runtime_path);
1671		Ok(())
1672	}
1673
1674	#[test]
1675	fn chain_spec_builder_node_profile_works() -> Result<()> {
1676		for profile in Profile::VARIANTS {
1677			let builder = ChainSpecBuilder::Node {
1678				node_path: PathBuf::from("/test/node"),
1679				default_bootnode: true,
1680				profile: profile.clone(),
1681			};
1682			assert_eq!(builder.profile(), *profile);
1683		}
1684		Ok(())
1685	}
1686
1687	#[test]
1688	fn chain_spec_builder_runtime_profile_works() -> Result<()> {
1689		for profile in Profile::VARIANTS {
1690			let builder = ChainSpecBuilder::Runtime {
1691				runtime_path: PathBuf::from("/test/runtime"),
1692				profile: profile.clone(),
1693			};
1694			assert_eq!(builder.profile(), *profile);
1695		}
1696		Ok(())
1697	}
1698
1699	#[test]
1700	fn chain_spec_builder_node_artifact_path_works() -> Result<()> {
1701		let temp_dir =
1702			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1703		mock_build_process(temp_dir.path())?;
1704
1705		let builder = ChainSpecBuilder::Node {
1706			node_path: temp_dir.path().join("node"),
1707			default_bootnode: true,
1708			profile: Profile::Release,
1709		};
1710		let artifact_path = builder.artifact_path()?;
1711		assert!(artifact_path.exists());
1712		assert!(artifact_path.ends_with("parachain-template-node"));
1713		Ok(())
1714	}
1715
1716	#[test]
1717	fn chain_spec_builder_runtime_artifact_path_works() -> Result<()> {
1718		let temp_dir =
1719			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1720		mock_build_runtime_process(temp_dir.path())?;
1721
1722		let builder = ChainSpecBuilder::Runtime {
1723			runtime_path: temp_dir.path().join("runtime"),
1724			profile: Profile::Release,
1725		};
1726		let artifact_path = builder.artifact_path()?;
1727		assert!(artifact_path.is_file());
1728		assert!(artifact_path.ends_with("parachain_template_runtime.wasm"));
1729		Ok(())
1730	}
1731
1732	#[test]
1733	fn chain_spec_builder_node_artifact_path_fails() -> Result<()> {
1734		let temp_dir =
1735			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1736
1737		let builder = ChainSpecBuilder::Node {
1738			node_path: temp_dir.path().join("node"),
1739			default_bootnode: true,
1740			profile: Profile::Release,
1741		};
1742		assert!(builder.artifact_path().is_err());
1743		Ok(())
1744	}
1745
1746	#[test]
1747	fn chain_spec_builder_runtime_artifact_path_fails() -> Result<()> {
1748		let temp_dir =
1749			setup_template_and_instantiate().expect("Failed to setup template and instantiate");
1750
1751		let builder = ChainSpecBuilder::Runtime {
1752			runtime_path: temp_dir.path().join("runtime"),
1753			profile: Profile::Release,
1754		};
1755		let result = builder.artifact_path();
1756		assert!(result.is_err());
1757		assert!(matches!(result, Err(e) if e.to_string().contains("No runtime found")));
1758		Ok(())
1759	}
1760
1761	#[test]
1762	fn chain_spec_builder_generate_raw_chain_spec_works() -> Result<()> {
1763		let temp_dir = tempdir()?;
1764		let builder = ChainSpecBuilder::Runtime {
1765			runtime_path: temp_dir.path().join("runtime"),
1766			profile: Profile::Release,
1767		};
1768		let original_chain_spec_path =
1769			PathBuf::from("artifacts/passet-hub-spec.json").canonicalize()?;
1770		assert!(original_chain_spec_path.exists());
1771		let chain_spec_path = temp_dir.path().join(original_chain_spec_path.file_name().unwrap());
1772		fs::copy(&original_chain_spec_path, &chain_spec_path)?;
1773		let raw_chain_spec_path = temp_dir.path().join("raw.json");
1774		let final_raw_path = builder.generate_raw_chain_spec(
1775			&chain_spec_path,
1776			raw_chain_spec_path.file_name().unwrap().to_str().unwrap(),
1777		)?;
1778		assert!(final_raw_path.is_file());
1779		assert_eq!(final_raw_path, raw_chain_spec_path);
1780
1781		// Check raw chain spec contains expected fields
1782		let raw_content = fs::read_to_string(&raw_chain_spec_path)?;
1783		let raw_json: Value = serde_json::from_str(&raw_content)?;
1784		assert!(raw_json.get("genesis").is_some());
1785		assert!(raw_json.get("genesis").unwrap().get("raw").is_some());
1786		assert!(raw_json.get("genesis").unwrap().get("raw").unwrap().get("top").is_some());
1787		Ok(())
1788	}
1789
1790	#[test]
1791	fn chain_spec_builder_export_wasm_works() -> Result<()> {
1792		let temp_dir = tempdir()?;
1793		let builder = ChainSpecBuilder::Runtime {
1794			runtime_path: temp_dir.path().join("runtime"),
1795			profile: Profile::Release,
1796		};
1797		let original_chain_spec_path =
1798			PathBuf::from("artifacts/passet-hub-spec.json").canonicalize()?;
1799		let chain_spec_path = temp_dir.path().join(original_chain_spec_path.file_name().unwrap());
1800		fs::copy(&original_chain_spec_path, &chain_spec_path)?;
1801		let final_wasm_path = temp_dir.path().join("runtime.wasm");
1802		let final_raw_path = builder.generate_raw_chain_spec(&chain_spec_path, "raw.json")?;
1803		let wasm_path = builder.export_wasm_file(
1804			&final_raw_path,
1805			final_wasm_path.file_name().unwrap().to_str().unwrap(),
1806		)?;
1807		assert!(wasm_path.is_file());
1808		assert_eq!(final_wasm_path, wasm_path);
1809		Ok(())
1810	}
1811}