pop-chains 0.14.0

Library for generating, building and running parachains.
Documentation
// SPDX-License-Identifier: GPL-3.0

use crate::errors::Error;
use sc_chain_spec::GenesisConfigBuilderRuntimeCaller;
use std::{
	fs::{self, OpenOptions},
	io::{self, Write, stdin, stdout},
	path::{Path, PathBuf},
};

pub(crate) type HostFunctions = (
	sp_statement_store::runtime_api::HostFunctions,
	cumulus_primitives_proof_size_hostfunction::storage_proof_size::HostFunctions,
);

pub(crate) fn sanitize(target: &Path) -> Result<(), Error> {
	if target.exists() {
		print!("\"{}\" directory exists. Do you want to clean it? [y/n]: ", target.display());
		stdout().flush()?;

		let mut input = String::new();
		stdin().read_line(&mut input)?;

		if input.trim().to_lowercase() == "y" {
			fs::remove_dir_all(target).map_err(|_| Error::Aborted)?;
		} else {
			return Err(Error::Aborted);
		}
	}
	Ok(())
}

/// Check if the initial endowment input by the user is a valid balance.
///
/// # Arguments
///
/// * `initial_endowment` - initial endowment amount to be checked for validity.
pub fn is_initial_endowment_valid(initial_endowment: &str) -> bool {
	initial_endowment.parse::<u128>().is_ok() ||
		is_valid_bitwise_left_shift(initial_endowment).is_ok()
}

// Auxiliary method to check if the endowment input with a shift left (1u64 << 60) format is valid.
// Parse the self << rhs format and check the shift left operation is valid.
fn is_valid_bitwise_left_shift(initial_endowment: &str) -> Result<u128, Error> {
	let v: Vec<&str> = initial_endowment.split(" << ").collect();
	if v.len() < 2 {
		return Err(Error::EndowmentError);
	}
	let left = v[0]
		.split('u') // parse 1u64 characters
		.take(1)
		.collect::<String>()
		.parse::<u128>()
		.map_err(|_e| Error::EndowmentError)?;
	let right = v[1]
		.chars()
		.filter(|c| c.is_numeric()) // parse 1u64 characters
		.collect::<String>()
		.parse::<u32>()
		.map_err(|_e| Error::EndowmentError)?;
	left.checked_shl(right).ok_or(Error::EndowmentError)
}

pub(crate) fn write_to_file(path: &Path, contents: &str) -> Result<(), Error> {
	let mut file = OpenOptions::new()
		.write(true)
		.truncate(true)
		.create(true)
		.open(path)
		.map_err(Error::RustfmtError)?;

	file.write_all(contents.as_bytes()).map_err(Error::RustfmtError)?;

	if path.extension().is_some_and(|ext| ext == "rs") {
		let output = std::process::Command::new("rustfmt")
			.arg(path.to_str().unwrap())
			.output()
			.map_err(Error::RustfmtError)?;

		if !output.status.success() {
			return Err(Error::RustfmtError(io::Error::other(
				"rustfmt exited with non-zero status code",
			)));
		}
	}

	Ok(())
}

/// Get genesis builder preset names of the runtime.
///
/// # Arguments
/// * `binary_path` - Path to the runtime binary.
pub fn get_preset_names(binary_path: &PathBuf) -> Result<Vec<String>, Error> {
	let binary = fs::read(binary_path)?;
	let genesis_config_builder = GenesisConfigBuilderRuntimeCaller::<HostFunctions>::new(&binary);
	genesis_config_builder
		.preset_names()
		.map_err(|e| Error::GenesisBuilderError(e.to_string()))
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::{ChainTemplate, generator::chain::ChainSpec};
	use askama::Template;
	use tempfile::tempdir;

	#[test]
	fn test_write_to_file() -> Result<(), Box<dyn std::error::Error>> {
		let temp_dir = tempdir()?;
		let chainspec = ChainSpec {
			token_symbol: "DOT".to_string(),
			decimals: 6,
			initial_endowment: "1000000".to_string(),
			based_on: ChainTemplate::Standard.to_string(),
		};
		let file_path = temp_dir.path().join("file.rs");
		let _ = fs::write(&file_path, "");
		write_to_file(&file_path, chainspec.render().expect("infallible").as_ref())?;
		let generated_file_content =
			fs::read_to_string(temp_dir.path().join("file.rs")).expect("Failed to read file");
		assert!(
			generated_file_content
				.contains("properties.insert(\"tokenSymbol\".into(), \"DOT\".into());")
		);
		assert!(
			generated_file_content
				.contains("properties.insert(\"tokenDecimals\".into(), 6.into());")
		);
		assert!(generated_file_content.contains("1000000"));
		assert!(generated_file_content.contains(
			"properties.insert(\"basedOn\".into(), \"r0gue-io/base-parachain\".into());"
		));

		Ok(())
	}

	#[test]
	fn test_is_initial_endowment_valid() {
		assert!(is_initial_endowment_valid("100000"));
		assert!(is_initial_endowment_valid("1u64 << 60"));
		assert!(!is_initial_endowment_valid("wrong"));
		assert!(!is_initial_endowment_valid(" "));
	}

	#[test]
	fn test_left_shift() {
		// Values from https://stackoverflow.com/questions/56392875/how-can-i-initialize-a-users-balance-in-a-substrate-blockchain
		assert_eq!(is_valid_bitwise_left_shift("1 << 60").unwrap(), 1152921504606846976);
		let result = is_valid_bitwise_left_shift("wrong");
		assert!(result.is_err());
	}

	#[test]
	fn test_get_preset_names() -> Result<(), Box<dyn std::error::Error>> {
		let path = PathBuf::from("../../tests/runtimes/base_parachain_benchmark.wasm");
		assert!(path.is_file());
		let presets = get_preset_names(&path)?;
		assert_eq!(presets, vec!["development", "local_testnet"]);
		Ok(())
	}
}