#![doc = include_str!("../README.md")]
#[cfg(feature = "generate-readme")]
docify::compile_markdown!("README.docify.md", "README.md");
use clap::{Parser, Subcommand};
use sc_chain_spec::{
json_patch, set_code_substitute_in_json_chain_spec, update_code_in_json_chain_spec, ChainType,
GenericChainSpec, GenesisConfigBuilderRuntimeCaller,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{
borrow::Cow,
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Parser)]
#[command(rename_all = "kebab-case", version, about)]
pub struct ChainSpecBuilder {
#[command(subcommand)]
pub command: ChainSpecBuilderCmd,
#[arg(long, short, default_value = "./chain_spec.json")]
pub chain_spec_path: PathBuf,
}
#[derive(Debug, Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum ChainSpecBuilderCmd {
Create(CreateCmd),
Verify(VerifyCmd),
UpdateCode(UpdateCodeCmd),
ConvertToRaw(ConvertToRawCmd),
ListPresets(ListPresetsCmd),
DisplayPreset(DisplayPresetCmd),
AddCodeSubstitute(AddCodeSubstituteCmd),
}
#[derive(Parser, Debug)]
pub struct CreateCmd {
#[arg(long, short = 'n', default_value = "Custom")]
chain_name: String,
#[arg(long, short = 'i', default_value = "custom")]
chain_id: String,
#[arg(value_enum, short = 't', default_value = "live")]
chain_type: ChainType,
#[arg(long, value_enum, short = 'p', requires = "relay_chain")]
pub para_id: Option<u32>,
#[arg(long, value_enum, short = 'c', requires = "para_id")]
pub relay_chain: Option<String>,
#[arg(long, short, alias = "runtime-wasm-path")]
runtime: PathBuf,
#[arg(long, short = 's')]
raw_storage: bool,
#[arg(long, short = 'v')]
verify: bool,
#[command(subcommand)]
action: GenesisBuildAction,
#[clap(skip)]
code: Option<Cow<'static, [u8]>>,
}
#[derive(Subcommand, Debug, Clone)]
enum GenesisBuildAction {
Patch(PatchCmd),
Full(FullCmd),
Default(DefaultCmd),
NamedPreset(NamedPresetCmd),
}
#[derive(Parser, Debug, Clone)]
struct PatchCmd {
patch_path: PathBuf,
}
#[derive(Parser, Debug, Clone)]
struct FullCmd {
config_path: PathBuf,
}
#[derive(Parser, Debug, Clone)]
struct DefaultCmd {}
#[derive(Parser, Debug, Clone)]
struct NamedPresetCmd {
preset_name: String,
}
#[derive(Parser, Debug, Clone)]
pub struct UpdateCodeCmd {
pub input_chain_spec: PathBuf,
#[arg(alias = "runtime-wasm-path")]
pub runtime: PathBuf,
}
#[derive(Parser, Debug, Clone)]
pub struct AddCodeSubstituteCmd {
pub input_chain_spec: PathBuf,
#[arg(alias = "runtime-wasm-path")]
pub runtime: PathBuf,
pub block_height: u64,
}
#[derive(Parser, Debug, Clone)]
pub struct ConvertToRawCmd {
pub input_chain_spec: PathBuf,
}
#[derive(Parser, Debug, Clone)]
pub struct ListPresetsCmd {
#[arg(long, short, alias = "runtime-wasm-path")]
pub runtime: PathBuf,
}
#[derive(Parser, Debug, Clone)]
pub struct DisplayPresetCmd {
#[arg(long, short, alias = "runtime-wasm-path")]
pub runtime: PathBuf,
#[arg(long, short)]
pub preset_name: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct VerifyCmd {
pub input_chain_spec: PathBuf,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct ParachainExtension {
pub relay_chain: String,
pub para_id: u32,
}
type ChainSpec = GenericChainSpec<()>;
impl ChainSpecBuilder {
pub fn run(self) -> Result<(), String> {
let chain_spec_path = self.chain_spec_path.to_path_buf();
match self.command {
ChainSpecBuilderCmd::Create(cmd) => {
let chain_spec_json = generate_chain_spec_for_runtime(&cmd)?;
fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
},
ChainSpecBuilderCmd::UpdateCode(UpdateCodeCmd {
ref input_chain_spec,
ref runtime,
}) => {
let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
update_code_in_json_chain_spec(
&mut chain_spec_json,
&fs::read(runtime.as_path())
.map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
);
let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
.map_err(|e| format!("to pretty failed: {e}"))?;
fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
},
ChainSpecBuilderCmd::AddCodeSubstitute(AddCodeSubstituteCmd {
ref input_chain_spec,
ref runtime,
block_height,
}) => {
let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
set_code_substitute_in_json_chain_spec(
&mut chain_spec_json,
&fs::read(runtime.as_path())
.map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
block_height,
);
let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
.map_err(|e| format!("to pretty failed: {e}"))?;
fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
},
ChainSpecBuilderCmd::ConvertToRaw(ConvertToRawCmd { ref input_chain_spec }) => {
let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
let mut genesis_json =
serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
.map_err(|e| format!("Conversion to json failed: {e}"))?;
genesis_json.as_object_mut().map(|map| {
map.retain(|key, _| key == "genesis");
});
let mut org_chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
org_chain_spec_json
.get_mut("genesis")
.and_then(|genesis| genesis.as_object_mut())
.and_then(|genesis| genesis.remove("runtimeGenesis"));
json_patch::merge(&mut org_chain_spec_json, genesis_json);
let chain_spec_json = serde_json::to_string_pretty(&org_chain_spec_json)
.map_err(|e| format!("Conversion to pretty failed: {e}"))?;
fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
},
ChainSpecBuilderCmd::Verify(VerifyCmd { ref input_chain_spec }) => {
let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
let _ = serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
.map_err(|e| format!("Conversion to json failed: {e}"))?;
},
ChainSpecBuilderCmd::ListPresets(ListPresetsCmd { runtime }) => {
let code = fs::read(runtime.as_path())
.map_err(|e| format!("wasm blob shall be readable {e}"))?;
let caller: GenesisConfigBuilderRuntimeCaller =
GenesisConfigBuilderRuntimeCaller::new(&code[..]);
let presets = caller
.preset_names()
.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
println!("{}", serde_json::json!({"presets":presets}).to_string());
},
ChainSpecBuilderCmd::DisplayPreset(DisplayPresetCmd { runtime, preset_name }) => {
let code = fs::read(runtime.as_path())
.map_err(|e| format!("wasm blob shall be readable {e}"))?;
let caller: GenesisConfigBuilderRuntimeCaller =
GenesisConfigBuilderRuntimeCaller::new(&code[..]);
let preset = caller
.get_named_preset(preset_name.as_ref())
.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
println!("{preset}");
},
}
Ok(())
}
pub fn set_create_cmd_runtime_code(&mut self, code: Cow<'static, [u8]>) {
match &mut self.command {
ChainSpecBuilderCmd::Create(cmd) => {
cmd.code = Some(code);
},
_ => {
panic!("Overwriting code blob is only supported for CreateCmd");
},
};
}
}
fn process_action<T: Serialize + Clone + Sync + 'static>(
cmd: &CreateCmd,
code: &[u8],
builder: sc_chain_spec::ChainSpecBuilder<T>,
) -> Result<String, String> {
let builder = match cmd.action {
GenesisBuildAction::NamedPreset(NamedPresetCmd { ref preset_name }) =>
builder.with_genesis_config_preset_name(&preset_name),
GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
let patch = fs::read(patch_path.as_path())
.map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
|e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
)?)
},
GenesisBuildAction::Full(FullCmd { ref config_path }) => {
let config = fs::read(config_path.as_path())
.map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
|e| format!("config file {config_path:?} shall contain a valid json: {e}"),
)?)
},
GenesisBuildAction::Default(DefaultCmd {}) => {
let caller: GenesisConfigBuilderRuntimeCaller =
GenesisConfigBuilderRuntimeCaller::new(&code);
let default_config = caller
.get_default_config()
.map_err(|e| format!("getting default config from runtime should work: {e}"))?;
builder.with_genesis_config(default_config)
},
};
let chain_spec = builder.build();
match (cmd.verify, cmd.raw_storage) {
(_, true) => chain_spec.as_json(true),
(true, false) => {
chain_spec.as_json(true)?;
println!("Genesis config verification: OK");
chain_spec.as_json(false)
},
(false, false) => chain_spec.as_json(false),
}
}
impl CreateCmd {
fn get_runtime_code(&self) -> Result<Cow<'static, [u8]>, String> {
Ok(if let Some(code) = self.code.clone() {
code
} else {
fs::read(self.runtime.as_path())
.map_err(|e| format!("wasm blob shall be readable {e}"))?
.into()
})
}
}
pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
let code = cmd.get_runtime_code()?;
let chain_type = &cmd.chain_type;
let mut properties = sc_chain_spec::Properties::new();
properties.insert("tokenSymbol".into(), "UNIT".into());
properties.insert("tokenDecimals".into(), 12.into());
let builder = ChainSpec::builder(&code[..], Default::default())
.with_name(&cmd.chain_name[..])
.with_id(&cmd.chain_id[..])
.with_properties(properties)
.with_chain_type(chain_type.clone());
let chain_spec_json_string = process_action(&cmd, &code[..], builder)?;
if let (Some(para_id), Some(ref relay_chain)) = (cmd.para_id, &cmd.relay_chain) {
let parachain_properties = serde_json::json!({
"relay_chain": relay_chain,
"para_id": para_id,
});
let mut chain_spec_json_blob = serde_json::from_str(chain_spec_json_string.as_str())
.map_err(|e| format!("deserialization a json failed {e}"))?;
json_patch::merge(&mut chain_spec_json_blob, parachain_properties);
Ok(serde_json::to_string_pretty(&chain_spec_json_blob)
.map_err(|e| format!("to pretty failed: {e}"))?)
} else {
Ok(chain_spec_json_string)
}
}
fn extract_chain_spec_json(input_chain_spec: &Path) -> Result<serde_json::Value, String> {
let chain_spec = &fs::read(input_chain_spec)
.map_err(|e| format!("Provided chain spec could not be read: {e}"))?;
serde_json::from_slice(&chain_spec).map_err(|e| format!("Conversion to json failed: {e}"))
}