1#![doc = include_str!("../README.md")]
19#[cfg(feature = "generate-readme")]
20docify::compile_markdown!("README.docify.md", "README.md");
21
22use clap::{Parser, Subcommand};
23use sc_chain_spec::{
24 json_patch, set_code_substitute_in_json_chain_spec, update_code_in_json_chain_spec, ChainType,
25 GenericChainSpec, GenesisConfigBuilderRuntimeCaller,
26};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::{
30 borrow::Cow,
31 fs,
32 path::{Path, PathBuf},
33};
34
35#[derive(Debug, Parser)]
37#[command(rename_all = "kebab-case", version, about)]
38pub struct ChainSpecBuilder {
39 #[command(subcommand)]
40 pub command: ChainSpecBuilderCmd,
41 #[arg(long, short, default_value = "./chain_spec.json")]
43 pub chain_spec_path: PathBuf,
44}
45
46#[derive(Debug, Subcommand)]
47#[command(rename_all = "kebab-case")]
48pub enum ChainSpecBuilderCmd {
49 Create(CreateCmd),
50 Verify(VerifyCmd),
51 UpdateCode(UpdateCodeCmd),
52 ConvertToRaw(ConvertToRawCmd),
53 ListPresets(ListPresetsCmd),
54 DisplayPreset(DisplayPresetCmd),
55 AddCodeSubstitute(AddCodeSubstituteCmd),
56}
57
58#[derive(Parser, Debug)]
60pub struct CreateCmd {
61 #[arg(long, short = 'n', default_value = "Custom")]
63 chain_name: String,
64 #[arg(long, short = 'i', default_value = "custom")]
66 chain_id: String,
67 #[arg(value_enum, short = 't', default_value = "live")]
69 chain_type: ChainType,
70 #[arg(long, value_enum, short = 'p', requires = "relay_chain")]
72 pub para_id: Option<u32>,
73 #[arg(long, value_enum, short = 'c')]
75 pub relay_chain: Option<String>,
76 #[arg(long, short, alias = "runtime-wasm-path")]
78 runtime: PathBuf,
79 #[arg(long, short = 's')]
81 raw_storage: bool,
82 #[arg(long, short = 'v')]
85 verify: bool,
86 #[arg(long, default_value = "tokenSymbol=UNIT,tokenDecimals=12")]
97 pub properties: Vec<String>,
98 #[command(subcommand)]
99 action: GenesisBuildAction,
100
101 #[clap(skip)]
103 code: Option<Cow<'static, [u8]>>,
104}
105
106#[derive(Subcommand, Debug, Clone)]
107enum GenesisBuildAction {
108 Patch(PatchCmd),
109 Full(FullCmd),
110 Default(DefaultCmd),
111 NamedPreset(NamedPresetCmd),
112}
113
114#[derive(Parser, Debug, Clone)]
116struct PatchCmd {
117 patch_path: PathBuf,
119}
120
121#[derive(Parser, Debug, Clone)]
123struct FullCmd {
124 config_path: PathBuf,
126}
127
128#[derive(Parser, Debug, Clone)]
132struct DefaultCmd {}
133
134#[derive(Parser, Debug, Clone)]
136struct NamedPresetCmd {
137 preset_name: String,
138}
139
140#[derive(Parser, Debug, Clone)]
148pub struct UpdateCodeCmd {
149 pub input_chain_spec: PathBuf,
153 #[arg(alias = "runtime-wasm-path")]
155 pub runtime: PathBuf,
156}
157
158#[derive(Parser, Debug, Clone)]
168pub struct AddCodeSubstituteCmd {
169 pub input_chain_spec: PathBuf,
171 #[arg(alias = "runtime-wasm-path")]
173 pub runtime: PathBuf,
174 pub block_height: u64,
176}
177
178#[derive(Parser, Debug, Clone)]
180pub struct ConvertToRawCmd {
181 pub input_chain_spec: PathBuf,
183}
184
185#[derive(Parser, Debug, Clone)]
187pub struct ListPresetsCmd {
188 #[arg(long, short, alias = "runtime-wasm-path")]
190 pub runtime: PathBuf,
191}
192
193#[derive(Parser, Debug, Clone)]
195pub struct DisplayPresetCmd {
196 #[arg(long, short, alias = "runtime-wasm-path")]
198 pub runtime: PathBuf,
199 #[arg(long, short)]
201 pub preset_name: Option<String>,
202}
203
204#[derive(Parser, Debug, Clone)]
210pub struct VerifyCmd {
211 pub input_chain_spec: PathBuf,
213}
214
215#[derive(Deserialize, Serialize, Clone)]
216pub struct ParachainExtension {
217 pub relay_chain: String,
219 pub para_id: Option<u32>,
221}
222
223type ChainSpec = GenericChainSpec<()>;
224
225impl ChainSpecBuilder {
226 pub fn run(&self) -> Result<(), String> {
228 let chain_spec_path = self.chain_spec_path.to_path_buf();
229
230 match &self.command {
231 ChainSpecBuilderCmd::Create(cmd) => {
232 let chain_spec_json = generate_chain_spec_for_runtime(&cmd)?;
233 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
234 },
235 ChainSpecBuilderCmd::UpdateCode(UpdateCodeCmd {
236 ref input_chain_spec,
237 ref runtime,
238 }) => {
239 let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
240
241 update_code_in_json_chain_spec(
242 &mut chain_spec_json,
243 &fs::read(runtime.as_path())
244 .map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
245 );
246
247 let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
248 .map_err(|e| format!("to pretty failed: {e}"))?;
249 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
250 },
251 ChainSpecBuilderCmd::AddCodeSubstitute(AddCodeSubstituteCmd {
252 ref input_chain_spec,
253 ref runtime,
254 block_height,
255 }) => {
256 let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
257
258 set_code_substitute_in_json_chain_spec(
259 &mut chain_spec_json,
260 &fs::read(runtime.as_path())
261 .map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
262 *block_height,
263 );
264 let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
265 .map_err(|e| format!("to pretty failed: {e}"))?;
266 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
267 },
268 ChainSpecBuilderCmd::ConvertToRaw(ConvertToRawCmd { ref input_chain_spec }) => {
269 let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
270
271 let mut genesis_json =
272 serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
273 .map_err(|e| format!("Conversion to json failed: {e}"))?;
274
275 genesis_json.as_object_mut().map(|map| {
278 map.retain(|key, _| key == "genesis");
279 });
280
281 let mut org_chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
282
283 org_chain_spec_json
286 .get_mut("genesis")
287 .and_then(|genesis| genesis.as_object_mut())
288 .and_then(|genesis| genesis.remove("runtimeGenesis"));
289 json_patch::merge(&mut org_chain_spec_json, genesis_json);
290
291 let chain_spec_json = serde_json::to_string_pretty(&org_chain_spec_json)
292 .map_err(|e| format!("Conversion to pretty failed: {e}"))?;
293 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
294 },
295 ChainSpecBuilderCmd::Verify(VerifyCmd { ref input_chain_spec }) => {
296 let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
297 serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
298 .map_err(|e| format!("Conversion to json failed: {e}"))?;
299 },
300 ChainSpecBuilderCmd::ListPresets(ListPresetsCmd { runtime }) => {
301 let code = fs::read(runtime.as_path())
302 .map_err(|e| format!("wasm blob shall be readable {e}"))?;
303 let caller: GenesisConfigBuilderRuntimeCaller =
304 GenesisConfigBuilderRuntimeCaller::new(&code[..]);
305 let presets = caller
306 .preset_names()
307 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
308 println!("{}", serde_json::json!({"presets":presets}).to_string());
309 },
310 ChainSpecBuilderCmd::DisplayPreset(DisplayPresetCmd { runtime, preset_name }) => {
311 let code = fs::read(runtime.as_path())
312 .map_err(|e| format!("wasm blob shall be readable {e}"))?;
313 let caller: GenesisConfigBuilderRuntimeCaller =
314 GenesisConfigBuilderRuntimeCaller::new(&code[..]);
315 let preset = caller
316 .get_named_preset(preset_name.as_ref())
317 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
318 println!("{preset}");
319 },
320 }
321 Ok(())
322 }
323
324 pub fn set_create_cmd_runtime_code(&mut self, code: Cow<'static, [u8]>) {
329 match &mut self.command {
330 ChainSpecBuilderCmd::Create(cmd) => {
331 cmd.code = Some(code);
332 },
333 _ => {
334 panic!("Overwriting code blob is only supported for CreateCmd");
335 },
336 };
337 }
338}
339
340fn process_action<T: Serialize + Clone + Sync + 'static>(
341 cmd: &CreateCmd,
342 code: &[u8],
343 builder: sc_chain_spec::ChainSpecBuilder<T>,
344) -> Result<String, String> {
345 let builder = match cmd.action {
346 GenesisBuildAction::NamedPreset(NamedPresetCmd { ref preset_name }) => {
347 builder.with_genesis_config_preset_name(&preset_name)
348 },
349 GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
350 let patch = fs::read(patch_path.as_path())
351 .map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
352 builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
353 |e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
354 )?)
355 },
356 GenesisBuildAction::Full(FullCmd { ref config_path }) => {
357 let config = fs::read(config_path.as_path())
358 .map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
359 builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
360 |e| format!("config file {config_path:?} shall contain a valid json: {e}"),
361 )?)
362 },
363 GenesisBuildAction::Default(DefaultCmd {}) => {
364 let caller: GenesisConfigBuilderRuntimeCaller =
365 GenesisConfigBuilderRuntimeCaller::new(&code);
366 let default_config = caller
367 .get_default_config()
368 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
369 builder.with_genesis_config(default_config)
370 },
371 };
372
373 let chain_spec = builder.build();
374
375 match (cmd.verify, cmd.raw_storage) {
376 (_, true) => chain_spec.as_json(true),
377 (true, false) => {
378 chain_spec.as_json(true)?;
379 println!("Genesis config verification: OK");
380 chain_spec.as_json(false)
381 },
382 (false, false) => chain_spec.as_json(false),
383 }
384}
385
386impl CreateCmd {
387 fn get_runtime_code(&self) -> Result<Cow<'static, [u8]>, String> {
391 Ok(if let Some(code) = self.code.clone() {
392 code
393 } else {
394 fs::read(self.runtime.as_path())
395 .map_err(|e| format!("wasm blob shall be readable {e}"))?
396 .into()
397 })
398 }
399}
400
401fn parse_properties(raw: &String, props: &mut sc_chain_spec::Properties) -> Result<(), String> {
403 for pair in raw.split(',') {
404 let mut iter = pair.splitn(2, '=');
405 let key = iter
406 .next()
407 .ok_or_else(|| format!("Invalid chain property key: {pair}"))?
408 .trim()
409 .to_owned();
410 let value_str = iter
411 .next()
412 .ok_or_else(|| format!("Invalid chain property value for key: {key}"))?
413 .trim();
414
415 let value = match value_str.parse::<bool>() {
417 Ok(b) => Value::Bool(b),
418 Err(_) => match value_str.parse::<u32>() {
419 Ok(i) => Value::Number(i.into()),
420 Err(_) => Value::String(value_str.to_string()),
421 },
422 };
423
424 props.insert(key, value);
425 }
426 Ok(())
427}
428
429pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
431 let code = cmd.get_runtime_code()?;
432
433 let chain_type = &cmd.chain_type;
434
435 let mut properties = sc_chain_spec::Properties::new();
436 for raw in &cmd.properties {
437 parse_properties(raw, &mut properties)?;
438 }
439
440 let builder = ChainSpec::builder(&code[..], Default::default())
441 .with_name(&cmd.chain_name[..])
442 .with_id(&cmd.chain_id[..])
443 .with_properties(properties)
444 .with_chain_type(chain_type.clone());
445
446 let chain_spec_json_string = process_action(&cmd, &code[..], builder)?;
447 let parachain_properties = cmd.relay_chain.as_ref().map(|rc| {
448 cmd.para_id
449 .map(|para_id| {
450 serde_json::json!({
451 "relay_chain": rc,
452 "para_id": para_id,
453 })
454 })
455 .unwrap_or(serde_json::json!({
456 "relay_chain": rc,
457 }))
458 });
459
460 let chain_spec = parachain_properties
461 .map(|props| {
462 let chain_spec_json_blob = serde_json::from_str(chain_spec_json_string.as_str())
463 .map_err(|e| format!("deserialization a json failed {e}"));
464 chain_spec_json_blob.and_then(|mut cs| {
465 json_patch::merge(&mut cs, props);
466 serde_json::to_string_pretty(&cs).map_err(|e| format!("to pretty failed: {e}"))
467 })
468 })
469 .unwrap_or(Ok(chain_spec_json_string));
470 chain_spec
471}
472
473fn extract_chain_spec_json(input_chain_spec: &Path) -> Result<serde_json::Value, String> {
475 let chain_spec = &fs::read(input_chain_spec)
476 .map_err(|e| format!("Provided chain spec could not be read: {e}"))?;
477
478 serde_json::from_slice(&chain_spec).map_err(|e| format!("Conversion to json failed: {e}"))
479}