fix: Convert vendor/pezkuwi-subxt from submodule to regular directory
This commit is contained in:
Vendored
+58
@@ -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 }
|
||||
Vendored
+58
@@ -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
|
||||
|
||||
```
|
||||
|
||||
Vendored
+34
@@ -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),
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
Vendored
+38
@@ -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
@@ -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,
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user