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:
Generated
+256
@@ -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"
|
||||
@@ -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"] }
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
secrets/
|
||||
Generated
+12236
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "usdt-bridge"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Custodial USDT Bridge: Polkadot Asset Hub <-> Pezkuwi Asset Hub (USDT.p)"
|
||||
authors = ["Dijital Kurdistan Tech Institute"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "usdt-bridge"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Async runtime
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
|
||||
# Subxt for blockchain interaction
|
||||
subxt = { version = "0.38", features = ["substrate-compat"] }
|
||||
subxt-signer = { version = "0.38", features = ["sr25519", "subxt"] }
|
||||
|
||||
# Crypto
|
||||
sp-core = { version = "34.0.0", default-features = false }
|
||||
sp-keyring = { version = "39.0.0" }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# CLI
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Hex encoding
|
||||
hex = "0.4"
|
||||
|
||||
# BIP39 mnemonic
|
||||
bip39 = "2.0"
|
||||
|
||||
# Date/time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# Async streams
|
||||
futures = "0.3"
|
||||
|
||||
# Error handling
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
# Simple file-based storage (no native compilation needed)
|
||||
# Database tracking via JSON files for simplicity
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"polkadot_rpc": "wss://polkadot-asset-hub-rpc.polkadot.io",
|
||||
"pezkuwi_rpc": "wss://asset-hub-rpc.pezkuwichain.io",
|
||||
"polkadot_usdt_asset_id": 1984,
|
||||
"pezkuwi_wusdt_asset_id": 1000,
|
||||
"min_deposit": 10000000,
|
||||
"min_withdraw": 10000000,
|
||||
"fee_basis_points": 10,
|
||||
"seed_path": "bridge_seed.json",
|
||||
"db_path": "bridge_db.json"
|
||||
}
|
||||
@@ -0,0 +1,864 @@
|
||||
//! wUSDT Bridge - Custodial bridge for Wrapped USDT on Pezkuwi
|
||||
//!
|
||||
//! This bridge enables users to:
|
||||
//! 1. Deposit USDT (Polkadot Asset Hub) -> Receive wUSDT on Pezkuwi Asset Hub
|
||||
//! 2. Withdraw wUSDT (burn on Pezkuwi) -> Receive USDT on Polkadot Asset Hub
|
||||
//!
|
||||
//! Backing:
|
||||
//! - 1:1 backed by real USDT on Polkadot Asset Hub
|
||||
//!
|
||||
//! Architecture:
|
||||
//! - Custodial: A single keypair controls both sides
|
||||
//! - Minimum deposit/withdraw: 10 USDT
|
||||
//! - Fees: Configurable (default 0.1%)
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sp_core::{crypto::Ss58Codec, sr25519, Pair};
|
||||
use std::path::PathBuf;
|
||||
use subxt::{OnlineClient, SubstrateConfig};
|
||||
use subxt::dynamic::{At, Value};
|
||||
use subxt_signer::sr25519::Keypair;
|
||||
use tracing::{info, warn, error};
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
/// Bridge configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BridgeConfig {
|
||||
/// Polkadot Asset Hub RPC endpoint
|
||||
pub polkadot_rpc: String,
|
||||
/// Pezkuwi Asset Hub RPC endpoint
|
||||
pub pezkuwi_rpc: String,
|
||||
/// USDT Asset ID on Polkadot Asset Hub (1984 is standard)
|
||||
pub polkadot_usdt_asset_id: u32,
|
||||
/// wUSDT Asset ID on Pezkuwi Asset Hub
|
||||
pub pezkuwi_wusdt_asset_id: u32,
|
||||
/// Minimum deposit amount (in USDT base units, 6 decimals)
|
||||
pub min_deposit: u128,
|
||||
/// Minimum withdraw amount
|
||||
pub min_withdraw: u128,
|
||||
/// Bridge fee percentage (e.g., 10 = 0.1%)
|
||||
pub fee_basis_points: u32,
|
||||
/// Bridge operator seed phrase path
|
||||
pub seed_path: PathBuf,
|
||||
/// Database path
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for BridgeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
polkadot_rpc: "wss://polkadot-asset-hub-rpc.polkadot.io".to_string(),
|
||||
pezkuwi_rpc: "wss://asset-hub-rpc.pezkuwichain.io".to_string(),
|
||||
polkadot_usdt_asset_id: 1984,
|
||||
pezkuwi_wusdt_asset_id: 1000,
|
||||
min_deposit: 10_000_000, // 10 USDT (6 decimals)
|
||||
min_withdraw: 10_000_000,
|
||||
fee_basis_points: 10, // 0.1%
|
||||
seed_path: PathBuf::from("bridge_seed.json"),
|
||||
db_path: PathBuf::from("bridge_db.json"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLI
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "usdt-bridge")]
|
||||
#[command(about = "Custodial wUSDT Bridge: Polkadot <-> Pezkuwi")]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Config file path
|
||||
#[arg(short, long, default_value = "bridge_config.json")]
|
||||
config: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Generate a new bridge wallet keypair
|
||||
GenerateWallet {
|
||||
#[arg(short, long, default_value = "bridge_seed.json")]
|
||||
output: PathBuf,
|
||||
},
|
||||
|
||||
/// Show bridge wallet addresses
|
||||
ShowAddresses {
|
||||
#[arg(short, long, default_value = "bridge_seed.json")]
|
||||
seed: PathBuf,
|
||||
},
|
||||
|
||||
/// Start the deposit listener (Polkadot -> Pezkuwi)
|
||||
ListenDeposits,
|
||||
|
||||
/// Process a single deposit manually
|
||||
ProcessDeposit {
|
||||
/// Polkadot tx hash
|
||||
#[arg(long)]
|
||||
tx_hash: String,
|
||||
/// Sender address (Polkadot)
|
||||
#[arg(long)]
|
||||
sender: String,
|
||||
/// Amount in USDT base units (6 decimals)
|
||||
#[arg(long)]
|
||||
amount: u128,
|
||||
},
|
||||
|
||||
/// Process pending withdrawals (Pezkuwi -> Polkadot)
|
||||
ProcessWithdrawals,
|
||||
|
||||
/// Mint wUSDT on Pezkuwi (manual)
|
||||
MintWusdt {
|
||||
/// Recipient address (Pezkuwi)
|
||||
#[arg(long)]
|
||||
to: String,
|
||||
/// Amount in USDT base units (6 decimals)
|
||||
#[arg(long)]
|
||||
amount: u128,
|
||||
},
|
||||
|
||||
/// Transfer USDT on Polkadot (manual)
|
||||
TransferUsdt {
|
||||
/// Recipient address (Polkadot)
|
||||
#[arg(long)]
|
||||
to: String,
|
||||
/// Amount in USDT base units (6 decimals)
|
||||
#[arg(long)]
|
||||
amount: u128,
|
||||
},
|
||||
|
||||
/// Show bridge status and balances
|
||||
Status,
|
||||
|
||||
/// Initialize the database
|
||||
InitDb,
|
||||
|
||||
/// Check balances on both chains
|
||||
Balances,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wallet Management
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct WalletSeed {
|
||||
mnemonic: String,
|
||||
polkadot_address: String,
|
||||
pezkuwi_address: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
fn generate_wallet(output: &PathBuf) -> Result<()> {
|
||||
use sp_core::crypto::Ss58AddressFormat;
|
||||
|
||||
let (pair, phrase, _) = sr25519::Pair::generate_with_phrase(None);
|
||||
|
||||
let polkadot_address = pair.public().to_ss58check_with_version(Ss58AddressFormat::custom(0));
|
||||
let pezkuwi_address = pair.public().to_ss58check_with_version(Ss58AddressFormat::custom(42));
|
||||
let public_key = hex::encode(pair.public().0);
|
||||
|
||||
let wallet = WalletSeed {
|
||||
mnemonic: phrase,
|
||||
polkadot_address: polkadot_address.clone(),
|
||||
pezkuwi_address: pezkuwi_address.clone(),
|
||||
public_key,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&wallet)?;
|
||||
std::fs::write(output, &json)?;
|
||||
|
||||
println!("=== NEW BRIDGE WALLET GENERATED ===\n");
|
||||
println!("Polkadot Asset Hub: {}", polkadot_address);
|
||||
println!("Pezkuwi Asset Hub: {}", pezkuwi_address);
|
||||
println!("\nSeed saved to: {}", output.display());
|
||||
println!("\nIMPORTANT: Back up the seed file securely!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_addresses(seed_path: &PathBuf) -> Result<()> {
|
||||
let content = std::fs::read_to_string(seed_path).context("Failed to read seed file")?;
|
||||
let wallet: WalletSeed = serde_json::from_str(&content).context("Failed to parse seed")?;
|
||||
|
||||
println!("=== BRIDGE WALLET ADDRESSES ===\n");
|
||||
println!("Polkadot Asset Hub: {}", wallet.polkadot_address);
|
||||
println!("Pezkuwi Asset Hub: {}", wallet.pezkuwi_address);
|
||||
println!("Public Key: 0x{}", wallet.public_key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_keypair(seed_path: &PathBuf) -> Result<Keypair> {
|
||||
let content = std::fs::read_to_string(seed_path).context("Failed to read seed file")?;
|
||||
let wallet: WalletSeed = serde_json::from_str(&content).context("Failed to parse seed")?;
|
||||
|
||||
let mnemonic = bip39::Mnemonic::parse(&wallet.mnemonic)
|
||||
.map_err(|e| anyhow!("Invalid mnemonic: {:?}", e))?;
|
||||
|
||||
let keypair = Keypair::from_phrase(&mnemonic, None)
|
||||
.map_err(|e| anyhow!("Failed to create keypair: {:?}", e))?;
|
||||
|
||||
Ok(keypair)
|
||||
}
|
||||
|
||||
fn load_wallet_addresses(seed_path: &PathBuf) -> Result<(String, String)> {
|
||||
let content = std::fs::read_to_string(seed_path).context("Failed to read seed file")?;
|
||||
let wallet: WalletSeed = serde_json::from_str(&content).context("Failed to parse seed")?;
|
||||
Ok((wallet.polkadot_address, wallet.pezkuwi_address))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Database
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
struct BridgeDatabase {
|
||||
deposits: Vec<DepositRecord>,
|
||||
withdrawals: Vec<WithdrawalRecord>,
|
||||
stats: BridgeStats,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct DepositRecord {
|
||||
id: u64,
|
||||
polkadot_tx_hash: String,
|
||||
polkadot_block: u64,
|
||||
sender_address: String,
|
||||
amount: u128,
|
||||
fee: u128,
|
||||
net_amount: u128,
|
||||
pezkuwi_tx_hash: Option<String>,
|
||||
status: String,
|
||||
created_at: String,
|
||||
processed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct WithdrawalRecord {
|
||||
id: u64,
|
||||
pezkuwi_tx_hash: String,
|
||||
pezkuwi_block: u64,
|
||||
sender_address: String,
|
||||
destination_address: String,
|
||||
amount: u128,
|
||||
fee: u128,
|
||||
net_amount: u128,
|
||||
polkadot_tx_hash: Option<String>,
|
||||
status: String,
|
||||
created_at: String,
|
||||
processed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
struct BridgeStats {
|
||||
total_deposits: u64,
|
||||
total_withdrawals: u64,
|
||||
total_fees_collected: u128,
|
||||
last_polkadot_block: u64,
|
||||
last_pezkuwi_block: u64,
|
||||
}
|
||||
|
||||
impl BridgeDatabase {
|
||||
fn load(path: &PathBuf) -> Result<Self> {
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self, path: &PathBuf) -> Result<()> {
|
||||
let json = serde_json::to_string_pretty(self)?;
|
||||
std::fs::write(path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_deposit(&mut self, deposit: DepositRecord) {
|
||||
self.stats.total_deposits += 1;
|
||||
self.stats.total_fees_collected += deposit.fee;
|
||||
self.deposits.push(deposit);
|
||||
}
|
||||
|
||||
fn next_deposit_id(&self) -> u64 {
|
||||
self.deposits.iter().map(|d| d.id).max().unwrap_or(0) + 1
|
||||
}
|
||||
}
|
||||
|
||||
fn init_db(db_path: &PathBuf) -> Result<()> {
|
||||
if db_path.exists() {
|
||||
println!("Database already exists at: {}", db_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db = BridgeDatabase::default();
|
||||
db.save(db_path)?;
|
||||
println!("Database initialized at: {}", db_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chain Operations
|
||||
// ============================================================================
|
||||
|
||||
async fn connect_to_chain(url: &str) -> Result<OnlineClient<SubstrateConfig>> {
|
||||
info!("Connecting to {}...", url);
|
||||
let client = OnlineClient::<SubstrateConfig>::from_url(url).await
|
||||
.context(format!("Failed to connect to {}", url))?;
|
||||
info!("Connected successfully");
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Get asset balance using dynamic API
|
||||
async fn get_asset_balance(
|
||||
client: &OnlineClient<SubstrateConfig>,
|
||||
asset_id: u32,
|
||||
account: &str,
|
||||
) -> Result<u128> {
|
||||
// Decode account from SS58
|
||||
let account_bytes = sp_core::crypto::AccountId32::from_ss58check(account)
|
||||
.map_err(|e| anyhow!("Invalid account: {:?}", e))?;
|
||||
|
||||
// Build storage query for Assets.Account
|
||||
let storage_query = subxt::dynamic::storage(
|
||||
"Assets",
|
||||
"Account",
|
||||
vec![
|
||||
Value::primitive(asset_id.into()),
|
||||
Value::from_bytes(<sp_core::crypto::AccountId32 as AsRef<[u8; 32]>>::as_ref(&account_bytes)),
|
||||
],
|
||||
);
|
||||
|
||||
let result = client.storage().at_latest().await?.fetch(&storage_query).await?;
|
||||
|
||||
if let Some(value) = result {
|
||||
// Parse the balance from the storage value
|
||||
// AssetAccount { balance: u128, ... }
|
||||
if let Some(balance) = value.to_value()?.at("balance") {
|
||||
if let Some(b) = balance.as_u128() {
|
||||
return Ok(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Get native balance
|
||||
async fn get_native_balance(
|
||||
client: &OnlineClient<SubstrateConfig>,
|
||||
account: &str,
|
||||
) -> Result<u128> {
|
||||
let account_bytes = sp_core::crypto::AccountId32::from_ss58check(account)
|
||||
.map_err(|e| anyhow!("Invalid account: {:?}", e))?;
|
||||
|
||||
let storage_query = subxt::dynamic::storage(
|
||||
"System",
|
||||
"Account",
|
||||
vec![Value::from_bytes(<sp_core::crypto::AccountId32 as AsRef<[u8; 32]>>::as_ref(&account_bytes))],
|
||||
);
|
||||
|
||||
let result = client.storage().at_latest().await?.fetch(&storage_query).await?;
|
||||
|
||||
if let Some(value) = result {
|
||||
if let Some(data) = value.to_value()?.at("data") {
|
||||
if let Some(free) = data.at("free") {
|
||||
if let Some(b) = free.as_u128() {
|
||||
return Ok(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Mint wUSDT on Pezkuwi Asset Hub
|
||||
async fn mint_wusdt(
|
||||
client: &OnlineClient<SubstrateConfig>,
|
||||
keypair: &Keypair,
|
||||
asset_id: u32,
|
||||
to: &str,
|
||||
amount: u128,
|
||||
) -> Result<String> {
|
||||
let to_bytes = sp_core::crypto::AccountId32::from_ss58check(to)
|
||||
.map_err(|e| anyhow!("Invalid recipient: {:?}", e))?;
|
||||
|
||||
// Build Assets.mint call
|
||||
let call = subxt::dynamic::tx(
|
||||
"Assets",
|
||||
"mint",
|
||||
vec![
|
||||
Value::primitive(asset_id.into()),
|
||||
Value::unnamed_variant("Id", [Value::from_bytes(<sp_core::crypto::AccountId32 as AsRef<[u8; 32]>>::as_ref(&to_bytes))]),
|
||||
Value::primitive(amount.into()),
|
||||
],
|
||||
);
|
||||
|
||||
info!("Submitting mint transaction...");
|
||||
let tx_progress = client
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&call, keypair)
|
||||
.await?;
|
||||
|
||||
let events = tx_progress.wait_for_finalized_success().await?;
|
||||
let tx_hash = format!("{:?}", events.extrinsic_hash());
|
||||
|
||||
info!("Mint successful! TX: {}", tx_hash);
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
/// Transfer USDT on Polkadot Asset Hub
|
||||
async fn transfer_usdt(
|
||||
client: &OnlineClient<SubstrateConfig>,
|
||||
keypair: &Keypair,
|
||||
asset_id: u32,
|
||||
to: &str,
|
||||
amount: u128,
|
||||
) -> Result<String> {
|
||||
let to_bytes = sp_core::crypto::AccountId32::from_ss58check(to)
|
||||
.map_err(|e| anyhow!("Invalid recipient: {:?}", e))?;
|
||||
|
||||
// Build Assets.transfer call
|
||||
let call = subxt::dynamic::tx(
|
||||
"Assets",
|
||||
"transfer",
|
||||
vec![
|
||||
Value::primitive(asset_id.into()),
|
||||
Value::unnamed_variant("Id", [Value::from_bytes(<sp_core::crypto::AccountId32 as AsRef<[u8; 32]>>::as_ref(&to_bytes))]),
|
||||
Value::primitive(amount.into()),
|
||||
],
|
||||
);
|
||||
|
||||
info!("Submitting transfer transaction...");
|
||||
let tx_progress = client
|
||||
.tx()
|
||||
.sign_and_submit_then_watch_default(&call, keypair)
|
||||
.await?;
|
||||
|
||||
let events = tx_progress.wait_for_finalized_success().await?;
|
||||
let tx_hash = format!("{:?}", events.extrinsic_hash());
|
||||
|
||||
info!("Transfer successful! TX: {}", tx_hash);
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Bridge Operations
|
||||
// ============================================================================
|
||||
|
||||
async fn show_status(config: &BridgeConfig) -> Result<()> {
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ wUSDT BRIDGE STATUS ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
|
||||
println!("Configuration:");
|
||||
println!(" Polkadot RPC: {}", config.polkadot_rpc);
|
||||
println!(" Pezkuwi RPC: {}", config.pezkuwi_rpc);
|
||||
println!(" Polkadot USDT ID: {}", config.polkadot_usdt_asset_id);
|
||||
println!(" Pezkuwi wUSDT ID: {}", config.pezkuwi_wusdt_asset_id);
|
||||
println!(" Min Deposit: {} USDT", config.min_deposit as f64 / 1_000_000.0);
|
||||
println!(" Min Withdraw: {} USDT", config.min_withdraw as f64 / 1_000_000.0);
|
||||
println!(" Fee: {}%", config.fee_basis_points as f64 / 100.0);
|
||||
println!();
|
||||
|
||||
// Load wallet
|
||||
if config.seed_path.exists() {
|
||||
let (polkadot_addr, pezkuwi_addr) = load_wallet_addresses(&config.seed_path)?;
|
||||
println!("Bridge Wallet:");
|
||||
println!(" Polkadot: {}", polkadot_addr);
|
||||
println!(" Pezkuwi: {}", pezkuwi_addr);
|
||||
} else {
|
||||
println!("WARNING: No bridge wallet found. Run 'generate-wallet' first.");
|
||||
}
|
||||
|
||||
// Load database stats
|
||||
if config.db_path.exists() {
|
||||
let db = BridgeDatabase::load(&config.db_path)?;
|
||||
println!("\nStatistics:");
|
||||
println!(" Total Deposits: {}", db.stats.total_deposits);
|
||||
println!(" Total Withdrawals: {}", db.stats.total_withdrawals);
|
||||
println!(" Fees Collected: {} USDT", db.stats.total_fees_collected as f64 / 1_000_000.0);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_balances(config: &BridgeConfig) -> Result<()> {
|
||||
let (polkadot_addr, pezkuwi_addr) = load_wallet_addresses(&config.seed_path)?;
|
||||
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ BRIDGE WALLET BALANCES ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
|
||||
// Connect to Polkadot Asset Hub
|
||||
println!("Connecting to Polkadot Asset Hub...");
|
||||
match connect_to_chain(&config.polkadot_rpc).await {
|
||||
Ok(polkadot_client) => {
|
||||
let usdt_balance = get_asset_balance(
|
||||
&polkadot_client,
|
||||
config.polkadot_usdt_asset_id,
|
||||
&polkadot_addr,
|
||||
).await.unwrap_or(0);
|
||||
|
||||
let native_balance = get_native_balance(&polkadot_client, &polkadot_addr)
|
||||
.await.unwrap_or(0);
|
||||
|
||||
println!("\nPolkadot Asset Hub ({}):", polkadot_addr);
|
||||
println!(" USDT: {} USDT", usdt_balance as f64 / 1_000_000.0);
|
||||
println!(" Native: {} DOT", native_balance as f64 / 10_000_000_000.0);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not connect to Polkadot: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to Pezkuwi Asset Hub
|
||||
println!("\nConnecting to Pezkuwi Asset Hub...");
|
||||
match connect_to_chain(&config.pezkuwi_rpc).await {
|
||||
Ok(pezkuwi_client) => {
|
||||
let wusdt_balance = get_asset_balance(
|
||||
&pezkuwi_client,
|
||||
config.pezkuwi_wusdt_asset_id,
|
||||
&pezkuwi_addr,
|
||||
).await.unwrap_or(0);
|
||||
|
||||
let native_balance = get_native_balance(&pezkuwi_client, &pezkuwi_addr)
|
||||
.await.unwrap_or(0);
|
||||
|
||||
println!("\nPezkuwi Asset Hub ({}):", pezkuwi_addr);
|
||||
println!(" wUSDT: {} USDT", wusdt_balance as f64 / 1_000_000.0);
|
||||
println!(" Native: {} HEZ", native_balance as f64 / 1_000_000_000_000.0);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not connect to Pezkuwi: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_fee(amount: u128, fee_basis_points: u32) -> u128 {
|
||||
amount * fee_basis_points as u128 / 10_000
|
||||
}
|
||||
|
||||
async fn process_deposit(
|
||||
config: &BridgeConfig,
|
||||
tx_hash: &str,
|
||||
sender: &str,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
// Validate amount
|
||||
if amount < config.min_deposit {
|
||||
return Err(anyhow!(
|
||||
"Amount {} below minimum deposit {}",
|
||||
amount as f64 / 1_000_000.0,
|
||||
config.min_deposit as f64 / 1_000_000.0
|
||||
));
|
||||
}
|
||||
|
||||
// Calculate fee
|
||||
let fee = calculate_fee(amount, config.fee_basis_points);
|
||||
let net_amount = amount - fee;
|
||||
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ PROCESSING DEPOSIT ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
|
||||
println!("Polkadot TX: {}", tx_hash);
|
||||
println!("Sender: {}", sender);
|
||||
println!("Amount: {} USDT", amount as f64 / 1_000_000.0);
|
||||
println!("Fee ({}%): {} USDT",
|
||||
config.fee_basis_points as f64 / 100.0,
|
||||
fee as f64 / 1_000_000.0
|
||||
);
|
||||
println!("Net Amount: {} USDT", net_amount as f64 / 1_000_000.0);
|
||||
println!();
|
||||
|
||||
// Load keypair
|
||||
let keypair = load_keypair(&config.seed_path)?;
|
||||
|
||||
// Connect to Pezkuwi and mint wUSDT
|
||||
let pezkuwi_client = connect_to_chain(&config.pezkuwi_rpc).await?;
|
||||
|
||||
// Convert sender address to Pezkuwi format (same public key, different SS58 prefix)
|
||||
// For custodial bridge, we need the user to provide their Pezkuwi address
|
||||
// For now, use sender address converted to Pezkuwi format
|
||||
let sender_account = sp_core::crypto::AccountId32::from_ss58check(sender)
|
||||
.map_err(|e| anyhow!("Invalid sender address: {:?}", e))?;
|
||||
let pezkuwi_recipient = sender_account.to_ss58check_with_version(
|
||||
sp_core::crypto::Ss58AddressFormat::custom(42)
|
||||
);
|
||||
|
||||
println!("Minting {} wUSDT to {}...", net_amount as f64 / 1_000_000.0, pezkuwi_recipient);
|
||||
|
||||
let pezkuwi_tx = mint_wusdt(
|
||||
&pezkuwi_client,
|
||||
&keypair,
|
||||
config.pezkuwi_wusdt_asset_id,
|
||||
&pezkuwi_recipient,
|
||||
net_amount,
|
||||
).await?;
|
||||
|
||||
// Save to database
|
||||
let mut db = BridgeDatabase::load(&config.db_path)?;
|
||||
let deposit = DepositRecord {
|
||||
id: db.next_deposit_id(),
|
||||
polkadot_tx_hash: tx_hash.to_string(),
|
||||
polkadot_block: 0, // Would need to fetch from chain
|
||||
sender_address: sender.to_string(),
|
||||
amount,
|
||||
fee,
|
||||
net_amount,
|
||||
pezkuwi_tx_hash: Some(pezkuwi_tx.clone()),
|
||||
status: "completed".to_string(),
|
||||
created_at: chrono::Utc::now().to_rfc3339(),
|
||||
processed_at: Some(chrono::Utc::now().to_rfc3339()),
|
||||
};
|
||||
db.add_deposit(deposit);
|
||||
db.save(&config.db_path)?;
|
||||
|
||||
println!("\n✅ DEPOSIT PROCESSED SUCCESSFULLY!");
|
||||
println!(" Pezkuwi TX: {}", pezkuwi_tx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_mint_wusdt(config: &BridgeConfig, to: &str, amount: u128) -> Result<()> {
|
||||
println!("Minting {} wUSDT to {}...", amount as f64 / 1_000_000.0, to);
|
||||
|
||||
let keypair = load_keypair(&config.seed_path)?;
|
||||
let client = connect_to_chain(&config.pezkuwi_rpc).await?;
|
||||
|
||||
let tx_hash = mint_wusdt(&client, &keypair, config.pezkuwi_wusdt_asset_id, to, amount).await?;
|
||||
|
||||
println!("\n✅ MINT SUCCESSFUL!");
|
||||
println!(" TX Hash: {}", tx_hash);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_transfer_usdt(config: &BridgeConfig, to: &str, amount: u128) -> Result<()> {
|
||||
println!("Transferring {} USDT to {}...", amount as f64 / 1_000_000.0, to);
|
||||
|
||||
let keypair = load_keypair(&config.seed_path)?;
|
||||
let client = connect_to_chain(&config.polkadot_rpc).await?;
|
||||
|
||||
let tx_hash = transfer_usdt(&client, &keypair, config.polkadot_usdt_asset_id, to, amount).await?;
|
||||
|
||||
println!("\n✅ TRANSFER SUCCESSFUL!");
|
||||
println!(" TX Hash: {}", tx_hash);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen_deposits(config: &BridgeConfig) -> Result<()> {
|
||||
let (polkadot_addr, _) = load_wallet_addresses(&config.seed_path)?;
|
||||
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ DEPOSIT LISTENER ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
|
||||
println!("Bridge Address (Polkadot): {}", polkadot_addr);
|
||||
println!("USDT Asset ID: {}", config.polkadot_usdt_asset_id);
|
||||
println!("Min Deposit: {} USDT", config.min_deposit as f64 / 1_000_000.0);
|
||||
println!("\nListening for deposits...\n");
|
||||
|
||||
let client = connect_to_chain(&config.polkadot_rpc).await?;
|
||||
let keypair = load_keypair(&config.seed_path)?;
|
||||
|
||||
// Subscribe to new blocks
|
||||
let mut blocks = client.blocks().subscribe_finalized().await?;
|
||||
|
||||
while let Some(block) = blocks.next().await {
|
||||
let block = block?;
|
||||
let block_number = block.number();
|
||||
|
||||
// Get events for this block
|
||||
let events = block.events().await?;
|
||||
|
||||
for event in events.iter() {
|
||||
let event = event?;
|
||||
|
||||
// Check for Assets.Transferred event
|
||||
if event.pallet_name() == "Assets" && event.variant_name() == "Transferred" {
|
||||
// Parse event data
|
||||
if let Ok(fields) = event.field_values() {
|
||||
// Fields: asset_id, from, to, amount
|
||||
let asset_id = fields.at("asset_id")
|
||||
.and_then(|v| v.as_u128())
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
if asset_id != config.polkadot_usdt_asset_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let to_field = fields.at("to");
|
||||
let from_field = fields.at("from");
|
||||
let amount = fields.at("amount")
|
||||
.and_then(|v| v.as_u128())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Check if transfer is to bridge address
|
||||
if let Some(to_value) = to_field {
|
||||
let to_str = format!("{:?}", to_value);
|
||||
if to_str.contains(&polkadot_addr[..10]) {
|
||||
let from_str = from_field.map(|f| format!("{:?}", f)).unwrap_or_default();
|
||||
|
||||
info!("📥 DEPOSIT DETECTED!");
|
||||
info!(" Block: #{}", block_number);
|
||||
info!(" From: {}", from_str);
|
||||
info!(" Amount: {} USDT", amount as f64 / 1_000_000.0);
|
||||
|
||||
if amount >= config.min_deposit {
|
||||
// Process deposit
|
||||
let fee = calculate_fee(amount, config.fee_basis_points);
|
||||
let net_amount = amount - fee;
|
||||
|
||||
// Get recipient Pezkuwi address
|
||||
// In production, this should come from a memo or separate registration
|
||||
// For now, convert sender's address
|
||||
|
||||
info!(" Processing... Fee: {} USDT, Net: {} USDT",
|
||||
fee as f64 / 1_000_000.0,
|
||||
net_amount as f64 / 1_000_000.0
|
||||
);
|
||||
|
||||
// Connect to Pezkuwi and mint
|
||||
match connect_to_chain(&config.pezkuwi_rpc).await {
|
||||
Ok(pezkuwi_client) => {
|
||||
// For demo, mint to bridge's own Pezkuwi address
|
||||
// In production, need user's Pezkuwi address
|
||||
let (_, pezkuwi_bridge) = load_wallet_addresses(&config.seed_path)?;
|
||||
|
||||
match mint_wusdt(
|
||||
&pezkuwi_client,
|
||||
&keypair,
|
||||
config.pezkuwi_wusdt_asset_id,
|
||||
&pezkuwi_bridge,
|
||||
net_amount,
|
||||
).await {
|
||||
Ok(tx) => {
|
||||
info!(" ✅ Minted wUSDT! TX: {}", tx);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(" ❌ Mint failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(" ❌ Failed to connect to Pezkuwi: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(" ⚠️ Amount below minimum, skipping");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_withdrawals(config: &BridgeConfig) -> Result<()> {
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ WITHDRAWAL PROCESSOR ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝\n");
|
||||
|
||||
println!("This would process pending withdrawals from the database.");
|
||||
println!("Withdrawals require burning wUSDT on Pezkuwi first.\n");
|
||||
|
||||
// Load database
|
||||
let db = BridgeDatabase::load(&config.db_path)?;
|
||||
|
||||
let pending: Vec<_> = db.withdrawals.iter()
|
||||
.filter(|w| w.status == "pending")
|
||||
.collect();
|
||||
|
||||
if pending.is_empty() {
|
||||
println!("No pending withdrawals.");
|
||||
} else {
|
||||
println!("Pending withdrawals: {}", pending.len());
|
||||
for w in pending {
|
||||
println!(" #{}: {} USDT -> {}", w.id, w.amount as f64 / 1_000_000.0, w.destination_address);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("usdt_bridge=info".parse().unwrap())
|
||||
.add_directive("subxt=warn".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Load or create config
|
||||
let config: BridgeConfig = if cli.config.exists() {
|
||||
let content = std::fs::read_to_string(&cli.config)?;
|
||||
serde_json::from_str(&content)?
|
||||
} else {
|
||||
let default_config = BridgeConfig::default();
|
||||
let json = serde_json::to_string_pretty(&default_config)?;
|
||||
std::fs::write(&cli.config, &json)?;
|
||||
info!("Created default config at: {}", cli.config.display());
|
||||
default_config
|
||||
};
|
||||
|
||||
match cli.command {
|
||||
Commands::GenerateWallet { output } => {
|
||||
generate_wallet(&output)?;
|
||||
}
|
||||
Commands::ShowAddresses { seed } => {
|
||||
show_addresses(&seed)?;
|
||||
}
|
||||
Commands::ListenDeposits => {
|
||||
listen_deposits(&config).await?;
|
||||
}
|
||||
Commands::ProcessDeposit { tx_hash, sender, amount } => {
|
||||
process_deposit(&config, &tx_hash, &sender, amount).await?;
|
||||
}
|
||||
Commands::ProcessWithdrawals => {
|
||||
process_withdrawals(&config).await?;
|
||||
}
|
||||
Commands::MintWusdt { to, amount } => {
|
||||
cmd_mint_wusdt(&config, &to, amount).await?;
|
||||
}
|
||||
Commands::TransferUsdt { to, amount } => {
|
||||
cmd_transfer_usdt(&config, &to, amount).await?;
|
||||
}
|
||||
Commands::Status => {
|
||||
show_status(&config).await?;
|
||||
}
|
||||
Commands::InitDb => {
|
||||
init_db(&config.db_path)?;
|
||||
}
|
||||
Commands::Balances => {
|
||||
show_balances(&config).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user