fix: Convert vendor/pezkuwi-subxt from submodule to regular directory

This commit is contained in:
2025-12-19 16:45:24 +03:00
parent 9a52edf0df
commit fdd023c499
393 changed files with 154124 additions and 1 deletions
+58
View File
@@ -0,0 +1,58 @@
[package]
name = "pezkuwi-subxt-cli"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
publish = true
license.workspace = true
repository.workspace = true
documentation = "https://docs.rs/subxt-cli"
homepage.workspace = true
description = "Command line utilities for working with subxt codegen"
[[bin]]
name = "subxt"
path = "src/main.rs"
doc = false
[lints]
workspace = true
[features]
# Compute the state root hash from the genesis entry.
# Enable this to create a smaller chain spec file.
chain-spec-pruning = ["smoldot"]
[dependencies]
pezkuwi-subxt-codegen = { workspace = true }
scale-typegen = { workspace = true }
pezkuwi-subxt-utils-fetchmetadata = { workspace = true, features = ["url"] }
pezkuwi-subxt-utils-stripmetadata = { workspace = true }
pezkuwi-subxt-metadata = { workspace = true, features = ["legacy"] }
pezkuwi-subxt = { workspace = true, features = ["default"] }
clap = { workspace = true }
serde = { workspace = true, features = ["derive"] }
color-eyre = { workspace = true }
serde_json = { workspace = true }
hex = { workspace = true }
frame-decode = { workspace = true, features = ["legacy-types"] }
frame-metadata = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true }
scale-info = { workspace = true }
scale-info-legacy = { workspace = true }
scale-value = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-tls", "http-client"] }
tokio = { workspace = true, features = ["rt-multi-thread"] }
scale-typegen-description = { workspace = true }
heck = { workspace = true }
indoc = { workspace = true }
thiserror = { workspace = true }
smoldot = { workspace = true, optional = true }
[dev-dependencies]
strip-ansi-escapes = { workspace = true }
pretty_assertions = { workspace = true }
+58
View File
@@ -0,0 +1,58 @@
# subxt-cli
Utilities for working with substrate metadata for `subxt`
```
USAGE:
subxt <SUBCOMMAND>
FLAGS:
-h, --help
Prints help information
-V, --version
Prints version information
SUBCOMMANDS:
codegen Generate runtime API client code from metadata
help Prints this message or the help of the given subcommand(s)
metadata Download metadata from a substrate node, for use with `subxt` codegen
```
## Metadata
Use to download metadata for inspection, or use in the `subxt` macro. e.g.
`subxt metadata -f bytes > metadata.scale`
```
USAGE:
subxt metadata [OPTIONS]
OPTIONS:
-f, --format <format> the format of the metadata to display: `json`, `hex` or `bytes` [default: json]
--url <url> the url of the substrate node to query for metadata [default: http://localhost:9933]
```
## Codegen
Use to invoke the `subxt-codegen` crate which is used by `subxt-macro` to generate the runtime API and types. Useful
for troubleshooting codegen as an alternative to `cargo expand`, and also provides the possibility to customize the
generated code if the macro does not produce the desired API. e.g.
`subxt codegen | rustfmt --edition=2018 --emit=stdout`
```
USAGE:
subxt codegen [OPTIONS]
OPTIONS:
-f, --file <file>
the path to the encoded metadata file
--url <url>
the url of the substrate node to query for metadata for codegen
```
+34
View File
@@ -0,0 +1,34 @@
//! Build script for the CLI.
use std::{borrow::Cow, process::Command};
fn main() {
// Make git hash available via GIT_HASH build-time env var:
output_git_short_hash();
}
fn output_git_short_hash() {
let output = Command::new("git")
.args(["rev-parse", "--short=11", "HEAD"])
.output();
let git_hash = match output {
Ok(o) if o.status.success() => {
let sha = String::from_utf8_lossy(&o.stdout).trim().to_owned();
Cow::from(sha)
}
Ok(o) => {
println!("cargo:warning=Git command failed with status: {}", o.status);
Cow::from("unknown")
}
Err(err) => {
println!("cargo:warning=Failed to execute git command: {err}");
Cow::from("unknown")
}
};
println!("cargo:rustc-env=GIT_HASH={git_hash}");
println!("cargo:rerun-if-changed=../.git/HEAD");
println!("cargo:rerun-if-changed=../.git/refs");
println!("cargo:rerun-if-changed=build.rs");
}
@@ -0,0 +1,63 @@
// 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 jsonrpsee::{
async_client::ClientBuilder,
client_transport::ws::WsTransportClientBuilder,
core::client::{ClientT, Error},
http_client::HttpClientBuilder,
};
use std::time::Duration;
use pezkuwi_subxt_utils_fetchmetadata::Url;
/// Returns the node's chainSpec from the provided URL.
pub async fn fetch_chain_spec(url: Url) -> Result<serde_json::Value, FetchSpecError> {
async fn fetch_ws(url: Url) -> Result<serde_json::Value, Error> {
let (sender, receiver) = WsTransportClientBuilder::default()
.build(url)
.await
.map_err(|e| Error::Transport(e.into()))?;
let client = ClientBuilder::default()
.request_timeout(Duration::from_secs(180))
.max_buffer_capacity_per_subscription(4096)
.build_with_tokio(sender, receiver);
inner_fetch(client).await
}
async fn fetch_http(url: Url) -> Result<serde_json::Value, Error> {
let client = HttpClientBuilder::default()
.request_timeout(Duration::from_secs(180))
.build(url)?;
inner_fetch(client).await
}
async fn inner_fetch(client: impl ClientT) -> Result<serde_json::Value, Error> {
client
.request("sync_state_genSyncSpec", jsonrpsee::rpc_params![true])
.await
}
let spec = match url.scheme() {
"http" | "https" => fetch_http(url).await.map_err(FetchSpecError::RequestError),
"ws" | "wss" => fetch_ws(url).await.map_err(FetchSpecError::RequestError),
invalid_scheme => Err(FetchSpecError::InvalidScheme(invalid_scheme.to_owned())),
}?;
Ok(spec)
}
/// Error attempting to fetch chainSpec.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FetchSpecError {
/// JSON-RPC error fetching metadata.
#[error("Request error: {0}")]
RequestError(#[from] jsonrpsee::core::ClientError),
/// URL scheme is not http, https, ws or wss.
#[error("'{0}' not supported, supported URI schemes are http, https, ws or wss.")]
InvalidScheme(String),
}
+131
View File
@@ -0,0 +1,131 @@
// 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::Parser as ClapParser;
#[cfg(feature = "chain-spec-pruning")]
use serde_json::Value;
use std::{io::Write, path::PathBuf};
use pezkuwi_subxt_utils_fetchmetadata::Url;
mod fetch;
/// Download chainSpec from a substrate node.
#[derive(Debug, ClapParser)]
pub struct Opts {
/// The url of the substrate node to query for metadata for codegen.
#[clap(long)]
url: Url,
/// Write the output of the command to the provided file path.
#[clap(long, short, value_parser)]
output_file: Option<PathBuf>,
/// Replaced the genesis raw entry with a stateRootHash to optimize
/// the spec size and avoid the need to calculate the genesis storage.
///
/// This option is enabled with the `chain-spec-pruning` feature.
///
/// Defaults to `false`.
#[cfg(feature = "chain-spec-pruning")]
#[clap(long)]
state_root_hash: bool,
/// Remove the `codeSubstitutes` entry from the chain spec.
/// This is useful when wanting to store a smaller chain spec.
/// At this moment, the light client does not utilize this object.
///
/// Defaults to `false`.
#[clap(long)]
remove_substitutes: bool,
}
/// Error attempting to fetch chainSpec.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ChainSpecError {
/// Failed to fetch the chain spec.
#[error("Failed to fetch the chain spec: {0}")]
FetchError(#[from] fetch::FetchSpecError),
#[cfg(feature = "chain-spec-pruning")]
/// The provided chain spec is invalid.
#[error("Error while parsing the chain spec: {0})")]
ParseError(String),
#[cfg(feature = "chain-spec-pruning")]
/// Cannot compute the state root hash.
#[error("Error computing state root hash: {0})")]
ComputeError(String),
/// Other error.
#[error("Other: {0})")]
Other(String),
}
#[cfg(feature = "chain-spec-pruning")]
fn compute_state_root_hash(spec: &Value) -> Result<[u8; 32], ChainSpecError> {
let chain_spec = smoldot::chain_spec::ChainSpec::from_json_bytes(spec.to_string().as_bytes())
.map_err(|err| ChainSpecError::ParseError(err.to_string()))?;
let genesis_chain_information = chain_spec.to_chain_information().map(|(ci, _)| ci);
let state_root = match genesis_chain_information {
Ok(genesis_chain_information) => {
let header = genesis_chain_information.as_ref().finalized_block_header;
*header.state_root
}
// From the smoldot code this error is encountered when the genesis already contains the
// state root hash entry instead of the raw entry.
Err(smoldot::chain_spec::FromGenesisStorageError::UnknownStorageItems) => *chain_spec
.genesis_storage()
.into_trie_root_hash()
.ok_or_else(|| {
ChainSpecError::ParseError(
"The chain spec does not contain the proper shape for the genesis.raw entry"
.to_string(),
)
})?,
Err(err) => return Err(ChainSpecError::ComputeError(err.to_string())),
};
Ok(state_root)
}
pub async fn run(opts: Opts, output: &mut impl Write) -> color_eyre::Result<()> {
let url = opts.url;
let mut spec = fetch::fetch_chain_spec(url).await?;
let mut output: Box<dyn Write> = match opts.output_file {
Some(path) => Box::new(std::fs::File::create(path)?),
None => Box::new(output),
};
#[cfg(feature = "chain-spec-pruning")]
if opts.state_root_hash {
let state_root_hash = compute_state_root_hash(&spec)?;
let state_root_hash = format!("0x{}", hex::encode(state_root_hash));
if let Some(genesis) = spec.get_mut("genesis") {
let object = genesis.as_object_mut().ok_or_else(|| {
ChainSpecError::Other("The genesis entry must be an object".to_string())
})?;
object.remove("raw").ok_or_else(|| {
ChainSpecError::Other("The genesis entry must contain a raw entry".to_string())
})?;
object.insert("stateRootHash".to_string(), Value::String(state_root_hash));
}
}
if opts.remove_substitutes {
let object = spec
.as_object_mut()
.ok_or_else(|| ChainSpecError::Other("The chain spec must be an object".to_string()))?;
object.remove("codeSubstitutes");
}
let json = serde_json::to_string_pretty(&spec)?;
write!(output, "{json}")?;
Ok(())
}
+470
View File
@@ -0,0 +1,470 @@
// 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 crate::utils::{FileOrUrl, validate_url_security};
use clap::Parser as ClapParser;
use color_eyre::eyre::eyre;
use scale_typegen_description::scale_typegen::typegen::{
settings::substitutes::path_segments,
validation::{registry_contains_type_path, similar_type_paths_in_registry},
};
use std::path::PathBuf;
use pezkuwi_subxt_codegen::CodegenBuilder;
use pezkuwi_subxt_metadata::Metadata;
/// Generate runtime API client code from metadata.
///
/// # Example (with code formatting)
///
/// `subxt codegen | rustfmt --edition=2018 --emit=stdout`
#[derive(Debug, ClapParser)]
pub struct Opts {
#[command(flatten)]
file_or_url: FileOrUrl,
/// Additional derives
#[clap(long = "derive")]
derives: Vec<String>,
/// Additional attributes
#[clap(long = "attribute")]
attributes: Vec<String>,
/// Path to legacy type definitions (required for metadatas pre-V14)
#[clap(long)]
legacy_types: Option<PathBuf>,
/// The spec version of the legacy metadata (required for metadatas pre-V14)
#[clap(long)]
legacy_spec_version: Option<u64>,
/// Additional derives for a given type.
///
/// Example 1: `--derive-for-type my_module::my_type=serde::Serialize`.
/// Example 2: `--derive-for-type my_module::my_type=serde::Serialize,recursive`.
#[clap(long = "derive-for-type", value_parser = derive_for_type_parser)]
derives_for_type: Vec<DeriveForType>,
/// Additional attributes for a given type.
///
/// Example 1: `--attributes-for-type my_module::my_type=#[allow(clippy::all)]`.
/// Example 2: `--attributes-for-type my_module::my_type=#[allow(clippy::all)],recursive`.
#[clap(long = "attributes-for-type", value_parser = attributes_for_type_parser)]
attributes_for_type: Vec<AttributeForType>,
/// Substitute a type for another.
///
/// Example `--substitute-type sp_runtime::MultiAddress<A,B>=subxt::utils::Static<::sp_runtime::MultiAddress<A,B>>`
#[clap(long = "substitute-type", value_parser = substitute_type_parser)]
substitute_types: Vec<(String, String)>,
/// The `subxt` crate access path in the generated code.
/// Defaults to `::pezkuwi_subxt::ext::pezkuwi_subxt_core`.
#[clap(long = "crate")]
crate_path: Option<String>,
/// Do not generate documentation for the runtime API code.
///
/// Defaults to `false` (documentation is generated).
#[clap(long, action)]
no_docs: bool,
/// Whether to limit code generation to only runtime types.
///
/// Defaults to `false` (all types are generated).
#[clap(long)]
runtime_types_only: bool,
/// Do not provide default trait derivations for the generated types.
///
/// Defaults to `false` (default trait derivations are provided).
#[clap(long)]
no_default_derives: bool,
/// Do not provide default substitutions for the generated types.
///
/// Defaults to `false` (default substitutions are provided).
#[clap(long)]
no_default_substitutions: bool,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
#[derive(Debug, Clone)]
struct DeriveForType {
type_path: String,
trait_path: String,
recursive: bool,
}
#[derive(Debug, Clone)]
struct AttributeForType {
type_path: String,
attribute: String,
recursive: bool,
}
fn derive_for_type_parser(src: &str) -> Result<DeriveForType, String> {
let (type_path, trait_path, recursive) = type_map_parser(src)
.ok_or_else(|| String::from("Invalid pattern for `derive-for-type`. It should be `type=derive` or `type=derive,recursive`, like `my_type=serde::Serialize` or `my_type=serde::Serialize,recursive`"))?;
Ok(DeriveForType {
type_path: type_path.to_string(),
trait_path: trait_path.to_string(),
recursive,
})
}
fn attributes_for_type_parser(src: &str) -> Result<AttributeForType, String> {
let (type_path, attribute, recursive) = type_map_parser(src)
.ok_or_else(|| String::from("Invalid pattern for `attributes-for-type`. It should be `type=attribute` like `my_type=serde::#[allow(clippy::all)]` or `type=attribute,recursive` like `my_type=serde::#[allow(clippy::all)],recursive`"))?;
Ok(AttributeForType {
type_path: type_path.to_string(),
attribute: attribute.to_string(),
recursive,
})
}
/// Parses a `&str` of the form `str1=str2` into `(str1, str2, false)` or `str1=str2,recursive` into `(str1, str2, true)`.
///
/// A `None` value returned is a parsing error.
fn type_map_parser(src: &str) -> Option<(&str, &str, bool)> {
let (str1, rest) = src.split_once('=')?;
let mut split_rest = rest.split(',');
let str2 = split_rest
.next()
.expect("split iter always returns at least one element; qed");
let mut recursive = false;
for r in split_rest {
match r {
// Note: later we can add other attributes to this match
"recursive" => {
recursive = true;
}
_ => return None,
}
}
Some((str1, str2, recursive))
}
fn substitute_type_parser(src: &str) -> Result<(String, String), String> {
let (from, to) = src
.split_once('=')
.ok_or_else(|| String::from("Invalid pattern for `substitute-type`. It should be something like `input::Type<A>=replacement::Type<A>`"))?;
Ok((from.to_string(), to.to_string()))
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
let bytes = opts.file_or_url.fetch().await?;
let legacy_types = opts
.legacy_types
.map(|path| {
let bytes = std::fs::read(path).map_err(|e| eyre!("Cannot read legacy_types: {e}"))?;
let types = frame_decode::legacy_types::from_bytes(&bytes)
.map_err(|e| eyre!("Cannot deserialize legacy_types: {e}"))?;
Ok::<_, color_eyre::eyre::Error>(types)
})
.transpose()?;
codegen(
&bytes,
legacy_types,
opts.legacy_spec_version,
opts.derives,
opts.attributes,
opts.derives_for_type,
opts.attributes_for_type,
opts.substitute_types,
opts.crate_path,
opts.no_docs,
opts.runtime_types_only,
opts.no_default_derives,
opts.no_default_substitutions,
output,
)?;
Ok(())
}
#[derive(Clone, Debug)]
struct OuterAttribute(syn::Attribute);
impl syn::parse::Parse for OuterAttribute {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(Self(input.call(syn::Attribute::parse_outer)?[0].clone()))
}
}
#[allow(clippy::too_many_arguments)]
fn codegen(
metadata_bytes: &[u8],
legacy_types: Option<scale_info_legacy::ChainTypeRegistry>,
legacy_spec_version: Option<u64>,
raw_derives: Vec<String>,
raw_attributes: Vec<String>,
derives_for_type: Vec<DeriveForType>,
attributes_for_type: Vec<AttributeForType>,
substitute_types: Vec<(String, String)>,
crate_path: Option<String>,
no_docs: bool,
runtime_types_only: bool,
no_default_derives: bool,
no_default_substitutions: bool,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let mut codegen = CodegenBuilder::new();
// Use the provided crate path:
if let Some(crate_path) = crate_path {
let crate_path =
syn::parse_str(&crate_path).map_err(|e| eyre!("Cannot parse crate path: {e}"))?;
codegen.set_subxt_crate_path(crate_path);
}
// Respect the boolean flags:
if runtime_types_only {
codegen.runtime_types_only()
}
if no_default_derives {
codegen.disable_default_derives()
}
if no_default_substitutions {
codegen.disable_default_substitutes()
}
if no_docs {
codegen.no_docs()
}
let metadata = {
let runtime_metadata = pezkuwi_subxt_metadata::decode_runtime_metadata(metadata_bytes)?;
let mut metadata = match runtime_metadata {
// Too old to work with:
frame_metadata::RuntimeMetadata::V0(_)
| frame_metadata::RuntimeMetadata::V1(_)
| frame_metadata::RuntimeMetadata::V2(_)
| frame_metadata::RuntimeMetadata::V3(_)
| frame_metadata::RuntimeMetadata::V4(_)
| frame_metadata::RuntimeMetadata::V5(_)
| frame_metadata::RuntimeMetadata::V6(_)
| frame_metadata::RuntimeMetadata::V7(_) => {
Err(eyre!("Metadata V1-V7 cannot be decoded from"))
}
// Converting legacy metadatas:
frame_metadata::RuntimeMetadata::V8(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V8 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V8 metadata"))?;
Metadata::from_v8(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V8 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V9(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V9 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V9 metadata"))?;
Metadata::from_v9(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V9 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V10(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V10 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V10 metadata"))?;
Metadata::from_v10(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V10 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V11(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V11 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V11 metadata"))?;
Metadata::from_v11(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V11 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V12(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V12 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V12 metadata"))?;
Metadata::from_v12(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V12 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V13(md) => {
let legacy_types = legacy_types
.ok_or_else(|| eyre!("--legacy-types needed to load V13 metadata"))?;
let legacy_spec = legacy_spec_version
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V13 metadata"))?;
Metadata::from_v13(&md, &legacy_types.for_spec_version(legacy_spec))
.map_err(|e| eyre!("Cannot load V13 metadata: {e}"))
}
// Converting modern metadatas:
frame_metadata::RuntimeMetadata::V14(md) => {
Metadata::from_v14(md).map_err(|e| eyre!("Cannot load V14 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V15(md) => {
Metadata::from_v15(md).map_err(|e| eyre!("Cannot load V15 metadata: {e}"))
}
frame_metadata::RuntimeMetadata::V16(md) => {
Metadata::from_v16(md).map_err(|e| eyre!("Cannot load V16 metadata: {e}"))
}
}?;
// Run this first to ensure type paths are unique (which may result in 1,2,3 suffixes being added
// to type paths), so that when we validate derives/substitutions below, they are allowed for such
// types. See <https://github.com/paritytech/subxt/issues/2011>.
scale_typegen::utils::ensure_unique_type_paths(metadata.types_mut())
.expect("ensure_unique_type_paths should not fail; please report an issue.");
metadata
};
// Configure derives:
let global_derives = raw_derives
.iter()
.map(|raw| syn::parse_str(raw))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| eyre!("Cannot parse global derives: {e}"))?;
codegen.set_additional_global_derives(global_derives);
for d in derives_for_type {
let ty_str = &d.type_path;
let ty: syn::TypePath = syn::parse_str(ty_str)
.map_err(|e| eyre!("Cannot parse derive for type {ty_str}: {e}"))?;
let derive = syn::parse_str(&d.trait_path)
.map_err(|e| eyre!("Cannot parse derive for type {ty_str}: {e}"))?;
validate_path_with_metadata(&ty.path, &metadata)?;
// Note: recursive derives and attributes not supported in the CLI => recursive: false
codegen.add_derives_for_type(ty, std::iter::once(derive), d.recursive);
}
// Configure attributes:
let universal_attributes = raw_attributes
.iter()
.map(|raw| syn::parse_str(raw))
.map(|attr: syn::Result<OuterAttribute>| attr.map(|attr| attr.0))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| eyre!("Cannot parse global attributes: {e}"))?;
codegen.set_additional_global_attributes(universal_attributes);
for a in attributes_for_type {
let ty_str = &a.type_path;
let ty: syn::TypePath = syn::parse_str(ty_str)
.map_err(|e| eyre!("Cannot parse attribute for type {ty_str}: {e}"))?;
let attribute: OuterAttribute = syn::parse_str(&a.attribute)
.map_err(|e| eyre!("Cannot parse attribute for type {ty_str}: {e}"))?;
validate_path_with_metadata(&ty.path, &metadata)?;
// Note: recursive derives and attributes not supported in the CLI => recursive: false
codegen.add_attributes_for_type(ty, std::iter::once(attribute.0), a.recursive);
}
// Insert type substitutions:
for (from_str, to_str) in substitute_types {
let from: syn::Path = syn::parse_str(&from_str)
.map_err(|e| eyre!("Cannot parse type substitution for path {from_str}: {e}"))?;
let to: syn::Path = syn::parse_str(&to_str)
.map_err(|e| eyre!("Cannot parse type substitution for path {from_str}: {e}"))?;
validate_path_with_metadata(&from, &metadata)?;
codegen.set_type_substitute(from, to);
}
let code = codegen
.generate(metadata)
.map_err(|e| eyre!("Cannot generate code: {e}"))?;
writeln!(output, "{code}")?;
Ok(())
}
/// Validates that the type path is part of the metadata.
fn validate_path_with_metadata(path: &syn::Path, metadata: &Metadata) -> color_eyre::Result<()> {
fn pretty_path(path: &syn::Path) -> String {
use quote::ToTokens;
path.to_token_stream().to_string().replace(' ', "")
}
let path_segments = path_segments(path);
let ident = &path
.segments
.last()
.expect("Empty path should be filtered out before already")
.ident;
if !registry_contains_type_path(metadata.types(), &path_segments) {
let alternatives = similar_type_paths_in_registry(metadata.types(), path);
let alternatives: String = if alternatives.is_empty() {
format!("There is no Type with name `{ident}` in the provided metadata.")
} else {
let mut s = "A type with the same name is present at: ".to_owned();
for p in alternatives {
s.push('\n');
s.push_str(&pretty_path(&p));
}
s
};
color_eyre::eyre::bail!(
"Type `{}` does not exist at path `{}`\n{}",
ident.to_string(),
pretty_path(path),
alternatives
);
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn parse_types() {
use crate::commands::codegen::type_map_parser;
assert_eq!(type_map_parser("Foo"), None);
assert_eq!(type_map_parser("Foo=Bar"), Some(("Foo", "Bar", false)));
assert_eq!(
type_map_parser("Foo=Bar,recursive"),
Some(("Foo", "Bar", true))
);
assert_eq!(type_map_parser("Foo=Bar,a"), None);
assert_eq!(type_map_parser("Foo=Bar,a,b,c,recursive"), None);
}
async fn run(args_str: &str) -> color_eyre::Result<String> {
let mut args = vec![
"codegen",
"--file=../artifacts/polkadot_metadata_small.scale",
];
args.extend(args_str.split(' ').filter(|e| !e.is_empty()));
let opts: super::Opts = clap::Parser::try_parse_from(args)?;
let mut output: Vec<u8> = Vec::new();
let r = super::run(opts, &mut output)
.await
.map(|_| String::from_utf8(output).unwrap())?;
Ok(r)
}
#[tokio::test]
async fn invalid_type_paths() {
let valid_type = "sp_runtime::multiaddress::MultiAddress";
let invalid_type = "my_module::MultiAddress";
let valid_cases = [
format!("--derive-for-type {valid_type}=serde::Serialize"),
format!("--attributes-for-type {valid_type}=#[allow(clippy::all)]"),
format!("--substitute-type {valid_type}=::my_crate::MultiAddress"),
];
for case in valid_cases.iter() {
let output = run(case).await;
assert!(output.is_ok());
}
let invalid_cases = [
format!("--derive-for-type {invalid_type}=serde::Serialize"),
format!("--attributes-for-type {invalid_type}=#[allow(clippy::all)]"),
format!("--substitute-type {invalid_type}=my_module::MultiAddress"),
];
for case in invalid_cases.iter() {
let output = run(case).await;
// assert that we make suggestions pointing the user to the valid type
assert!(output.unwrap_err().to_string().contains(valid_type));
}
}
}
+143
View File
@@ -0,0 +1,143 @@
// 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::Parser as ClapParser;
use codec::Decode;
use color_eyre::eyre::WrapErr;
use jsonrpsee::client_transport::ws::Url;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use pezkuwi_subxt_metadata::Metadata;
use pezkuwi_subxt_utils_fetchmetadata::MetadataVersion;
use crate::utils::validate_url_security;
/// Verify metadata compatibility between substrate nodes.
#[derive(Debug, ClapParser)]
pub struct Opts {
/// Urls of the substrate nodes to verify for metadata compatibility.
#[clap(name = "nodes", long, use_value_delimiter = true, value_parser)]
nodes: Vec<Url>,
/// Check the compatibility of metadata for a particular pallet.
///
/// ### Note
/// The validation will omit the full metadata check and focus instead on the pallet.
#[clap(long, value_parser)]
pallet: Option<String>,
/// Specify the metadata version.
///
/// - unstable:
///
/// Use the latest unstable metadata of the node.
///
/// - number
///
/// Use this specific metadata version.
///
/// Defaults to latest.
#[clap(long = "version", default_value = "latest")]
version: MetadataVersion,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
for url in opts.nodes.iter() {
validate_url_security(Some(url), opts.allow_insecure)?;
}
match opts.pallet {
Some(pallet) => {
handle_pallet_metadata(opts.nodes.as_slice(), pallet.as_str(), opts.version, output)
.await
}
None => handle_full_metadata(opts.nodes.as_slice(), opts.version, output).await,
}
}
async fn handle_pallet_metadata(
nodes: &[Url],
name: &str,
version: MetadataVersion,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct CompatibilityPallet {
pallet_present: HashMap<String, Vec<String>>,
pallet_not_found: Vec<String>,
}
let mut compatibility: CompatibilityPallet = Default::default();
for node in nodes.iter() {
let metadata = fetch_runtime_metadata(node.clone(), version).await?;
match metadata.pallet_by_name(name) {
Some(pallet_metadata) => {
let hash = pallet_metadata.hash();
let hex_hash = hex::encode(hash);
writeln!(
output,
"Node {node:?} has pallet metadata hash {hex_hash:?}"
)?;
compatibility
.pallet_present
.entry(hex_hash)
.or_default()
.push(node.to_string());
}
None => {
compatibility.pallet_not_found.push(node.to_string());
}
}
}
writeln!(
output,
"\nCompatible nodes by pallet\n{}",
serde_json::to_string_pretty(&compatibility)
.context("Failed to parse compatibility map")?
)?;
Ok(())
}
async fn handle_full_metadata(
nodes: &[Url],
version: MetadataVersion,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let mut compatibility_map: HashMap<String, Vec<String>> = HashMap::new();
for node in nodes.iter() {
let metadata = fetch_runtime_metadata(node.clone(), version).await?;
let hash = metadata.hasher().hash();
let hex_hash = hex::encode(hash);
writeln!(output, "Node {node:?} has metadata hash {hex_hash:?}",)?;
compatibility_map
.entry(hex_hash)
.or_default()
.push(node.to_string());
}
writeln!(
output,
"\nCompatible nodes\n{}",
serde_json::to_string_pretty(&compatibility_map)
.context("Failed to parse compatibility map")?
)?;
Ok(())
}
async fn fetch_runtime_metadata(
url: Url,
version: MetadataVersion,
) -> color_eyre::Result<Metadata> {
let bytes = pezkuwi_subxt_utils_fetchmetadata::from_url(url, version, None).await?;
let metadata = Metadata::decode(&mut &bytes[..])?;
Ok(metadata)
}
+450
View File
@@ -0,0 +1,450 @@
use clap::Args;
use codec::Decode;
use frame_metadata::RuntimeMetadataPrefixed;
use std::collections::HashMap;
use std::hash::Hash;
use crate::utils::{FileOrUrl, validate_url_security};
use color_eyre::owo_colors::OwoColorize;
use scale_info::Variant;
use scale_info::form::PortableForm;
use pezkuwi_subxt_metadata::{
ConstantMetadata, Metadata, PalletMetadata, RuntimeApiMetadata, StorageEntryMetadata,
};
/// Explore the differences between two nodes
///
/// # Example
/// ```text
/// subxt diff ./artifacts/polkadot_metadata_small.scale ./artifacts/polkadot_metadata_tiny.scale
/// subxt diff ./artifacts/polkadot_metadata_small.scale wss://rpc.polkadot.io:443
/// ```
#[derive(Debug, Args)]
#[command(author, version, about, long_about = None)]
pub struct Opts {
/// metadata file or node URL
metadata_or_url_1: FileOrUrl,
/// metadata file or node URL
metadata_or_url_2: FileOrUrl,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
validate_url_security(opts.metadata_or_url_1.url.as_ref(), opts.allow_insecure)?;
validate_url_security(opts.metadata_or_url_2.url.as_ref(), opts.allow_insecure)?;
let (entry_1_metadata, entry_2_metadata) = get_metadata(&opts).await?;
let metadata_diff = MetadataDiff::construct(&entry_1_metadata, &entry_2_metadata);
if metadata_diff.is_empty() {
writeln!(output, "No difference in metadata found.")?;
return Ok(());
}
if !metadata_diff.pallets.is_empty() {
writeln!(output, "Pallets:")?;
for diff in metadata_diff.pallets {
match diff {
Diff::Added(new) => {
writeln!(output, "{}", format!(" + {}", new.name()).green())?
}
Diff::Removed(old) => {
writeln!(output, "{}", format!(" - {}", old.name()).red())?
}
Diff::Changed { from, to } => {
writeln!(output, "{}", format!(" ~ {}", from.name()).yellow())?;
let pallet_diff = PalletDiff::construct(&from, &to);
if !pallet_diff.calls.is_empty() {
writeln!(output, " Calls:")?;
for diff in pallet_diff.calls {
match diff {
Diff::Added(new) => writeln!(
output,
"{}",
format!(" + {}", &new.name).green()
)?,
Diff::Removed(old) => writeln!(
output,
"{}",
format!(" - {}", &old.name).red()
)?,
Diff::Changed { from, to: _ } => {
writeln!(
output,
"{}",
format!(" ~ {}", &from.name).yellow()
)?;
}
}
}
}
if !pallet_diff.constants.is_empty() {
writeln!(output, " Constants:")?;
for diff in pallet_diff.constants {
match diff {
Diff::Added(new) => writeln!(
output,
"{}",
format!(" + {}", new.name()).green()
)?,
Diff::Removed(old) => writeln!(
output,
"{}",
format!(" - {}", old.name()).red()
)?,
Diff::Changed { from, to: _ } => writeln!(
output,
"{}",
format!(" ~ {}", from.name()).yellow()
)?,
}
}
}
if !pallet_diff.storage_entries.is_empty() {
writeln!(output, " Storage Entries:")?;
for diff in pallet_diff.storage_entries {
match diff {
Diff::Added(new) => writeln!(
output,
"{}",
format!(" + {}", new.name()).green()
)?,
Diff::Removed(old) => writeln!(
output,
"{}",
format!(" - {}", old.name()).red()
)?,
Diff::Changed { from, to } => {
let storage_diff = StorageEntryDiff::construct(
from,
to,
&entry_1_metadata,
&entry_2_metadata,
);
writeln!(
output,
"{}",
format!(
" ~ {} (Changed: {})",
from.name(),
storage_diff.to_strings().join(", ")
)
.yellow()
)?;
}
}
}
}
}
}
}
}
if !metadata_diff.runtime_apis.is_empty() {
writeln!(output, "Runtime APIs:")?;
for diff in metadata_diff.runtime_apis {
match diff {
Diff::Added(new) => {
writeln!(output, "{}", format!(" + {}", new.name()).green())?
}
Diff::Removed(old) => {
writeln!(output, "{}", format!(" - {}", old.name()).red())?
}
Diff::Changed { from, to: _ } => {
writeln!(output, "{}", format!(" ~ {}", from.name()).yellow())?
}
}
}
}
Ok(())
}
struct MetadataDiff<'a> {
pallets: Vec<Diff<PalletMetadata<'a>>>,
runtime_apis: Vec<Diff<RuntimeApiMetadata<'a>>>,
}
impl<'a> MetadataDiff<'a> {
fn construct(metadata_1: &'a Metadata, metadata_2: &'a Metadata) -> MetadataDiff<'a> {
let pallets = pallet_differences(metadata_1, metadata_2);
let runtime_apis = runtime_api_differences(metadata_1, metadata_2);
MetadataDiff {
pallets,
runtime_apis,
}
}
fn is_empty(&self) -> bool {
self.pallets.is_empty() && self.runtime_apis.is_empty()
}
}
#[derive(Default)]
struct PalletDiff<'a> {
calls: Vec<Diff<&'a Variant<PortableForm>>>,
constants: Vec<Diff<&'a ConstantMetadata>>,
storage_entries: Vec<Diff<&'a StorageEntryMetadata>>,
}
impl<'a> PalletDiff<'a> {
fn construct(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> PalletDiff<'a> {
let calls = calls_differences(pallet_metadata_1, pallet_metadata_2);
let constants = constants_differences(pallet_metadata_1, pallet_metadata_2);
let storage_entries = storage_differences(pallet_metadata_1, pallet_metadata_2);
PalletDiff {
calls,
constants,
storage_entries,
}
}
}
struct StorageEntryDiff {
key_different: bool,
value_different: bool,
default_different: bool,
}
impl StorageEntryDiff {
fn construct(
storage_entry_1: &StorageEntryMetadata,
storage_entry_2: &StorageEntryMetadata,
metadata_1: &Metadata,
metadata_2: &Metadata,
) -> Self {
let value_1_ty_id = storage_entry_1.value_ty();
let value_1_hash = metadata_1
.type_hash(value_1_ty_id)
.expect("type is in metadata; qed");
let value_2_ty_id = storage_entry_2.value_ty();
let value_2_hash = metadata_2
.type_hash(value_2_ty_id)
.expect("type is in metadata; qed");
let value_different = value_1_hash != value_2_hash;
let key_parts_same = storage_entry_1.keys().len() == storage_entry_2.keys().len()
&& storage_entry_1
.keys()
.zip(storage_entry_2.keys())
.all(|(a, b)| {
let a_hash = metadata_1.type_hash(a.key_id).expect("type is in metadata");
let b_hash = metadata_2.type_hash(b.key_id).expect("type is in metadata");
a.hasher == b.hasher && a_hash == b_hash
});
let key_different = !key_parts_same;
StorageEntryDiff {
key_different,
value_different,
default_different: storage_entry_1.default_value() != storage_entry_2.default_value(),
}
}
fn to_strings(&self) -> Vec<&str> {
let mut strings = Vec::<&str>::new();
if self.key_different {
strings.push("key type");
}
if self.value_different {
strings.push("value type");
}
if self.default_different {
strings.push("default value");
}
strings
}
}
async fn get_metadata(opts: &Opts) -> color_eyre::Result<(Metadata, Metadata)> {
let bytes = opts.metadata_or_url_1.fetch().await?;
let entry_1_metadata: Metadata =
RuntimeMetadataPrefixed::decode(&mut &bytes[..])?.try_into()?;
let bytes = opts.metadata_or_url_2.fetch().await?;
let entry_2_metadata: Metadata =
RuntimeMetadataPrefixed::decode(&mut &bytes[..])?.try_into()?;
Ok((entry_1_metadata, entry_2_metadata))
}
fn storage_differences<'a>(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> Vec<Diff<&'a StorageEntryMetadata>> {
diff(
pallet_metadata_1
.storage()
.map(|s| s.entries())
.unwrap_or_default(),
pallet_metadata_2
.storage()
.map(|s| s.entries())
.unwrap_or_default(),
|e| {
pallet_metadata_1
.storage_hash(e.name())
.expect("storage entry is in metadata; qed")
},
|e| {
pallet_metadata_2
.storage_hash(e.name())
.expect("storage entry is in metadata; qed")
},
|e| e.name(),
)
}
fn calls_differences<'a>(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> Vec<Diff<&'a Variant<PortableForm>>> {
diff(
pallet_metadata_1.call_variants().unwrap_or_default(),
pallet_metadata_2.call_variants().unwrap_or_default(),
|e| {
pallet_metadata_1
.call_hash(&e.name)
.expect("call is in metadata; qed")
},
|e| {
pallet_metadata_2
.call_hash(&e.name)
.expect("call is in metadata; qed")
},
|e| &e.name,
)
}
fn constants_differences<'a>(
pallet_metadata_1: &'a PalletMetadata<'a>,
pallet_metadata_2: &'a PalletMetadata<'a>,
) -> Vec<Diff<&'a ConstantMetadata>> {
diff(
pallet_metadata_1.constants(),
pallet_metadata_2.constants(),
|e| {
pallet_metadata_1
.constant_hash(e.name())
.expect("constant is in metadata; qed")
},
|e| {
pallet_metadata_2
.constant_hash(e.name())
.expect("constant is in metadata; qed")
},
|e| e.name(),
)
}
fn runtime_api_differences<'a>(
metadata_1: &'a Metadata,
metadata_2: &'a Metadata,
) -> Vec<Diff<RuntimeApiMetadata<'a>>> {
diff(
metadata_1.runtime_api_traits(),
metadata_2.runtime_api_traits(),
RuntimeApiMetadata::hash,
RuntimeApiMetadata::hash,
RuntimeApiMetadata::name,
)
}
fn pallet_differences<'a>(
metadata_1: &'a Metadata,
metadata_2: &'a Metadata,
) -> Vec<Diff<PalletMetadata<'a>>> {
diff(
metadata_1.pallets(),
metadata_2.pallets(),
PalletMetadata::hash,
PalletMetadata::hash,
PalletMetadata::name,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Diff<T> {
Added(T),
Changed { from: T, to: T },
Removed(T),
}
fn diff<T, C: PartialEq, I: Hash + PartialEq + Eq + Ord>(
items_a: impl IntoIterator<Item = T>,
items_b: impl IntoIterator<Item = T>,
hash_fn_a: impl Fn(&T) -> C,
hash_fn_b: impl Fn(&T) -> C,
key_fn: impl Fn(&T) -> I,
) -> Vec<Diff<T>> {
let mut entries: HashMap<I, (Option<T>, Option<T>)> = HashMap::new();
for t1 in items_a {
let key = key_fn(&t1);
let (e1, _) = entries.entry(key).or_default();
*e1 = Some(t1);
}
for t2 in items_b {
let key = key_fn(&t2);
let (e1, e2) = entries.entry(key).or_default();
// skip all entries with the same hash:
if let Some(e1_inner) = e1 {
let e1_hash = hash_fn_a(e1_inner);
let e2_hash = hash_fn_b(&t2);
if e1_hash == e2_hash {
entries.remove(&key_fn(&t2));
continue;
}
}
*e2 = Some(t2);
}
// sort the values by key before returning
let mut diff_vec_with_keys: Vec<_> = entries.into_iter().collect();
diff_vec_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
diff_vec_with_keys
.into_iter()
.map(|(_, tuple)| match tuple {
(None, None) => panic!("At least one value is inserted when the key exists; qed"),
(Some(old), None) => Diff::Removed(old),
(None, Some(new)) => Diff::Added(new),
(Some(old), Some(new)) => Diff::Changed { from: old, to: new },
})
.collect()
}
#[cfg(test)]
mod test {
use crate::commands::diff::{Diff, diff};
#[test]
fn test_diff_fn() {
let old_pallets = [("Babe", 7), ("Claims", 9), ("Balances", 23)];
let new_pallets = [("Claims", 9), ("Balances", 22), ("System", 3), ("NFTs", 5)];
let hash_fn = |e: &(&str, i32)| e.0.len() as i32 * e.1;
let differences = diff(old_pallets, new_pallets, hash_fn, hash_fn, |e| e.0);
let expected_differences = vec![
Diff::Removed(("Babe", 7)),
Diff::Changed {
from: ("Balances", 23),
to: ("Balances", 22),
},
Diff::Added(("NFTs", 5)),
Diff::Added(("System", 3)),
];
assert_eq!(differences, expected_differences);
}
}
+469
View File
@@ -0,0 +1,469 @@
use crate::utils::FileOrUrl;
use crate::utils::validate_url_security;
use clap::{Parser, Subcommand, command};
use codec::Decode;
use color_eyre::eyre::eyre;
use color_eyre::owo_colors::OwoColorize;
use indoc::writedoc;
use std::fmt::Write;
use std::write;
use subxt::Metadata;
use self::pallets::PalletSubcommand;
mod pallets;
mod runtime_apis;
/// Explore pallets, calls, call parameters, storage entries and constants. Also allows for creating (unsigned) extrinsics.
///
/// # Example
///
/// Show the pallets and runtime apis that are available:
///
/// ```text
/// subxt explore --file=polkadot_metadata.scale
/// ```
///
/// ## Pallets
///
/// each pallet has `calls`, `constants`, `storage` and `events` that can be explored.
///
/// ### Calls
///
/// Show the calls in a pallet:
///
/// ```text
/// subxt explore pallet Balances calls
/// ```
///
/// Show the call parameters a call expects:
///
/// ```text
/// subxt explore pallet Balances calls transfer
/// ```
///
/// Create an unsigned extrinsic from a scale value, validate it and output its hex representation
///
/// ```text
/// subxt explore pallet Grandpa calls note_stalled { "delay": 5, "best_finalized_block_number": 5 }
/// # Encoded call data:
/// # 0x2c0411020500000005000000
/// subxt explore pallet Balances calls transfer "{ \"dest\": v\"Raw\"((255, 255, 255)), \"value\": 0 }"
/// # Encoded call data:
/// # 0x24040607020cffffff00
/// ```
///
/// ### Constants
///
/// Show the constants in a pallet:
///
/// ```text
/// subxt explore pallet Balances constants
/// ```
///
/// ### Storage
///
/// Show the storage entries in a pallet
///
/// ```text
/// subxt explore pallet Alliance storage
/// ```
///
/// Show the types and value of a specific storage entry
///
/// ```text
/// subxt explore pallet Alliance storage Announcements [KEY_SCALE_VALUE]
/// ```
///
/// ### Events
///
/// ```text
/// subxt explore pallet Balances events
/// ```
///
/// Show the type of a specific event
///
/// ```text
/// subxt explore pallet Balances events frozen
/// ```
///
/// ## Runtime APIs
/// Show the input and output types of a runtime api method.
/// In this example "core" is the name of the runtime api and "version" is a method on it:
///
/// ```text
/// subxt explore api core version
/// ```
///
/// Execute a runtime API call with the `--execute` (`-e`) flag, to see the return value.
/// For example here we get the "version", via the "core" runtime API from the connected node:
///
/// ```text
/// subxt explore api core version --execute
/// ```
///
#[derive(Debug, Parser)]
pub struct Opts {
#[command(flatten)]
file_or_url: FileOrUrl,
#[command(subcommand)]
subcommand: Option<PalletOrRuntimeApi>,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
#[derive(Debug, Subcommand)]
pub enum PalletOrRuntimeApi {
Pallet(PalletOpts),
Api(RuntimeApiOpts),
}
#[derive(Debug, Parser)]
pub struct PalletOpts {
pub name: Option<String>,
#[command(subcommand)]
pub subcommand: Option<PalletSubcommand>,
}
#[derive(Debug, Parser)]
pub struct RuntimeApiOpts {
pub name: Option<String>,
#[clap(required = false)]
pub method: Option<String>,
#[clap(long, short, action)]
pub execute: bool,
#[clap(required = false)]
trailing_args: Vec<String>,
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
// get the metadata
let file_or_url = opts.file_or_url;
let bytes = file_or_url.fetch().await?;
let metadata = Metadata::decode(&mut &bytes[..])?;
let pallet_placeholder = "<PALLET>".blue();
let runtime_api_placeholder = "<RUNTIME_API>".blue();
// if no pallet/runtime_api specified, show user the pallets/runtime_apis to choose from:
let Some(pallet_or_runtime_api) = opts.subcommand else {
let pallets = pallets_as_string(&metadata);
let runtime_apis = runtime_apis_as_string(&metadata);
writedoc! {output, "
Usage:
subxt explore pallet {pallet_placeholder}
explore a specific pallet
subxt explore api {runtime_api_placeholder}
explore a specific runtime api
{pallets}
{runtime_apis}
"}?;
return Ok(());
};
match pallet_or_runtime_api {
PalletOrRuntimeApi::Pallet(opts) => {
let Some(name) = opts.name else {
let pallets = pallets_as_string(&metadata);
writedoc! {output, "
Usage:
subxt explore pallet {pallet_placeholder}
explore a specific pallet
{pallets}
"}?;
return Ok(());
};
if let Some(pallet) = metadata
.pallets()
.find(|e| e.name().eq_ignore_ascii_case(&name))
{
pallets::run(opts.subcommand, pallet, &metadata, file_or_url, output).await
} else {
Err(eyre!(
"pallet \"{name}\" not found in metadata!\n{}",
pallets_as_string(&metadata),
))
}
}
PalletOrRuntimeApi::Api(opts) => {
let Some(name) = opts.name else {
let runtime_apis = runtime_apis_as_string(&metadata);
writedoc! {output, "
Usage:
subxt explore api {runtime_api_placeholder}
explore a specific runtime api
{runtime_apis}
"}?;
return Ok(());
};
if let Some(runtime_api) = metadata
.runtime_api_traits()
.find(|e| e.name().eq_ignore_ascii_case(&name))
{
runtime_apis::run(
opts.method,
opts.execute,
opts.trailing_args,
runtime_api,
&metadata,
file_or_url,
output,
)
.await
} else {
Err(eyre!(
"runtime api \"{name}\" not found in metadata!\n{}",
runtime_apis_as_string(&metadata),
))
}
}
}
}
fn pallets_as_string(metadata: &Metadata) -> String {
let pallet_placeholder = "<PALLET>".blue();
if metadata.pallets().len() == 0 {
format!("There are no {pallet_placeholder}'s available.")
} else {
let mut output = format!("Available {pallet_placeholder}'s are:");
let mut strings: Vec<_> = metadata.pallets().map(|p| p.name()).collect();
strings.sort();
for pallet in strings {
write!(output, "\n {pallet}").unwrap();
}
output
}
}
pub fn runtime_apis_as_string(metadata: &Metadata) -> String {
let runtime_api_placeholder = "<RUNTIME_API>".blue();
if metadata.runtime_api_traits().len() == 0 {
format!("There are no {runtime_api_placeholder}'s available.")
} else {
let mut output = format!("Available {runtime_api_placeholder}'s are:");
let mut strings: Vec<_> = metadata.runtime_api_traits().map(|p| p.name()).collect();
strings.sort();
for api in strings {
write!(output, "\n {api}").unwrap();
}
output
}
}
#[cfg(test)]
pub mod tests {
use indoc::formatdoc;
use pretty_assertions::assert_eq;
use super::Opts;
async fn run(cli_command: &str) -> color_eyre::Result<String> {
let mut args = vec!["explore"];
let mut split: Vec<&str> = cli_command.split(' ').filter(|e| !e.is_empty()).collect();
args.append(&mut split);
let opts: Opts = clap::Parser::try_parse_from(args)?;
let mut output: Vec<u8> = Vec::new();
let r = super::run(opts, &mut output)
.await
.map(|_| String::from_utf8(output).unwrap())?;
Ok(r)
}
trait StripAnsi: ToString {
fn strip_ansi(&self) -> String {
let bytes = strip_ansi_escapes::strip(self.to_string().as_bytes());
String::from_utf8(bytes).unwrap()
}
}
impl<T: ToString> StripAnsi for T {}
macro_rules! assert_eq_start {
($a:expr, $b:expr) => {
assert_eq!(&$a[0..$b.len()], &$b[..]);
};
}
async fn run_against_file(cli_command: &str) -> color_eyre::Result<String> {
run(&format!(
"--file=../artifacts/polkadot_metadata_small.scale {cli_command}"
))
.await
}
#[tokio::test]
async fn test_commands() {
// shows pallets and runtime apis:
let output = run_against_file("").await.unwrap().strip_ansi();
let expected_output = formatdoc! {
"Usage:
subxt explore pallet <PALLET>
explore a specific pallet
subxt explore api <RUNTIME_API>
explore a specific runtime api
Available <PALLET>'s are:
Balances
Multisig
ParaInherent
System
Timestamp
Available <RUNTIME_API>'s are:
AccountNonceApi
AuthorityDiscoveryApi
BabeApi
BeefyApi
BeefyMmrApi
BlockBuilder
Core
DryRunApi
GenesisBuilder
GrandpaApi
LocationToAccountApi
Metadata
MmrApi
OffchainWorkerApi
ParachainHost
SessionKeys
TaggedTransactionQueue
TransactionPaymentApi
TrustedQueryApi
XcmPaymentApi
"};
assert_eq!(output, expected_output);
// if incorrect pallet, error:
let output = run_against_file("abc123").await;
assert!(output.is_err());
// if correct pallet, show options (calls, constants, storage)
let output = run_against_file("pallet Balances")
.await
.unwrap()
.strip_ansi();
let expected_output = formatdoc! {"
Usage:
subxt explore pallet Balances calls
explore the calls that can be made into a pallet
subxt explore pallet Balances constants
explore the constants of a pallet
subxt explore pallet Balances storage
explore the storage values of a pallet
subxt explore pallet Balances events
explore the events of a pallet
"};
assert_eq!(output, expected_output);
// check that exploring calls, storage entries and constants is possible:
let output = run_against_file("pallet Balances calls")
.await
.unwrap()
.strip_ansi();
let start = formatdoc! {"
Usage:
subxt explore pallet Balances calls <CALL>
explore a specific call of this pallet
Available <CALL>'s in the \"Balances\" pallet:"};
assert_eq_start!(output, start);
let output = run_against_file("pallet Balances storage")
.await
.unwrap()
.strip_ansi();
let start = formatdoc! {"
Usage:
subxt explore pallet Balances storage <STORAGE_ENTRY>
explore a specific storage entry of this pallet
Available <STORAGE_ENTRY>'s in the \"Balances\" pallet:
"};
assert_eq_start!(output, start);
let output = run_against_file("pallet Balances constants")
.await
.unwrap()
.strip_ansi();
let start = formatdoc! {"
Usage:
subxt explore pallet Balances constants <CONSTANT>
explore a specific constant of this pallet
Available <CONSTANT>'s in the \"Balances\" pallet:
"};
assert_eq_start!(output, start);
let output = run_against_file("pallet Balances events")
.await
.unwrap()
.strip_ansi();
let start = formatdoc! {"
Usage:
subxt explore pallet Balances events <EVENT>
explore a specific event of this pallet
Available <EVENT>'s in the \"Balances\" pallet:
"};
assert_eq_start!(output, start);
// check that invalid subcommands don't work:
let output = run_against_file("pallet Balances abc123").await;
assert!(output.is_err());
// check that we can explore a certain call:
let output = run_against_file("pallet Balances calls transfer_keep_alive")
.await
.unwrap()
.strip_ansi();
// Note: at some point we want to switch to new metadata in the artifacts folder which has e.g. transfer_keep_alive instead of transfer.
let start = formatdoc! {"
Usage:
subxt explore pallet Balances calls transfer_keep_alive <SCALE_VALUE>
construct the call by providing a valid argument
"};
assert_eq_start!(output, start);
// check that we can see methods of a runtime api:
let output = run_against_file("api metadata").await.unwrap().strip_ansi();
let start = formatdoc! {"
Description:
The `Metadata` api trait that returns metadata for the runtime.
Usage:
subxt explore api Metadata <METHOD>
explore a specific runtime api method
Available <METHOD>'s available for the \"Metadata\" runtime api:
"};
assert_eq_start!(output, start);
}
#[tokio::test]
async fn insecure_urls_get_denied() {
// Connection should work fine:
run("--url wss://rpc.polkadot.io:443").await.unwrap();
// Errors, because the --allow-insecure is not set:
assert!(
run("--url ws://rpc.polkadot.io:443")
.await
.unwrap_err()
.to_string()
.contains("is not secure")
);
// This checks, that we never prevent (insecure) requests to localhost, even if the `--allow-insecure` flag is not set.
// It errors, because there is no node running locally, which results in the "Request error".
assert!(
run("--url ws://localhost")
.await
.unwrap_err()
.to_string()
.contains("Request error")
);
}
}
@@ -0,0 +1,171 @@
use clap::Args;
use color_eyre::eyre::eyre;
use color_eyre::owo_colors::OwoColorize;
use indoc::{formatdoc, writedoc};
use scale_info::form::PortableForm;
use scale_info::{PortableRegistry, Type, TypeDef, TypeDefVariant};
use scale_value::{Composite, ValueDef};
use std::str::FromStr;
use subxt::tx;
use subxt::utils::H256;
use subxt::{
OfflineClient,
config::SubstrateConfig,
metadata::{Metadata, PalletMetadata},
};
use crate::utils::{
Indent, SyntaxHighlight, fields_composite_example, fields_description,
parse_string_into_scale_value,
};
#[derive(Debug, Clone, Args)]
pub struct CallsSubcommand {
call: Option<String>,
#[clap(required = false)]
trailing_args: Vec<String>,
}
pub fn explore_calls(
command: CallsSubcommand,
pallet_metadata: PalletMetadata,
metadata: &Metadata,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name();
// get the enum that stores the possible calls:
let (calls_enum_type_def, _calls_enum_type) =
get_calls_enum_type(pallet_metadata, metadata.types())?;
let usage = || {
let calls = calls_to_string(calls_enum_type_def, pallet_name);
formatdoc! {"
Usage:
subxt explore pallet {pallet_name} calls <CALL>
explore a specific call of this pallet
{calls}
"}
};
// if no call specified, show user the calls to choose from:
let Some(call_name) = command.call else {
writeln!(output, "{}", usage())?;
return Ok(());
};
// if specified call is wrong, show user the calls to choose from (but this time as an error):
let Some(call) = calls_enum_type_def
.variants
.iter()
.find(|variant| variant.name.eq_ignore_ascii_case(&call_name))
else {
return Err(eyre!(
"\"{call_name}\" call not found in \"{pallet_name}\" pallet!\n\n{}",
usage()
));
};
// collect all the trailing arguments into a single string that is later into a scale_value::Value
let trailing_args = command.trailing_args.join(" ");
// if no trailing arguments specified show user the expected type of arguments with examples:
if trailing_args.is_empty() {
let fields: Vec<(Option<&str>, u32)> = call
.fields
.iter()
.map(|f| (f.name.as_deref(), f.ty.id))
.collect();
let type_description = fields_description(&fields, &call.name, metadata.types()).indent(4);
let fields_example =
fields_composite_example(call.fields.iter().map(|e| e.ty.id), metadata.types())
.indent(4)
.highlight();
let scale_value_placeholder = "<SCALE_VALUE>".blue();
writedoc! {output, "
Usage:
subxt explore pallet {pallet_name} calls {call_name} {scale_value_placeholder}
construct the call by providing a valid argument
The call expects a {scale_value_placeholder} with this shape:
{type_description}
For example you could provide this {scale_value_placeholder}:
{fields_example}
"}?;
return Ok(());
}
// parse scale_value from trailing arguments and try to create an unsigned extrinsic with it:
let value = parse_string_into_scale_value(&trailing_args)?;
let value_as_composite = value_into_composite(value);
let offline_client = mocked_offline_client(metadata.clone());
let payload = tx::dynamic(pallet_name, call_name, value_as_composite);
let unsigned_extrinsic = offline_client.tx().create_unsigned(&payload)?;
let hex_bytes = format!("0x{}", hex::encode(unsigned_extrinsic.encoded()));
writedoc! {output, "
Encoded call data:
{hex_bytes}
"}?;
Ok(())
}
fn calls_to_string(pallet_calls: &TypeDefVariant<PortableForm>, pallet_name: &str) -> String {
if pallet_calls.variants.is_empty() {
return format!("No <CALL>'s available in the \"{pallet_name}\" pallet.");
}
let mut output = format!("Available <CALL>'s in the \"{pallet_name}\" pallet:");
let mut strings: Vec<_> = pallet_calls.variants.iter().map(|c| &c.name).collect();
strings.sort();
for variant in strings {
output.push_str("\n ");
output.push_str(variant);
}
output
}
fn get_calls_enum_type<'a>(
pallet: PalletMetadata,
registry: &'a PortableRegistry,
) -> color_eyre::Result<(&'a TypeDefVariant<PortableForm>, &'a Type<PortableForm>)> {
let call_ty = pallet
.call_ty_id()
.ok_or(eyre!("The \"{}\" pallet has no calls.", pallet.name()))?;
let calls_enum_type = registry
.resolve(call_ty)
.ok_or(eyre!("calls type with id {} not found.", call_ty))?;
// should always be a variant type, where each variant corresponds to one call.
let TypeDef::Variant(calls_enum_type_def) = &calls_enum_type.type_def else {
return Err(eyre!("calls type is not a variant"));
};
Ok((calls_enum_type_def, calls_enum_type))
}
/// The specific values used for construction do not matter too much, we just need any OfflineClient to create unsigned extrinsics
fn mocked_offline_client(metadata: Metadata) -> OfflineClient<SubstrateConfig> {
let genesis_hash =
H256::from_str("91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3")
.expect("Valid hash; qed");
let runtime_version = subxt::client::RuntimeVersion {
spec_version: 9370,
transaction_version: 20,
};
OfflineClient::<SubstrateConfig>::new(genesis_hash, runtime_version, metadata)
}
/// composites stay composites, all other types are converted into a 1-fielded unnamed composite
fn value_into_composite(value: scale_value::Value) -> scale_value::Composite<()> {
match value.value {
ValueDef::Composite(composite) => composite,
_ => Composite::Unnamed(vec![value]),
}
}
@@ -0,0 +1,96 @@
use clap::Args;
use color_eyre::eyre::eyre;
use indoc::{formatdoc, writedoc};
use scale_typegen_description::type_description;
use subxt::metadata::{Metadata, PalletMetadata};
use crate::utils::{Indent, SyntaxHighlight, first_paragraph_of_docs, format_scale_value};
#[derive(Debug, Clone, Args)]
pub struct ConstantsSubcommand {
constant: Option<String>,
}
pub fn explore_constants(
command: ConstantsSubcommand,
pallet_metadata: PalletMetadata,
metadata: &Metadata,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name();
let usage = || {
let constants = constants_to_string(pallet_metadata, pallet_name);
formatdoc! {"
Usage:
subxt explore pallet {pallet_name} constants <CONSTANT>
explore a specific constant of this pallet
{constants}
"}
};
let Some(constant_name) = command.constant else {
writeln!(output, "{}", usage())?;
return Ok(());
};
// if specified constant is wrong, show user the constants to choose from (but this time as an error):
let Some(constant) = pallet_metadata
.constants()
.find(|constant| constant.name().eq_ignore_ascii_case(&constant_name))
else {
let err = eyre!(
"constant \"{constant_name}\" not found in \"{pallet_name}\" pallet!\n\n{}",
usage()
);
return Err(err);
};
// docs
let doc_string = first_paragraph_of_docs(constant.docs()).indent(4);
if !doc_string.is_empty() {
writedoc! {output, "
Description:
{doc_string}
"}?;
}
// shape
let type_description = type_description(constant.ty(), metadata.types(), true)
.expect("No Type Description")
.indent(4)
.highlight();
// value
let value =
scale_value::scale::decode_as_type(&mut constant.value(), constant.ty(), metadata.types())?;
let value = format_scale_value(&value).indent(4);
writedoc!(
output,
"
The constant has the following shape:
{type_description}
The value of the constant is:
{value}
"
)?;
Ok(())
}
fn constants_to_string(pallet_metadata: PalletMetadata, pallet_name: &str) -> String {
if pallet_metadata.constants().len() == 0 {
return format!("No <CONSTANT>'s available in the \"{pallet_name}\" pallet.");
}
let mut output = format!("Available <CONSTANT>'s in the \"{pallet_name}\" pallet:");
let mut strings: Vec<_> = pallet_metadata.constants().map(|c| c.name()).collect();
strings.sort();
for constant in strings {
output.push_str("\n ");
output.push_str(constant);
}
output
}
@@ -0,0 +1,88 @@
use clap::Args;
use color_eyre::eyre::eyre;
use indoc::{formatdoc, writedoc};
use scale_info::{Variant, form::PortableForm};
use subxt::metadata::{Metadata, PalletMetadata};
use crate::utils::{Indent, fields_description, first_paragraph_of_docs};
#[derive(Debug, Clone, Args)]
pub struct EventsSubcommand {
event: Option<String>,
}
pub fn explore_events(
command: EventsSubcommand,
pallet_metadata: PalletMetadata,
metadata: &Metadata,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name();
let event_variants = pallet_metadata.event_variants().unwrap_or(&[]);
let usage = || {
let events = events_to_string(event_variants, pallet_name);
formatdoc! {"
Usage:
subxt explore pallet {pallet_name} events <EVENT>
explore a specific event of this pallet
{events}
"}
};
let Some(event_name) = command.event else {
writeln!(output, "{}", usage())?;
return Ok(());
};
// if specified event is wrong, show user the events to choose from (but this time as an error):
let Some(event) = event_variants
.iter()
.find(|event| event.name.eq_ignore_ascii_case(&event_name))
else {
let err = eyre!(
"event \"{event_name}\" not found in \"{pallet_name}\" pallet!\n\n{}",
usage()
);
return Err(err);
};
let doc_string = first_paragraph_of_docs(&event.docs).indent(4);
if !doc_string.is_empty() {
writedoc! {output, "
Description:
{doc_string}
"}?;
}
let fields: Vec<(Option<&str>, u32)> = event
.fields
.iter()
.map(|f| (f.name.as_deref(), f.ty.id))
.collect();
let type_description = fields_description(&fields, &event.name, metadata.types()).indent(4);
writedoc!(
output,
"
The event has the following shape:
{type_description}
"
)?;
Ok(())
}
fn events_to_string(event_variants: &[Variant<PortableForm>], pallet_name: &str) -> String {
if event_variants.is_empty() {
return format!("No <EVENT>'s available in the \"{pallet_name}\" pallet.");
}
let mut output = format!("Available <EVENT>'s in the \"{pallet_name}\" pallet:");
let mut strings: Vec<_> = event_variants.iter().map(|c| &c.name).collect();
strings.sort();
for event in strings {
output.push_str("\n ");
output.push_str(event);
}
output
}
@@ -0,0 +1,80 @@
use clap::Subcommand;
use indoc::writedoc;
use subxt::Metadata;
use pezkuwi_subxt_metadata::PalletMetadata;
use crate::utils::{FileOrUrl, Indent, first_paragraph_of_docs};
use self::{
calls::CallsSubcommand,
constants::ConstantsSubcommand,
events::{EventsSubcommand, explore_events},
storage::StorageSubcommand,
};
use calls::explore_calls;
use constants::explore_constants;
use storage::explore_storage;
mod calls;
mod constants;
mod events;
mod storage;
#[derive(Debug, Clone, Subcommand)]
pub enum PalletSubcommand {
Calls(CallsSubcommand),
Constants(ConstantsSubcommand),
Storage(StorageSubcommand),
Events(EventsSubcommand),
}
pub async fn run<'a>(
subcommand: Option<PalletSubcommand>,
pallet_metadata: PalletMetadata<'a>,
metadata: &'a Metadata,
file_or_url: FileOrUrl,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name();
let Some(subcommand) = subcommand else {
let docs_string = first_paragraph_of_docs(pallet_metadata.docs()).indent(4);
if !docs_string.is_empty() {
writedoc! {output, "
Description:
{docs_string}
"}?;
}
writedoc! {output, "
Usage:
subxt explore pallet {pallet_name} calls
explore the calls that can be made into a pallet
subxt explore pallet {pallet_name} constants
explore the constants of a pallet
subxt explore pallet {pallet_name} storage
explore the storage values of a pallet
subxt explore pallet {pallet_name} events
explore the events of a pallet
"}?;
return Ok(());
};
match subcommand {
PalletSubcommand::Calls(command) => {
explore_calls(command, pallet_metadata, metadata, output)
}
PalletSubcommand::Constants(command) => {
explore_constants(command, pallet_metadata, metadata, output)
}
PalletSubcommand::Storage(command) => {
// if the metadata came from some url, we use that same url to make storage calls against.
explore_storage(command, pallet_metadata, metadata, file_or_url, output).await
}
PalletSubcommand::Events(command) => {
explore_events(command, pallet_metadata, metadata, output)
}
}
}
@@ -0,0 +1,236 @@
use clap::Args;
use color_eyre::{eyre::bail, owo_colors::OwoColorize};
use indoc::{formatdoc, writedoc};
use scale_typegen_description::type_description;
use scale_value::Value;
use std::fmt::Write;
use std::write;
use subxt::metadata::{Metadata, PalletMetadata, StorageMetadata};
use crate::utils::{
FileOrUrl, Indent, SyntaxHighlight, create_client, first_paragraph_of_docs,
parse_string_into_scale_value, type_example,
};
#[derive(Debug, Clone, Args)]
pub struct StorageSubcommand {
storage_entry: Option<String>,
#[clap(long, short, action)]
execute: bool,
#[clap(required = false)]
trailing_args: Vec<String>,
}
pub async fn explore_storage(
command: StorageSubcommand,
pallet_metadata: PalletMetadata<'_>,
metadata: &Metadata,
file_or_url: FileOrUrl,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name();
let trailing_args = command.trailing_args;
let Some(storage_metadata) = pallet_metadata.storage() else {
writeln!(
output,
"The \"{pallet_name}\" pallet has no storage entries."
)?;
return Ok(());
};
let storage_entry_placeholder = "<STORAGE_ENTRY>".blue();
let usage = || {
let storage_entries = storage_entries_string(storage_metadata, pallet_name);
formatdoc! {"
Usage:
subxt explore pallet {pallet_name} storage {storage_entry_placeholder}
explore a specific storage entry of this pallet
{storage_entries}
"}
};
// if no storage entry specified, show user the calls to choose from:
let Some(entry_name) = command.storage_entry else {
writeln!(output, "{}", usage())?;
return Ok(());
};
// if specified call storage entry wrong, show user the storage entries to choose from (but this time as an error):
let Some(storage) = storage_metadata
.entries()
.iter()
.find(|entry| entry.name().eq_ignore_ascii_case(&entry_name))
else {
bail!(
"Storage entry \"{entry_name}\" not found in \"{pallet_name}\" pallet!\n\n{}",
usage()
);
};
let return_ty_id = storage.value_ty();
let key_value_placeholder = "<KEY_VALUE>".blue();
let docs_string = first_paragraph_of_docs(storage.docs()).indent(4);
if !docs_string.is_empty() {
writedoc! {output, "
Description:
{docs_string}
"}?;
}
// only inform user about usage if `execute` flag not provided
if !command.execute {
writedoc! {output, "
Usage:
subxt explore pallet {pallet_name} storage {entry_name} --execute {key_value_placeholder}
retrieve a value from storage
"}?;
}
let return_ty_description = type_description(return_ty_id, metadata.types(), true)
.expect("No type Description")
.indent(4)
.highlight();
writedoc! {output, "
The storage entry has the following shape:
{return_ty_description}
"}?;
// inform user about shape of the key if it can be provided:
let storage_keys = storage.keys().collect::<Vec<_>>();
if !storage_keys.is_empty() {
let key_ty_description = format!(
"({})",
storage_keys
.iter()
.map(|key| type_description(key.key_id, metadata.types(), true)
.expect("No type Description"))
.collect::<Vec<_>>()
.join(", ")
)
.indent(4)
.highlight();
let key_ty_example = format!(
"({})",
storage_keys
.iter()
.map(|key| type_example(key.key_id, metadata.types()).to_string())
.collect::<Vec<_>>()
.join(", ")
)
.indent(4)
.highlight();
writedoc! {output, "
The {key_value_placeholder} has the following shape:
{key_ty_description}
For example you could provide this {key_value_placeholder}:
{key_ty_example}
"}?;
} else {
writedoc! {output,"
Can be accessed without providing a {key_value_placeholder}.
"}?;
}
// if `--execute`/`-e` flag is set, try to execute the storage entry request
if !command.execute {
return Ok(());
}
let storage_entry_keys: Vec<Value> = match (!trailing_args.is_empty(), !storage_keys.is_empty())
{
// keys provided, keys not needed.
(true, false) => {
let trailing_args_str = trailing_args.join(" ");
let warning = format!(
"Warning: You submitted one or more keys \"{trailing_args_str}\", but no key is needed. To access the storage value, please do not provide any keys."
);
writeln!(output, "{}", warning.yellow())?;
return Ok(());
}
// Keys not provided, keys needed.
(false, true) => {
// just return. The user was instructed above how to provide a value if they want to.
return Ok(());
}
// Keys not provided, keys not needed.
(false, false) => vec![],
// Keys provided, keys needed.
(true, true) => {
// Each trailing arg is parsed into its own value, to be provided as a separate storage key.
let values = trailing_args
.iter()
.map(|arg| parse_string_into_scale_value(arg))
.collect::<color_eyre::Result<Vec<_>>>()?;
// We do this just to print them out.
let values_str = values
.iter()
.map(|v| v.to_string().highlight())
.collect::<Vec<_>>()
.join("\n");
let value_str = values_str.indent(4);
writedoc! {output, "
You submitted the following {key_value_placeholder}:
{value_str}
"}?;
values
}
};
// construct the client:
let client = create_client(&file_or_url).await?;
// Fetch the value:
let storage_value = client
.storage()
.at_latest()
.await?
.fetch((pallet_name, storage.name()), storage_entry_keys)
.await?
.decode()?;
let value = storage_value.to_string().highlight();
writedoc! {output, "
The value of the storage entry is:
{value}
"}?;
Ok(())
}
fn storage_entries_string(storage_metadata: &StorageMetadata, pallet_name: &str) -> String {
let storage_entry_placeholder = "<STORAGE_ENTRY>".blue();
if storage_metadata.entries().is_empty() {
format!("No {storage_entry_placeholder}'s available in the \"{pallet_name}\" pallet.")
} else {
let mut output =
format!("Available {storage_entry_placeholder}'s in the \"{pallet_name}\" pallet:");
let mut strings: Vec<_> = storage_metadata
.entries()
.iter()
.map(|s| s.name())
.collect();
strings.sort();
for entry in strings {
write!(output, "\n {entry}").unwrap();
}
output
}
}
@@ -0,0 +1,204 @@
use crate::utils::{
FileOrUrl, Indent, SyntaxHighlight, create_client, fields_composite_example,
fields_description, first_paragraph_of_docs, parse_string_into_scale_value,
};
use color_eyre::{
eyre::{bail, eyre},
owo_colors::OwoColorize,
};
use indoc::{formatdoc, writedoc};
use scale_typegen_description::type_description;
use scale_value::Value;
use subxt::{
Metadata,
ext::{scale_decode::DecodeAsType, scale_encode::EncodeAsType},
};
use pezkuwi_subxt_metadata::RuntimeApiMetadata;
/// Runs for a specified runtime API trait.
/// Cases to consider:
/// ```text
/// method is:
/// None => Show pallet docs + available methods
/// Some (invalid) => Show Error + available methods
/// Some (valid) => Show method docs + output type description
/// execute is:
/// false => Show input type description + Example Value
/// true => validate (trailing args + build node connection)
/// validation is:
/// Err => Show Error
/// Ok => Make a runtime api call with the provided args.
/// response is:
/// Err => Show Error
/// Ok => Show the result
/// ```
pub async fn run<'a>(
method: Option<String>,
execute: bool,
trailing_args: Vec<String>,
runtime_api_metadata: RuntimeApiMetadata<'a>,
metadata: &'a Metadata,
file_or_url: FileOrUrl,
output: &mut impl std::io::Write,
) -> color_eyre::Result<()> {
let api_name = runtime_api_metadata.name();
let usage = || {
let methods = methods_to_string(&runtime_api_metadata);
formatdoc! {"
Usage:
subxt explore api {api_name} <METHOD>
explore a specific runtime api method
{methods}
"}
};
// If method is None: Show pallet docs + available methods
let Some(method_name) = method else {
let doc_string = first_paragraph_of_docs(runtime_api_metadata.docs()).indent(4);
if !doc_string.is_empty() {
writedoc! {output, "
Description:
{doc_string}
"}?;
}
writeln!(output, "{}", usage())?;
return Ok(());
};
// If method is invalid: Show Error + available methods
let Some(method) = runtime_api_metadata
.methods()
.find(|e| e.name().eq_ignore_ascii_case(&method_name))
else {
return Err(eyre!(
"\"{method_name}\" method not found for \"{method_name}\" runtime api!\n\n{}",
usage()
));
};
// redeclare to not use the wrong capitalization of the input from here on:
let method_name = method.name();
// Method is valid. Show method docs + output type description
let doc_string = first_paragraph_of_docs(method.docs()).indent(4);
if !doc_string.is_empty() {
writedoc! {output, "
Description:
{doc_string}
"}?;
}
let input_value_placeholder = "<INPUT_VALUE>".blue();
// Output type description
let input_values = || {
if method.inputs().len() == 0 {
return format!("The method does not require an {input_value_placeholder}");
}
let fields: Vec<(Option<&str>, u32)> =
method.inputs().map(|f| (Some(&*f.name), f.id)).collect();
let fields_description =
fields_description(&fields, method.name(), metadata.types()).indent(4);
let fields_example =
fields_composite_example(method.inputs().map(|e| e.id), metadata.types())
.indent(4)
.highlight();
formatdoc! {"
The method expects an {input_value_placeholder} with this shape:
{fields_description}
For example you could provide this {input_value_placeholder}:
{fields_example}"}
};
let execute_usage = || {
let output = type_description(method.output_ty(), metadata.types(), true)
.expect("No Type Description")
.indent(4)
.highlight();
let input = input_values();
formatdoc! {"
Usage:
subxt explore api {api_name} {method_name} --execute {input_value_placeholder}
make a runtime api request
The Output of this method has the following shape:
{output}
{input}"}
};
writeln!(output, "{}", execute_usage())?;
if !execute {
return Ok(());
}
if trailing_args.len() != method.inputs().len() {
bail!(
"The number of trailing arguments you provided after the `execute` flag does not match the expected number of inputs!\n{}",
execute_usage()
);
}
// encode each provided input as bytes of the correct type:
let args_data: Vec<Value> = method
.inputs()
.zip(trailing_args.iter())
.map(|(ty, arg)| {
let value = parse_string_into_scale_value(arg)?;
let value_str = value.indent(4);
// convert to bytes:
writedoc! {output, "
You submitted the following {input_value_placeholder}:
{value_str}
"}?;
// encode, then decode. This ensures that the scale value is of the correct shape for the param:
let bytes = value.encode_as_type(ty.id, metadata.types())?;
let value = Value::decode_as_type(&mut &bytes[..], ty.id, metadata.types())?;
Ok(value)
})
.collect::<color_eyre::Result<Vec<Value>>>()?;
let method_call =
subxt::dynamic::runtime_api_call::<_, Value>(api_name, method.name(), args_data);
let client = create_client(&file_or_url).await?;
let output_value = client
.runtime_api()
.at_latest()
.await?
.call(method_call)
.await?;
let output_value = output_value.to_string().highlight();
writedoc! {output, "
Returned value:
{output_value}
"}?;
Ok(())
}
fn methods_to_string(runtime_api_metadata: &RuntimeApiMetadata<'_>) -> String {
let api_name = runtime_api_metadata.name();
if runtime_api_metadata.methods().len() == 0 {
return format!("No <METHOD>'s available for the \"{api_name}\" runtime api.");
}
let mut output = format!("Available <METHOD>'s available for the \"{api_name}\" runtime api:");
let mut strings: Vec<_> = runtime_api_metadata.methods().map(|e| e.name()).collect();
strings.sort();
for variant in strings {
output.push_str("\n ");
output.push_str(variant);
}
output
}
+98
View File
@@ -0,0 +1,98 @@
// 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 crate::utils::{FileOrUrl, validate_url_security};
use clap::Parser as ClapParser;
use codec::{Decode, Encode};
use color_eyre::eyre::{self, bail};
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
use std::{io::Write, path::PathBuf};
use pezkuwi_subxt_utils_stripmetadata::StripMetadata;
/// Download metadata from a substrate node, for use with `subxt` codegen.
#[derive(Debug, ClapParser)]
pub struct Opts {
#[command(flatten)]
file_or_url: FileOrUrl,
/// The format of the metadata to display: `json`, `hex` or `bytes`.
#[clap(long, short, default_value = "bytes")]
format: String,
/// Generate a subset of the metadata that contains only the
/// types needed to represent the provided pallets.
///
/// The returned metadata is updated to the latest available version
/// when using the option.
#[clap(long, use_value_delimiter = true, value_parser)]
pallets: Option<Vec<String>>,
/// Generate a subset of the metadata that contains only the
/// runtime APIs needed.
///
/// The returned metadata is updated to the latest available version
/// when using the option.
#[clap(long, use_value_delimiter = true, value_parser)]
runtime_apis: Option<Vec<String>>,
/// Write the output of the metadata command to the provided file path.
#[clap(long, short, value_parser)]
pub output_file: Option<PathBuf>,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
pub async fn run(opts: Opts, output: &mut impl Write) -> color_eyre::Result<()> {
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
let bytes = opts.file_or_url.fetch().await?;
let mut metadata = RuntimeMetadataPrefixed::decode(&mut &bytes[..])?;
// Strip pallets or runtime APIs if names are provided:
if opts.pallets.is_some() || opts.runtime_apis.is_some() {
let keep_pallets_fn: Box<dyn Fn(&str) -> bool> = match opts.pallets.as_ref() {
Some(pallets) => Box::new(|name| pallets.iter().any(|p| &**p == name)),
None => Box::new(|_| true),
};
let keep_runtime_apis_fn: Box<dyn Fn(&str) -> bool> = match opts.runtime_apis.as_ref() {
Some(apis) => Box::new(|name| apis.iter().any(|p| &**p == name)),
None => Box::new(|_| true),
};
match &mut metadata.1 {
RuntimeMetadata::V14(md) => md.strip_metadata(keep_pallets_fn, keep_runtime_apis_fn),
RuntimeMetadata::V15(md) => md.strip_metadata(keep_pallets_fn, keep_runtime_apis_fn),
RuntimeMetadata::V16(md) => md.strip_metadata(keep_pallets_fn, keep_runtime_apis_fn),
_ => {
bail!(
"Unsupported metadata version for stripping pallets/runtime APIs: V14, V15 or V16 metadata is expected."
)
}
}
}
let mut output: Box<dyn Write> = match opts.output_file {
Some(path) => Box::new(std::fs::File::create(path)?),
None => Box::new(output),
};
match opts.format.as_str() {
"json" => {
let json = serde_json::to_string_pretty(&metadata)?;
write!(output, "{json}")?;
Ok(())
}
"hex" => {
let hex_data = format!("0x{}", hex::encode(metadata.encode()));
write!(output, "{hex_data}")?;
Ok(())
}
"bytes" => {
let bytes = metadata.encode();
output.write_all(&bytes)?;
Ok(())
}
_ => Err(eyre::eyre!(
"Unsupported format `{}`, expected `json`, `hex` or `bytes`",
opts.format
)),
}
}
+11
View File
@@ -0,0 +1,11 @@
// 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.
pub mod chain_spec;
pub mod codegen;
pub mod compatibility;
pub mod diff;
pub mod explore;
pub mod metadata;
pub mod version;
+17
View File
@@ -0,0 +1,17 @@
use clap::Parser as ClapParser;
/// Prints version information
#[derive(Debug, ClapParser)]
pub struct Opts {}
pub fn run(_opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
let git_hash = env!("GIT_HASH");
writeln!(
output,
"{} {}-{}",
clap::crate_name!(),
clap::crate_version!(),
git_hash
)?;
Ok(())
}
+38
View File
@@ -0,0 +1,38 @@
// 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.
//! The Subxt CLI tool.
mod commands;
mod utils;
use clap::Parser as ClapParser;
/// Subxt utilities for interacting with Substrate based nodes.
#[derive(Debug, ClapParser)]
enum Command {
Metadata(commands::metadata::Opts),
Codegen(commands::codegen::Opts),
Compatibility(commands::compatibility::Opts),
Diff(commands::diff::Opts),
Version(commands::version::Opts),
Explore(commands::explore::Opts),
ChainSpec(commands::chain_spec::Opts),
}
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let args = Command::parse();
let mut output = std::io::stdout();
match args {
Command::Metadata(opts) => commands::metadata::run(opts, &mut output).await,
Command::Codegen(opts) => commands::codegen::run(opts, &mut output).await,
Command::Compatibility(opts) => commands::compatibility::run(opts, &mut output).await,
Command::Diff(opts) => commands::diff::run(opts, &mut output).await,
Command::Version(opts) => commands::version::run(opts, &mut output),
Command::Explore(opts) => commands::explore::run(opts, &mut output).await,
Command::ChainSpec(opts) => commands::chain_spec::run(opts, &mut output).await,
}
}
+393
View File
@@ -0,0 +1,393 @@
// 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,
})
));
}
}