Skip to content

Commit

Permalink
CLI subxt explore commands (paritytech#950)
Browse files Browse the repository at this point in the history
* add cli command to explore metadata

* fmt and clippy

* Bump serde from 1.0.160 to 1.0.162 (paritytech#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](serde-rs/serde@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 (paritytech#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>
  • Loading branch information
4 people authored May 23, 2023
1 parent 5960cd2 commit f344d0d
Show file tree
Hide file tree
Showing 11 changed files with 1,242 additions and 2 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ path = "src/main.rs"
[dependencies]
subxt-codegen = { workspace = true }
subxt-metadata = { workspace = true }
subxt = { workspace = true }
clap = { workspace = true }
serde = { workspace = true, features = ["derive"] }
color-eyre = { workspace = true }
serde_json = { workspace = true }
hex = { workspace = true }
frame-metadata = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true }
scale-info = { workspace = true }
scale-value = { workspace = true }
syn = { workspace = true }
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport", "http-client"] }
tokio = { workspace = true }
146 changes: 146 additions & 0 deletions cli/src/commands/explore/calls.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 86 additions & 0 deletions cli/src/commands/explore/constants.rs
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit f344d0d

Please sign in to comment.