// 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, /// The path to the encoded metadata file. #[clap(long, value_parser)] pub file: Option, /// 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, /// 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, } impl FromStr for FileOrUrl { type Err = &'static str; fn from_str(s: &str) -> Result { 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 { 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> { 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::, _>>(); 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, types: &PortableRegistry, ) -> Value { let examples: Vec = 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::>() .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(value: &Value) -> 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::>() .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::>() .join("\n") } } impl Indent for T {} pub async fn create_client( file_or_url: &FileOrUrl, ) -> color_eyre::Result> { let client = match &file_or_url.url { Some(url) => OnlineClient::::from_url(url).await?, None => OnlineClient::::new().await?, }; Ok(client) } pub fn parse_string_into_scale_value(str: &str) -> color_eyre::Result { 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> 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 = 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, }) )); } }