feat: add chain-spec-tool and usdt-bridge utilities

chain-spec-tool:
- CLI utility for chain spec manipulation and validation
- Used for mainnet chain spec generation and verification

usdt-bridge:
- Custodial bridge for wUSDT token operations
- Handles USDT wrapping/unwrapping between external chains and Asset Hub
- Configuration in bridge_config.json
This commit is contained in:
2026-01-25 19:43:33 +03:00
parent 3b8b709fb1
commit 23d52f2f98
8 changed files with 13768 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "chain-spec-tool"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"serde",
"serde_json",
]
[[package]]
name = "clap"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "chain-spec-tool"
version = "0.1.0"
edition = "2021"
description = "CLI tool for adding teyrchains to relay chain spec"
license = "GPL-3.0-only"
[workspace]
[[bin]]
name = "chain-spec-tool"
path = "src/main.rs"
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision"] }
+327
View File
@@ -0,0 +1,327 @@
//! Chain Spec Tool - Add teyrchains to relay chain spec
//!
//! This tool modifies plain chain specs to include teyrchain genesis data.
//! It's a standalone extraction of the zombienet-sdk logic for manual use.
use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use serde_json::{json, Value};
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Parser)]
#[command(name = "chain-spec-tool")]
#[command(about = "CLI tool for modifying Pezkuwi chain specs", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Add a teyrchain to a relay chain spec
AddTeyrchain {
/// Path to the plain chain spec JSON file
#[arg(long, short = 'c')]
chain_spec: PathBuf,
/// Path to the teyrchain genesis state hex file
#[arg(long, short = 's')]
state: PathBuf,
/// Path to the teyrchain validation code (wasm) hex file
#[arg(long, short = 'w')]
wasm: PathBuf,
/// Teyrchain ID (e.g., 1000 for Asset Hub)
#[arg(long, short = 'i')]
id: u32,
/// Output path for the modified chain spec
#[arg(long, short = 'o')]
output: PathBuf,
/// Register as teyrchain (true) or parathread (false)
#[arg(long, default_value = "true")]
as_teyrchain: bool,
},
/// Show info about a chain spec
Info {
/// Path to the chain spec JSON file
#[arg(long, short = 'c')]
chain_spec: PathBuf,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::AddTeyrchain {
chain_spec,
state,
wasm,
id,
output,
as_teyrchain,
} => {
add_teyrchain_to_spec(&chain_spec, &state, &wasm, id, &output, as_teyrchain)?;
}
Commands::Info { chain_spec } => {
show_chain_spec_info(&chain_spec)?;
}
}
Ok(())
}
/// Recursively fix scientific notation in JSON values
/// Converts floats like 2e+19 back to integers
fn fix_scientific_notation(value: &mut Value) {
match value {
Value::Number(n) => {
// If it's a float that can be represented as integer, convert it
if let Some(f) = n.as_f64() {
if f.fract() == 0.0 && f >= 0.0 && f <= u128::MAX as f64 {
// Convert to u128 string, then parse back to Number
let int_val = f as u128;
if let Ok(new_num) = serde_json::Number::from_str(&int_val.to_string()) {
*n = new_num;
}
}
}
}
Value::Array(arr) => {
for item in arr {
fix_scientific_notation(item);
}
}
Value::Object(map) => {
for (_, v) in map {
fix_scientific_notation(v);
}
}
_ => {}
}
}
/// Get the runtime config pointer from a chain spec JSON
fn get_runtime_config_pointer(chain_spec_json: &Value) -> Result<String> {
let pointers = [
"/genesis/runtimeGenesis/config",
"/genesis/runtimeGenesis/patch",
"/genesis/runtimeGenesisConfigPatch",
"/genesis/runtime/runtime_genesis_config",
"/genesis/runtime",
];
for pointer in pointers {
if chain_spec_json.pointer(pointer).is_some() {
return Ok(pointer.to_string());
}
}
Err(anyhow!("Cannot find the runtime config pointer in chain spec"))
}
/// Add a teyrchain to a relay chain spec
fn add_teyrchain_to_spec(
chain_spec_path: &PathBuf,
state_path: &PathBuf,
wasm_path: &PathBuf,
para_id: u32,
output_path: &PathBuf,
as_teyrchain: bool,
) -> Result<()> {
println!("Reading chain spec from: {}", chain_spec_path.display());
// Read chain spec
let content = fs::read_to_string(chain_spec_path)
.with_context(|| format!("Failed to read chain spec: {}", chain_spec_path.display()))?;
let mut chain_spec_json: Value = serde_json::from_str(&content)
.with_context(|| "Failed to parse chain spec as JSON")?;
// Check if it's raw format
if chain_spec_json.pointer("/genesis/raw/top").is_some() {
return Err(anyhow!(
"Chain spec is in RAW format. This tool only works with PLAIN format.\n\
Generate a plain chain spec first, then convert to raw after adding teyrchains."
));
}
// Get runtime config pointer
let runtime_ptr = get_runtime_config_pointer(&chain_spec_json)?;
println!("Found runtime config at: {}", runtime_ptr);
// Read genesis state and wasm
let genesis_head = fs::read_to_string(state_path)
.with_context(|| format!("Failed to read genesis state: {}", state_path.display()))?;
let validation_code = fs::read_to_string(wasm_path)
.with_context(|| format!("Failed to read validation code: {}", wasm_path.display()))?;
println!("Genesis state size: {} bytes", genesis_head.trim().len());
println!("Validation code size: {} bytes", validation_code.trim().len());
// Add teyrchain to genesis
add_parachain_to_genesis(
&runtime_ptr,
&mut chain_spec_json,
para_id,
genesis_head.trim(),
validation_code.trim(),
as_teyrchain,
)?;
println!("Added teyrchain {} to genesis", para_id);
// Fix scientific notation in JSON (Rust parser doesn't accept 2e+19 format)
// Convert all floats that are actually integers back to integers
fix_scientific_notation(&mut chain_spec_json);
// Write output
let output_content = serde_json::to_string_pretty(&chain_spec_json)
.with_context(|| "Failed to serialize chain spec")?;
fs::write(output_path, output_content)
.with_context(|| format!("Failed to write output: {}", output_path.display()))?;
println!("Wrote modified chain spec to: {}", output_path.display());
println!("\nNext step: Convert to raw format with:");
println!(" ./target/release/pezkuwi build-spec --chain {} --raw > <raw-output.json>", output_path.display());
Ok(())
}
/// Add a parachain to the genesis config (extracted from zombienet-sdk)
fn add_parachain_to_genesis(
runtime_config_ptr: &str,
chain_spec_json: &mut Value,
para_id: u32,
genesis_head: &str,
validation_code: &str,
as_teyrchain: bool,
) -> Result<()> {
let val = chain_spec_json
.pointer_mut(runtime_config_ptr)
.ok_or_else(|| anyhow!("Runtime config pointer not found: {}", runtime_config_ptr))?;
// Determine paras pointer
let paras_pointer = if val.get("paras").is_some() {
"/paras/paras"
} else if val.get("parachainsParas").is_some() {
// For retro-compatibility with substrate pre Polkadot 0.9.5
"/parachainsParas/paras"
} else {
// The config may not contain paras. Since chainspec allows RuntimeGenesisConfig patch we can inject it.
val["paras"] = json!({ "paras": [] });
"/paras/paras"
};
let paras = val
.pointer_mut(paras_pointer)
.ok_or_else(|| anyhow!("Paras pointer not found: {}", paras_pointer))?;
let paras_vec = paras
.as_array_mut()
.ok_or_else(|| anyhow!("Paras should be an array"))?;
// Check if para_id already exists
for existing in paras_vec.iter() {
if let Some(existing_id) = existing.get(0).and_then(|v| v.as_u64()) {
if existing_id == para_id as u64 {
return Err(anyhow!("Teyrchain {} already exists in genesis", para_id));
}
}
}
// Use object format for ParaGenesisArgs as expected by runtime
// Note: field is renamed to "teyrchain" via #[serde(rename = "teyrchain")] in runtime
// Boolean value: true = Teyrchain, false = Parathread
paras_vec.push(json!([
para_id,
{
"genesis_head": genesis_head,
"validation_code": validation_code,
"teyrchain": as_teyrchain
}
]));
Ok(())
}
/// Show information about a chain spec
fn show_chain_spec_info(chain_spec_path: &PathBuf) -> Result<()> {
let content = fs::read_to_string(chain_spec_path)
.with_context(|| format!("Failed to read chain spec: {}", chain_spec_path.display()))?;
let chain_spec_json: Value = serde_json::from_str(&content)
.with_context(|| "Failed to parse chain spec as JSON")?;
// Basic info
println!("Chain Spec Information");
println!("======================");
if let Some(name) = chain_spec_json.get("name").and_then(|v| v.as_str()) {
println!("Name: {}", name);
}
if let Some(id) = chain_spec_json.get("id").and_then(|v| v.as_str()) {
println!("ID: {}", id);
}
if let Some(chain_type) = chain_spec_json.get("chainType").and_then(|v| v.as_str()) {
println!("Chain Type: {}", chain_type);
}
// Check format
let is_raw = chain_spec_json.pointer("/genesis/raw/top").is_some();
println!("Format: {}", if is_raw { "RAW" } else { "PLAIN" });
// Check for paras
if !is_raw {
if let Ok(runtime_ptr) = get_runtime_config_pointer(&chain_spec_json) {
println!("Runtime Config: {}", runtime_ptr);
if let Some(val) = chain_spec_json.pointer(&runtime_ptr) {
let paras_pointer = if val.get("paras").is_some() {
"/paras/paras"
} else if val.get("parachainsParas").is_some() {
"/parachainsParas/paras"
} else {
""
};
if !paras_pointer.is_empty() {
let full_ptr = format!("{}{}", runtime_ptr, paras_pointer);
if let Some(paras) = chain_spec_json.pointer(&full_ptr) {
if let Some(paras_arr) = paras.as_array() {
println!("\nRegistered Teyrchains: {}", paras_arr.len());
for para in paras_arr {
if let Some(id) = para.get(0).and_then(|v| v.as_u64()) {
let is_teyrchain = para
.get(1)
.and_then(|v| v.get("teyrchain"))
.and_then(|v| v.as_bool())
.unwrap_or(true);
println!(
" - ID: {} ({})",
id,
if is_teyrchain { "teyrchain" } else { "parathread" }
);
}
}
}
}
} else {
println!("\nNo teyrchains registered in genesis");
}
}
}
}
Ok(())
}