mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-25 10:37:56 +00:00
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:
@@ -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]),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@
|
||||
|
||||
pub mod codegen;
|
||||
pub mod compatibility;
|
||||
pub mod explore;
|
||||
pub mod metadata;
|
||||
pub mod version;
|
||||
|
||||
Reference in New Issue
Block a user