Skip to main content

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