mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-22 02:08:00 +00:00
b8ee6a084f
- Renamed all crate names from subxt-* to pezkuwi-subxt-* - Updated codegen to use pezsp_runtime, pezsp_core, pezframe_support instead of sp_runtime, sp_core, frame_support - Replaced all internal references from subxt_* to pezkuwi_subxt_* - Added local path dependencies to Pezkuwi SDK crates - Updated workspace configuration for edition 2024
394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
|
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
|
// see LICENSE for license details.
|
|
|
|
use clap::Args;
|
|
use color_eyre::eyre::{bail, eyre};
|
|
use color_eyre::owo_colors::OwoColorize;
|
|
use heck::ToUpperCamelCase;
|
|
use scale_info::PortableRegistry;
|
|
use scale_typegen_description::{format_type_description, type_description};
|
|
use std::fmt::Display;
|
|
use std::str::FromStr;
|
|
use std::{fs, io::Read, path::PathBuf};
|
|
use subxt::{OnlineClient, PolkadotConfig};
|
|
|
|
use scale_value::Value;
|
|
use pezkuwi_subxt_utils_fetchmetadata::{self as fetch_metadata, MetadataVersion, Url};
|
|
|
|
/// The source of the metadata.
|
|
#[derive(Debug, Args, Clone)]
|
|
pub struct FileOrUrl {
|
|
/// The url of the substrate node to query for metadata for codegen.
|
|
#[clap(long, value_parser)]
|
|
pub url: Option<Url>,
|
|
/// The path to the encoded metadata file.
|
|
#[clap(long, value_parser)]
|
|
pub file: Option<PathOrStdIn>,
|
|
/// Specify the metadata version.
|
|
///
|
|
/// - "latest": Use the latest stable version available.
|
|
/// - "unstable": Use the unstable metadata, if present.
|
|
/// - a number: Use a specific metadata version.
|
|
///
|
|
/// Defaults to asking for the latest stable metadata version.
|
|
#[clap(long)]
|
|
pub version: Option<MetadataVersion>,
|
|
/// Block hash (hex encoded) to attempt to fetch the metadata from.
|
|
/// If not provided, we default to the latest finalized block.
|
|
/// Non-archive nodes will be unable to provide metadata from old blocks.
|
|
#[clap(long)]
|
|
pub at_block: Option<String>,
|
|
}
|
|
|
|
impl FromStr for FileOrUrl {
|
|
type Err = &'static str;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
if let Ok(path) = PathOrStdIn::from_str(s) {
|
|
Ok(FileOrUrl {
|
|
url: None,
|
|
file: Some(path),
|
|
version: None,
|
|
at_block: None,
|
|
})
|
|
} else {
|
|
Url::parse(s)
|
|
.map_err(|_| "Parsing Path or Uri failed.")
|
|
.map(|uri| FileOrUrl {
|
|
url: Some(uri),
|
|
file: None,
|
|
version: None,
|
|
at_block: None,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If `--path -` is provided, read bytes for metadata from stdin
|
|
const STDIN_PATH_NAME: &str = "-";
|
|
#[derive(Debug, Clone)]
|
|
pub enum PathOrStdIn {
|
|
Path(PathBuf),
|
|
StdIn,
|
|
}
|
|
|
|
impl FromStr for PathOrStdIn {
|
|
type Err = &'static str;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let s = s.trim();
|
|
if s == STDIN_PATH_NAME {
|
|
Ok(PathOrStdIn::StdIn)
|
|
} else {
|
|
let path = std::path::Path::new(s);
|
|
if path.exists() {
|
|
Ok(PathOrStdIn::Path(PathBuf::from(path)))
|
|
} else {
|
|
Err("Path does not exist.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FileOrUrl {
|
|
/// Fetch the metadata bytes.
|
|
pub async fn fetch(&self) -> color_eyre::Result<Vec<u8>> {
|
|
match (&self.file, &self.url, self.version, &self.at_block) {
|
|
// Can't provide both --file and --url
|
|
(Some(_), Some(_), _, _) => {
|
|
bail!("specify one of `--url` or `--file` but not both")
|
|
}
|
|
// --at-block must be provided with --url
|
|
(Some(_path_or_stdin), _, _, Some(_at_block)) => {
|
|
bail!("`--at-block` can only be used with `--url`")
|
|
}
|
|
// Load from --file path
|
|
(Some(PathOrStdIn::Path(path)), None, None, None) => {
|
|
let mut file = fs::File::open(path)?;
|
|
let mut bytes = Vec::new();
|
|
file.read_to_end(&mut bytes)?;
|
|
Ok(bytes)
|
|
}
|
|
(Some(PathOrStdIn::StdIn), None, None, None) => {
|
|
let reader = std::io::BufReader::new(std::io::stdin());
|
|
let res = reader.bytes().collect::<Result<Vec<u8>, _>>();
|
|
|
|
match res {
|
|
Ok(bytes) => Ok(bytes),
|
|
Err(err) => bail!("reading bytes from stdin (`--file -`) failed: {err}"),
|
|
}
|
|
}
|
|
// Cannot load the metadata from the file and specify a version to fetch.
|
|
(Some(_), None, Some(_), None) => {
|
|
// Note: we could provide the ability to convert between metadata versions
|
|
// but that would be involved because we'd need to convert
|
|
// from each metadata to the latest one and from the
|
|
// latest one to each metadata version. For now, disable the conversion.
|
|
bail!("`--file` is incompatible with `--version`")
|
|
}
|
|
// Fetch from --url
|
|
(None, Some(uri), version, at_block) => Ok(fetch_metadata::from_url(
|
|
uri.clone(),
|
|
version.unwrap_or_default(),
|
|
at_block.as_deref(),
|
|
)
|
|
.await?),
|
|
// Default if neither is provided; fetch from local url
|
|
(None, None, version, at_block) => {
|
|
let url = Url::parse("ws://localhost:9944").expect("Valid URL; qed");
|
|
Ok(
|
|
fetch_metadata::from_url(url, version.unwrap_or_default(), at_block.as_deref())
|
|
.await?,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// creates an example value for each of the fields and
|
|
/// packages all of them into one unnamed composite value.
|
|
pub fn fields_composite_example(
|
|
fields: impl Iterator<Item = u32>,
|
|
types: &PortableRegistry,
|
|
) -> Value {
|
|
let examples: Vec<Value> = fields.map(|e| type_example(e, types)).collect();
|
|
Value::unnamed_composite(examples)
|
|
}
|
|
|
|
/// Returns a field description that is already formatted.
|
|
pub fn fields_description(
|
|
fields: &[(Option<&str>, u32)],
|
|
name: &str,
|
|
types: &PortableRegistry,
|
|
) -> String {
|
|
if fields.is_empty() {
|
|
return "Zero Sized Type, no fields.".to_string();
|
|
}
|
|
let all_named = fields.iter().all(|f| f.0.is_some());
|
|
|
|
let fields = fields
|
|
.iter()
|
|
.map(|field| {
|
|
let field_description =
|
|
type_description(field.1, types, false).expect("No Description.");
|
|
if all_named {
|
|
let field_name = field.0.unwrap();
|
|
format!("{field_name}: {field_description}")
|
|
} else {
|
|
field_description.to_string()
|
|
}
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join(",");
|
|
|
|
let name = name.to_upper_camel_case();
|
|
let end_result = if all_named {
|
|
format!("{name} {{{fields}}}")
|
|
} else {
|
|
format!("{name} ({fields})")
|
|
};
|
|
// end_result
|
|
format_type_description(&end_result).highlight()
|
|
}
|
|
|
|
pub fn format_scale_value<T>(value: &Value<T>) -> String {
|
|
scale_typegen_description::format_type_description(&value.to_string()).highlight()
|
|
}
|
|
|
|
pub fn type_example(type_id: u32, types: &PortableRegistry) -> Value {
|
|
scale_typegen_description::scale_value_from_seed(type_id, types, time_based_seed()).expect("")
|
|
}
|
|
|
|
fn time_based_seed() -> u64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("We should always live in the future.")
|
|
.subsec_millis() as u64
|
|
}
|
|
|
|
pub fn first_paragraph_of_docs(docs: &[String]) -> String {
|
|
// take at most the first paragraph of documentation, such that it does not get too long.
|
|
docs.iter()
|
|
.map(|e| e.trim())
|
|
.take_while(|e| !e.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
pub trait Indent: ToString {
|
|
fn indent(&self, indent: usize) -> String {
|
|
let indent_str = " ".repeat(indent);
|
|
self.to_string()
|
|
.lines()
|
|
.map(|line| format!("{indent_str}{line}"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
}
|
|
|
|
impl<T: Display> Indent for T {}
|
|
|
|
pub async fn create_client(
|
|
file_or_url: &FileOrUrl,
|
|
) -> color_eyre::Result<OnlineClient<PolkadotConfig>> {
|
|
let client = match &file_or_url.url {
|
|
Some(url) => OnlineClient::<PolkadotConfig>::from_url(url).await?,
|
|
None => OnlineClient::<PolkadotConfig>::new().await?,
|
|
};
|
|
Ok(client)
|
|
}
|
|
|
|
pub fn parse_string_into_scale_value(str: &str) -> color_eyre::Result<Value> {
|
|
let value = scale_value::stringify::from_str(str).0.map_err(|err| {
|
|
eyre!(
|
|
"scale_value::stringify::from_str led to a ParseError.\n\ntried parsing: \"{str}\"\n\n{err}",
|
|
)
|
|
})?;
|
|
Ok(value)
|
|
}
|
|
|
|
pub trait SyntaxHighlight {
|
|
fn highlight(&self) -> String;
|
|
}
|
|
|
|
impl<T: AsRef<str>> SyntaxHighlight for T {
|
|
fn highlight(&self) -> String {
|
|
let _e = 323.0;
|
|
let mut output: String = String::new();
|
|
let mut word: String = String::new();
|
|
|
|
let mut in_word: Option<InWord> = None;
|
|
|
|
for c in self.as_ref().chars() {
|
|
match c {
|
|
'{' | '}' | ',' | '(' | ')' | ':' | '<' | '>' | ' ' | '\n' | '[' | ']' | ';' => {
|
|
// flush the current word:
|
|
if let Some(is_word) = in_word {
|
|
let word = if word == "enum" {
|
|
word.blue().to_string()
|
|
} else {
|
|
is_word.colorize(&word)
|
|
};
|
|
output.push_str(&word);
|
|
}
|
|
|
|
in_word = None;
|
|
word.clear();
|
|
// push the symbol itself:
|
|
output.push(c);
|
|
}
|
|
l => {
|
|
if in_word.is_none() {
|
|
in_word = Some(InWord::from_first_char(l))
|
|
}
|
|
word.push(l);
|
|
}
|
|
}
|
|
}
|
|
// flush if ending on a word:
|
|
if let Some(word_kind) = in_word {
|
|
output.push_str(&word_kind.colorize(&word));
|
|
}
|
|
|
|
return output;
|
|
|
|
enum InWord {
|
|
Lower,
|
|
Upper,
|
|
Number,
|
|
}
|
|
|
|
impl InWord {
|
|
fn colorize(&self, str: &str) -> String {
|
|
let color = match self {
|
|
InWord::Lower => (156, 220, 254),
|
|
InWord::Upper => (78, 201, 176),
|
|
InWord::Number => (181, 206, 168),
|
|
};
|
|
str.truecolor(color.0, color.1, color.2).to_string()
|
|
}
|
|
|
|
fn from_first_char(c: char) -> Self {
|
|
if c.is_numeric() {
|
|
Self::Number
|
|
} else if c.is_uppercase() {
|
|
Self::Upper
|
|
} else {
|
|
Self::Lower
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn validate_url_security(url: Option<&Url>, allow_insecure: bool) -> color_eyre::Result<()> {
|
|
let Some(url) = url else {
|
|
return Ok(());
|
|
};
|
|
match subxt::utils::url_is_secure(url.as_str()) {
|
|
Ok(is_secure) => {
|
|
if !allow_insecure && !is_secure {
|
|
bail!(
|
|
"URL {url} is not secure!\nIf you are really want to use this URL, try using --allow-insecure (-a)"
|
|
);
|
|
}
|
|
}
|
|
Err(err) => {
|
|
bail!("URL {url} is not valid: {err}")
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::utils::{FileOrUrl, PathOrStdIn};
|
|
use std::str::FromStr;
|
|
|
|
#[test]
|
|
fn parsing() {
|
|
assert!(matches!(
|
|
FileOrUrl::from_str("-"),
|
|
Ok(FileOrUrl {
|
|
url: None,
|
|
file: Some(PathOrStdIn::StdIn),
|
|
version: None,
|
|
at_block: None,
|
|
})
|
|
),);
|
|
|
|
assert!(matches!(
|
|
FileOrUrl::from_str(" - "),
|
|
Ok(FileOrUrl {
|
|
url: None,
|
|
file: Some(PathOrStdIn::StdIn),
|
|
version: None,
|
|
at_block: None,
|
|
})
|
|
),);
|
|
|
|
assert!(matches!(
|
|
FileOrUrl::from_str("./src/main.rs"),
|
|
Ok(FileOrUrl {
|
|
url: None,
|
|
file: Some(PathOrStdIn::Path(_)),
|
|
version: None,
|
|
at_block: None,
|
|
})
|
|
),);
|
|
|
|
assert!(FileOrUrl::from_str("./src/i_dont_exist.rs").is_err());
|
|
|
|
assert!(matches!(
|
|
FileOrUrl::from_str("https://github.com/paritytech/subxt"),
|
|
Ok(FileOrUrl {
|
|
url: Some(_),
|
|
file: None,
|
|
version: None,
|
|
at_block: None,
|
|
})
|
|
));
|
|
}
|
|
}
|