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 GenesisBuildAction::Patch(PatchCmd { ref patch_path }) => {
349 let patch = fs::read(patch_path.as_path())
350 .map_err(|e| format!("patch file {patch_path:?} shall be readable: {e}"))?;
351 builder.with_genesis_config_patch(serde_json::from_slice::<Value>(&patch[..]).map_err(
352 |e| format!("patch file {patch_path:?} shall contain a valid json: {e}"),
353 )?)
354 },
355 GenesisBuildAction::Full(FullCmd { ref config_path }) => {
356 let config = fs::read(config_path.as_path())
357 .map_err(|e| format!("config file {config_path:?} shall be readable: {e}"))?;
358 builder.with_genesis_config(serde_json::from_slice::<Value>(&config[..]).map_err(
359 |e| format!("config file {config_path:?} shall contain a valid json: {e}"),
360 )?)
361 },
362 GenesisBuildAction::Default(DefaultCmd {}) => {
363 let caller: GenesisConfigBuilderRuntimeCaller =
364 GenesisConfigBuilderRuntimeCaller::new(&code);
365 let default_config = caller
366 .get_default_config()
367 .map_err(|e| format!("getting default config from runtime should work: {e}"))?;
368 builder.with_genesis_config(default_config)
369 },
370 };
371
372 let chain_spec = builder.build();
373
374 match (cmd.verify, cmd.raw_storage) {
375 (_, true) => chain_spec.as_json(true),
376 (true, false) => {
377 chain_spec.as_json(true)?;
378 println!("Genesis config verification: OK");
379 chain_spec.as_json(false)
380 },
381 (false, false) => chain_spec.as_json(false),
382 }
383}
384
385impl CreateCmd {
386 fn get_runtime_code(&self) -> Result<Cow<'static, [u8]>, String> {
390 Ok(if let Some(code) = self.code.clone() {
391 code
392 } else {
393 fs::read(self.runtime.as_path())
394 .map_err(|e| format!("wasm blob shall be readable {e}"))?
395 .into()
396 })
397 }
398}
399
400fn parse_properties(raw: &String, props: &mut sc_chain_spec::Properties) -> Result<(), String> {
402 for pair in raw.split(',') {
403 let mut iter = pair.splitn(2, '=');
404 let key = iter
405 .next()
406 .ok_or_else(|| format!("Invalid chain property key: {pair}"))?
407 .trim()
408 .to_owned();
409 let value_str = iter
410 .next()
411 .ok_or_else(|| format!("Invalid chain property value for key: {key}"))?
412 .trim();
413
414 let value = match value_str.parse::<bool>() {
416 Ok(b) => Value::Bool(b),
417 Err(_) => match value_str.parse::<u32>() {
418 Ok(i) => Value::Number(i.into()),
419 Err(_) => Value::String(value_str.to_string()),
420 },
421 };
422
423 props.insert(key, value);
424 }
425 Ok(())
426}
427
428pub fn generate_chain_spec_for_runtime(cmd: &CreateCmd) -> Result<String, String> {
430 let code = cmd.get_runtime_code()?;
431
432 let chain_type = &cmd.chain_type;
433
434 let mut properties = sc_chain_spec::Properties::new();
435 for raw in &cmd.properties {
436 parse_properties(raw, &mut properties)?;
437 }
438
439 let builder = ChainSpec::builder(&code[..], Default::default())
440 .with_name(&cmd.chain_name[..])
441 .with_id(&cmd.chain_id[..])
442 .with_properties(properties)
443 .with_chain_type(chain_type.clone());
444
445 let chain_spec_json_string = process_action(&cmd, &code[..], builder)?;
446 let parachain_properties = cmd.relay_chain.as_ref().map(|rc| {
447 cmd.para_id
448 .map(|para_id| {
449 serde_json::json!({
450 "relay_chain": rc,
451 "para_id": para_id,
452 })
453 })
454 .unwrap_or(serde_json::json!({
455 "relay_chain": rc,
456 }))
457 });
458
459 let chain_spec = parachain_properties
460 .map(|props| {
461 let chain_spec_json_blob = serde_json::from_str(chain_spec_json_string.as_str())
462 .map_err(|e| format!("deserialization a json failed {e}"));
463 chain_spec_json_blob.and_then(|mut cs| {
464 json_patch::merge(&mut cs, props);
465 serde_json::to_string_pretty(&cs).map_err(|e| format!("to pretty failed: {e}"))
466 })
467 })
468 .unwrap_or(Ok(chain_spec_json_string));
469 chain_spec
470}
471
472fn extract_chain_spec_json(input_chain_spec: &Path) -> Result<serde_json::Value, String> {
474 let chain_spec = &fs::read(input_chain_spec)
475 .map_err(|e| format!("Provided chain spec could not be read: {e}"))?;
476
477 serde_json::from_slice(&chain_spec).map_err(|e| format!("Conversion to json failed: {e}"))
478}