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