CLI subxt explore commands (#950)

* add cli command to explore metadata

* fmt and clippy

* Bump serde from 1.0.160 to 1.0.162 (#948)

Bumps [serde](https://github.com/serde-rs/serde) from 1.0.160 to 1.0.162.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.160...1.0.162)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* extrinsics: Decode extrinsics from blocks (#929)

* Update polkadot.scale

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Add extrinsics client

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Decode extrinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add extrinsic error

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Expose extrinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Fetch and decode block extrinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Fix clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Fetch pallet and variant index

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Move extrinsics on the subxt::blocks

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* example: Adjust example

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata: Collect ExtrinsicMetadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Implement StaticExtrinsic for the calls

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust examples

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* codegen: Add root level Call enum

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust testing

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Add new decode interface

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* subxt: Merge ExtrinsicError with BlockError

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Find first extrinsic

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Move code to extrinsic_types

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Add Extrinsic struct

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust examples

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* test: Decode extinsics

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Add fake metadata for static decoding

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Decode from insufficient bytes

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Check unsupported versions

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Statically decode to root and pallet enums

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/tests: Remove clones

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Fetch block body inline

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Rename ExtrinsicIds to ExtrinsicPartTypeIds

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics/test: Check decode as_extrinsic

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Remove InsufficientData error

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* blocks: Return error from extrinsic_metadata

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* extrinsics: Postpone decoding of call bytes

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* metadata_type: Rename variables

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Adjust calls path for example and tests

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* examples: Remove traces

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* book: Add extrinsics documentation

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* book: Improve extrinsics docs

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: James Wilson <james@jsdw.me>

* change doc comments

* add constants exploration

* add storage access interface but not done yet

* add storage exploration

* formatting

* remove dbg

* some small tweaks

* fix formatting and scale value for storage

* split up files, sort entries, change formatting

* fmt and clippy fix

* fix minor formatting issue

* implement suggestions

* implement other suggestion, fix bug

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com>
Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
Tadeo Hepperle
2023-05-23 13:33:07 +02:00
committed by GitHub
parent 5960cd2ac8
commit f344d0dd4d
11 changed files with 1242 additions and 2 deletions
+146
View File
@@ -0,0 +1,146 @@
use crate::utils::type_description::print_type_description;
use crate::utils::type_example::print_type_examples;
use crate::utils::with_indent;
use clap::Args;
use std::fmt::Write;
use std::str::FromStr;
use std::write;
use color_eyre::eyre::eyre;
use frame_metadata::v15::PalletMetadata;
use scale_info::form::PortableForm;
use scale_info::{PortableRegistry, Type, TypeDef, TypeDefVariant};
use scale_value::{Composite, ValueDef};
use subxt::tx;
use subxt::utils::H256;
use subxt::{config::SubstrateConfig, Metadata, OfflineClient};
#[derive(Debug, Clone, Args)]
pub struct CallsSubcommand {
call: Option<String>,
#[clap(required = false)]
trailing_args: Vec<String>,
}
pub(crate) fn explore_calls(
command: CallsSubcommand,
metadata: &Metadata,
pallet_metadata: &PalletMetadata<PortableForm>,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name.as_str();
// get the enum that stores the possible calls:
let (calls_enum_type_def, _calls_enum_type) =
get_calls_enum_type(pallet_metadata, &metadata.runtime_metadata().types)?;
// if no call specified, show user the calls to choose from:
let Some(call_name) = command.call else {
let available_calls = print_available_calls(calls_enum_type_def, pallet_name);
println!("Usage:\n subxt explore {pallet_name} calls <CALL>\n explore a specific call within this pallet\n\n{available_calls}", );
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.to_lowercase() == call_name.to_lowercase()) else {
let available_calls = print_available_calls(calls_enum_type_def, pallet_name);
let description = format!("Usage:\n subxt explore {pallet_name} calls <CALL>\n explore a specific call within this pallet\n\n{available_calls}", );
return Err(eyre!("\"{call_name}\" call not found in \"{pallet_name}\" pallet!\n\n{description}"));
};
// 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 mut type_description =
print_type_description(&call.fields, &metadata.runtime_metadata().types)?;
type_description = with_indent(type_description, 4);
let mut type_examples = print_type_examples(
&call.fields,
&metadata.runtime_metadata().types,
"SCALE_VALUE",
)?;
type_examples = with_indent(type_examples, 4);
let mut output = String::new();
write!(output, "Usage:\n subxt explore {pallet_name} calls {call_name} <SCALE_VALUE>\n construct the call by providing a valid argument\n\n")?;
write!(
output,
"The call expect expects a <SCALE_VALUE> with this shape:\n{type_description}\n\n{}\n\nYou may need to surround the value in single quotes when providing it as an argument."
, &type_examples[4..])?;
println!("{output}");
return Ok(());
}
// parse scale_value from trailing arguments and try to create an unsigned extrinsic with it:
let value = scale_value::stringify::from_str(&trailing_args).0.map_err(|err| eyre!("scale_value::stringify::from_str led to a ParseError.\n\ntried parsing: \"{}\"\n\n{}", trailing_args, err))?;
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()));
println!("Encoded call data:\n {hex_bytes}");
Ok(())
}
fn print_available_calls(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: &'a frame_metadata::v15::PalletMetadata<PortableForm>,
registry: &'a PortableRegistry,
) -> color_eyre::Result<(&'a TypeDefVariant<PortableForm>, &'a Type<PortableForm>)> {
let calls = pallet
.calls
.as_ref()
.ok_or(eyre!("The \"{}\" pallet has no calls.", pallet.name))?;
let calls_enum_type = registry
.resolve(calls.ty.id)
.ok_or(eyre!("calls type with id {} not found.", calls.ty.id))?;
// should always be a variant type, where each variant corresponds to one call.
let calls_enum_type_def = match &calls_enum_type.type_def {
TypeDef::Variant(variant) => variant,
_ => {
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::rpc::types::RuntimeVersion {
spec_version: 9370,
transaction_version: 20,
other: Default::default(),
};
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]),
}
}
+86
View File
@@ -0,0 +1,86 @@
use crate::utils::type_description::print_type_description;
use crate::utils::{print_docs_with_indent, with_indent};
use clap::Args;
use std::fmt::Write;
use std::write;
use color_eyre::eyre::eyre;
use frame_metadata::v15::PalletMetadata;
use scale_info::form::PortableForm;
use subxt::Metadata;
#[derive(Debug, Clone, Args)]
pub struct ConstantsSubcommand {
constant: Option<String>,
}
pub(crate) fn explore_constants(
command: ConstantsSubcommand,
metadata: &Metadata,
pallet_metadata: &PalletMetadata<PortableForm>,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name.as_str();
let Some(constant_name) = command.constant else {
let available_constants = print_available_constants(pallet_metadata, pallet_name);
println!("Usage:\n subxt explore {pallet_name} constants <CONSTANT>\n explore a specific call within this pallet\n\n{available_constants}", );
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.iter().find(|constant| constant.name.to_lowercase() == constant_name.to_lowercase()) else {
let available_constants = print_available_constants(pallet_metadata, pallet_name);
let description = format!("Usage:\n subxt explore {pallet_name} constants <CONSTANT>\n explore a specific call within this pallet\n\n{available_constants}", );
let err = eyre!("constant \"{constant_name}\" not found in \"{pallet_name}\" pallet!\n\n{description}");
return Err(err);
};
// docs
let mut output = String::new();
let doc_string = print_docs_with_indent(&constant.docs, 4);
if !doc_string.is_empty() {
write!(output, "Description:\n{doc_string}")?;
}
// shape
let mut type_description = print_type_description(&constant.ty.id, metadata.types())?;
type_description = with_indent(type_description, 4);
write!(
output,
"\n\nThe constant has the following shape:\n{type_description}"
)?;
// value
let scale_val = scale_value::scale::decode_as_type(
&mut &constant.value[..],
constant.ty.id,
metadata.types(),
)?;
write!(
output,
"\n\nThe value of the constant is:\n {}",
scale_value::stringify::to_string(&scale_val)
)?;
println!("{output}");
Ok(())
}
fn print_available_constants(
pallet_metadata: &PalletMetadata<PortableForm>,
pallet_name: &str,
) -> String {
if pallet_metadata.constants.is_empty() {
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.iter().map(|c| &c.name).collect();
strings.sort();
for constant in strings {
output.push_str("\n ");
output.push_str(constant);
}
output
}
+147
View File
@@ -0,0 +1,147 @@
use crate::utils::{print_docs_with_indent, FileOrUrl};
use clap::{Parser as ClapParser, Subcommand};
use std::fmt::Write;
use std::write;
use codec::Decode;
use color_eyre::eyre::eyre;
use frame_metadata::v15::RuntimeMetadataV15;
use frame_metadata::RuntimeMetadataPrefixed;
use syn::__private::str;
use crate::commands::explore::calls::{explore_calls, CallsSubcommand};
use crate::commands::explore::constants::{explore_constants, ConstantsSubcommand};
use crate::commands::explore::storage::{explore_storage, StorageSubcommand};
use subxt::Metadata;
mod calls;
mod constants;
mod storage;
/// Explore pallets, calls, call parameters, storage entries and constants. Also allows for creating (unsigned) extrinsics.
///
/// # Example
///
/// ## Pallets
///
/// Show the pallets that are available:
/// ```
/// subxt explore --file=polkadot_metadata.scale
/// ```
///
/// ## Calls
///
/// Show the calls in a pallet:
/// ```
/// subxt explore Balances calls
/// ```
/// Show the call parameters a call expects:
/// ```
/// subxt explore Balances calls transfer
/// ```
/// Create an unsigned extrinsic from a scale value, validate it and output its hex representation
/// ```
/// subxt explore Grandpa calls note_stalled { "delay": 5, "best_finalized_block_number": 5 }
/// # Encoded call data:
/// # 0x2c0411020500000005000000
/// subxt explore Balances calls transfer "{ \"dest\": v\"Raw\"((255, 255, 255)), \"value\": 0 }"
/// # Encoded call data:
/// # 0x24040607020cffffff00
/// ```
/// ## Constants
///
/// Show the constants in a pallet:
/// ```
/// subxt explore Balances constants
/// ```
/// ## Storage
///
/// Show the storage entries in a pallet
/// ```
/// subxt explore Alliance storage
/// ```
/// Show the types and value of a specific storage entry
/// ```
/// subxt explore Alliance storage Announcements [KEY_SCALE_VALUE]
/// ```
///
#[derive(Debug, ClapParser)]
pub struct Opts {
#[command(flatten)]
file_or_url: FileOrUrl,
pallet: Option<String>,
#[command(subcommand)]
pallet_subcommand: Option<PalletSubcommand>,
}
#[derive(Debug, Clone, Subcommand)]
pub enum PalletSubcommand {
Calls(CallsSubcommand),
Constants(ConstantsSubcommand),
Storage(StorageSubcommand),
}
/// cargo run -- explore --file=../artifacts/polkadot_metadata.scale
pub async fn run(opts: Opts) -> color_eyre::Result<()> {
// get the metadata
let bytes = opts.file_or_url.fetch().await?;
let metadata_prefixed = <RuntimeMetadataPrefixed as Decode>::decode(&mut &bytes[..])?;
let metadata = Metadata::try_from(metadata_prefixed)?;
// if no pallet specified, show user the pallets to choose from:
let Some(pallet_name) = opts.pallet else {
let available_pallets = print_available_pallets(metadata.runtime_metadata());
println!("Usage:\n subxt explore <PALLET>\n explore a specific pallet\n\n{available_pallets}", );
return Ok(());
};
// if specified pallet is wrong, show user the pallets to choose from (but this time as an error):
let Some(pallet_metadata) = metadata.runtime_metadata().pallets.iter().find(|pallet| pallet.name.to_lowercase() == pallet_name.to_lowercase())else {
return Err(eyre!("pallet \"{}\" not found in metadata!\n{}", pallet_name, print_available_pallets(metadata.runtime_metadata())));
};
// if correct pallet was specified but no subcommand, instruct the user how to proceed:
let Some(pallet_subcomand) = opts.pallet_subcommand else {
let docs_string = print_docs_with_indent(&pallet_metadata.docs, 4);
let mut output = String::new();
if !docs_string.is_empty() {
write!(output, "Description:\n{docs_string}")?;
}
write!(output, "Usage:")?;
write!(output, "\n subxt explore {pallet_name} calls\n explore the calls that can be made into this pallet")?;
write!(output, "\n subxt explore {pallet_name} constants\n explore the constants held in this pallet")?;
write!(output, "\n subxt explore {pallet_name} storage\n explore the storage values held in this pallet")?;
println!("{output}");
return Ok(());
};
match pallet_subcomand {
PalletSubcommand::Calls(command) => explore_calls(command, &metadata, pallet_metadata),
PalletSubcommand::Constants(command) => {
explore_constants(command, &metadata, pallet_metadata)
}
PalletSubcommand::Storage(command) => {
// if the metadata came from some url, we use that same url to make storage calls against.
let node_url = opts.file_or_url.url.map(|url| url.to_string());
explore_storage(command, &metadata, pallet_metadata, node_url).await
}
}
}
fn print_available_pallets(metadata_v15: &RuntimeMetadataV15) -> String {
if metadata_v15.pallets.is_empty() {
"There are no <PALLET> values available.".to_string()
} else {
let mut output = "Available <PALLET> values are:".to_string();
let mut strings: Vec<_> = metadata_v15.pallets.iter().map(|p| &p.name).collect();
strings.sort();
for pallet in strings {
write!(output, "\n {}", pallet).unwrap();
}
output
}
}
+176
View File
@@ -0,0 +1,176 @@
use crate::utils::type_description::print_type_description;
use crate::utils::type_example::print_type_examples;
use crate::utils::{print_docs_with_indent, with_indent};
use clap::Args;
use std::fmt::Write;
use std::write;
use color_eyre::eyre::eyre;
use frame_metadata::v15::{PalletMetadata, PalletStorageMetadata, StorageEntryType};
use scale_info::form::PortableForm;
use subxt::OnlineClient;
use subxt::{config::SubstrateConfig, Metadata};
#[derive(Debug, Clone, Args)]
pub struct StorageSubcommand {
storage_entry: Option<String>,
#[clap(required = false)]
trailing_args: Vec<String>,
}
pub(crate) async fn explore_storage(
command: StorageSubcommand,
metadata: &Metadata,
pallet_metadata: &PalletMetadata<PortableForm>,
custom_online_client_url: Option<String>,
) -> color_eyre::Result<()> {
let pallet_name = pallet_metadata.name.as_str();
let trailing_args = command.trailing_args.join(" ");
let trailing_args = trailing_args.trim();
let Some(storage_metadata) = &pallet_metadata.storage else {
println!("The \"{pallet_name}\" pallet has no storage entries.");
return Ok(());
};
// if no storage entry specified, show user the calls to choose from:
let Some(entry_name) = command.storage_entry else {
let storage_entries = print_available_storage_entries(storage_metadata, pallet_name);
println!("Usage:\n subxt explore {pallet_name} storage <STORAGE_ENTRY>\n view details for a specific storage entry\n\n{storage_entries}");
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.to_lowercase() == entry_name.to_lowercase()) else {
let storage_entries = print_available_storage_entries(storage_metadata, pallet_name);
let description = format!("Usage:\n subxt explore {pallet_name} storage <STORAGE_ENTRY>\n view details for a specific storage entry\n\n{storage_entries}");
return Err(eyre!("Storage entry \"{entry_name}\" not found in \"{pallet_name}\" pallet!\n\n{description}"));
};
let (return_ty_id, key_ty_id) = match storage.ty {
StorageEntryType::Plain(value) => (value.id, None),
StorageEntryType::Map { value, key, .. } => (value.id, Some(key.id)),
};
// get the type and type description for the return and key type:
let mut output = String::new();
// only inform user about usage if a key can be provided:
if key_ty_id.is_some() && trailing_args.is_empty() {
write!(
output,
"Usage:\n subxt explore {pallet_name} storage {entry_name} <KEY_VALUE>\n\n"
)?;
}
let docs_string = print_docs_with_indent(&storage.docs, 4);
if !docs_string.is_empty() {
write!(output, "Description:\n{docs_string}")?;
}
// inform user about shape of key if it can be provided:
if let Some(key_ty_id) = key_ty_id {
let mut key_ty_description = print_type_description(&key_ty_id, metadata.types())?;
key_ty_description = with_indent(key_ty_description, 4);
let mut key_ty_examples = print_type_examples(&key_ty_id, metadata.types(), "<KEY_VALUE>")?;
key_ty_examples = with_indent(key_ty_examples, 4);
write!(
output,
"\n\nThe <KEY_VALUE> has the following shape:\n {key_ty_description}\n\n{}",
&key_ty_examples[4..]
)?;
} else {
write!(
output,
"\n\nThe constant can be accessed without providing a key."
)?;
}
let mut return_ty_description = print_type_description(&return_ty_id, metadata.types())?;
return_ty_description = if return_ty_description.contains('\n') {
format!("\n{}", with_indent(return_ty_description, 4))
} else {
return_ty_description
};
write!(
output,
"\n\nThe storage entry has the following shape: {}",
return_ty_description
)?;
// construct the vector of scale_values that should be used as a key to the storage (often empty)
let key_scale_values = if let Some(key_ty_id) = key_ty_id.filter(|_| !trailing_args.is_empty())
{
let key_scale_value = scale_value::stringify::from_str(trailing_args).0.map_err(|err| eyre!("scale_value::stringify::from_str led to a ParseError.\n\ntried parsing: \"{}\"\n\n{}", trailing_args, err))?;
write!(
output,
"\n\nYou submitted the following value as a key:\n{}",
with_indent(scale_value::stringify::to_string(&key_scale_value), 4)
)?;
let mut key_bytes: Vec<u8> = Vec::new();
scale_value::scale::encode_as_type(
&key_scale_value,
key_ty_id,
metadata.types(),
&mut key_bytes,
)?;
let bytes_composite = scale_value::Value::from_bytes(&key_bytes);
vec![bytes_composite]
} else {
Vec::new()
};
if key_ty_id.is_none() && !trailing_args.is_empty() {
write!(output, "\n\nWarning: You submitted the following value as a key, but it will be ignored, because the storage entry does not require a key: \"{}\"", trailing_args)?;
}
println!("{output}");
// construct and submit the storage entry request if either no key is needed or som key was provided as a scale value
if key_ty_id.is_none() || !key_scale_values.is_empty() {
let online_client = match custom_online_client_url {
None => OnlineClient::<SubstrateConfig>::new().await?,
Some(url) => OnlineClient::<SubstrateConfig>::from_url(url).await?,
};
let storage_query = subxt::dynamic::storage(pallet_name, entry_name, key_scale_values);
let decoded_value_thunk_or_none = online_client
.storage()
.at_latest()
.await?
.fetch(&storage_query)
.await?;
let decoded_value_thunk =
decoded_value_thunk_or_none.ok_or(eyre!("Value not found in storage."))?;
let value = decoded_value_thunk.to_value()?;
let mut value_string = scale_value::stringify::to_string(&value);
value_string = with_indent(value_string, 4);
println!("\nThe value of the storage entry is:\n{value_string}");
}
Ok(())
}
fn print_available_storage_entries(
storage_metadata: &PalletStorageMetadata<PortableForm>,
pallet_name: &str,
) -> String {
if storage_metadata.entries.is_empty() {
format!("No <STORAGE_ENTRY>'s available in the \"{pallet_name}\" pallet.")
} else {
let mut output = format!(
"Available <STORAGE_ENTRY>'s in the \"{}\" pallet:",
pallet_name
);
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
}
}
+1
View File
@@ -4,5 +4,6 @@
pub mod codegen;
pub mod compatibility;
pub mod explore;
pub mod metadata;
pub mod version;
+2
View File
@@ -15,6 +15,7 @@ enum Command {
Codegen(commands::codegen::Opts),
Compatibility(commands::compatibility::Opts),
Version(commands::version::Opts),
Explore(commands::explore::Opts),
}
#[tokio::main]
@@ -27,5 +28,6 @@ async fn main() -> color_eyre::Result<()> {
Command::Codegen(opts) => commands::codegen::run(opts).await,
Command::Compatibility(opts) => commands::compatibility::run(opts).await,
Command::Version(opts) => commands::version::run(opts),
Command::Explore(opts) => commands::explore::run(opts).await,
}
}
+25 -2
View File
@@ -4,18 +4,22 @@
use clap::Args;
use color_eyre::eyre;
use std::{fs, io::Read, path::PathBuf};
use subxt_codegen::utils::{MetadataVersion, Uri};
pub mod type_description;
pub mod type_example;
/// The source of the metadata.
#[derive(Debug, Args)]
pub struct FileOrUrl {
/// The url of the substrate node to query for metadata for codegen.
#[clap(long, value_parser)]
url: Option<Uri>,
pub(crate) url: Option<Uri>,
/// The path to the encoded metadata file.
#[clap(long, value_parser)]
file: Option<PathBuf>,
pub(crate) file: Option<PathBuf>,
/// Specify the metadata version.
///
/// - unstable:
@@ -71,3 +75,22 @@ impl FileOrUrl {
}
}
}
pub(crate) fn print_docs_with_indent(docs: &[String], indent: usize) -> String {
// take at most the first paragraph of documentation, such that it does not get too long.
let docs_str = docs
.iter()
.map(|e| e.trim())
.take_while(|e| !e.is_empty())
.collect::<Vec<_>>()
.join("\n");
with_indent(docs_str, indent)
}
pub(crate) fn with_indent(s: String, indent: usize) -> String {
let indent_str = " ".repeat(indent);
s.lines()
.map(|line| format!("{indent_str}{line}"))
.collect::<Vec<_>>()
.join("\n")
}
+273
View File
@@ -0,0 +1,273 @@
use color_eyre::eyre::eyre;
use scale_info::{
form::PortableForm, Field, PortableRegistry, TypeDef, TypeDefArray, TypeDefBitSequence,
TypeDefCompact, TypeDefPrimitive, TypeDefSequence, TypeDefTuple, TypeDefVariant, Variant,
};
/// pretty formatted type description
pub fn print_type_description<T>(ty: &T, registry: &PortableRegistry) -> color_eyre::Result<String>
where
T: TypeDescription,
{
let type_description = ty.type_description(registry)?;
let type_description = format_type_description(&type_description);
Ok(type_description)
}
/// a trait for producing human readable type descriptions with a rust-like syntax.
pub trait TypeDescription {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String>;
}
impl TypeDescription for u32 {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let ty = registry
.resolve(*self)
.ok_or(eyre!("Type with id {} not found in registry", *self))?;
let ident = ty.path.ident();
let prefix = type_def_prefix(&ty.type_def);
let mut type_def_description = ty.type_def.type_description(registry)?;
if let Some(ident) = ident {
type_def_description = format!("{} {}", ident, type_def_description)
}
if let Some(prefix) = prefix {
type_def_description = format!("{} {}", prefix, type_def_description)
}
Ok(type_def_description)
}
}
fn type_def_prefix(type_def: &TypeDef<PortableForm>) -> Option<&str> {
match type_def {
TypeDef::Composite(_) => Some("struct"),
TypeDef::Variant(_) => Some("enum"),
_ => None,
}
}
impl TypeDescription for TypeDef<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
match self {
TypeDef::Composite(composite) => composite.fields.type_description(registry),
TypeDef::Variant(variant) => variant.type_description(registry),
TypeDef::Sequence(sequence) => sequence.type_description(registry),
TypeDef::Array(array) => array.type_description(registry),
TypeDef::Tuple(tuple) => tuple.type_description(registry),
TypeDef::Primitive(primitive) => primitive.type_description(registry),
TypeDef::Compact(compact) => compact.type_description(registry),
TypeDef::BitSequence(bit_sequence) => bit_sequence.type_description(registry),
}
}
}
impl TypeDescription for TypeDefTuple<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let mut output = "(".to_string();
let mut iter = self.fields.iter().peekable();
while let Some(ty) = iter.next() {
let type_description = ty.id.type_description(registry)?;
output.push_str(&type_description);
if iter.peek().is_some() {
output.push(',')
}
}
output.push(')');
Ok(output)
}
}
impl TypeDescription for TypeDefBitSequence<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let bit_order_type = self.bit_order_type.id.type_description(registry)?;
let bit_store_type = self.bit_store_type.id.type_description(registry)?;
Ok(format!("BitSequence({bit_order_type}, {bit_store_type})"))
}
}
impl TypeDescription for TypeDefSequence<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let type_description = self.type_param.id.type_description(registry)?;
Ok(format!("Sequence({type_description})"))
}
}
impl TypeDescription for TypeDefCompact<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let type_description = self.type_param.id.type_description(registry)?;
Ok(format!("Compact({type_description})"))
}
}
impl TypeDescription for TypeDefArray<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let type_description = self.type_param.id.type_description(registry)?;
Ok(format!("[{type_description}; {}]", self.len))
}
}
impl TypeDescription for TypeDefPrimitive {
fn type_description(&self, _registry: &PortableRegistry) -> color_eyre::Result<String> {
Ok(match &self {
TypeDefPrimitive::Bool => "bool",
TypeDefPrimitive::Char => "char",
TypeDefPrimitive::Str => "String",
TypeDefPrimitive::U8 => "u8",
TypeDefPrimitive::U16 => "u16",
TypeDefPrimitive::U32 => "u32",
TypeDefPrimitive::U64 => "u64",
TypeDefPrimitive::U128 => "u128",
TypeDefPrimitive::U256 => "u256",
TypeDefPrimitive::I8 => "i8",
TypeDefPrimitive::I16 => "i16",
TypeDefPrimitive::I32 => "i32",
TypeDefPrimitive::I64 => "i64",
TypeDefPrimitive::I128 => "i128",
TypeDefPrimitive::I256 => "i256",
}
.into())
}
}
impl TypeDescription for TypeDefVariant<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
const MIN_VARIANT_COUNT_FOR_TRAILING_COMMA: usize = 100;
let add_trailing_comma = self.variants.len() >= MIN_VARIANT_COUNT_FOR_TRAILING_COMMA;
let mut variants_string = String::new();
variants_string.push('{');
let mut iter = self.variants.iter().peekable();
while let Some(variant) = iter.next() {
let variant_string = variant.type_description(registry)?;
variants_string.push_str(&variant_string);
if iter.peek().is_some() || add_trailing_comma {
variants_string.push(',');
}
}
variants_string.push('}');
Ok(variants_string)
}
}
impl TypeDescription for Variant<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let fields_string = self.fields.type_description(registry)?;
let output = if fields_string.is_empty() {
self.name.to_string()
} else if fields_string.starts_with('(') {
format!("{}{}", &self.name, fields_string)
} else {
format!("{} {}", &self.name, fields_string)
};
Ok(output)
}
}
impl TypeDescription for Vec<Field<PortableForm>> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
if self.is_empty() {
return Ok("()".to_string());
}
const MIN_FIELD_COUNT_FOR_TRAILING_COMMA: usize = 100;
let add_trailing_comma = self.len() >= MIN_FIELD_COUNT_FOR_TRAILING_COMMA;
let all_fields_named = self.iter().all(|f| f.name.is_some());
let all_fields_unnamed = self.iter().all(|f| f.name.is_none());
let brackets = match (all_fields_named, all_fields_unnamed) {
(true, false) => ('{', '}'),
(false, true) => ('(', ')'),
_ => {
return Err(eyre!(
"combination of named and unnamed fields in compound type"
));
}
};
let mut fields_string = String::new();
fields_string.push(brackets.0);
let mut iter = self.iter().peekable();
while let Some(field) = iter.next() {
let field_description = field.type_description(registry)?;
fields_string.push_str(&field_description);
if iter.peek().is_some() || add_trailing_comma {
fields_string.push(',')
}
}
fields_string.push(brackets.1);
Ok(fields_string)
}
}
impl TypeDescription for Field<PortableForm> {
fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String> {
let type_description = self.ty.id.type_description(registry)?;
let type_description_maybe_named = if let Some(name) = &self.name {
format!("{}: {}", name, type_description)
} else {
type_description
};
Ok(type_description_maybe_named)
}
}
fn format_type_description(input: &str) -> String {
fn add_indentation(output: &mut String, indent_level: i32) {
for _ in 0..indent_level {
output.push_str(" ");
}
}
let mut output = String::new();
let mut indent_level = 0;
// in a tuple we will not set line breaks on comma, so we keep track of it here:
let mut in_tuple = 0;
let mut tokens_since_last_bracket_or_comma: usize = 0;
for ch in input.chars() {
let mut token_is_bracket_or_comma = true;
match ch {
'{' => {
indent_level += 1;
output.push(ch);
output.push('\n');
add_indentation(&mut output, indent_level);
}
'}' => {
indent_level -= 1;
output.push('\n');
add_indentation(&mut output, indent_level);
output.push(ch);
}
',' => {
output.push(ch);
// makes small tuples e.g. (u8, u16, u8, u8) not cause line breaks.
if in_tuple > 0 && tokens_since_last_bracket_or_comma < 5 {
output.push(' ');
} else {
output.push('\n');
add_indentation(&mut output, indent_level);
}
}
'(' => {
output.push(ch);
in_tuple += 1;
}
')' => {
output.push(ch);
in_tuple -= 1;
}
_ => {
token_is_bracket_or_comma = false;
output.push(ch);
}
}
if token_is_bracket_or_comma {
tokens_since_last_bracket_or_comma = 0;
} else {
tokens_since_last_bracket_or_comma += 1;
}
}
output
}
+380
View File
@@ -0,0 +1,380 @@
use color_eyre::eyre::eyre;
use scale_info::{
form::PortableForm, Field, PortableRegistry, TypeDef, TypeDefArray, TypeDefPrimitive,
TypeDefTuple, TypeDefVariant,
};
use scale_value::{Value, ValueDef};
use std::fmt::Write;
use std::write;
pub fn print_type_examples<T>(
ty: &T,
registry: &PortableRegistry,
type_placeholder: &str,
) -> color_eyre::Result<String>
where
T: TypeExample,
{
let type_examples = ty.type_example(registry)?;
let mut output = String::new();
match type_examples.len() {
0 => {
write!(
output,
"There are no examples available for a {type_placeholder} matching this shape:"
)?;
}
1 => {
write!(
output,
"Here is an example of a {type_placeholder} matching this shape:"
)?;
}
i => {
write!(
output,
"Here are {i} examples of a {type_placeholder} matching this shape:"
)?;
}
};
for self_value in type_examples {
let value = <T as TypeExample>::upcast(self_value);
let example_str = scale_value::stringify::to_string(&value);
write!(output, "\n{}", example_str)?;
}
Ok(output)
}
/// a trait for producing scale value examples for a type.
pub trait TypeExample {
type Value;
fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>>;
fn upcast(self_value: Self::Value) -> scale_value::Value;
}
impl TypeExample for u32 {
type Value = scale_value::Value;
fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>> {
let ty = registry
.resolve(*self)
.ok_or(eyre!("Type with id {} not found in registry", *self))?;
let examples = match &ty.type_def {
TypeDef::Composite(composite) => composite
.fields
.type_example(registry)?
.into_iter()
.map(|e| scale_value::Value {
value: scale_value::ValueDef::Composite(e),
context: (),
})
.collect(),
TypeDef::Variant(variant) => variant
.type_example(registry)?
.into_iter()
.map(|e| scale_value::Value {
value: scale_value::ValueDef::Variant(e),
context: (),
})
.collect(),
TypeDef::Array(array) => array
.type_example(registry)?
.into_iter()
.map(|e| scale_value::Value {
value: scale_value::ValueDef::Composite(e),
context: (),
})
.collect(),
TypeDef::Tuple(tuple) => tuple
.type_example(registry)?
.into_iter()
.map(|e| scale_value::Value {
value: scale_value::ValueDef::Composite(e),
context: (),
})
.collect(),
TypeDef::Primitive(primitive) => primitive
.type_example(registry)?
.into_iter()
.map(scale_value::Value::primitive)
.collect(),
TypeDef::Compact(compact) => compact.type_param.id.type_example(registry)?,
TypeDef::BitSequence(_) => {
return Err(eyre!("no examples for BitSequence available"));
}
TypeDef::Sequence(sequence) => {
// for sequences we just give an example of an array with 3 elements:
TypeDefArray {
len: 3,
type_param: sequence.type_param,
}
.type_example(registry)?
.into_iter()
.map(|e| scale_value::Value {
value: scale_value::ValueDef::Composite(e),
context: (),
})
.collect()
}
};
Ok(examples)
}
fn upcast(self_value: Self::Value) -> Value {
self_value
}
}
impl TypeExample for TypeDefVariant<PortableForm> {
type Value = scale_value::Variant<()>;
fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>> {
let mut examples: Vec<scale_value::Variant<()>> = Vec::new();
// returns one example for each variant
for variant in &self.variants {
// get the first example for the variant's data and use it
let mut variant_value_examples = variant.fields.type_example(registry)?;
let Some(values) = variant_value_examples.pop() else {
return Err(eyre!("no example element for variant {}", variant.name));
};
examples.push(scale_value::Variant {
name: variant.name.clone(),
values,
});
}
Ok(examples)
}
fn upcast(self_value: Self::Value) -> Value {
Value {
value: ValueDef::Variant(self_value),
context: (),
}
}
}
impl TypeExample for TypeDefArray<PortableForm> {
type Value = scale_value::Composite<()>;
fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>> {
// take the first example value and set it to each element of the array
let mut value_examples = self.type_param.id.type_example(registry)?;
let Some(first_value_example) = value_examples.pop() else {
return Err(eyre!("no example element for array"));
};
let one_example = {
let mut values = Vec::with_capacity(self.len as usize);
for _ in 0..self.len as usize {
values.push(first_value_example.clone());
}
scale_value::Composite::<()>::Unnamed(values)
};
Ok(vec![one_example])
}
fn upcast(self_value: Self::Value) -> Value {
Value {
value: ValueDef::Composite(self_value),
context: (),
}
}
}
impl TypeExample for TypeDefTuple<PortableForm> {
type Value = scale_value::Composite<()>;
fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>> {
// create unnamed fields to use the same logic already used for struct example generation
let fields_vector: Vec<Field<PortableForm>> = self
.fields
.iter()
.map(|ty| Field {
name: None,
ty: *ty,
type_name: None,
docs: Vec::new(),
})
.collect();
fields_vector.type_example(registry)
}
fn upcast(self_value: Self::Value) -> Value {
Value {
value: ValueDef::Composite(self_value),
context: (),
}
}
}
impl TypeExample for Vec<Field<PortableForm>> {
type Value = scale_value::Composite<()>;
fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>> {
let all_fields_named = self.iter().all(|f| f.name.is_some());
let all_fields_unnamed = self.iter().all(|f| f.name.is_none());
// composite apparently has no fields:
if all_fields_named && all_fields_unnamed {
let one_empty_example = scale_value::Composite::Unnamed(Vec::new());
return Ok(vec![one_empty_example]);
}
// composite apparently has mix of named and unnamed fields:
if !all_fields_named && !all_fields_unnamed {
return Err(eyre!(
"combination of named and unnamed fields in compound type"
));
}
// for each field get all the examples the type of that field can offer:
let mut field_examples: Vec<(&Field<PortableForm>, Vec<scale_value::Value>)> = Vec::new();
for field in self.iter() {
let examples = field.ty.id.type_example(registry)?;
field_examples.push((field, examples));
}
// Let N be the mininum number of examples any field has.
// Return N examples for the Compound type, by choosing the ith example for each of the 0..N examples for that field.
let n = field_examples
.iter()
.map(|(_, examples)| examples.len())
.min()
.expect("Iterator is not non-empty checked above; qed");
let mut composite_examples: Vec<Vec<(&Field<PortableForm>, scale_value::Value)>> =
Vec::new();
for _ in 0..n {
let composite_example: Vec<(&Field<PortableForm>, scale_value::Value)> = field_examples
.iter_mut()
.map(|(field, examples)| (*field, examples.pop().unwrap()))
.collect(); // the pop() is safe to unwrap because of the minimum we checked before
composite_examples.push(composite_example);
}
// create the vector of composite scale values. Distingiush between named and unnamed here.
let composite_examples = composite_examples
.into_iter()
.map(|composite_example| {
if all_fields_named {
let composite_example = composite_example
.into_iter()
.map(|(field, value)| (field.name.as_ref().unwrap().clone(), value))
.collect();
scale_value::Composite::Named(composite_example)
} else {
let composite_example = composite_example
.into_iter()
.map(|(_, value)| (value))
.collect();
scale_value::Composite::Unnamed(composite_example)
}
})
.collect();
Ok(composite_examples)
}
fn upcast(self_value: Self::Value) -> Value {
Value {
value: ValueDef::Composite(self_value),
context: (),
}
}
}
/// 3-4 example values for each primitive
impl TypeExample for TypeDefPrimitive {
type Value = scale_value::Primitive;
fn type_example(&self, _registry: &PortableRegistry) -> color_eyre::Result<Vec<Self::Value>> {
let value = match &self {
TypeDefPrimitive::Bool => vec![
scale_value::Primitive::Bool(true),
scale_value::Primitive::Bool(false),
],
TypeDefPrimitive::Char => vec![
scale_value::Primitive::Char('r'),
scale_value::Primitive::Char('u'),
scale_value::Primitive::Char('s'),
scale_value::Primitive::Char('t'),
],
TypeDefPrimitive::Str => vec![
scale_value::Primitive::String("Alice".into()),
scale_value::Primitive::String("Bob".into()),
scale_value::Primitive::String("Foo".into()),
scale_value::Primitive::String("Bar".into()),
],
TypeDefPrimitive::U8 => vec![
scale_value::Primitive::U128(u8::MIN as u128),
scale_value::Primitive::U128(69),
scale_value::Primitive::U128(u8::MAX as u128),
],
TypeDefPrimitive::U16 => vec![
scale_value::Primitive::U128(u16::MIN as u128),
scale_value::Primitive::U128(420),
scale_value::Primitive::U128(u16::MAX as u128),
],
TypeDefPrimitive::U32 => vec![
scale_value::Primitive::U128(u32::MIN as u128),
scale_value::Primitive::U128(99000),
scale_value::Primitive::U128(u32::MAX as u128),
],
TypeDefPrimitive::U64 => vec![
scale_value::Primitive::U128(u64::MIN as u128),
scale_value::Primitive::U128(99000),
scale_value::Primitive::U128(u64::MAX as u128),
],
TypeDefPrimitive::U128 => vec![
scale_value::Primitive::U128(u128::MIN),
scale_value::Primitive::U128(99000),
scale_value::Primitive::U128(u128::MAX),
],
TypeDefPrimitive::U256 => vec![
scale_value::Primitive::U256([u8::MIN; 32]),
scale_value::Primitive::U256([3; 32]),
scale_value::Primitive::U256([u8::MAX; 32]),
],
TypeDefPrimitive::I8 => vec![
scale_value::Primitive::I128(i8::MIN as i128),
scale_value::Primitive::I128(69),
scale_value::Primitive::I128(i8::MAX as i128),
],
TypeDefPrimitive::I16 => vec![
scale_value::Primitive::I128(i16::MIN as i128),
scale_value::Primitive::I128(420),
scale_value::Primitive::I128(i16::MAX as i128),
],
TypeDefPrimitive::I32 => vec![
scale_value::Primitive::I128(i32::MIN as i128),
scale_value::Primitive::I128(99000),
scale_value::Primitive::I128(i32::MAX as i128),
],
TypeDefPrimitive::I64 => vec![
scale_value::Primitive::I128(i64::MIN as i128),
scale_value::Primitive::I128(99000),
scale_value::Primitive::I128(i64::MAX as i128),
],
TypeDefPrimitive::I128 => vec![
scale_value::Primitive::I128(i128::MIN),
scale_value::Primitive::I128(99000),
scale_value::Primitive::I128(i128::MAX),
],
TypeDefPrimitive::I256 => vec![
scale_value::Primitive::I256([u8::MIN; 32]),
scale_value::Primitive::I256([3; 32]),
scale_value::Primitive::I256([u8::MAX; 32]),
],
};
Ok(value)
}
fn upcast(self_value: Self::Value) -> Value {
Value {
value: ValueDef::Primitive(self_value),
context: (),
}
}
}