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, MultiaddrWithPeerId,
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 #[command(subcommand, alias = "bootnode")]
58 Bootnodes(BootnodesCmd),
59}
60
61#[derive(Debug, Subcommand)]
65pub enum BootnodesCmd {
66 Add(AddBootnodesCmd),
67 Remove(RemoveBootnodesCmd),
68 List(ListBootnodesCmd),
69}
70
71#[derive(Parser, Debug)]
73pub struct CreateCmd {
74 #[arg(long, short = 'n', default_value = "Custom")]
76 chain_name: String,
77 #[arg(long, short = 'i', default_value = "custom")]
79 chain_id: String,
80 #[arg(value_enum, short = 't', default_value = "live")]
82 chain_type: ChainType,
83 #[arg(long, value_enum, short = 'p', requires = "relay_chain")]
85 pub para_id: Option<u32>,
86 #[arg(long, value_enum, short = 'c')]
88 pub relay_chain: Option<String>,
89 #[arg(long, short, alias = "runtime-wasm-path")]
91 runtime: PathBuf,
92 #[arg(long, short = 's')]
94 raw_storage: bool,
95 #[arg(long, short = 'v')]
98 verify: bool,
99 #[arg(long, default_value = "tokenSymbol=UNIT,tokenDecimals=12")]
110 pub properties: Vec<String>,
111 #[arg(long, short = 'b', value_delimiter = ',')]
118 pub bootnodes: Vec<MultiaddrWithPeerId>,
119 #[command(subcommand)]
120 action: GenesisBuildAction,
121
122 #[clap(skip)]
124 code: Option<Cow<'static, [u8]>>,
125}
126
127#[derive(Subcommand, Debug, Clone)]
128enum GenesisBuildAction {
129 Patch(PatchCmd),
130 Full(FullCmd),
131 Default(DefaultCmd),
132 NamedPreset(NamedPresetCmd),
133}
134
135#[derive(Parser, Debug, Clone)]
137struct PatchCmd {
138 patch_path: PathBuf,
140}
141
142#[derive(Parser, Debug, Clone)]
144struct FullCmd {
145 config_path: PathBuf,
147}
148
149#[derive(Parser, Debug, Clone)]
153struct DefaultCmd {}
154
155#[derive(Parser, Debug, Clone)]
157struct NamedPresetCmd {
158 preset_name: String,
159}
160
161#[derive(Parser, Debug, Clone)]
169pub struct UpdateCodeCmd {
170 pub input_chain_spec: PathBuf,
174 #[arg(alias = "runtime-wasm-path")]
176 pub runtime: PathBuf,
177}
178
179#[derive(Parser, Debug, Clone)]
189pub struct AddCodeSubstituteCmd {
190 pub input_chain_spec: PathBuf,
192 #[arg(alias = "runtime-wasm-path")]
194 pub runtime: PathBuf,
195 pub block_height: u64,
197}
198
199#[derive(Parser, Debug, Clone)]
207pub struct AddBootnodesCmd {
208 pub input_chain_spec: PathBuf,
212 #[arg(required = true, num_args = 1..)]
217 pub bootnodes: Vec<MultiaddrWithPeerId>,
218}
219
220#[derive(Parser, Debug, Clone)]
228pub struct RemoveBootnodesCmd {
229 pub input_chain_spec: PathBuf,
233 #[arg(required_unless_present = "all", conflicts_with = "all", num_args = 1..)]
238 pub bootnodes: Vec<MultiaddrWithPeerId>,
239 #[arg(long)]
241 pub all: bool,
242}
243
244#[derive(Parser, Debug, Clone)]
246pub struct ListBootnodesCmd {
247 pub input_chain_spec: PathBuf,
249}
250
251#[derive(Parser, Debug, Clone)]
253pub struct ConvertToRawCmd {
254 pub input_chain_spec: PathBuf,
256}
257
258#[derive(Parser, Debug, Clone)]
260pub struct ListPresetsCmd {
261 #[arg(long, short, alias = "runtime-wasm-path")]
263 pub runtime: PathBuf,
264}
265
266#[derive(Parser, Debug, Clone)]
268pub struct DisplayPresetCmd {
269 #[arg(long, short, alias = "runtime-wasm-path")]
271 pub runtime: PathBuf,
272 #[arg(long, short)]
274 pub preset_name: Option<String>,
275}
276
277#[derive(Parser, Debug, Clone)]
283pub struct VerifyCmd {
284 pub input_chain_spec: PathBuf,
286}
287
288#[derive(Deserialize, Serialize, Clone)]
289pub struct ParachainExtension {
290 pub relay_chain: String,
292 pub para_id: Option<u32>,
294}
295
296type ChainSpec = GenericChainSpec<()>;
297
298impl ChainSpecBuilder {
299 pub fn run(&self) -> Result<(), String> {
301 let chain_spec_path = self.chain_spec_path.to_path_buf();
302
303 match &self.command {
304 ChainSpecBuilderCmd::Create(cmd) => {
305 let chain_spec_json = generate_chain_spec_for_runtime(&cmd)?;
306 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
307 },
308 ChainSpecBuilderCmd::UpdateCode(UpdateCodeCmd {
309 ref input_chain_spec,
310 ref runtime,
311 }) => {
312 let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
313
314 update_code_in_json_chain_spec(
315 &mut chain_spec_json,
316 &fs::read(runtime.as_path())
317 .map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
318 );
319
320 let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
321 .map_err(|e| format!("to pretty failed: {e}"))?;
322 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
323 },
324 ChainSpecBuilderCmd::AddCodeSubstitute(AddCodeSubstituteCmd {
325 ref input_chain_spec,
326 ref runtime,
327 block_height,
328 }) => {
329 let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
330
331 set_code_substitute_in_json_chain_spec(
332 &mut chain_spec_json,
333 &fs::read(runtime.as_path())
334 .map_err(|e| format!("Wasm blob file could not be read: {e}"))?[..],
335 *block_height,
336 );
337 let chain_spec_json = serde_json::to_string_pretty(&chain_spec_json)
338 .map_err(|e| format!("to pretty failed: {e}"))?;
339 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
340 },
341 ChainSpecBuilderCmd::Bootnodes(BootnodesCmd::Add(AddBootnodesCmd {
342 ref input_chain_spec,
343 ref bootnodes,
344 })) => {
345 let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
346 let mut updated_bootnodes = extract_bootnodes(&chain_spec_json)?;
347 for bootnode in bootnodes {
348 if !updated_bootnodes.contains(bootnode) {
349 updated_bootnodes.push(bootnode.clone());
350 }
351 }
352
353 set_bootnodes(&mut chain_spec_json, updated_bootnodes)?;
354 write_chain_spec_json(&chain_spec_json, chain_spec_path.as_path())?;
355 },
356 ChainSpecBuilderCmd::Bootnodes(BootnodesCmd::Remove(RemoveBootnodesCmd {
357 ref input_chain_spec,
358 ref bootnodes,
359 all,
360 })) => {
361 let mut chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
362 let remaining_bootnodes = if *all {
365 Vec::new()
366 } else {
367 extract_bootnodes(&chain_spec_json)?
368 .into_iter()
369 .filter(|bootnode| !bootnodes.contains(bootnode))
370 .collect()
371 };
372
373 set_bootnodes(&mut chain_spec_json, remaining_bootnodes)?;
374 write_chain_spec_json(&chain_spec_json, chain_spec_path.as_path())?;
375 },
376 ChainSpecBuilderCmd::Bootnodes(BootnodesCmd::List(ListBootnodesCmd {
377 ref input_chain_spec,
378 })) => {
379 let chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
380 let bootnodes = extract_bootnodes(&chain_spec_json)?;
381 println!("{}", serde_json::json!({ "bootNodes": bootnodes }).to_string());
382 },
383 ChainSpecBuilderCmd::ConvertToRaw(ConvertToRawCmd { ref input_chain_spec }) => {
384 let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
385
386 let mut genesis_json =
387 serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
388 .map_err(|e| format!("Conversion to json failed: {e}"))?;
389
390 genesis_json.as_object_mut().map(|map| {
393 map.retain(|key, _| key == "genesis");
394 });
395
396 let mut org_chain_spec_json = extract_chain_spec_json(input_chain_spec.as_path())?;
397
398 org_chain_spec_json
401 .get_mut("genesis")
402 .and_then(|genesis| genesis.as_object_mut())
403 .and_then(|genesis| genesis.remove("runtimeGenesis"));
404 json_patch::merge(&mut org_chain_spec_json, genesis_json);
405
406 let chain_spec_json = serde_json::to_string_pretty(&org_chain_spec_json)
407 .map_err(|e| format!("Conversion to pretty failed: {e}"))?;
408 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())?;
409 },
410 ChainSpecBuilderCmd::Verify(VerifyCmd { ref input_chain_spec }) => {
411 let chain_spec = ChainSpec::from_json_file(input_chain_spec.clone())?;
412 serde_json::from_str::<serde_json::Value>(&chain_spec.as_json(true)?)
413 .map_err(|e| format!("Conversion to json failed: {e}"))?;
414 },
415 ChainSpecBuilderCmd::ListPresets(ListPresetsCmd { runtime }) => {
416 let code = fs::read(runtime.as_path())
417 .map_err(|e| format!("wasm blob shall be readable {e}"))?;
418 let caller: GenesisConfigBuilderRuntimeCaller =
419 GenesisConfigBuilderRuntimeCaller::new(&code[..]);
420 let presets = caller
421 .preset_names()
422 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
423 println!("{}", serde_json::json!({"presets":presets}).to_string());
424 },
425 ChainSpecBuilderCmd::DisplayPreset(DisplayPresetCmd { runtime, preset_name }) => {
426 let code = fs::read(runtime.as_path())
427 .map_err(|e| format!("wasm blob shall be readable {e}"))?;
428 let caller: GenesisConfigBuilderRuntimeCaller =
429 GenesisConfigBuilderRuntimeCaller::new(&code[..]);
430 let preset = caller
431 .get_named_preset(preset_name.as_ref())
432 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
433 println!("{preset}");
434 },
435 }
436 Ok(())
437 }
438
439 pub fn set_create_cmd_runtime_code(&mut self, code: Cow<'static, [u8]>) {
444 match &mut self.command {
445 ChainSpecBuilderCmd::Create(cmd) => {
446 cmd.code = Some(code);
447 },
448 _ => {
449 panic!("Overwriting code blob is only supported for CreateCmd");
450 },
451 };
452 }
453}
454
455fn process_action<T: Serialize + Clone + Sync + 'static>(
456 cmd: &CreateCmd,
457 code: &[u8],
458 builder: sc_chain_spec::ChainSpecBuilder<T>,
459) -> Result<String, String> {
460 let builder = match cmd.action {
461 GenesisBuildAction::NamedPreset(NamedPresetCmd { ref preset_name }) => {
462 builder.with_genesis_config_preset_name(&preset_name)
463 },
464 GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
465 let patch = fs::read(patch_path.as_path())
466 .map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
467 builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
468 |e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
469 )?)
470 },
471 GenesisBuildAction::Full(FullCmd { ref config_path }) => {
472 let config = fs::read(config_path.as_path())
473 .map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
474 builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
475 |e| format!("config file {config_path:?} shall contain a valid json: {e}"),
476 )?)
477 },
478 GenesisBuildAction::Default(DefaultCmd {}) => {
479 let caller: GenesisConfigBuilderRuntimeCaller =
480 GenesisConfigBuilderRuntimeCaller::new(&code);
481 let default_config = caller
482 .get_default_config()
483 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
484 builder.with_genesis_config(default_config)
485 },
486 };
487
488 let chain_spec = builder.build();
489
490 match (cmd.verify, cmd.raw_storage) {
491 (_, true) => chain_spec.as_json(true),
492 (true, false) => {
493 chain_spec.as_json(true)?;
494 println!("Genesis config verification: OK");
495 chain_spec.as_json(false)
496 },
497 (false, false) => chain_spec.as_json(false),
498 }
499}
500
501impl CreateCmd {
502 fn get_runtime_code(&self) -> Result<Cow<'static, [u8]>, String> {
506 Ok(if let Some(code) = self.code.clone() {
507 code
508 } else {
509 fs::read(self.runtime.as_path())
510 .map_err(|e| format!("wasm blob shall be readable {e}"))?
511 .into()
512 })
513 }
514}
515
516fn parse_properties(raw: &String, props: &mut sc_chain_spec::Properties) -> Result<(), String> {
518 for pair in raw.split(',') {
519 let mut iter = pair.splitn(2, '=');
520 let key = iter
521 .next()
522 .ok_or_else(|| format!("Invalid chain property key: {pair}"))?
523 .trim()
524 .to_owned();
525 let value_str = iter
526 .next()
527 .ok_or_else(|| format!("Invalid chain property value for key: {key}"))?
528 .trim();
529
530 let value = match value_str.parse::<bool>() {
532 Ok(b) => Value::Bool(b),
533 Err(_) => match value_str.parse::<u32>() {
534 Ok(i) => Value::Number(i.into()),
535 Err(_) => Value::String(value_str.to_string()),
536 },
537 };
538
539 props.insert(key, value);
540 }
541 Ok(())
542}
543
544pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
546 let code = cmd.get_runtime_code()?;
547
548 let chain_type = &cmd.chain_type;
549
550 let mut properties = sc_chain_spec::Properties::new();
551 for raw in &cmd.properties {
552 parse_properties(raw, &mut properties)?;
553 }
554
555 let builder = ChainSpec::builder(&code[..], Default::default())
556 .with_name(&cmd.chain_name[..])
557 .with_id(&cmd.chain_id[..])
558 .with_properties(properties)
559 .with_chain_type(chain_type.clone())
560 .with_boot_nodes(cmd.bootnodes.clone());
561
562 let chain_spec_json_string = process_action(&cmd, &code[..], builder)?;
563 let parachain_properties = cmd.relay_chain.as_ref().map(|rc| {
564 cmd.para_id
565 .map(|para_id| {
566 serde_json::json!({
567 "relay_chain": rc,
568 "para_id": para_id,
569 })
570 })
571 .unwrap_or(serde_json::json!({
572 "relay_chain": rc,
573 }))
574 });
575
576 let chain_spec = parachain_properties
577 .map(|props| {
578 let chain_spec_json_blob = serde_json::from_str(chain_spec_json_string.as_str())
579 .map_err(|e| format!("deserialization a json failed {e}"));
580 chain_spec_json_blob.and_then(|mut cs| {
581 json_patch::merge(&mut cs, props);
582 serde_json::to_string_pretty(&cs).map_err(|e| format!("to pretty failed: {e}"))
583 })
584 })
585 .unwrap_or(Ok(chain_spec_json_string));
586 chain_spec
587}
588
589const BOOT_NODES_KEY: &str = "bootNodes";
591
592fn extract_bootnodes(chain_spec_json: &Value) -> Result<Vec<MultiaddrWithPeerId>, String> {
596 let Some(bootnodes) = chain_spec_json.get(BOOT_NODES_KEY) else { return Ok(Vec::new()) };
597
598 serde_json::from_value(bootnodes.clone())
599 .map_err(|e| format!("`{BOOT_NODES_KEY}` field of the chain spec is invalid: {e}"))
600}
601
602fn set_bootnodes(
604 chain_spec_json: &mut Value,
605 bootnodes: Vec<MultiaddrWithPeerId>,
606) -> Result<(), String> {
607 let bootnodes = serde_json::to_value(bootnodes)
608 .map_err(|e| format!("Conversion of the boot nodes to json failed: {e}"))?;
609
610 chain_spec_json
611 .as_object_mut()
612 .ok_or_else(|| "Provided chain spec is not a json object".to_string())?
613 .insert(BOOT_NODES_KEY.to_string(), bootnodes);
614 Ok(())
615}
616
617fn write_chain_spec_json(chain_spec_json: &Value, chain_spec_path: &Path) -> Result<(), String> {
619 let chain_spec_json = serde_json::to_string_pretty(chain_spec_json)
620 .map_err(|e| format!("to pretty failed: {e}"))?;
621 fs::write(chain_spec_path, chain_spec_json).map_err(|err| err.to_string())
622}
623
624fn extract_chain_spec_json(input_chain_spec: &Path) -> Result<serde_json::Value, String> {
626 let chain_spec = &fs::read(input_chain_spec)
627 .map_err(|e| format!("Provided chain spec could not be read: {e}"))?;
628
629 serde_json::from_slice(&chain_spec).map_err(|e| format!("Conversion to json failed: {e}"))
630}