pop_chains/up/
mod.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::{
4	errors::Error, omni_node::PolkadotOmniNodeCli::PolkadotOmniNode, registry::traits::Rollup,
5	up::chain_specs::Runtime,
6};
7pub use chain_specs::Runtime as Relay;
8use glob::glob;
9use indexmap::IndexMap;
10use pop_common::sourcing::traits::{Source as _, enums::Source as _};
11pub use pop_common::{
12	Profile,
13	git::{GitHub, Repository},
14	sourcing::{Binary, GitHub::*, Source, Source::*},
15};
16use std::{
17	collections::BTreeSet,
18	fmt::Debug,
19	iter::once,
20	path::{Path, PathBuf},
21};
22use strum::VariantArray;
23use symlink::{remove_symlink_file, symlink_file};
24use toml_edit::DocumentMut;
25use zombienet_configuration::{
26	NodeConfig,
27	shared::node::{Buildable, Initial, NodeConfigBuilder},
28};
29pub use zombienet_sdk::NetworkConfigBuilder;
30use zombienet_sdk::{LocalFileSystem, Network, NetworkConfig, NetworkConfigExt};
31
32mod chain_specs;
33/// Configuration for supported parachains.
34pub mod chains;
35mod relay;
36
37const VALIDATORS: [&str; 6] = ["alice", "bob", "charlie", "dave", "eve", "ferdie"];
38
39/// Configuration to launch a local network.
40pub struct Zombienet {
41	/// The config to be used to launch a network.
42	network_config: NetworkConfiguration,
43	/// The configuration required to launch the relay chain.
44	relay_chain: RelayChain,
45	/// The configuration required to launch parachains.
46	parachains: IndexMap<u32, Chain>,
47	/// Whether any HRMP channels are to be pre-opened.
48	hrmp_channels: bool,
49}
50
51impl Zombienet {
52	/// Initializes the configuration for launching a local network.
53	///
54	/// # Arguments
55	/// * `cache` - The location used for caching binaries.
56	/// * `network_config` - The configuration to be used to launch a network.
57	/// * `relay_chain_version` - The specific binary version used for the relay chain (`None` will
58	///   use the latest available version).
59	/// * `relay_chain_runtime_version` - The specific runtime version used for the relay chain
60	///   runtime (`None` will use the latest available version).
61	/// * `system_parachain_version` - The specific binary version used for system parachains
62	///   (`None` will use the latest available version).
63	/// * `system_parachain_runtime_version` - The specific runtime version used for system
64	///   parachains (`None` will use the latest available version).
65	/// * `parachains` - The parachain(s) specified.
66	pub async fn new(
67		cache: &Path,
68		network_config: NetworkConfiguration,
69		relay_chain_version: Option<&str>,
70		relay_chain_runtime_version: Option<&str>,
71		system_parachain_version: Option<&str>,
72		system_parachain_runtime_version: Option<&str>,
73		parachains: Option<&Vec<String>>,
74	) -> Result<Self, Error> {
75		// Determine relay and parachain requirements based on arguments and config
76		let relay_chain = Self::init_relay_chain(
77			relay_chain_version,
78			relay_chain_runtime_version,
79			&network_config,
80			cache,
81		)
82		.await?;
83		let parachains = match parachains {
84			Some(parachains) => Some(
85				parachains
86					.iter()
87					.map(|url| Repository::parse(url))
88					.collect::<Result<Vec<_>, _>>()?,
89			),
90			None => None,
91		};
92		let parachains = Self::parachains(
93			&relay_chain,
94			system_parachain_version,
95			system_parachain_runtime_version.or(relay_chain_runtime_version),
96			parachains,
97			&network_config,
98			cache,
99		)
100		.await?;
101		let hrmp_channels = !network_config.0.hrmp_channels().is_empty();
102		Ok(Self { network_config, relay_chain, parachains, hrmp_channels })
103	}
104
105	/// The binaries required to launch the network.
106	pub fn binaries(&mut self) -> impl Iterator<Item = &mut Binary> {
107		once([Some(&mut self.relay_chain.binary), self.relay_chain.chain_spec_generator.as_mut()])
108			.chain(
109				self.parachains
110					.values_mut()
111					.map(|p| [Some(&mut p.binary), p.chain_spec_generator.as_mut()]),
112			)
113			.flatten()
114			.flatten()
115	}
116
117	/// Determine parachain configuration based on specified version and network configuration.
118	///
119	/// # Arguments
120	/// * `relay_chain` - The configuration required to launch the relay chain.
121	/// * `system_parachain_version` - The specific binary version used for system parachains
122	///   (`None` will use the latest available version).
123	/// * `system_parachain_runtime_version` - The specific runtime version used for system
124	///   parachains (`None` will use the latest available version).
125	/// * `parachains` - The parachain repositories specified.
126	/// * `network_config` - The network configuration to be used to launch a network.
127	/// * `cache` - The location used for caching binaries.
128	async fn parachains(
129		relay_chain: &RelayChain,
130		system_parachain_version: Option<&str>,
131		system_parachain_runtime_version: Option<&str>,
132		parachains: Option<Vec<Repository>>,
133		network_config: &NetworkConfiguration,
134		cache: &Path,
135	) -> Result<IndexMap<u32, Chain>, Error> {
136		let mut paras: IndexMap<u32, Chain> = IndexMap::new();
137		'outer: for parachain in network_config.0.parachains() {
138			let id = parachain.id();
139			let chain = parachain.chain().map(|c| c.as_str());
140
141			let command = parachain
142				.default_command()
143				.map(|c| c.as_str())
144				.or_else(|| {
145					// Check if any collators define command
146					for collator in parachain.collators() {
147						if let Some(command) = collator.command().map(|i| i.as_str()) {
148							return Some(command);
149						}
150					}
151
152					// Otherwise default to polkadot-parachain
153					Some("polkadot-parachain")
154				})
155				.expect("missing default_command set above")
156				.to_lowercase();
157
158			// Check if system parachain
159			if let Some(parachain) = chains::system(
160				id,
161				&command,
162				system_parachain_version,
163				system_parachain_runtime_version,
164				relay_chain.binary.version().expect("expected relay chain to have version"),
165				chain,
166				cache,
167			)
168			.await?
169			{
170				paras.insert(id, parachain);
171				continue;
172			}
173
174			// Check if known parachain
175			let version = parachains.as_ref().and_then(|r| {
176				r.iter()
177					.filter_map(|r| {
178						(r.package == command).then_some(r.reference.as_ref()).flatten()
179					})
180					.nth(0)
181					.map(|v| v.as_str())
182			});
183			if let Some(parachain) =
184				chains::from(&relay_chain.runtime, id, &command, version, chain, cache).await?
185			{
186				paras.insert(id, parachain);
187				continue;
188			}
189
190			// Check if parachain binary source specified as an argument
191			if let Some(parachains) = parachains.as_ref() &&
192				let Some(repo) = parachains.iter().find(|r| command == r.package)
193			{
194				paras.insert(id, Chain::from_repository(id, repo, chain, cache)?);
195				continue 'outer;
196			}
197
198			// Check if command references a local binary
199			if ["./", "../", "/"].iter().any(|p| command.starts_with(p)) {
200				paras.insert(id, Chain::from_local(id, command.into(), chain)?);
201				continue;
202			}
203
204			// Check if command references a parachain template binary without a specified path
205			// (e.g. Polkadot SDK parachain template)
206			if ["parachain-template-node", "substrate-contracts-node"].contains(&command.as_str()) {
207				for profile in Profile::VARIANTS {
208					let binary_path = profile.target_directory(Path::new("./")).join(&command);
209					if binary_path.exists() {
210						paras.insert(id, Chain::from_local(id, binary_path, chain)?);
211						continue 'outer;
212					}
213				}
214				return Err(Error::MissingBinary(command));
215			}
216
217			if command.starts_with(PolkadotOmniNode.binary()) {
218				paras.insert(id, Chain::from_omni_node(id, cache)?);
219				continue 'outer;
220			}
221
222			return Err(Error::MissingBinary(command));
223		}
224		Ok(paras)
225	}
226
227	/// Determines relay chain configuration based on specified version and network configuration.
228	///
229	/// # Arguments
230	/// * `version` - The specific binary version used for the relay chain (`None` will use the
231	///   latest available version).
232	/// * `runtime_version` - The specific runtime version used for the relay chain runtime (`None`
233	///   will use the latest available version).
234	/// * `network_config` - The network configuration to be used to launch a network.
235	/// * `cache` - The location used for caching binaries.
236	async fn init_relay_chain(
237		version: Option<&str>,
238		runtime_version: Option<&str>,
239		network_config: &NetworkConfiguration,
240		cache: &Path,
241	) -> Result<RelayChain, Error> {
242		// Attempt to determine relay from configuration
243		let relay_chain = network_config.0.relaychain();
244		let chain = relay_chain.chain().as_str();
245		if let Some(default_command) = relay_chain.default_command().map(|c| c.as_str()) {
246			let relay =
247				relay::from(default_command, version, runtime_version, chain, cache).await?;
248			// Validate any node config is supported
249			for node in relay_chain.nodes() {
250				if let Some(command) = node.command().map(|c| c.as_str()) &&
251					command.to_lowercase() != relay.binary.name()
252				{
253					return Err(Error::UnsupportedCommand(format!(
254						"the relay chain command is unsupported: {command}",
255					)));
256				}
257			}
258			return Ok(relay);
259		}
260		// Attempt to determine from nodes
261		let mut relay: Option<RelayChain> = None;
262		for node in relay_chain.nodes() {
263			if let Some(command) = node.command().map(|c| c.as_str()) {
264				match &relay {
265					Some(relay) =>
266						if command.to_lowercase() != relay.binary.name() {
267							return Err(Error::UnsupportedCommand(format!(
268								"the relay chain command is unsupported: {command}",
269							)));
270						},
271					None => {
272						relay = Some(
273							relay::from(command, version, runtime_version, chain, cache).await?,
274						);
275					},
276				}
277			}
278		}
279		if let Some(relay) = relay {
280			return Ok(relay);
281		}
282		// Otherwise use default
283		relay::default(version, runtime_version, chain, cache).await
284	}
285
286	/// The name of the relay chain.
287	pub fn relay_chain(&self) -> &str {
288		&self.relay_chain.chain
289	}
290
291	/// Whether any HRMP channels are to be pre-opened.
292	pub fn hrmp_channels(&self) -> bool {
293		self.hrmp_channels
294	}
295
296	/// Launches the local network.
297	pub async fn spawn(&mut self) -> Result<Network<LocalFileSystem>, Error> {
298		// Symlink polkadot workers
299		let relay_chain_binary_path = self.relay_chain.binary.path();
300		if !relay_chain_binary_path.exists() {
301			return Err(Error::MissingBinary(self.relay_chain.binary.name().to_string()));
302		}
303		let cache = relay_chain_binary_path
304			.parent()
305			.expect("expected relay chain binary path to exist");
306		let version = self.relay_chain.binary.version().ok_or_else(|| {
307			Error::MissingBinary(format!(
308				"Could not determine version for `{}` binary",
309				self.relay_chain.binary.name()
310			))
311		})?;
312		for worker in &self.relay_chain.workers {
313			let dest = cache.join(worker);
314			if dest.exists() {
315				remove_symlink_file(&dest)?;
316			}
317			symlink_file(cache.join(format!("{worker}-{version}")), dest)?;
318		}
319
320		// Load from config and spawn network
321		let network_config = self.network_config.adapt(&self.relay_chain, &self.parachains)?;
322		Ok(network_config.spawn_native().await?)
323	}
324}
325
326/// The network configuration.
327///
328/// Network configuration can be provided via [Path] or by using the [NetworkConfigBuilder].
329#[derive(Debug, PartialEq)]
330pub struct NetworkConfiguration(NetworkConfig, BTreeSet<u32>);
331
332impl NetworkConfiguration {
333	/// Build a network configuration for the specified relay chain and rollups.
334	///
335	/// # Arguments
336	/// * `relay_chain` - The relay chain runtime to be used.
337	/// * `port` - The port to be used for the first relay chain validator.
338	/// * `rollups` - The optional rollups to be included.
339	pub fn build(
340		relay_chain: Relay,
341		port: Option<u16>,
342		rollups: Option<&[Box<dyn Rollup>]>,
343	) -> Result<Self, Error> {
344		let validators: Vec<_> = VALIDATORS
345			.into_iter()
346			.take(rollups.as_ref().map(|v| v.len()).unwrap_or_default().max(2))
347			.map(String::from)
348			.collect();
349
350		let mut builder = NetworkConfigBuilder::new().with_relaychain(|builder| {
351			let mut builder = builder.with_chain(relay_chain.chain()).with_validator(|builder| {
352				let mut builder = builder
353					.with_name(validators.first().expect("at least two validators defined above"));
354				if let Some(port) = port {
355					builder = builder.with_rpc_port(port)
356				}
357				builder
358			});
359
360			for validator in validators.iter().skip(1) {
361				builder = builder.with_validator(|builder| builder.with_name(validator))
362			}
363			builder
364		});
365
366		if let Some(rollups) = rollups {
367			let mut dependencies =
368				rollups.iter().filter_map(|p| p.requires()).flatten().collect::<Vec<_>>();
369
370			for rollup in rollups {
371				builder = builder.with_parachain(|builder| {
372					let mut builder = builder
373						.with_id(rollup.id())
374						.with_chain(rollup.chain())
375						.with_default_command(rollup.binary());
376
377					// Apply any genesis overrides
378					let mut genesis_overrides = serde_json::Map::new();
379					if let Some(mut r#override) = rollup.genesis_overrides() {
380						r#override(&mut genesis_overrides);
381					}
382					for (_, r#override) in
383						dependencies.iter_mut().filter(|(t, _)| t == &rollup.as_any().type_id())
384					{
385						r#override(&mut genesis_overrides);
386					}
387					if !genesis_overrides.is_empty() {
388						builder = builder.with_genesis_overrides(genesis_overrides);
389					}
390
391					builder.with_collator(|builder| {
392						let mut builder =
393							builder.with_name(&format!("{}-collator", rollup.name())).with_args(
394								rollup
395									.args()
396									.map(|args| args.into_iter().map(|arg| arg.into()).collect())
397									.unwrap_or_default(),
398							);
399						if let Some(port) = rollup.port() {
400							builder = builder.with_rpc_port(*port)
401						}
402						builder
403					})
404				})
405			}
406
407			// Open HRMP channels between all rollups
408			let rollups = || rollups.iter().map(|p| p.id());
409			for (sender, recipient) in
410				rollups().flat_map(|s| rollups().filter(move |r| s != *r).map(move |r| (s, r)))
411			{
412				builder = builder.with_hrmp_channel(|channel| {
413					channel
414						.with_sender(sender)
415						.with_recipient(recipient)
416						.with_max_capacity(1_000)
417						.with_max_message_size(8_000)
418				})
419			}
420		}
421
422		Ok(NetworkConfiguration(
423			builder.build().map_err(Error::NetworkConfigurationError)?,
424			Default::default(),
425		))
426	}
427
428	/// Adapts user-provided configuration to one with resolved binary paths and which is compatible
429	/// with [zombienet-sdk](zombienet_sdk) requirements.
430	///
431	/// # Arguments
432	/// * `relay_chain` - The configuration required to launch the relay chain.
433	/// * `parachains` - The configuration required to launch the parachain(s).
434	fn adapt(
435		&self,
436		relay_chain: &RelayChain,
437		parachains: &IndexMap<u32, Chain>,
438	) -> Result<NetworkConfig, Error> {
439		// Resolve paths to relay binary and chain spec generator
440		let binary_path = NetworkConfiguration::resolve_path(&relay_chain.binary.path())?;
441		let chain_spec_generator = match &relay_chain.chain_spec_generator {
442			None => None,
443			Some(path) => Some(format!(
444				"{} {}",
445				NetworkConfiguration::resolve_path(&path.path())?,
446				"{{chainName}}"
447			)),
448		};
449
450		// Use builder to clone network config, adapting binary paths as necessary
451		let mut builder = NetworkConfigBuilder::new()
452			.with_relaychain(|relay| {
453				let source = self.0.relaychain();
454				let nodes = source.nodes();
455
456				let mut builder = relay
457					.with_chain(source.chain().as_str())
458					.with_default_args(source.default_args().into_iter().cloned().collect())
459					// Replace default command with resolved binary path
460					.with_default_command(binary_path.as_str());
461
462				// Chain spec
463				if let Some(command) = source.chain_spec_command() {
464					builder = builder.with_chain_spec_command(command);
465				}
466				if source.chain_spec_command_is_local() {
467					builder = builder.chain_spec_command_is_local(true);
468				}
469				if let Some(location) = source.chain_spec_path() {
470					builder = builder.with_chain_spec_path(location.clone());
471				}
472				if let Some(chain_spec_command_output_path) =
473					source.chain_spec_command_output_path()
474				{
475					builder =
476						builder.with_chain_spec_command_output_path(chain_spec_command_output_path);
477				}
478				// Configure chain spec generator
479				if let Some(command) = chain_spec_generator {
480					builder = builder.with_chain_spec_command(command);
481				}
482				// Overrides: genesis/wasm
483				if let Some(genesis) = source.runtime_genesis_patch() {
484					builder = builder.with_genesis_overrides(genesis.clone());
485				}
486				if let Some(location) = source.wasm_override() {
487					builder = builder.with_wasm_override(location.clone());
488				}
489
490				// Add nodes from source
491				let mut builder = builder.with_validator(|builder| {
492					let source = nodes.first().expect("expected at least one node");
493					Self::build_node_from_source(builder, source, binary_path.as_str())
494				});
495				for source in nodes.iter().skip(1) {
496					builder = builder.with_validator(|builder| {
497						Self::build_node_from_source(builder, source, binary_path.as_str())
498					});
499				}
500
501				builder
502			})
503			// Add global settings
504			.with_global_settings(|settings| {
505				settings.with_network_spawn_timeout(1_000).with_node_spawn_timeout(300)
506			});
507
508		// Process parachains
509		let parachains = &parachains;
510		for source in self.0.parachains() {
511			let id = source.id();
512			let collators = source.collators();
513			let para =
514				parachains.get(&id).expect("expected parachain existence due to preprocessing");
515
516			// Resolve paths to parachain binary and chain spec generator
517			let binary_path = NetworkConfiguration::resolve_path(&para.binary.path())?;
518			let mut chain_spec_generator = match &para.chain_spec_generator {
519				None => None,
520				Some(path) => Some(format!(
521					"{} {}",
522					NetworkConfiguration::resolve_path(&path.path())?,
523					"{{chainName}}"
524				)),
525			};
526
527			builder = builder.with_parachain(|builder| {
528				let mut builder = builder
529					.with_id(id)
530					.with_default_args(source.default_args().into_iter().cloned().collect())
531					// Replace default command with resolved binary path
532					.with_default_command(binary_path.as_str());
533
534				// Chain spec
535				if let Some(chain) = source.chain() {
536					builder = builder.with_chain(chain.as_str());
537					// TODO: Just a temporary fix, once Paseo chain-spec-generator supports
538					// passet-hub just remove this.
539					if chain.as_str().contains("passet-hub") {
540						let chain_spec = crate::get_passet_hub_spec_content();
541						let temp_dir = std::env::temp_dir();
542						let spec_path = temp_dir.join("passet-hub-spec.json");
543						std::fs::write(&spec_path, chain_spec)
544							.expect("Failed to write passet-hub chain spec");
545						builder = builder.with_chain_spec_path(spec_path);
546						chain_spec_generator = None;
547					}
548				}
549				if let Some(command) = source.chain_spec_command() {
550					builder = builder.with_chain_spec_command(command);
551				}
552				if source.chain_spec_command_is_local() {
553					builder = builder.chain_spec_command_is_local(true);
554				}
555				if let Some(chain_spec_command_output_path) =
556					source.chain_spec_command_output_path()
557				{
558					builder =
559						builder.with_chain_spec_command_output_path(chain_spec_command_output_path)
560				}
561				if let Some(location) = source.chain_spec_path() {
562					builder = builder.with_chain_spec_path(location.clone());
563				}
564				// Configure chain spec generator
565				if let Some(command) = chain_spec_generator {
566					builder = builder.with_chain_spec_command(command);
567				}
568				// Overrides: genesis/wasm
569				if let Some(genesis) = source.genesis_overrides() {
570					builder = builder.with_genesis_overrides(genesis.clone());
571				}
572				if let Some(location) = source.wasm_override() {
573					builder = builder.with_wasm_override(location.clone());
574				}
575				// Configure whether EVM based
576				builder = builder.evm_based(self.1.contains(&id) || source.is_evm_based());
577
578				// Add collators from source
579				let mut builder = builder.with_collator(|builder| {
580					let source = collators.first().expect("expected at least one collator");
581					Self::build_node_from_source(builder, source, binary_path.as_str())
582				});
583				for source in collators.iter().skip(1) {
584					builder = builder.with_collator(|builder| {
585						Self::build_node_from_source(builder, source, binary_path.as_str())
586					});
587				}
588
589				builder
590			});
591		}
592
593		// Process HRMP channels
594		for source in self.0.hrmp_channels() {
595			builder = builder.with_hrmp_channel(|channel| {
596				channel
597					.with_sender(source.sender())
598					.with_recipient(source.recipient())
599					.with_max_capacity(source.max_capacity())
600					.with_max_message_size(source.max_message_size())
601			})
602		}
603
604		builder
605			.build()
606			.map_err(|e| Error::Config(format!("could not configure network {:?}", e)))
607	}
608
609	// Build a node using the provided builder and source config.
610	fn build_node_from_source(
611		builder: NodeConfigBuilder<Initial>,
612		source: &NodeConfig,
613		binary_path: &str,
614	) -> NodeConfigBuilder<Buildable> {
615		let mut builder = builder
616			.with_name(source.name())
617			.bootnode(source.is_bootnode())
618			.invulnerable(source.is_invulnerable())
619			.validator(source.is_validator())
620			.with_args(source.args().into_iter().cloned().collect())
621			.with_command(binary_path)
622			.with_env(source.env().into_iter().cloned().collect());
623		if let Some(command) = source.subcommand() {
624			builder = builder.with_subcommand(command.clone())
625		}
626		if let Some(port) = source.rpc_port() {
627			builder = builder.with_rpc_port(port)
628		}
629		if let Some(port) = source.ws_port() {
630			builder = builder.with_ws_port(port)
631		}
632		builder
633	}
634
635	/// Resolves the canonical path of a command specified within a network configuration file.
636	///
637	/// # Arguments
638	/// * `path` - The path to be resolved.
639	fn resolve_path(path: &Path) -> Result<String, Error> {
640		path.canonicalize()
641			.map_err(|_| {
642				Error::Config(format!("the canonical path of {:?} could not be resolved", path))
643			})
644			.map(|p| p.to_str().map(|p| p.to_string()))?
645			.ok_or_else(|| Error::Config("the path is invalid".into()))
646	}
647}
648
649impl TryFrom<&Path> for NetworkConfiguration {
650	type Error = Error;
651
652	fn try_from(file: &Path) -> Result<Self, Self::Error> {
653		if !file.exists() {
654			return Err(Error::Config(format!("The {file:?} configuration file was not found")));
655		}
656
657		// Parse the file to determine if there are any parachains using `force_decorator`
658		let contents = std::fs::read_to_string(file)?;
659		let config = contents.parse::<DocumentMut>().map_err(|err| Error::TomlError(err.into()))?;
660		let evm_based = config
661			.get("parachains")
662			.and_then(|p| p.as_array_of_tables())
663			.map(|tables| {
664				tables
665					.iter()
666					.filter_map(|table| {
667						table
668							.get("force_decorator")
669							.and_then(|i| i.as_str())
670							.filter(|v| *v == "generic-evm")
671							.and_then(|_| table.get("id"))
672							.and_then(|i| i.as_integer())
673							.map(|id| id as u32)
674					})
675					.collect()
676			})
677			.unwrap_or_default();
678
679		Ok(NetworkConfiguration(
680			NetworkConfig::load_from_toml(
681				file.to_str().expect("expected file path to be convertible to string"),
682			)
683			.map_err(|e| Error::Config(e.to_string()))?,
684			evm_based,
685		))
686	}
687}
688
689impl TryFrom<NetworkConfig> for NetworkConfiguration {
690	type Error = ();
691
692	fn try_from(value: NetworkConfig) -> Result<Self, Self::Error> {
693		Ok(NetworkConfiguration(value, Default::default()))
694	}
695}
696
697/// The configuration required to launch the relay chain.
698struct RelayChain {
699	// The runtime used.
700	runtime: Runtime,
701	/// The binary used to launch a relay chain node.
702	binary: Binary,
703	/// The additional workers required by the relay chain node.
704	workers: [&'static str; 2],
705	/// The name of the chain.
706	#[allow(dead_code)]
707	chain: String,
708	/// If applicable, the binary used to generate a chain specification.
709	chain_spec_generator: Option<Binary>,
710}
711
712/// The configuration required to launch a parachain.
713#[derive(Debug, PartialEq)]
714struct Chain {
715	/// The parachain identifier on the local network.
716	id: u32,
717	/// The binary used to launch a parachain node.
718	binary: Binary,
719	/// The name of the chain.
720	chain: Option<String>,
721	/// If applicable, the binary used to generate a chain specification.
722	chain_spec_generator: Option<Binary>,
723}
724
725impl Chain {
726	/// Initializes the configuration required to launch a parachain using a local binary.
727	///
728	/// # Arguments
729	/// * `id` - The parachain identifier on the local network.
730	/// * `path` - The path to the local binary.
731	/// * `chain` - The chain specified.
732	fn from_local(id: u32, path: PathBuf, chain: Option<&str>) -> Result<Chain, Error> {
733		let name = path
734			.file_name()
735			.and_then(|f| f.to_str())
736			.ok_or_else(|| Error::Config(format!("unable to determine file name for {path:?}")))?
737			.to_string();
738		// Check if package manifest can be found within path
739		let manifest = resolve_manifest(&name, &path)?;
740		Ok(Chain {
741			id,
742			binary: Binary::Local { name, path, manifest },
743			chain: chain.map(|c| c.to_string()),
744			chain_spec_generator: None,
745		})
746	}
747
748	/// Initializes the configuration required to launch a parachain using a binary sourced from the
749	/// specified repository.
750	///
751	/// # Arguments
752	/// * `id` - The parachain identifier on the local network.
753	/// * `repo` - The repository to be used to source the binary.
754	/// * `chain` - The chain specified.
755	/// * `cache` - The location used for caching binaries.
756	fn from_repository(
757		id: u32,
758		repo: &Repository,
759		chain: Option<&str>,
760		cache: &Path,
761	) -> Result<Chain, Error> {
762		// Check for GitHub repository to be able to download source as an archive
763		if repo.url.host_str().is_some_and(|h| h.to_lowercase() == "github.com") {
764			let github = GitHub::parse(repo.url.as_str())?;
765			let source = Source::GitHub(SourceCodeArchive {
766				owner: github.org,
767				repository: github.name,
768				reference: repo.reference.clone(),
769				manifest: None,
770				package: repo.package.clone(),
771				artifacts: vec![repo.package.clone()],
772			})
773			.into();
774			Ok(Chain {
775				id,
776				binary: Binary::Source {
777					name: repo.package.clone(),
778					source,
779					cache: cache.to_path_buf(),
780				},
781				chain: chain.map(|c| c.to_string()),
782				chain_spec_generator: None,
783			})
784		} else {
785			Ok(Chain {
786				id,
787				binary: Binary::Source {
788					name: repo.package.clone(),
789					source: Git {
790						url: repo.url.clone(),
791						reference: repo.reference.clone(),
792						manifest: None,
793						package: repo.package.clone(),
794						artifacts: vec![repo.package.clone()],
795					}
796					.into(),
797					cache: cache.to_path_buf(),
798				},
799				chain: chain.map(|c| c.to_string()),
800				chain_spec_generator: None,
801			})
802		}
803	}
804
805	fn from_omni_node(id: u32, cache: &Path) -> Result<Chain, Error> {
806		Ok(Chain {
807			id,
808			binary: Binary::Source {
809				name: PolkadotOmniNode.binary().to_string(),
810				source: Box::new(PolkadotOmniNode.source()?),
811				cache: cache.to_path_buf(),
812			},
813			chain: None,
814			chain_spec_generator: None,
815		})
816	}
817}
818
819/// Attempts to resolve the package manifest from the specified path.
820///
821/// # Arguments
822/// * `package` - The name of the package.
823/// * `path` - The path to start searching.
824fn resolve_manifest(package: &str, path: &Path) -> Result<Option<PathBuf>, Error> {
825	let matches_package = |config: &DocumentMut| {
826		config
827			.get("package")
828			.and_then(|i| i.as_table())
829			.and_then(|t| t.get("name"))
830			.and_then(|i| i.as_str()) ==
831			Some(package)
832	};
833
834	let mut manifest = Some(path);
835	'outer: while let Some(path) = manifest {
836		let manifest_path = path.join("Cargo.toml");
837		if !manifest_path.exists() {
838			manifest = path.parent();
839			continue;
840		}
841		let contents = std::fs::read_to_string(&manifest_path)?;
842		let config = contents.parse::<DocumentMut>().map_err(|err| Error::TomlError(err.into()))?;
843		// Check if package manifest
844		if matches_package(&config) {
845			break 'outer;
846		}
847		// Check if package defined as a workspace member
848		if let Some(members) = config
849			.get("workspace")
850			.and_then(|i| i.as_table())
851			.and_then(|t| t.get("members"))
852			.and_then(|m| m.as_array())
853			.map(|a| a.iter().filter_map(|v| v.as_str()))
854		{
855			// Check manifest of each member
856			for member in members {
857				let member_path = path.join(member);
858				for entry in glob(member_path.to_string_lossy().as_ref())
859					.expect("expected valid glob for workspace member")
860					.filter_map(Result::ok)
861				{
862					let manifest_path = entry.join("Cargo.toml");
863					if manifest_path.exists() {
864						let contents = std::fs::read_to_string(&manifest_path)?;
865						let config = contents
866							.parse::<DocumentMut>()
867							.map_err(|err| Error::TomlError(err.into()))?;
868						if matches_package(&config) {
869							break 'outer;
870						}
871					}
872				}
873			}
874		};
875		manifest = path.parent();
876	}
877	Ok(manifest.map(|p| p.join("Cargo.toml")))
878}
879
880#[cfg(test)]
881mod tests {
882	use super::*;
883	use anyhow::Result;
884	use std::{
885		env::current_dir,
886		fs::{File, create_dir_all, remove_dir, remove_file},
887		io::Write,
888	};
889	use tempfile::{Builder, tempdir};
890
891	pub(crate) const FALLBACK: &str = "stable2412";
892	pub(crate) const RELAY_BINARY_VERSION: &str = "stable2412-4";
893	pub(crate) const SYSTEM_PARA_BINARY_VERSION: &str = "stable2503";
894	const SYSTEM_PARA_RUNTIME_VERSION: &str = "v1.4.1";
895
896	mod zombienet {
897		use super::*;
898		use pop_common::{Status, helpers::with_current_dir_async};
899
900		pub(crate) struct Output;
901		impl Status for Output {
902			fn update(&self, status: &str) {
903				println!("{status}")
904			}
905		}
906
907		#[tokio::test]
908		async fn new_with_relay_only_works() -> Result<()> {
909			let temp_dir = tempdir()?;
910			let cache = PathBuf::from(temp_dir.path());
911			let config = Builder::new().suffix(".toml").tempfile()?;
912			writeln!(
913				config.as_file(),
914				r#"
915[relaychain]
916chain = "paseo-local"
917"#
918			)?;
919
920			let zombienet = Zombienet::new(
921				&cache,
922				config.path().try_into()?,
923				Some(RELAY_BINARY_VERSION),
924				None,
925				None,
926				None,
927				None,
928			)
929			.await?;
930
931			let relay_chain = &zombienet.relay_chain.binary;
932			assert_eq!(relay_chain.name(), "polkadot");
933			assert_eq!(
934				relay_chain.path(),
935				temp_dir.path().join(format!("polkadot-{RELAY_BINARY_VERSION}"))
936			);
937			assert_eq!(relay_chain.version().unwrap(), RELAY_BINARY_VERSION);
938			assert!(matches!(
939				relay_chain,
940				Binary::Source { source, .. }
941					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
942						if *tag == Some(format!("polkadot-{RELAY_BINARY_VERSION}"))
943					)
944			));
945			assert!(zombienet.parachains.is_empty());
946			assert_eq!(zombienet.relay_chain(), "paseo-local");
947			assert!(!zombienet.hrmp_channels());
948			Ok(())
949		}
950
951		#[tokio::test]
952		async fn new_with_relay_only_from_network_config_works() -> Result<()> {
953			let temp_dir = tempdir()?;
954			let cache = PathBuf::from(temp_dir.path());
955			let config = NetworkConfigBuilder::new()
956				.with_relaychain(|b| {
957					b.with_chain("paseo-local").with_validator(|b| b.with_name("alice"))
958				})
959				.build()
960				.unwrap();
961
962			let zombienet = Zombienet::new(
963				&cache,
964				config.try_into().unwrap(),
965				Some(RELAY_BINARY_VERSION),
966				None,
967				None,
968				None,
969				None,
970			)
971			.await?;
972
973			let relay_chain = &zombienet.relay_chain.binary;
974			assert_eq!(relay_chain.name(), "polkadot");
975			assert_eq!(
976				relay_chain.path(),
977				temp_dir.path().join(format!("polkadot-{RELAY_BINARY_VERSION}"))
978			);
979			assert_eq!(relay_chain.version().unwrap(), RELAY_BINARY_VERSION);
980			assert!(matches!(
981				relay_chain,
982				Binary::Source { source, .. }
983					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
984						if *tag == Some(format!("polkadot-{RELAY_BINARY_VERSION}"))
985					)
986			));
987			assert!(zombienet.parachains.is_empty());
988			assert_eq!(zombienet.relay_chain(), "paseo-local");
989			assert!(!zombienet.hrmp_channels());
990			Ok(())
991		}
992
993		#[tokio::test]
994		async fn new_with_relay_chain_spec_generator_works() -> Result<()> {
995			let temp_dir = tempdir()?;
996			let cache = PathBuf::from(temp_dir.path());
997			let config = Builder::new().suffix(".toml").tempfile()?;
998			writeln!(
999				config.as_file(),
1000				r#"
1001[relaychain]
1002chain = "paseo-local"
1003"#
1004			)?;
1005			let version = "v1.3.3";
1006
1007			let zombienet = Zombienet::new(
1008				&cache,
1009				config.path().try_into()?,
1010				None,
1011				Some(version),
1012				None,
1013				None,
1014				None,
1015			)
1016			.await?;
1017
1018			assert_eq!(zombienet.relay_chain.chain, "paseo-local");
1019			let chain_spec_generator = &zombienet.relay_chain.chain_spec_generator.unwrap();
1020			assert_eq!(chain_spec_generator.name(), "paseo-chain-spec-generator");
1021			assert_eq!(
1022				chain_spec_generator.path(),
1023				temp_dir.path().join(format!("paseo-chain-spec-generator-{version}"))
1024			);
1025			assert_eq!(chain_spec_generator.version().unwrap(), version);
1026			assert!(matches!(
1027				chain_spec_generator,
1028				Binary::Source { source, .. }
1029					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1030						if *tag == Some(version.to_string())
1031					)
1032			));
1033			assert!(zombienet.parachains.is_empty());
1034			Ok(())
1035		}
1036
1037		#[tokio::test]
1038		async fn new_with_default_command_works() -> Result<()> {
1039			let temp_dir = tempdir()?;
1040			let cache = PathBuf::from(temp_dir.path());
1041			let config = Builder::new().suffix(".toml").tempfile()?;
1042			writeln!(
1043				config.as_file(),
1044				r#"
1045[relaychain]
1046chain = "paseo-local"
1047default_command = "./bin-stable2503/polkadot"
1048"#
1049			)?;
1050
1051			let zombienet = Zombienet::new(
1052				&cache,
1053				config.path().try_into()?,
1054				Some(RELAY_BINARY_VERSION),
1055				None,
1056				None,
1057				None,
1058				None,
1059			)
1060			.await?;
1061
1062			let relay_chain = &zombienet.relay_chain.binary;
1063			assert_eq!(relay_chain.name(), "polkadot");
1064			assert_eq!(
1065				relay_chain.path(),
1066				temp_dir.path().join(format!("polkadot-{RELAY_BINARY_VERSION}"))
1067			);
1068			assert_eq!(relay_chain.version().unwrap(), RELAY_BINARY_VERSION);
1069			assert!(matches!(
1070				relay_chain,
1071				Binary::Source { source, ..}
1072					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1073						if *tag == Some(format!("polkadot-{RELAY_BINARY_VERSION}"))
1074					)
1075			));
1076			assert!(zombienet.parachains.is_empty());
1077			Ok(())
1078		}
1079
1080		#[tokio::test]
1081		async fn new_with_node_command_works() -> Result<()> {
1082			let temp_dir = tempdir()?;
1083			let cache = PathBuf::from(temp_dir.path());
1084			let config = Builder::new().suffix(".toml").tempfile()?;
1085			writeln!(
1086				config.as_file(),
1087				r#"
1088[relaychain]
1089chain = "paseo-local"
1090
1091[[relaychain.nodes]]
1092name = "alice"
1093validator = true
1094command = "polkadot"
1095"#
1096			)?;
1097
1098			let zombienet = Zombienet::new(
1099				&cache,
1100				config.path().try_into()?,
1101				Some(RELAY_BINARY_VERSION),
1102				None,
1103				None,
1104				None,
1105				None,
1106			)
1107			.await?;
1108
1109			let relay_chain = &zombienet.relay_chain.binary;
1110			assert_eq!(relay_chain.name(), "polkadot");
1111			assert_eq!(
1112				relay_chain.path(),
1113				temp_dir.path().join(format!("polkadot-{RELAY_BINARY_VERSION}"))
1114			);
1115			assert_eq!(relay_chain.version().unwrap(), RELAY_BINARY_VERSION);
1116			assert!(matches!(
1117				relay_chain,
1118				Binary::Source { source, .. }
1119					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1120						if *tag == Some(format!("polkadot-{RELAY_BINARY_VERSION}"))
1121					)
1122			));
1123			assert!(zombienet.parachains.is_empty());
1124			Ok(())
1125		}
1126
1127		#[tokio::test]
1128		async fn new_with_node_command_from_network_config_works() -> Result<()> {
1129			let temp_dir = tempdir()?;
1130			let cache = PathBuf::from(temp_dir.path());
1131			let config = NetworkConfigBuilder::new()
1132				.with_relaychain(|b| {
1133					b.with_chain("paseo-local")
1134						.with_validator(|b| b.with_name("alice").with_command("polkadot"))
1135				})
1136				.build()
1137				.unwrap();
1138
1139			let zombienet = Zombienet::new(
1140				&cache,
1141				config.try_into().unwrap(),
1142				Some(RELAY_BINARY_VERSION),
1143				None,
1144				None,
1145				None,
1146				None,
1147			)
1148			.await?;
1149
1150			let relay_chain = &zombienet.relay_chain.binary;
1151			assert_eq!(relay_chain.name(), "polkadot");
1152			assert_eq!(
1153				relay_chain.path(),
1154				temp_dir.path().join(format!("polkadot-{RELAY_BINARY_VERSION}"))
1155			);
1156			assert_eq!(relay_chain.version().unwrap(), RELAY_BINARY_VERSION);
1157			assert!(matches!(
1158				relay_chain,
1159				Binary::Source { source, .. }
1160					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1161						if *tag == Some(format!("polkadot-{RELAY_BINARY_VERSION}"))
1162					)
1163			));
1164			assert!(zombienet.parachains.is_empty());
1165			Ok(())
1166		}
1167
1168		#[tokio::test]
1169		async fn new_ensures_node_commands_valid() -> Result<()> {
1170			let temp_dir = tempdir()?;
1171			let cache = PathBuf::from(temp_dir.path());
1172			let config = Builder::new().suffix(".toml").tempfile()?;
1173			writeln!(
1174				config.as_file(),
1175				r#"
1176[relaychain]
1177chain = "paseo-local"
1178
1179[[relaychain.nodes]]
1180name = "alice"
1181validator = true
1182command = "polkadot"
1183
1184[[relaychain.nodes]]
1185name = "bob"
1186validator = true
1187command = "polkadot-stable2503"
1188"#
1189			)?;
1190
1191			assert!(matches!(
1192				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None).await,
1193				Err(Error::UnsupportedCommand(error))
1194				if error == "the relay chain command is unsupported: polkadot-stable2503"
1195			));
1196			Ok(())
1197		}
1198
1199		#[tokio::test]
1200		async fn new_ensures_node_command_valid() -> Result<()> {
1201			let temp_dir = tempdir()?;
1202			let cache = PathBuf::from(temp_dir.path());
1203			let config = Builder::new().suffix(".toml").tempfile()?;
1204			writeln!(
1205				config.as_file(),
1206				r#"
1207[relaychain]
1208chain = "paseo-local"
1209default_command = "polkadot"
1210
1211[[relaychain.nodes]]
1212name = "alice"
1213validator = true
1214command = "polkadot-stable2503"
1215"#
1216			)?;
1217
1218			assert!(matches!(
1219				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None).await,
1220				Err(Error::UnsupportedCommand(error))
1221				if error == "the relay chain command is unsupported: polkadot-stable2503"
1222			));
1223			Ok(())
1224		}
1225
1226		#[tokio::test]
1227		async fn new_ensures_node_command_from_network_config_valid() -> Result<()> {
1228			let temp_dir = tempdir()?;
1229			let cache = PathBuf::from(temp_dir.path());
1230			let config = NetworkConfigBuilder::new()
1231				.with_relaychain(|b| {
1232					b.with_chain("paseo-local")
1233						.with_validator(|b| b.with_name("alice").with_command("polkadot"))
1234						.with_validator(|b| b.with_name("bob").with_command("polkadot"))
1235						.with_validator(|b| b.with_name("charlie").with_command("p0lk4d0t"))
1236				})
1237				.build()
1238				.unwrap();
1239
1240			assert!(matches!(
1241				Zombienet::new(&cache, config.try_into().unwrap(), None, None, None, None,None).await,
1242				Err(Error::UnsupportedCommand(error))
1243					if error == "the relay chain command is unsupported: p0lk4d0t"
1244			));
1245			Ok(())
1246		}
1247
1248		#[tokio::test]
1249		async fn new_with_system_chain_works() -> Result<()> {
1250			let temp_dir = tempdir()?;
1251			let cache = PathBuf::from(temp_dir.path());
1252			let config = Builder::new().suffix(".toml").tempfile()?;
1253			writeln!(
1254				config.as_file(),
1255				r#"
1256[relaychain]
1257chain = "paseo-local"
1258
1259[[parachains]]
1260id = 1000
1261chain = "asset-hub-paseo-local"
1262"#
1263			)?;
1264
1265			let zombienet = Zombienet::new(
1266				&cache,
1267				config.path().try_into()?,
1268				Some(RELAY_BINARY_VERSION),
1269				None,
1270				Some(SYSTEM_PARA_BINARY_VERSION),
1271				None,
1272				None,
1273			)
1274			.await?;
1275
1276			assert_eq!(zombienet.parachains.len(), 1);
1277			let system_parachain = &zombienet.parachains.get(&1000).unwrap().binary;
1278			assert_eq!(system_parachain.name(), "polkadot-parachain");
1279			assert_eq!(
1280				system_parachain.path(),
1281				temp_dir.path().join(format!("polkadot-parachain-{SYSTEM_PARA_BINARY_VERSION}"))
1282			);
1283			assert_eq!(system_parachain.version().unwrap(), SYSTEM_PARA_BINARY_VERSION);
1284			assert!(matches!(
1285				system_parachain,
1286				Binary::Source { source, .. }
1287					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1288						if *tag == Some(format!("polkadot-{SYSTEM_PARA_BINARY_VERSION}"))
1289					)
1290			));
1291			Ok(())
1292		}
1293
1294		#[tokio::test]
1295		async fn new_with_system_chain_spec_generator_works() -> Result<()> {
1296			let temp_dir = tempdir()?;
1297			let cache = PathBuf::from(temp_dir.path());
1298			let config = Builder::new().suffix(".toml").tempfile()?;
1299			writeln!(
1300				config.as_file(),
1301				r#"
1302[relaychain]
1303chain = "paseo-local"
1304
1305[[parachains]]
1306id = 1000
1307chain = "asset-hub-paseo-local"
1308"#
1309			)?;
1310
1311			let zombienet = Zombienet::new(
1312				&cache,
1313				config.path().try_into()?,
1314				None,
1315				None,
1316				None,
1317				Some(SYSTEM_PARA_RUNTIME_VERSION),
1318				None,
1319			)
1320			.await?;
1321
1322			assert_eq!(zombienet.parachains.len(), 1);
1323			let system_parachain = &zombienet.parachains.get(&1000).unwrap();
1324			assert_eq!(system_parachain.chain.as_ref().unwrap(), "asset-hub-paseo-local");
1325			let chain_spec_generator = system_parachain.chain_spec_generator.as_ref().unwrap();
1326			assert_eq!(chain_spec_generator.name(), "paseo-chain-spec-generator");
1327			assert_eq!(
1328				chain_spec_generator.path(),
1329				temp_dir
1330					.path()
1331					.join(format!("paseo-chain-spec-generator-{SYSTEM_PARA_RUNTIME_VERSION}"))
1332			);
1333			assert_eq!(chain_spec_generator.version().unwrap(), SYSTEM_PARA_RUNTIME_VERSION);
1334			assert!(matches!(
1335				chain_spec_generator,
1336				Binary::Source { source, .. }
1337					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1338						if *tag == Some(SYSTEM_PARA_RUNTIME_VERSION.to_string())
1339					)
1340			));
1341			Ok(())
1342		}
1343
1344		#[tokio::test]
1345		async fn new_with_pop_works() -> Result<()> {
1346			let temp_dir = tempdir()?;
1347			let cache = PathBuf::from(temp_dir.path());
1348			let config = Builder::new().suffix(".toml").tempfile()?;
1349			writeln!(
1350				config.as_file(),
1351				r#"
1352[relaychain]
1353chain = "paseo-local"
1354
1355[[parachains]]
1356id = 4385
1357default_command = "pop-node"
1358"#
1359			)?;
1360
1361			let zombienet =
1362				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1363					.await?;
1364
1365			assert_eq!(zombienet.parachains.len(), 1);
1366			let pop = &zombienet.parachains.get(&4385).unwrap().binary;
1367			let version = pop.latest().unwrap();
1368			assert_eq!(pop.name(), "pop-node");
1369			assert_eq!(pop.path(), temp_dir.path().join(format!("pop-node-{version}")));
1370			assert_eq!(pop.version().unwrap(), version);
1371			assert!(matches!(
1372				pop,
1373				Binary::Source { source, .. }
1374					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1375						if *tag == Some(format!("node-{version}"))
1376					)
1377			));
1378			Ok(())
1379		}
1380
1381		#[tokio::test]
1382		async fn new_with_pop_version_works() -> Result<()> {
1383			let temp_dir = tempdir()?;
1384			let cache = PathBuf::from(temp_dir.path());
1385			let config = Builder::new().suffix(".toml").tempfile()?;
1386			writeln!(
1387				config.as_file(),
1388				r#"
1389[relaychain]
1390chain = "paseo-local"
1391
1392[[parachains]]
1393id = 4385
1394default_command = "pop-node"
1395"#
1396			)?;
1397			let version = "v1.0";
1398
1399			let zombienet = Zombienet::new(
1400				&cache,
1401				config.path().try_into()?,
1402				None,
1403				None,
1404				None,
1405				None,
1406				Some(&vec![format!("https://github.com/r0gue-io/pop-node#{version}")]),
1407			)
1408			.await?;
1409
1410			assert_eq!(zombienet.parachains.len(), 1);
1411			let pop = &zombienet.parachains.get(&4385).unwrap().binary;
1412			assert_eq!(pop.name(), "pop-node");
1413			assert_eq!(pop.path(), temp_dir.path().join(format!("pop-node-{version}")));
1414			assert_eq!(pop.version().unwrap(), version);
1415			assert!(matches!(
1416				pop,
1417				Binary::Source { source, .. }
1418					if matches!(source.as_ref(), Source::GitHub(ReleaseArchive { tag, .. })
1419						if *tag == Some(format!("node-{version}"))
1420					)
1421			));
1422			Ok(())
1423		}
1424
1425		#[tokio::test]
1426		async fn new_with_local_parachain_works() -> Result<()> {
1427			let temp_dir = tempdir()?;
1428			let cache = PathBuf::from(temp_dir.path());
1429			let config = Builder::new().suffix(".toml").tempfile()?;
1430			writeln!(
1431				config.as_file(),
1432				r#"
1433[relaychain]
1434chain = "paseo-local"
1435
1436[[parachains]]
1437id = 2000
1438default_command = "./target/release/parachain-template-node"
1439"#
1440			)?;
1441
1442			let zombienet =
1443				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1444					.await?;
1445
1446			assert_eq!(zombienet.parachains.len(), 1);
1447			let pop = &zombienet.parachains.get(&2000).unwrap().binary;
1448			assert_eq!(pop.name(), "parachain-template-node");
1449			assert_eq!(pop.path(), Path::new("./target/release/parachain-template-node"));
1450			assert_eq!(pop.version(), None);
1451			assert!(matches!(pop, Binary::Local { .. }));
1452			Ok(())
1453		}
1454
1455		#[tokio::test]
1456		async fn new_with_local_parachain_without_path_works() -> Result<()> {
1457			let temp_dir = tempdir()?;
1458			let cache = PathBuf::from(temp_dir.path());
1459			let config = Builder::new().suffix(".toml").tempfile()?;
1460			writeln!(
1461				config.as_file(),
1462				r#"
1463[relaychain]
1464chain = "paseo-local"
1465
1466[[parachains]]
1467id = 1000
1468
1469[parachains.collator]
1470name = "collator"
1471command = "parachain-template-node"
1472
1473[[parachains]]
1474id = 2000
1475
1476[parachains.collator]
1477name = "collator"
1478command = "substrate-contracts-node"
1479"#
1480			)?;
1481			let temp_workspace = tempdir()?;
1482			with_current_dir_async(temp_workspace.path(), async || {
1483				// Expecting failure since no custom path is provided and binaries don't exist in
1484				// the default build directory.
1485				assert!(matches!(
1486					Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None).await,
1487					Err(Error::MissingBinary(command))
1488					if command == "parachain-template-node"
1489				));
1490				// Create the binaries in the default build directory.
1491				let parachain_template = PathBuf::from("target/release/parachain-template-node");
1492				create_dir_all(parachain_template.parent().unwrap())?;
1493				File::create(&parachain_template)?;
1494				// Ensure the the binary is detected in the debug profile too.
1495				let parachain_contracts_template =
1496					PathBuf::from("target/debug/substrate-contracts-node");
1497				create_dir_all(parachain_contracts_template.parent().unwrap())?;
1498				File::create(&parachain_contracts_template)?;
1499
1500				let zombienet =
1501					Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1502						.await?;
1503				// Remove the binaries created above after Zombienet initialization, as they are no
1504				// longer needed.
1505				remove_file(&parachain_template)?;
1506				remove_file(&parachain_contracts_template)?;
1507				remove_dir(parachain_template.parent().unwrap())?;
1508				remove_dir(parachain_contracts_template.parent().unwrap())?;
1509
1510				assert_eq!(zombienet.parachains.len(), 2);
1511				let parachain = &zombienet.parachains.get(&1000).unwrap().binary;
1512				assert_eq!(parachain.name(), "parachain-template-node");
1513				assert_eq!(parachain.path(), Path::new("./target/release/parachain-template-node"));
1514				assert_eq!(parachain.version(), None);
1515				assert!(matches!(parachain, Binary::Local { .. }));
1516				let contract_parachain = &zombienet.parachains.get(&2000).unwrap().binary;
1517				assert_eq!(contract_parachain.name(), "substrate-contracts-node");
1518				assert_eq!(
1519					contract_parachain.path(),
1520					Path::new("./target/debug/substrate-contracts-node")
1521				);
1522				assert_eq!(contract_parachain.version(), None);
1523				assert!(matches!(contract_parachain, Binary::Local { .. }));
1524				Ok(())
1525			})
1526			.await
1527		}
1528
1529		#[tokio::test]
1530		async fn new_with_collator_command_works() -> Result<()> {
1531			let temp_dir = tempdir()?;
1532			let cache = PathBuf::from(temp_dir.path());
1533			let config = Builder::new().suffix(".toml").tempfile()?;
1534			writeln!(
1535				config.as_file(),
1536				r#"
1537[relaychain]
1538chain = "paseo-local"
1539
1540[[parachains]]
1541id = 2000
1542
1543[[parachains.collators]]
1544name = "collator-01"
1545command = "./target/release/parachain-template-node"
1546"#
1547			)?;
1548
1549			let zombienet =
1550				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1551					.await?;
1552
1553			assert_eq!(zombienet.parachains.len(), 1);
1554			let pop = &zombienet.parachains.get(&2000).unwrap().binary;
1555			assert_eq!(pop.name(), "parachain-template-node");
1556			assert_eq!(pop.path(), Path::new("./target/release/parachain-template-node"));
1557			assert_eq!(pop.version(), None);
1558			assert!(matches!(pop, Binary::Local { .. }));
1559			Ok(())
1560		}
1561
1562		#[tokio::test]
1563		async fn new_with_moonbeam_works() -> Result<()> {
1564			let temp_dir = tempdir()?;
1565			let cache = PathBuf::from(temp_dir.path());
1566			let config = Builder::new().suffix(".toml").tempfile()?;
1567			writeln!(
1568				config.as_file(),
1569				r#"
1570[relaychain]
1571chain = "paseo-local"
1572
1573[[parachains]]
1574id = 2000
1575default_command = "moonbeam"
1576"#
1577			)?;
1578			let version = "v0.38.0";
1579
1580			let zombienet = Zombienet::new(
1581				&cache,
1582				config.path().try_into()?,
1583				None,
1584				None,
1585				None,
1586				None,
1587				Some(&vec![format!("https://github.com/moonbeam-foundation/moonbeam#{version}")]),
1588			)
1589			.await?;
1590
1591			assert_eq!(zombienet.parachains.len(), 1);
1592			let pop = &zombienet.parachains.get(&2000).unwrap().binary;
1593			assert_eq!(pop.name(), "moonbeam");
1594			assert_eq!(pop.path(), temp_dir.path().join(format!("moonbeam-{version}")));
1595			assert_eq!(pop.version().unwrap(), version);
1596			assert!(matches!(
1597				pop,
1598				Binary::Source { source, .. }
1599					if matches!(source.as_ref(), Source::GitHub(SourceCodeArchive { reference, .. })
1600						if *reference == Some(version.to_string())
1601					)
1602			));
1603			Ok(())
1604		}
1605
1606		#[tokio::test]
1607		async fn new_with_hrmp_channels_works() -> Result<()> {
1608			let temp_dir = tempdir()?;
1609			let cache = PathBuf::from(temp_dir.path());
1610			let config = Builder::new().suffix(".toml").tempfile()?;
1611			writeln!(
1612				config.as_file(),
1613				r#"
1614[relaychain]
1615chain = "paseo-local"
1616
1617[[parachains]]
1618id = 1000
1619chain = "asset-hub-paseo-local"
1620
1621[[parachains]]
1622id = 4385
1623default_command = "pop-node"
1624
1625[[hrmp_channels]]
1626sender = 4385
1627recipient = 1000
1628max_capacity = 1000
1629max_message_size = 8000
1630"#
1631			)?;
1632
1633			let zombienet =
1634				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1635					.await?;
1636
1637			assert!(zombienet.hrmp_channels());
1638			Ok(())
1639		}
1640
1641		#[tokio::test]
1642		async fn new_handles_missing_binary() -> Result<()> {
1643			let temp_dir = tempdir()?;
1644			let cache = PathBuf::from(temp_dir.path());
1645			let config = Builder::new().suffix(".toml").tempfile()?;
1646			writeln!(
1647				config.as_file(),
1648				r#"
1649[relaychain]
1650chain = "paseo-local"
1651
1652[[parachains]]
1653id = 404
1654default_command = "missing-binary"
1655"#
1656			)?;
1657
1658			assert!(matches!(
1659				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None).await,
1660				Err(Error::MissingBinary(command))
1661				if command == "missing-binary"
1662			));
1663			Ok(())
1664		}
1665
1666		#[tokio::test]
1667		async fn binaries_works() -> Result<()> {
1668			let temp_dir = tempdir()?;
1669			let cache = PathBuf::from(temp_dir.path());
1670			let config = Builder::new().suffix(".toml").tempfile()?;
1671			writeln!(
1672				config.as_file(),
1673				r#"
1674[relaychain]
1675chain = "paseo-local"
1676
1677[[parachains]]
1678id = 1000
1679chain = "asset-hub-paseo-local"
1680
1681[[parachains]]
1682id = 2000
1683default_command = "./target/release/parachain-template-node"
1684
1685[[parachains]]
1686id = 4385
1687default_command = "pop-node"
1688"#
1689			)?;
1690
1691			let mut zombienet =
1692				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1693					.await?;
1694			assert_eq!(zombienet.binaries().count(), 6);
1695			Ok(())
1696		}
1697
1698		#[tokio::test]
1699		async fn binaries_includes_chain_spec_generators() -> Result<()> {
1700			let temp_dir = tempdir()?;
1701			let cache = PathBuf::from(temp_dir.path());
1702			let config = Builder::new().suffix(".toml").tempfile()?;
1703			writeln!(
1704				config.as_file(),
1705				r#"
1706[relaychain]
1707chain = "paseo-local"
1708
1709[[parachains]]
1710id = 1000
1711chain = "asset-hub-paseo-local"
1712"#
1713			)?;
1714
1715			let mut zombienet =
1716				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1717					.await?;
1718			assert_eq!(zombienet.binaries().count(), 4);
1719			Ok(())
1720		}
1721
1722		#[tokio::test]
1723		async fn spawn_ensures_relay_chain_binary_exists() -> Result<()> {
1724			let temp_dir = tempdir()?;
1725			let cache = PathBuf::from(temp_dir.path());
1726			let config = Builder::new().suffix(".toml").tempfile()?;
1727			writeln!(
1728				config.as_file(),
1729				r#"
1730[relaychain]
1731chain = "paseo-local"
1732"#
1733			)?;
1734
1735			let mut zombienet =
1736				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1737					.await?;
1738			assert!(matches!(
1739				zombienet.spawn().await,
1740				Err(Error::MissingBinary(error))
1741				if error == "polkadot"
1742			));
1743			Ok(())
1744		}
1745
1746		#[tokio::test]
1747		async fn spawn_ensures_relay_chain_version_set() -> Result<()> {
1748			let temp_dir = tempdir()?;
1749			let cache = PathBuf::from(temp_dir.path());
1750			let config = Builder::new().suffix(".toml").tempfile()?;
1751			writeln!(
1752				config.as_file(),
1753				r#"
1754[relaychain]
1755chain = "paseo-local"
1756"#
1757			)?;
1758			File::create(cache.join("polkadot"))?;
1759
1760			let mut zombienet =
1761				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1762					.await?;
1763			let Binary::Source { source, .. } = &mut zombienet.relay_chain.binary else {
1764				panic!("expected binary which needs to be sourced")
1765			};
1766			if let Source::GitHub(ReleaseArchive { tag, .. }) = source.as_mut() {
1767				*tag = None
1768			}
1769			assert!(matches!(
1770				zombienet.spawn().await,
1771				Err(Error::MissingBinary(error))
1772				if error == "Could not determine version for `polkadot` binary",
1773			));
1774			Ok(())
1775		}
1776
1777		#[tokio::test]
1778		async fn spawn_symlinks_workers() -> Result<()> {
1779			let temp_dir = tempdir()?;
1780			let cache = PathBuf::from(temp_dir.path());
1781			let config = Builder::new().suffix(".toml").tempfile()?;
1782			writeln!(
1783				config.as_file(),
1784				r#"
1785[relaychain]
1786chain = "paseo-local"
1787"#
1788			)?;
1789			File::create(cache.join(format!("polkadot-{RELAY_BINARY_VERSION}")))?;
1790			File::create(cache.join(format!("polkadot-execute-worker-{RELAY_BINARY_VERSION}")))?;
1791			File::create(cache.join(format!("polkadot-prepare-worker-{RELAY_BINARY_VERSION}")))?;
1792
1793			let mut zombienet = Zombienet::new(
1794				&cache,
1795				config.path().try_into()?,
1796				Some(RELAY_BINARY_VERSION),
1797				None,
1798				None,
1799				None,
1800				None,
1801			)
1802			.await?;
1803			assert!(!cache.join("polkadot-execute-worker").exists());
1804			assert!(!cache.join("polkadot-prepare-worker").exists());
1805			let _ = zombienet.spawn().await;
1806			assert!(cache.join("polkadot-execute-worker").exists());
1807			assert!(cache.join("polkadot-prepare-worker").exists());
1808			let _ = zombienet.spawn().await;
1809			Ok(())
1810		}
1811
1812		#[tokio::test]
1813		async fn spawn_works() -> Result<()> {
1814			let temp_dir = tempdir()?;
1815			let cache = PathBuf::from(temp_dir.path());
1816			let config = Builder::new().suffix(".toml").tempfile()?;
1817			writeln!(
1818				config.as_file(),
1819				r#"
1820[relaychain]
1821chain = "paseo-local"
1822
1823[[relaychain.nodes]]
1824name = "alice"
1825validator = true
1826"#
1827			)?;
1828
1829			let mut zombienet =
1830				Zombienet::new(&cache, config.path().try_into()?, None, None, None, None, None)
1831					.await?;
1832			for b in zombienet.binaries() {
1833				b.source(true, &Output, true).await?;
1834			}
1835
1836			zombienet.spawn().await?;
1837			Ok(())
1838		}
1839	}
1840
1841	mod network_config {
1842		use super::{Relay::*, *};
1843		use crate::registry::rollups;
1844		use std::{
1845			fs::{File, create_dir_all},
1846			io::Write,
1847			path::PathBuf,
1848		};
1849		use tempfile::{Builder, tempdir};
1850
1851		#[test]
1852		fn initializing_from_file_fails_when_missing() {
1853			assert!(NetworkConfiguration::try_from(PathBuf::new().as_path()).is_err());
1854		}
1855
1856		#[test]
1857		fn initializing_from_file_fails_when_malformed() -> Result<(), Error> {
1858			let config = Builder::new().suffix(".toml").tempfile()?;
1859			writeln!(config.as_file(), "[")?;
1860			assert!(matches!(
1861				NetworkConfiguration::try_from(config.path()),
1862				Err(Error::TomlError(..))
1863			));
1864			Ok(())
1865		}
1866
1867		#[test]
1868		fn initializing_from_file_fails_when_relaychain_missing() -> Result<(), Error> {
1869			let config = Builder::new().suffix(".toml").tempfile()?;
1870			assert!(matches!(
1871				NetworkConfiguration::try_from(config.path()),
1872				Err(Error::Config(error)) if error == "Relay chain does not exist."
1873			));
1874			Ok(())
1875		}
1876
1877		#[tokio::test]
1878		async fn initializing_from_file_fails_when_parachain_id_missing() -> Result<()> {
1879			let config = Builder::new().suffix(".toml").tempfile()?;
1880			writeln!(
1881				config.as_file(),
1882				r#"
1883[relaychain]
1884chain = "paseo-local"
1885
1886[[parachains]]
1887"#
1888			)?;
1889
1890			assert!(matches!(
1891				<&Path as TryInto<NetworkConfiguration>>::try_into(config.path()),
1892				Err(Error::Config(error))
1893				if error == "TOML parse error at line 5, column 1\n  |\n5 | [[parachains]]\n  | ^^^^^^^^^^^^^^\nmissing field `id`\n"
1894			));
1895			Ok(())
1896		}
1897
1898		#[test]
1899		fn initializes_relay_from_file() -> Result<(), Error> {
1900			let config = Builder::new().suffix(".toml").tempfile()?;
1901			writeln!(
1902				config.as_file(),
1903				r#"
1904				[relaychain]
1905				chain = "paseo-local"
1906				default_command = "polkadot"
1907				[[relaychain.nodes]]
1908				name = "alice"
1909			"#
1910			)?;
1911			let network_config = NetworkConfiguration::try_from(config.path())?;
1912			let relay_chain = network_config.0.relaychain();
1913			assert_eq!("paseo-local", relay_chain.chain().as_str());
1914			assert_eq!(Some("polkadot"), relay_chain.default_command().map(|c| c.as_str()));
1915			let nodes = relay_chain.nodes();
1916			assert_eq!("alice", nodes.first().unwrap().name());
1917			assert!(network_config.0.parachains().is_empty());
1918			Ok(())
1919		}
1920
1921		#[test]
1922		fn initializes_parachains_from_file() -> Result<(), Error> {
1923			let config = Builder::new().suffix(".toml").tempfile()?;
1924			writeln!(
1925				config.as_file(),
1926				r#"
1927				[relaychain]
1928				chain = "paseo-local"
1929				[[parachains]]
1930				id = 2000
1931				default_command = "node"
1932			"#
1933			)?;
1934			let network_config = NetworkConfiguration::try_from(config.path())?;
1935			let parachains = network_config.0.parachains();
1936			let para_2000 = parachains.first().unwrap();
1937			assert_eq!(2000, para_2000.id());
1938			assert_eq!(Some("node"), para_2000.default_command().map(|c| c.as_str()));
1939			Ok(())
1940		}
1941
1942		#[test]
1943		fn initializing_from_network_config_works() -> Result<(), Error> {
1944			let network_config = NetworkConfigBuilder::new()
1945				.with_relaychain(|b| {
1946					b.with_chain("paseo-local").with_validator(|b| b.with_name("alice"))
1947				})
1948				.build()
1949				.unwrap();
1950			let config = NetworkConfiguration::try_from(network_config.clone()).unwrap();
1951			assert_eq!(config, NetworkConfiguration(network_config, Default::default()));
1952			Ok(())
1953		}
1954
1955		#[test]
1956		fn build_works() -> Result<(), Error> {
1957			let port = 9944;
1958			for relay in [Paseo, Kusama, Polkadot, Westend] {
1959				let mut rollups: Vec<_> = rollups(&relay).to_vec();
1960				rollups
1961					.iter_mut()
1962					.enumerate()
1963					.for_each(|(i, rollup)| rollup.set_port(port + i as u16 + 1));
1964				let relay_chain = relay.chain();
1965
1966				let config =
1967					NetworkConfiguration::build(relay, Some(port), Some(rollups.as_slice()))?;
1968
1969				let relay_config = config.0.relaychain();
1970				assert_eq!(relay_config.chain().as_str(), relay_chain);
1971				// TODO: Just a temporary removal, once Paseo chain-spec-generator supports
1972				// passet-hub just remove the comment.
1973				//assert_eq!(relay_config.nodes().len(), rollups.len().max(2));
1974				assert_eq!(
1975					relay_config.nodes().iter().map(|n| n.name()).collect::<Vec<_>>(),
1976					VALIDATORS.into_iter().take(relay_config.nodes().len()).collect::<Vec<_>>()
1977				);
1978				assert_eq!(relay_config.nodes().first().unwrap().rpc_port().unwrap(), port);
1979
1980				let parachains = config.0.parachains();
1981				assert_eq!(parachains.len(), rollups.len());
1982				for (i, rollup) in rollups.iter().enumerate() {
1983					let parachain = parachains.iter().find(|p| p.id() == rollup.id()).unwrap();
1984					assert_eq!(parachain.chain().unwrap().as_str(), rollup.chain());
1985					assert_eq!(parachain.default_command().unwrap().as_str(), rollup.binary());
1986					println!("{} {}", relay_chain, rollup.name());
1987					assert_eq!(
1988						parachain.genesis_overrides().is_some(),
1989						rollup.genesis_overrides().is_some() ||
1990							rollups.iter().any(|r| r
1991								.requires()
1992								.map(|r| r.contains_key(&rollup.as_any().type_id()))
1993								.unwrap_or_default())
1994					);
1995					let collators = parachain.collators();
1996					assert_eq!(collators.len(), 1);
1997					let collator = collators.first().unwrap();
1998					assert_eq!(collator.name(), &format!("{}-collator", rollup.name()));
1999					assert_eq!(
2000						collator.args().len(),
2001						rollup.args().map(|a| a.len()).unwrap_or_default()
2002					);
2003					assert_eq!(collator.rpc_port(), Some(port + i as u16 + 1));
2004				}
2005
2006				// Ensure channels open between all rollups
2007				let channels = config.0.hrmp_channels();
2008				assert_eq!(channels.len(), rollups.len() * (rollups.len() - 1));
2009				for rollup in rollups.iter() {
2010					for other in rollups.iter().filter(|r| r.id() != rollup.id()) {
2011						assert!(
2012							channels
2013								.iter()
2014								.any(|c| c.sender() == rollup.id() && c.recipient() == other.id())
2015						);
2016						assert!(
2017							channels
2018								.iter()
2019								.any(|c| c.sender() == other.id() && c.recipient() == rollup.id())
2020						);
2021					}
2022				}
2023				assert!(
2024					channels
2025						.iter()
2026						.all(|c| c.max_capacity() == 1000 && c.max_message_size() == 8000)
2027				);
2028			}
2029			Ok(())
2030		}
2031
2032		#[test]
2033		fn adapt_works() -> Result<(), Error> {
2034			let config = Builder::new().suffix(".toml").tempfile()?;
2035			writeln!(
2036				config.as_file(),
2037				r#"
2038[relaychain]
2039chain = "paseo-local"
2040
2041[[relaychain.nodes]]
2042name = "alice"
2043command = "polkadot"
2044
2045[[relaychain.nodes]]
2046name = "bob"
2047
2048[[parachains]]
2049id = 1000
2050chain = "asset-hub-paseo-local"
2051
2052[[parachains.collators]]
2053name = "asset-hub-1"
2054command = "polkadot-parachain"
2055
2056[[parachains.collators]]
2057name = "asset-hub-2"
2058
2059[[parachains]]
2060id = 2000
2061default_command = "pop-node"
2062
2063[[parachains.collators]]
2064name = "pop"
2065command = "pop-node"
2066
2067[[parachains]]
2068id = 2001
2069default_command = "./target/release/parachain-template-node"
2070
2071[[parachains.collators]]
2072name = "collator-2001"
2073command = "./target/release/parachain-template-node"
2074
2075[[parachains]]
2076id = 2002
2077default_command = "./target/release/parachain-template-node"
2078
2079[parachains.collator]
2080name = "collator-2002"
2081command = "./target/release/parachain-template-node"
2082subcommand = "test"
2083ws_port = 9945
2084rpc_port = 9944
2085"#
2086			)?;
2087			let network_config = NetworkConfiguration::try_from(config.path())?;
2088
2089			let relay_chain_binary = Builder::new().tempfile()?;
2090			let relay_chain = relay_chain_binary.path();
2091			File::create(relay_chain)?;
2092			let system_chain_binary = Builder::new().tempfile()?;
2093			let system_chain = system_chain_binary.path();
2094			File::create(system_chain)?;
2095			let pop_binary = Builder::new().tempfile()?;
2096			let pop = pop_binary.path();
2097			File::create(pop)?;
2098			let parachain_template_node = Builder::new().tempfile()?;
2099			let parachain_template = parachain_template_node.path();
2100			create_dir_all(parachain_template.parent().unwrap())?;
2101			File::create(parachain_template)?;
2102
2103			let adapted = network_config.adapt(
2104				&RelayChain {
2105					runtime: Paseo,
2106					binary: Binary::Local {
2107						name: "polkadot".to_string(),
2108						path: relay_chain.to_path_buf(),
2109						manifest: None,
2110					},
2111					workers: ["polkadot-execute-worker", ""],
2112					chain: "paseo-local".to_string(),
2113					chain_spec_generator: None,
2114				},
2115				&[
2116					(
2117						1000,
2118						Chain {
2119							id: 1000,
2120							binary: Binary::Local {
2121								name: "polkadot-parachain".to_string(),
2122								path: system_chain.to_path_buf(),
2123								manifest: None,
2124							},
2125							chain: None,
2126							chain_spec_generator: None,
2127						},
2128					),
2129					(
2130						2000,
2131						Chain {
2132							id: 2000,
2133							binary: Binary::Local {
2134								name: "pop-node".to_string(),
2135								path: pop.to_path_buf(),
2136								manifest: None,
2137							},
2138							chain: None,
2139							chain_spec_generator: None,
2140						},
2141					),
2142					(
2143						2001,
2144						Chain {
2145							id: 2001,
2146							binary: Binary::Local {
2147								name: "parachain-template-node".to_string(),
2148								path: parachain_template.to_path_buf(),
2149								manifest: None,
2150							},
2151							chain: None,
2152							chain_spec_generator: None,
2153						},
2154					),
2155					(
2156						2002,
2157						Chain {
2158							id: 2002,
2159							binary: Binary::Local {
2160								name: "parachain-template-node".to_string(),
2161								path: parachain_template.to_path_buf(),
2162								manifest: None,
2163							},
2164							chain: None,
2165							chain_spec_generator: None,
2166						},
2167					),
2168				]
2169				.into(),
2170			)?;
2171
2172			let contents = adapted.dump_to_toml().unwrap();
2173			assert_eq!(
2174				contents,
2175				format!(
2176					r#"[settings]
2177timeout = 1000
2178node_spawn_timeout = 300
2179tear_down_on_failure = true
2180
2181[relaychain]
2182chain = "paseo-local"
2183default_command = "{0}"
2184
2185[[relaychain.nodes]]
2186name = "alice"
2187validator = true
2188invulnerable = true
2189bootnode = false
2190balance = 2000000000000
2191
2192[[relaychain.nodes]]
2193name = "bob"
2194validator = true
2195invulnerable = true
2196bootnode = false
2197balance = 2000000000000
2198
2199[[parachains]]
2200id = 1000
2201chain = "asset-hub-paseo-local"
2202add_to_genesis = true
2203balance = 2000000000000
2204default_command = "{1}"
2205cumulus_based = true
2206evm_based = false
2207
2208[[parachains.collators]]
2209name = "asset-hub-1"
2210validator = true
2211invulnerable = true
2212bootnode = false
2213balance = 2000000000000
2214
2215[[parachains.collators]]
2216name = "asset-hub-2"
2217validator = true
2218invulnerable = true
2219bootnode = false
2220balance = 2000000000000
2221
2222[[parachains]]
2223id = 2000
2224add_to_genesis = true
2225balance = 2000000000000
2226default_command = "{2}"
2227cumulus_based = true
2228evm_based = false
2229
2230[[parachains.collators]]
2231name = "pop"
2232validator = true
2233invulnerable = true
2234bootnode = false
2235balance = 2000000000000
2236
2237[[parachains]]
2238id = 2001
2239add_to_genesis = true
2240balance = 2000000000000
2241default_command = "{3}"
2242cumulus_based = true
2243evm_based = false
2244
2245[[parachains.collators]]
2246name = "collator-2001"
2247validator = true
2248invulnerable = true
2249bootnode = false
2250balance = 2000000000000
2251
2252[[parachains]]
2253id = 2002
2254add_to_genesis = true
2255balance = 2000000000000
2256default_command = "{3}"
2257cumulus_based = true
2258evm_based = false
2259
2260[[parachains.collators]]
2261name = "collator-2002"
2262subcommand = "test"
2263validator = true
2264invulnerable = true
2265bootnode = false
2266balance = 2000000000000
2267ws_port = 9945
2268rpc_port = 9944
2269"#,
2270					relay_chain.canonicalize()?.to_str().unwrap(),
2271					system_chain.canonicalize()?.to_str().unwrap(),
2272					pop.canonicalize()?.to_str().unwrap(),
2273					parachain_template.canonicalize()?.to_str().unwrap()
2274				)
2275			);
2276			Ok(())
2277		}
2278
2279		#[test]
2280		fn adapt_with_chain_spec_generator_works() -> Result<(), Error> {
2281			let config = Builder::new().suffix(".toml").tempfile()?;
2282			writeln!(
2283				config.as_file(),
2284				r#"
2285[relaychain]
2286chain = "paseo-local"
2287
2288[[relaychain.nodes]]
2289name = "alice"
2290command = "polkadot"
2291
2292[[parachains]]
2293id = 1000
2294chain = "asset-hub-paseo-local"
2295
2296[[parachains.collators]]
2297name = "asset-hub"
2298command = "polkadot-parachain"
2299
2300"#
2301			)?;
2302			let network_config = NetworkConfiguration::try_from(config.path())?;
2303
2304			let relay_chain_binary = Builder::new().tempfile()?;
2305			let relay_chain = relay_chain_binary.path();
2306			File::create(relay_chain)?;
2307			let relay_chain_spec_generator = Builder::new().tempfile()?;
2308			let relay_chain_spec_generator = relay_chain_spec_generator.path();
2309			File::create(relay_chain_spec_generator)?;
2310			let system_chain_binary = Builder::new().tempfile()?;
2311			let system_chain = system_chain_binary.path();
2312			File::create(system_chain)?;
2313			let system_chain_spec_generator = Builder::new().tempfile()?;
2314			let system_chain_spec_generator = system_chain_spec_generator.path();
2315			File::create(system_chain_spec_generator)?;
2316
2317			let adapted = network_config.adapt(
2318				&RelayChain {
2319					runtime: Paseo,
2320					binary: Binary::Local {
2321						name: "polkadot".to_string(),
2322						path: relay_chain.to_path_buf(),
2323						manifest: None,
2324					},
2325					workers: ["polkadot-execute-worker", ""],
2326					chain: "paseo-local".to_string(),
2327					chain_spec_generator: Some(Binary::Local {
2328						name: "paseo-chain-spec-generator".to_string(),
2329						path: relay_chain_spec_generator.to_path_buf(),
2330						manifest: None,
2331					}),
2332				},
2333				&[(
2334					1000,
2335					Chain {
2336						id: 1000,
2337						binary: Binary::Local {
2338							name: "polkadot-parachain".to_string(),
2339							path: system_chain.to_path_buf(),
2340							manifest: None,
2341						},
2342						chain: Some("asset-hub-paseo-local".to_string()),
2343						chain_spec_generator: Some(Binary::Local {
2344							name: "paseo-chain-spec-generator".to_string(),
2345							path: system_chain_spec_generator.to_path_buf(),
2346							manifest: None,
2347						}),
2348					},
2349				)]
2350				.into(),
2351			)?;
2352
2353			let contents = adapted.dump_to_toml().unwrap();
2354			assert_eq!(
2355				contents,
2356				format!(
2357					r#"[settings]
2358timeout = 1000
2359node_spawn_timeout = 300
2360tear_down_on_failure = true
2361
2362[relaychain]
2363chain = "paseo-local"
2364default_command = "{0}"
2365chain_spec_command = "{1} {2}"
2366
2367[[relaychain.nodes]]
2368name = "alice"
2369validator = true
2370invulnerable = true
2371bootnode = false
2372balance = 2000000000000
2373
2374[[parachains]]
2375id = 1000
2376chain = "asset-hub-paseo-local"
2377add_to_genesis = true
2378balance = 2000000000000
2379default_command = "{3}"
2380chain_spec_command = "{4} {2}"
2381cumulus_based = true
2382evm_based = false
2383
2384[[parachains.collators]]
2385name = "asset-hub"
2386validator = true
2387invulnerable = true
2388bootnode = false
2389balance = 2000000000000
2390"#,
2391					relay_chain.canonicalize()?.to_str().unwrap(),
2392					relay_chain_spec_generator.canonicalize()?.to_str().unwrap(),
2393					"{{chainName}}",
2394					system_chain.canonicalize()?.to_str().unwrap(),
2395					system_chain_spec_generator.canonicalize()?.to_str().unwrap(),
2396				)
2397			);
2398			Ok(())
2399		}
2400
2401		#[test]
2402		fn adapt_with_hrmp_channels_works() -> Result<(), Error> {
2403			let config = Builder::new().suffix(".toml").tempfile()?;
2404			writeln!(
2405				config.as_file(),
2406				r#"
2407[relaychain]
2408chain = "paseo-local"
2409
2410[[relaychain.nodes]]
2411name = "alice"
2412
2413[[parachains]]
2414id = 1000
2415chain = "asset-hub-paseo-local"
2416
2417[[parachains.collators]]
2418name = "asset-hub"
2419
2420[[parachains]]
2421id = 2000
2422default_command = "pop-node"
2423
2424[[parachains.collators]]
2425name = "pop"
2426
2427[[hrmp_channels]]
2428sender = 1000
2429recipient = 2000
2430max_capacity = 1000
2431max_message_size = 5000
2432
2433[[hrmp_channels]]
2434sender = 2000
2435recipient = 1000
2436max_capacity = 1000
2437max_message_size = 8000
2438
2439"#
2440			)?;
2441			let network_config = NetworkConfiguration::try_from(config.path())?;
2442
2443			let relay_chain_binary = Builder::new().tempfile()?;
2444			let relay_chain = relay_chain_binary.path();
2445			File::create(relay_chain)?;
2446			let system_chain_binary = Builder::new().tempfile()?;
2447			let system_chain = system_chain_binary.path();
2448			File::create(system_chain)?;
2449			let pop_binary = Builder::new().tempfile()?;
2450			let pop = pop_binary.path();
2451			File::create(pop)?;
2452
2453			let adapted = network_config.adapt(
2454				&RelayChain {
2455					runtime: Paseo,
2456					binary: Binary::Local {
2457						name: "polkadot".to_string(),
2458						path: relay_chain.to_path_buf(),
2459						manifest: None,
2460					},
2461					workers: ["polkadot-execute-worker", ""],
2462					chain: "paseo-local".to_string(),
2463					chain_spec_generator: None,
2464				},
2465				&[
2466					(
2467						1000,
2468						Chain {
2469							id: 1000,
2470							binary: Binary::Local {
2471								name: "polkadot-parachain".to_string(),
2472								path: system_chain.to_path_buf(),
2473								manifest: None,
2474							},
2475							chain: Some("asset-hub-paseo-local".to_string()),
2476							chain_spec_generator: None,
2477						},
2478					),
2479					(
2480						2000,
2481						Chain {
2482							id: 2000,
2483							binary: Binary::Local {
2484								name: "pop-node".to_string(),
2485								path: pop.to_path_buf(),
2486								manifest: None,
2487							},
2488							chain: None,
2489							chain_spec_generator: None,
2490						},
2491					),
2492				]
2493				.into(),
2494			)?;
2495
2496			let contents = adapted.dump_to_toml().unwrap();
2497			assert_eq!(
2498				contents,
2499				format!(
2500					r#"[settings]
2501timeout = 1000
2502node_spawn_timeout = 300
2503tear_down_on_failure = true
2504
2505[relaychain]
2506chain = "paseo-local"
2507default_command = "{0}"
2508
2509[[relaychain.nodes]]
2510name = "alice"
2511validator = true
2512invulnerable = true
2513bootnode = false
2514balance = 2000000000000
2515
2516[[parachains]]
2517id = 1000
2518chain = "asset-hub-paseo-local"
2519add_to_genesis = true
2520balance = 2000000000000
2521default_command = "{1}"
2522cumulus_based = true
2523evm_based = false
2524
2525[[parachains.collators]]
2526name = "asset-hub"
2527validator = true
2528invulnerable = true
2529bootnode = false
2530balance = 2000000000000
2531
2532[[parachains]]
2533id = 2000
2534add_to_genesis = true
2535balance = 2000000000000
2536default_command = "{2}"
2537cumulus_based = true
2538evm_based = false
2539
2540[[parachains.collators]]
2541name = "pop"
2542validator = true
2543invulnerable = true
2544bootnode = false
2545balance = 2000000000000
2546
2547[[hrmp_channels]]
2548sender = 1000
2549recipient = 2000
2550max_capacity = 1000
2551max_message_size = 5000
2552
2553[[hrmp_channels]]
2554sender = 2000
2555recipient = 1000
2556max_capacity = 1000
2557max_message_size = 8000
2558"#,
2559					relay_chain.canonicalize()?.to_str().unwrap(),
2560					system_chain.canonicalize()?.to_str().unwrap(),
2561					pop.canonicalize()?.to_str().unwrap(),
2562				)
2563			);
2564			Ok(())
2565		}
2566
2567		#[test]
2568		fn adapt_with_chain_spec_works() -> Result<(), Error> {
2569			let config = Builder::new().suffix(".toml").tempfile()?;
2570			writeln!(
2571				config.as_file(),
2572				r#"
2573[relaychain]
2574chain = "paseo-local"
2575chain_spec_command = "cmd_template"
2576chain_spec_command_is_local = true
2577chain_spec_path = "./path/to/paseo-local.spec.json"
2578
2579[[relaychain.nodes]]
2580name = "alice"
2581
2582[[parachains]]
2583id = 1000
2584chain = "asset-hub-paseo-local"
2585chain_spec_command = "cmd_template"
2586chain_spec_command_is_local = true
2587chain_spec_path = "./path/to/asset-hub-paseo-local.spec.json"
2588
2589[[parachains.collators]]
2590name = "asset-hub"
2591"#
2592			)?;
2593			let network_config = NetworkConfiguration::try_from(config.path())?;
2594
2595			let relay_chain_binary = Builder::new().tempfile()?;
2596			let relay_chain = relay_chain_binary.path();
2597			File::create(relay_chain)?;
2598			let system_chain_binary = Builder::new().tempfile()?;
2599			let system_chain = system_chain_binary.path();
2600			File::create(system_chain)?;
2601			let pop_binary = Builder::new().tempfile()?;
2602			let pop = pop_binary.path();
2603			File::create(pop)?;
2604
2605			let adapted = network_config.adapt(
2606				&RelayChain {
2607					runtime: Paseo,
2608					binary: Binary::Local {
2609						name: "polkadot".to_string(),
2610						path: relay_chain.to_path_buf(),
2611						manifest: None,
2612					},
2613					workers: ["polkadot-execute-worker", ""],
2614					chain: "paseo-local".to_string(),
2615					chain_spec_generator: None,
2616				},
2617				&[(
2618					1000,
2619					Chain {
2620						id: 1000,
2621						binary: Binary::Local {
2622							name: "polkadot-parachain".to_string(),
2623							path: system_chain.to_path_buf(),
2624							manifest: None,
2625						},
2626						chain: Some("asset-hub-paseo-local".to_string()),
2627						chain_spec_generator: None,
2628					},
2629				)]
2630				.into(),
2631			)?;
2632
2633			let contents = adapted.dump_to_toml().unwrap();
2634			assert_eq!(
2635				contents,
2636				format!(
2637					r#"[settings]
2638timeout = 1000
2639node_spawn_timeout = 300
2640tear_down_on_failure = true
2641
2642[relaychain]
2643chain = "paseo-local"
2644default_command = "{0}"
2645chain_spec_path = "./path/to/paseo-local.spec.json"
2646chain_spec_command = "cmd_template"
2647chain_spec_command_is_local = true
2648
2649[[relaychain.nodes]]
2650name = "alice"
2651validator = true
2652invulnerable = true
2653bootnode = false
2654balance = 2000000000000
2655
2656[[parachains]]
2657id = 1000
2658chain = "asset-hub-paseo-local"
2659add_to_genesis = true
2660balance = 2000000000000
2661default_command = "{1}"
2662chain_spec_path = "./path/to/asset-hub-paseo-local.spec.json"
2663chain_spec_command = "cmd_template"
2664chain_spec_command_is_local = true
2665cumulus_based = true
2666evm_based = false
2667
2668[[parachains.collators]]
2669name = "asset-hub"
2670validator = true
2671invulnerable = true
2672bootnode = false
2673balance = 2000000000000
2674"#,
2675					relay_chain.canonicalize()?.to_str().unwrap(),
2676					system_chain.canonicalize()?.to_str().unwrap(),
2677				)
2678			);
2679			Ok(())
2680		}
2681
2682		#[test]
2683		fn adapt_with_overrides_works() -> Result<(), Error> {
2684			let config = Builder::new().suffix(".toml").tempfile()?;
2685			writeln!(
2686				config.as_file(),
2687				r#"
2688[relaychain]
2689chain = "paseo-local"
2690wasm_override = "./path/to/paseo-local.wasm"
2691
2692[[relaychain.nodes]]
2693name = "alice"
2694
2695[relaychain.genesis.balances]
2696balances = [["5Ec4AhPKXY9B4ayGshkz2wFMh7N8gP7XKfAvtt1cigpG9FkJ", 420000000000]]
2697
2698[[parachains]]
2699id = 1000
2700chain = "asset-hub-paseo-local"
2701wasm_override = "./path/to/asset-hub-paseo-local.wasm"
2702
2703[[parachains.collators]]
2704name = "asset-hub"
2705
2706[parachains.genesis.balances]
2707balances = [["5Ec4AhPKXY9B4ayGshkz2wFMh7N8gP7XKfAvtt1cigpG9FkJ", 420000000000]]
2708
2709"#
2710			)?;
2711			let network_config = NetworkConfiguration::try_from(config.path())?;
2712
2713			let relay_chain_binary = Builder::new().tempfile()?;
2714			let relay_chain = relay_chain_binary.path();
2715			File::create(relay_chain)?;
2716			let system_chain_binary = Builder::new().tempfile()?;
2717			let system_chain = system_chain_binary.path();
2718			File::create(system_chain)?;
2719			let pop_binary = Builder::new().tempfile()?;
2720			let pop = pop_binary.path();
2721			File::create(pop)?;
2722
2723			let adapted = network_config.adapt(
2724				&RelayChain {
2725					runtime: Paseo,
2726					binary: Binary::Local {
2727						name: "polkadot".to_string(),
2728						path: relay_chain.to_path_buf(),
2729						manifest: None,
2730					},
2731					workers: ["polkadot-execute-worker", ""],
2732					chain: "paseo-local".to_string(),
2733					chain_spec_generator: None,
2734				},
2735				&[(
2736					1000,
2737					Chain {
2738						id: 1000,
2739						binary: Binary::Local {
2740							name: "polkadot-parachain".to_string(),
2741							path: system_chain.to_path_buf(),
2742							manifest: None,
2743						},
2744						chain: Some("asset-hub-paseo-local".to_string()),
2745						chain_spec_generator: None,
2746					},
2747				)]
2748				.into(),
2749			)?;
2750
2751			let contents = adapted.dump_to_toml().unwrap();
2752			assert_eq!(
2753				contents,
2754				format!(
2755					r#"[settings]
2756timeout = 1000
2757node_spawn_timeout = 300
2758tear_down_on_failure = true
2759
2760[relaychain]
2761chain = "paseo-local"
2762default_command = "{0}"
2763wasm_override = "./path/to/paseo-local.wasm"
2764
2765[[relaychain.nodes]]
2766name = "alice"
2767validator = true
2768invulnerable = true
2769bootnode = false
2770balance = 2000000000000
2771
2772[relaychain.genesis.balances]
2773balances = [[
2774    "5Ec4AhPKXY9B4ayGshkz2wFMh7N8gP7XKfAvtt1cigpG9FkJ",
2775    {{ "$serde_json::private::Number" = "420000000000" }},
2776]]
2777
2778[[parachains]]
2779id = 1000
2780chain = "asset-hub-paseo-local"
2781add_to_genesis = true
2782balance = 2000000000000
2783default_command = "{1}"
2784wasm_override = "./path/to/asset-hub-paseo-local.wasm"
2785cumulus_based = true
2786evm_based = false
2787
2788[parachains.genesis.balances]
2789balances = [[
2790    "5Ec4AhPKXY9B4ayGshkz2wFMh7N8gP7XKfAvtt1cigpG9FkJ",
2791    {{ "$serde_json::private::Number" = "420000000000" }},
2792]]
2793
2794[[parachains.collators]]
2795name = "asset-hub"
2796validator = true
2797invulnerable = true
2798bootnode = false
2799balance = 2000000000000
2800"#,
2801					relay_chain.canonicalize()?.to_str().unwrap(),
2802					system_chain.canonicalize()?.to_str().unwrap(),
2803				)
2804			);
2805			Ok(())
2806		}
2807
2808		#[test]
2809		fn resolves_path() -> Result<(), Error> {
2810			let working_dir = tempdir()?;
2811			let path = working_dir.path().join("./target/release/node");
2812			assert!(
2813				matches!(NetworkConfiguration::resolve_path(&path), Err(Error::Config(message))
2814						if message == format!("the canonical path of {:?} could not be resolved", path)
2815				)
2816			);
2817
2818			create_dir_all(path.parent().unwrap())?;
2819			File::create(&path)?;
2820			assert_eq!(
2821				NetworkConfiguration::resolve_path(&path)?,
2822				path.canonicalize()?.to_str().unwrap().to_string()
2823			);
2824			Ok(())
2825		}
2826	}
2827
2828	mod parachain {
2829		use super::*;
2830		use pop_common::sourcing::GitHub::SourceCodeArchive;
2831		use std::path::PathBuf;
2832
2833		#[test]
2834		fn initializes_from_local_binary() -> Result<(), Error> {
2835			let name = "parachain-template-node";
2836			let command = PathBuf::from("./target/release").join(name);
2837			assert_eq!(
2838				Chain::from_local(2000, command.clone(), Some("dev"))?,
2839				Chain {
2840					id: 2000,
2841					binary: Binary::Local { name: name.to_string(), path: command, manifest: None },
2842					chain: Some("dev".to_string()),
2843					chain_spec_generator: None,
2844				}
2845			);
2846			Ok(())
2847		}
2848
2849		#[test]
2850		fn initializes_from_local_package() -> Result<(), Error> {
2851			let name = "pop-chains";
2852			let command = PathBuf::from("./target/release").join(name);
2853			assert_eq!(
2854				Chain::from_local(2000, command.clone(), Some("dev"))?,
2855				Chain {
2856					id: 2000,
2857					binary: Binary::Local {
2858						name: name.to_string(),
2859						path: command,
2860						manifest: Some(PathBuf::from("./Cargo.toml"))
2861					},
2862					chain: Some("dev".to_string()),
2863					chain_spec_generator: None,
2864				}
2865			);
2866			Ok(())
2867		}
2868
2869		#[test]
2870		fn initializes_from_git() -> Result<(), Error> {
2871			let repo = Repository::parse("https://git.com/r0gue-io/pop-node#v1.0")?;
2872			let cache = tempdir()?;
2873			assert_eq!(
2874				Chain::from_repository(2000, &repo, Some("dev"), cache.path())?,
2875				Chain {
2876					id: 2000,
2877					binary: Binary::Source {
2878						name: "pop-node".to_string(),
2879						source: Git {
2880							url: repo.url,
2881							reference: repo.reference,
2882							manifest: None,
2883							package: "pop-node".to_string(),
2884							artifacts: vec!["pop-node".to_string()],
2885						}
2886						.into(),
2887						cache: cache.path().to_path_buf(),
2888					},
2889					chain: Some("dev".to_string()),
2890					chain_spec_generator: None,
2891				}
2892			);
2893			Ok(())
2894		}
2895
2896		#[test]
2897		fn initializes_from_github() -> Result<(), Error> {
2898			let repo = Repository::parse("https://github.com/r0gue-io/pop-node#v1.0")?;
2899			let cache = tempdir()?;
2900			assert_eq!(
2901				Chain::from_repository(2000, &repo, Some("dev"), cache.path())?,
2902				Chain {
2903					id: 2000,
2904					binary: Binary::Source {
2905						name: "pop-node".to_string(),
2906						source: Source::GitHub(SourceCodeArchive {
2907							owner: "r0gue-io".to_string(),
2908							repository: "pop-node".to_string(),
2909							reference: Some("v1.0".to_string()),
2910							manifest: None,
2911							package: "pop-node".to_string(),
2912							artifacts: vec!["pop-node".to_string()],
2913						})
2914						.into(),
2915						cache: cache.path().to_path_buf(),
2916					},
2917					chain: Some("dev".to_string()),
2918					chain_spec_generator: None,
2919				},
2920			);
2921			Ok(())
2922		}
2923	}
2924
2925	#[test]
2926	fn resolve_manifest_works() -> Result<()> {
2927		let current_dir = current_dir()?;
2928		// Crate
2929		assert_eq!(
2930			current_dir.join("Cargo.toml"),
2931			resolve_manifest("pop-chains", &current_dir)?.unwrap()
2932		);
2933		// Workspace
2934		assert_eq!(
2935			current_dir.join("../../Cargo.toml").canonicalize()?,
2936			resolve_manifest("pop-cli", &current_dir)?.unwrap()
2937		);
2938		Ok(())
2939	}
2940}