From 05ced83f74d9fe7738f6fb437f91f2b7ec262cdc Mon Sep 17 00:00:00 2001 From: Tadeo Hepperle <62739623+tadeohepperle@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:57:16 +0100 Subject: [PATCH] CLI Tool Explore Command: Runtime APIs, Events, Colorized outputs, scale-typegen integration (#1290) * restructure cli commands * config: Add `SkipCheckIfFeeless` signed extension (#1264) * config: Add `SkipCheckIfFeeless` signed extension Signed-off-by: Alexandru Vasile * config: Add extra extension to the default params Signed-off-by: Alexandru Vasile * examples: Adjust signed extension example Signed-off-by: Alexandru Vasile * config: Extend SkipCheckIfFeeless with inner signed extension Signed-off-by: Alexandru Vasile * config: Configure SkipCheck with inner signed extension params Signed-off-by: Alexandru Vasile * config: Implement Deafult for SkipCheckIfFeelessParams with Option Signed-off-by: Alexandru Vasile * examples: Fix example with proper extension Signed-off-by: Alexandru Vasile * config: Extend ::AssetId with EncodeAsType and Clone Signed-off-by: Alexandru Vasile * config: Add SkipCheck with AssetTx Signed-off-by: Alexandru Vasile * config: Encode as type from metadata the inner signed extensions Signed-off-by: Alexandru Vasile * Adjust examples Signed-off-by: Alexandru Vasile * blocks: Use `SkipCheckIfFeeless` for decoding the tip of extensions Signed-off-by: Alexandru Vasile * config: Decode `SkipCheckIfFeeless` with `Self` Signed-off-by: Alexandru Vasile * tests: Adjust testing Signed-off-by: Alexandru Vasile * config: Descriptive errors for building `SkipCheckIfFeeless` Signed-off-by: Alexandru Vasile * config: Add docs for extra error types Signed-off-by: Alexandru Vasile * subxt: Add extra derives to signed extensions Signed-off-by: Alexandru Vasile * config: Use `Default::default` to simplify type init Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile * subxt: Replace removed lint (#1270) Signed-off-by: Alexandru Vasile * lightclient: Add support for multi-chain usecase (#1238) * lightclient: Make `smoldot::chainID` part of the RPC requests Signed-off-by: Alexandru Vasile * lightclient: Make `BackgroundTask` generic over `PlatformRef` and chain Signed-off-by: Alexandru Vasile * lightclient: Construct from raw smoldot and target different chains Signed-off-by: Alexandru Vasile * testing: Update cargo lock for wasm tests Signed-off-by: Alexandru Vasile * lightclient: Reuse `new_from_client` method and removed unused imports Signed-off-by: Alexandru Vasile * lightclient: Reexport smoldot client and RPC objects used in pub interface Signed-off-by: Alexandru Vasile * lightclient: Adjust `new_from_client` interface Signed-off-by: Alexandru Vasile * lightclient: Extend background to poll over multiple RPC objects Signed-off-by: Alexandru Vasile * subxt: Build light client from raw and target different chains Signed-off-by: Alexandru Vasile * artifacts: Add demo chain specs Signed-off-by: Alexandru Vasile * artifacts: Move artifacts to dedicated folder Signed-off-by: Alexandru Vasile * lightclient: Use SelectAll to drive all streams Signed-off-by: Alexandru Vasile * lightclient: Fetch initial data from the target chain Signed-off-by: Alexandru Vasile * lightclient: Reexport other smoldot objects Signed-off-by: Alexandru Vasile * subxt: Target chain with potentially different config Signed-off-by: Alexandru Vasile * subxt/rpc: Log chainID for debugging Signed-off-by: Alexandru Vasile * subxt/examples: Add smoldot client with parachain example Signed-off-by: Alexandru Vasile * lightclient: Propagate chain ID together with rpc responses object Signed-off-by: Alexandru Vasile * lightclient: Multiplex responses by request ID and chain ID Signed-off-by: Alexandru Vasile * subxt: Add raw light client builder Signed-off-by: Alexandru Vasile * subxt: Add cargo feature flag for parachains example Signed-off-by: Alexandru Vasile * lightclient: Derive default for internal structure Signed-off-by: Alexandru Vasile * lightclient: Guard reexports by std feature flag Signed-off-by: Alexandru Vasile * Update subxt/src/client/light_client/mod.rs Co-authored-by: James Wilson * lightclient: Update the builder pattern and chain targetting Signed-off-by: Alexandru Vasile * lightclient: Fix documentation Signed-off-by: Alexandru Vasile * Provide more insightful docs wrt native/wasm panics Signed-off-by: Alexandru Vasile * examples: Adjust comment location Signed-off-by: Alexandru Vasile * lightclient: Refactor UniqueChainId into the background task Signed-off-by: Alexandru Vasile * Update lightclient/src/background.rs Co-authored-by: Niklas Adolfsson * Update subxt/src/client/light_client/builder.rs Co-authored-by: James Wilson * lightclient: Update docs wrt panics Signed-off-by: Alexandru Vasile * subxt: Update docs wrt to smoldot instance -> client Signed-off-by: Alexandru Vasile * lightclient: Use IntoIter instead of Iterator Signed-off-by: Alexandru Vasile * subxt: Adjsut docs wrt [`Self::new_from_client`] Signed-off-by: Alexandru Vasile * subxt: Remove RawRpc from LightClient in favor of chainID Signed-off-by: Alexandru Vasile * lightclient: Reexport everything under smoldot module Signed-off-by: Alexandru Vasile * artifacts: Use stateRootHash instead of genesis.raw Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile Co-authored-by: James Wilson Co-authored-by: Niklas Adolfsson * Bump futures from 0.3.28 to 0.3.29 (#1272) Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.28 to 0.3.29. - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.28...0.3.29) --- updated-dependencies: - dependency-name: futures dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump zeroize from 1.6.0 to 1.7.0 (#1274) Bumps [zeroize](https://github.com/RustCrypto/utils) from 1.6.0 to 1.7.0. - [Commits](https://github.com/RustCrypto/utils/commits) --- updated-dependencies: - dependency-name: zeroize dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump tracing-subscriber from 0.3.17 to 0.3.18 (#1275) Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.17 to 0.3.18. - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.17...tracing-subscriber-0.3.18) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump tracing-subscriber from 0.3.17 to 0.3.18 (#1275) Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.17 to 0.3.18. - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.17...tracing-subscriber-0.3.18) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump getrandom from 0.2.10 to 0.2.11 (#1273) Bumps [getrandom](https://github.com/rust-random/getrandom) from 0.2.10 to 0.2.11. - [Changelog](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-random/getrandom/compare/v0.2.10...v0.2.11) --- updated-dependencies: - dependency-name: getrandom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * impl RpcClientT for Arc and Box (#1277) * impl RpcClientT for Arc * fix grumbles: impl for Box and Arc * grumbles: move RpcClientT impls * first iteration of using scale_typegen * introduce indoc for formatting * calls, constants and home are cleaner now * added event subcommand * show runtime apis working * add better code formatting * fix style * adjust tests, use owo_colorize to not add extra dependency * fmt * adjust docs * move scale-typegen-description dependency to workspace * improve `substrate-compat` (#1265) * improve `substrate-compat` * From => Into --------- Co-authored-by: James Wilson * Bump proc-macro2 from 1.0.69 to 1.0.70 (#1292) Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.69 to 1.0.70. - [Release notes](https://github.com/dtolnay/proc-macro2/releases) - [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.69...1.0.70) --- updated-dependencies: - dependency-name: proc-macro2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump serde from 1.0.192 to 1.0.193 (#1291) Bumps [serde](https://github.com/serde-rs/serde) from 1.0.192 to 1.0.193. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.192...v1.0.193) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * lightclient: Fix wasm socket closure called after being dropped (#1289) * lightclient: Close wasm socket while dropping from connecting state Signed-off-by: Alexandru Vasile * lightclient: Construct one time only closures Signed-off-by: Alexandru Vasile * testing: Enable console logs for lightclient WASM testing Signed-off-by: Alexandru Vasile * lightclient: Separate wakes and check connectivity on poll_read Signed-off-by: Alexandru Vasile * lightclient: Close the socket depending on internal state Signed-off-by: Alexandru Vasile * Revert "lightclient: Separate wakes and check connectivity on poll_read" This reverts commit 866094001d4c0b119a80ed681a74b323f74eae1b. * lightclient: Return pending if socket is opening from poll_read Signed-off-by: Alexandru Vasile * lightclient: Close the socket on `poll_close` Signed-off-by: Alexandru Vasile * lightclient: Reset closures on Drop to avoid recursive invokation Signed-off-by: Alexandru Vasile * lightclient: Close the socket if not already closing Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile * workflows: Install rustup component for building substrate (#1295) Signed-off-by: Alexandru Vasile * cli: Command to fetch chainSpec and optimise its size (#1278) * cli: Add chainSpec command Signed-off-by: Alexandru Vasile * cli/chainSpec: Move to dedicated module Signed-off-by: Alexandru Vasile * cli: Compute the state root hash Signed-off-by: Alexandru Vasile * cli: Remove code substitutes Signed-off-by: Alexandru Vasile * artifacts: Update polkadot.json Signed-off-by: Alexandru Vasile * scripts: Generate the chain spec Signed-off-by: Alexandru Vasile * cli: Remove testing artifacts Signed-off-by: Alexandru Vasile * cli: Fix clippy Signed-off-by: Alexandru Vasile * cli: Apply rustfmt Signed-off-by: Alexandru Vasile * cli: Introduce feature flag for smoldot dependency Signed-off-by: Alexandru Vasile * cli: Rename chain-spec to chain-spec-pruning Signed-off-by: Alexandru Vasile * scripts: Update chain-spec command Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile * update to new scale-typegen interfaces * use released version of scale-typegen * Merge branch 'master' into tadeohepperle/cli-support-runtime-apis * remove unused debug file * resolve merge errors * adjustments * constants file adjustment * method renaming * fix issue with encoding runtime api params * Add logging to submit_transaction and unstable driver, and ensure unpin evs complete * panic if None returned from subscription too, also with stats * change panic to Err just to be on the safe side * clippy * make long tests run only after clippy + fmt pass * megre in light client test change pr * chore(subxt/src): typo fix (#1370) * rpcmethods * followstr * mod and else * Weekly Cronjob fetching artifacts and generating polkadot.rs file. (#1352) * github CI action cronjob * add commit message * fix the CI yml files * binary crate for CI script with substrate-runner * update the CI script * correct the artifacts script * remove bash script * lightclient(fix): Ensure lightclient chainSpec is at least one block old (#1372) * testing(fix): Ensure lightclient chainSpec is at least one block old Signed-off-by: Alexandru Vasile * Revert "testing(fix): Ensure lightclient chainSpec is at least one block old" This reverts commit 0eafcb2ca59d1f1cd2cef86b770f5a0401cce59f. * lightclient(fix): Ensure lightclient chainSpec is at least one block old Signed-off-by: Alexandru Vasile * lightclient: Link smoldot issue Signed-off-by: Alexandru Vasile * subxt: Use tokio under lightclient feature flag Signed-off-by: Alexandru Vasile * lightclient: Do not sleep on errors to fetch the chainSpec Signed-off-by: Alexandru Vasile * artifacts: Remove test file Signed-off-by: Alexandru Vasile * lightclient: Subscribe to two finalized blocks Signed-off-by: Alexandru Vasile * subxt: Revert cargo toml Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile * ci: Reduce the light client timeout to 15 minutes (#1373) * ci: Reduce the light client timpeut to 15 seconds Signed-off-by: Alexandru Vasile * ci: Use ubuntu-latest for light-client tests Signed-off-by: Alexandru Vasile --------- Signed-off-by: Alexandru Vasile * actually only wait for machete+fmt, clippy can be much slower * update CI file from Alex PR * resolve clippy err * Try a few RPC nodes in case one of them is not working * fix submit_transaction debug logging of message * Improve Signed Extension and Block Decoding Examples/Book (#1357) * asset hub example and book adjustment * formatting * recursive derives * polkadot monitor example and book adjustments * formatting * adjust docs and examples, add dynamic example * james suggestions * fmt * chore(subxt/src): typo fix (#1370) * rpcmethods * followstr * mod and else * Weekly Cronjob fetching artifacts and generating polkadot.rs file. (#1352) * github CI action cronjob * add commit message * fix the CI yml files * binary crate for CI script with substrate-runner * update the CI script * correct the artifacts script * remove bash script --------- Co-authored-by: James Wilson Co-authored-by: Pan chao <152830401+Pan-chao@users.noreply.github.com> * fix formatting of returned sections * make storage use execute flag as well --------- Signed-off-by: Alexandru Vasile Signed-off-by: dependabot[bot] Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Co-authored-by: James Wilson Co-authored-by: Niklas Adolfsson Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: yjh Co-authored-by: Pan chao <152830401+Pan-chao@users.noreply.github.com> --- Cargo.lock | 86 ++++ Cargo.toml | 2 + cli/Cargo.toml | 9 +- cli/src/commands/explore/constants.rs | 94 ---- cli/src/commands/explore/mod.rs | 448 +++++++++++++----- .../commands/explore/{ => pallets}/calls.rs | 83 ++-- cli/src/commands/explore/pallets/constants.rs | 96 ++++ cli/src/commands/explore/pallets/events.rs | 88 ++++ cli/src/commands/explore/pallets/mod.rs | 80 ++++ cli/src/commands/explore/pallets/storage.rs | 222 +++++++++ cli/src/commands/explore/runtime_apis/mod.rs | 203 ++++++++ cli/src/commands/explore/storage.rs | 192 -------- cli/src/utils.rs | 188 +++++++- cli/src/utils/type_description.rs | 303 ------------ cli/src/utils/type_example.rs | 380 --------------- 15 files changed, 1342 insertions(+), 1132 deletions(-) delete mode 100644 cli/src/commands/explore/constants.rs rename cli/src/commands/explore/{ => pallets}/calls.rs (68%) create mode 100644 cli/src/commands/explore/pallets/constants.rs create mode 100644 cli/src/commands/explore/pallets/events.rs create mode 100644 cli/src/commands/explore/pallets/mod.rs create mode 100644 cli/src/commands/explore/pallets/storage.rs create mode 100644 cli/src/commands/explore/runtime_apis/mod.rs delete mode 100644 cli/src/commands/explore/storage.rs delete mode 100644 cli/src/utils/type_description.rs delete mode 100644 cli/src/utils/type_example.rs diff --git a/Cargo.lock b/Cargo.lock index 2e5a5bb427..014d09c5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1299,6 +1299,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.8.1" @@ -2129,6 +2135,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + [[package]] name = "inout" version = "0.1.3" @@ -2784,6 +2796,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "peekmore" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9163e1259760e83d528d1b3171e5100c1767f10c52e1c4d6afad26e63d47d758" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2908,6 +2926,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3465,6 +3493,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "scale-typegen-description" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae69c1dfd81e9859a5fb42c1b560369e6ed82d2c5b5cb4cac3bba1962a73f017" +dependencies = [ + "anyhow", + "peekmore", + "proc-macro2", + "quote", + "rand 0.8.5", + "rand_chacha 0.3.1", + "scale-info", + "scale-typegen", + "scale-value", + "smallvec", +] + [[package]] name = "scale-value" version = "0.13.0" @@ -4307,6 +4353,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -4409,14 +4464,19 @@ dependencies = [ "clap 4.4.16", "color-eyre", "frame-metadata 16.0.0", + "heck", "hex", + "indoc", "jsonrpsee", "parity-scale-codec", + "pretty_assertions", "scale-info", + "scale-typegen-description", "scale-value", "serde", "serde_json", "smoldot", + "strip-ansi-escapes", "subxt", "subxt-codegen", "subxt-metadata", @@ -5039,6 +5099,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "w3f-bls" version = "0.1.3" @@ -5656,6 +5736,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index d52a1126e2..8d5d8e7c5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ getrandom = { version = "0.2", default-features = false } hex = "0.4.3" heck = "0.4.1" impl-serde = { version = "0.4.0" } +indoc = "2" jsonrpsee = { version = "0.21" } pretty_assertions = "1.4.0" primitive-types = { version = "0.12.2", default-features = false, features = ["codec", "scale-info", "serde"] } @@ -97,6 +98,7 @@ wasm-bindgen-test = "0.3.24" which = "5.0.0" scale-typegen-description = "0.1.0" scale-typegen = "0.1.1" +strip-ansi-escapes = "0.2.0" # Light client support: smoldot = { version = "0.16.0", default-features = false } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f569a30005..e00cd996f3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -40,5 +40,12 @@ scale-value = { workspace = true } syn = { workspace = true } jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-native-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} +smoldot = { workspace = true, optional = true } + +[dev-dependencies] +strip-ansi-escapes = { workspace = true } +pretty_assertions = { workspace = true } diff --git a/cli/src/commands/explore/constants.rs b/cli/src/commands/explore/constants.rs deleted file mode 100644 index 6f321881ae..0000000000 --- a/cli/src/commands/explore/constants.rs +++ /dev/null @@ -1,94 +0,0 @@ -use clap::Args; -use color_eyre::eyre::eyre; -use std::fmt::Write; - -use subxt::metadata::{types::PalletMetadata, Metadata}; - -use crate::utils::type_description::print_type_description; -use crate::utils::{print_first_paragraph_with_indent, with_indent}; - -#[derive(Debug, Clone, Args)] -pub struct ConstantsSubcommand { - constant: Option, -} - -pub fn explore_constants( - command: ConstantsSubcommand, - metadata: &Metadata, - pallet_metadata: PalletMetadata, - output: &mut impl std::io::Write, -) -> color_eyre::Result<()> { - let pallet_name = pallet_metadata.name(); - let Some(constant_name) = command.constant else { - let available_constants = print_available_constants(pallet_metadata, pallet_name); - writeln!(output, "Usage:")?; - writeln!( - output, - " subxt explore {pallet_name} constants " - )?; - writeln!( - output, - " 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() - .find(|constant| constant.name().to_lowercase() == constant_name.to_lowercase()) - else { - let available_constants = print_available_constants(pallet_metadata, pallet_name); - let mut description = "Usage:".to_string(); - writeln!( - description, - " subxt explore {pallet_name} constants " - )?; - writeln!( - description, - " 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 doc_string = print_first_paragraph_with_indent(constant.docs(), 4); - if !doc_string.is_empty() { - writeln!(output, "Description:\n{doc_string}")?; - } - - // shape - let mut type_description = print_type_description(&constant.ty(), metadata.types())?; - type_description = with_indent(type_description, 4); - writeln!( - output, - "\nThe constant has the following shape:\n{type_description}" - )?; - - // value - let scale_val = - scale_value::scale::decode_as_type(&mut constant.value(), constant.ty(), metadata.types())?; - writeln!( - output, - "\nThe value of the constant is:\n {}", - scale_value::stringify::to_string(&scale_val) - )?; - Ok(()) -} - -fn print_available_constants(pallet_metadata: PalletMetadata, pallet_name: &str) -> String { - if pallet_metadata.constants().len() == 0 { - return format!("No 's available in the \"{pallet_name}\" pallet."); - } - let mut output = format!("Available '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 -} diff --git a/cli/src/commands/explore/mod.rs b/cli/src/commands/explore/mod.rs index cd033f3dc7..abbcd2b4b2 100644 --- a/cli/src/commands/explore/mod.rs +++ b/cli/src/commands/explore/mod.rs @@ -1,159 +1,240 @@ -use crate::utils::{print_first_paragraph_with_indent, validate_url_security, FileOrUrl}; -use clap::{Parser as ClapParser, Subcommand}; - -use std::fmt::Write; -use std::write; - +use crate::utils::validate_url_security; +use crate::utils::FileOrUrl; +use clap::{command, Parser, Subcommand}; use codec::Decode; use color_eyre::eyre::eyre; - -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 color_eyre::owo_colors::OwoColorize; +use indoc::writedoc; +use std::fmt::Write; +use std::write; use subxt::Metadata; -mod calls; -mod constants; -mod storage; +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 /// -/// ## Pallets -/// -/// Show the pallets that are available: +/// Show the pallets and runtime apis that are available: /// ```text /// subxt explore --file=polkadot_metadata.scale /// ``` /// -/// ## Calls +/// ## Pallets +/// +/// each pallet has `calls`, `constants`, `storage` and `events` that can be explored. +/// +/// ### Calls /// /// Show the calls in a pallet: +/// /// ```text -/// subxt explore Balances calls +/// subxt explore pallet Balances calls /// ``` +/// /// Show the call parameters a call expects: +/// /// ```text -/// subxt explore Balances calls transfer +/// subxt explore pallet Balances calls transfer /// ``` +/// /// Create an unsigned extrinsic from a scale value, validate it and output its hex representation +/// /// ```text -/// subxt explore Grandpa calls note_stalled { "delay": 5, "best_finalized_block_number": 5 } +/// subxt explore pallet 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 }" +/// subxt explore pallet Balances calls transfer "{ \"dest\": v\"Raw\"((255, 255, 255)), \"value\": 0 }" /// # Encoded call data: /// # 0x24040607020cffffff00 /// ``` -/// ## Constants +/// +/// ### Constants /// /// Show the constants in a pallet: +/// /// ```text -/// subxt explore Balances constants +/// subxt explore pallet Balances constants /// ``` -/// ## Storage +/// +/// ### Storage /// /// Show the storage entries in a pallet +/// /// ```text -/// subxt explore Alliance storage +/// 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 Alliance storage Announcements [KEY_SCALE_VALUE] +/// subxt explore api core version --execute /// ``` /// -#[derive(Debug, ClapParser)] +#[derive(Debug, Parser)] pub struct Opts { #[command(flatten)] file_or_url: FileOrUrl, - pallet: Option, #[command(subcommand)] - pallet_subcommand: Option, + subcommand: Option, /// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption #[clap(long, short)] allow_insecure: bool, } -#[derive(Debug, Clone, Subcommand)] -pub enum PalletSubcommand { - Calls(CallsSubcommand), - Constants(ConstantsSubcommand), - Storage(StorageSubcommand), +#[derive(Debug, Subcommand)] +pub enum PalletOrRuntimeApi { + Pallet(PalletOpts), + Api(RuntimeApiOpts), +} + +#[derive(Debug, Parser)] +pub struct PalletOpts { + pub name: Option, + #[command(subcommand)] + pub subcommand: Option, +} + +#[derive(Debug, Parser)] +pub struct RuntimeApiOpts { + pub name: Option, + #[clap(required = false)] + pub method: Option, + #[clap(long, short, action)] + pub execute: bool, + #[clap(required = false)] + trailing_args: Vec, } 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 bytes = opts.file_or_url.fetch().await?; + let file_or_url = opts.file_or_url; + let bytes = file_or_url.fetch().await?; let metadata = Metadata::decode(&mut &bytes[..])?; - // 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); - writeln!(output, "Usage:",)?; - writeln!(output, " subxt explore ",)?; - writeln!(output, " explore a specific pallet",)?; - writeln!(output, "\n{available_pallets}",)?; - return Ok(()); - }; + let pallet_placeholder = "".blue(); + let runtime_api_placeholder = "".blue(); - // if specified pallet is wrong, show user the pallets to choose from (but this time as an error): - let Some(pallet_metadata) = metadata - .pallets() - .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) - )); - }; + // 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 - // 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_first_paragraph_with_indent(pallet_metadata.docs(), 4); - if !docs_string.is_empty() { - writeln!(output, "Description:\n{docs_string}")?; - } - writeln!(output, "Usage:")?; - writeln!(output, " subxt explore {pallet_name} calls")?; - writeln!( - output, - " explore the calls that can be made into this pallet" - )?; - writeln!(output, " subxt explore {pallet_name} constants")?; - writeln!(output, " explore the constants held in this pallet")?; - writeln!(output, " subxt explore {pallet_name} storage")?; - writeln!( - output, - " explore the storage values held in this pallet" - )?; + {pallets} + + {runtime_apis} + "}?; return Ok(()); }; - match pallet_subcomand { - PalletSubcommand::Calls(command) => { - explore_calls(command, &metadata, pallet_metadata, output) - } - PalletSubcommand::Constants(command) => { - explore_constants(command, &metadata, pallet_metadata, output) + 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), + )) + } } - PalletSubcommand::Storage(command) => { - // if the metadata is in 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, output).await + 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 print_available_pallets(metadata: &Metadata) -> String { +fn pallets_as_string(metadata: &Metadata) -> String { + let pallet_placeholder = "".blue(); if metadata.pallets().len() == 0 { - "There are no values available.".to_string() + format!("There are no {pallet_placeholder}'s available.") } else { - let mut output = "Available values are:".to_string(); + 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 { @@ -163,8 +244,27 @@ fn print_available_pallets(metadata: &Metadata) -> String { } } +pub fn runtime_apis_as_string(metadata: &Metadata) -> String { + let runtime_api_placeholder = "".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 { @@ -173,9 +273,25 @@ pub mod tests { args.append(&mut split); let opts: Opts = clap::Parser::try_parse_from(args)?; let mut output: Vec = Vec::new(); - super::run(opts, &mut output) + let r = super::run(opts, &mut output) .await - .map(|_| String::from_utf8(output).unwrap()) + .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 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 { @@ -187,44 +303,138 @@ pub mod tests { #[tokio::test] async fn test_commands() { - // show pallets: - let output = run_against_file("").await; - assert_eq!(output.unwrap(), "Usage:\n subxt explore \n explore a specific pallet\n\nAvailable values are:\n Balances\n Multisig\n ParaInherent\n System\n Timestamp\n"); + // shows pallets and runtime apis: + let output = run_against_file("").await.unwrap().strip_ansi(); + let expected_output = formatdoc! { + "Usage: + subxt explore pallet + explore a specific pallet + subxt explore api + explore a specific runtime api + + Available 's are: + Balances + Multisig + ParaInherent + System + Timestamp + + Available 's are: + AccountNonceApi + AuthorityDiscoveryApi + BabeApi + BeefyApi + BeefyMmrApi + BlockBuilder + Core + GenesisBuilder + GrandpaApi + Metadata + MmrApi + OffchainWorkerApi + ParachainHost + SessionKeys + TaggedTransactionQueue + TransactionPaymentApi + "}; + 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("Balances").await; - assert_eq!(output.unwrap(), "Usage:\n subxt explore Balances calls\n explore the calls that can be made into this pallet\n subxt explore Balances constants\n explore the constants held in this pallet\n subxt explore Balances storage\n explore the storage values held in this pallet\n"); + 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("Balances calls").await; - assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances calls \n explore a specific call within this pallet\n\nAvailable 's in the \"Balances\" pallet:\n")); - let output = run_against_file("Balances storage").await; - assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances storage \n view details for a specific storage entry\n\nAvailable 's in the \"Balances\" pallet:\n")); - let output = run_against_file("Balances constants").await; - assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances constants \n explore a specific call within this pallet\n\nAvailable 's in the \"Balances\" pallet:\n")); + let output = run_against_file("pallet Balances calls") + .await + .unwrap() + .strip_ansi(); + let start = formatdoc! {" + Usage: + subxt explore pallet Balances calls + explore a specific call of this pallet + + Available '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 + explore a specific storage entry of this pallet + + Available '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 + explore a specific constant of this pallet + + Available '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 + explore a specific event of this pallet + + Available 's in the \"Balances\" pallet: + "}; + assert_eq_start!(output, start); // check that invalid subcommands don't work: - let output = run_against_file("Balances abc123").await; + 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("Balances calls transfer_allow_death").await; - assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances calls transfer_allow_death \n construct the call by providing a valid argument\n\nThe call expect expects a with this shape:\n {\n dest: enum MultiAddress")); - // check that unsigned extrinsic can be constructed: - let output = run_against_file( - "Balances calls transfer_allow_death {\"dest\":v\"Raw\"((255,255, 255)),\"value\":0}", - ) - .await; - assert_eq!( - output.unwrap(), - "Encoded call data:\n 0x24040400020cffffff00\n" - ); - // check that we can explore a certain constant: - let output = run_against_file("Balances constants ExistentialDeposit").await; - assert_eq!(output.unwrap(), "Description:\n The minimum amount required to keep an account open. MUST BE GREATER THAN ZERO!\n\nThe constant has the following shape:\n u128\n\nThe value of the constant is:\n 33333333\n"); - // check that we can explore a certain storage entry: - let output = run_against_file("System storage Account").await; - assert!(output.unwrap().starts_with("Usage:\n subxt explore System storage Account \n\nDescription:\n The full account information for a particular account ID.")); - // in the future we could also integrate with substrate-testrunner to spawn up a node and send an actual storage query to it: e.g. `subxt explore System storage Digest` + 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 + 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 + explore a specific runtime api method + + Available 's available for the \"Metadata\" runtime api: + "}; + assert_eq_start!(output, start); } #[tokio::test] diff --git a/cli/src/commands/explore/calls.rs b/cli/src/commands/explore/pallets/calls.rs similarity index 68% rename from cli/src/commands/explore/calls.rs rename to cli/src/commands/explore/pallets/calls.rs index 1cbe27ee10..f5d9609a2e 100644 --- a/cli/src/commands/explore/calls.rs +++ b/cli/src/commands/explore/pallets/calls.rs @@ -1,5 +1,7 @@ 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}; @@ -13,9 +15,10 @@ use subxt::{ OfflineClient, }; -use crate::utils::type_description::print_type_description; -use crate::utils::type_example::print_type_examples; -use crate::utils::with_indent; +use crate::utils::{ + fields_composite_example, fields_description, parse_string_into_scale_value, Indent, + SyntaxHighlight, +}; #[derive(Debug, Clone, Args)] pub struct CallsSubcommand { @@ -26,8 +29,8 @@ pub struct CallsSubcommand { pub fn explore_calls( command: CallsSubcommand, - metadata: &Metadata, pallet_metadata: PalletMetadata, + metadata: &Metadata, output: &mut impl std::io::Write, ) -> color_eyre::Result<()> { let pallet_name = pallet_metadata.name(); @@ -36,10 +39,20 @@ pub fn explore_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 + 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 { - let available_calls = print_available_calls(calls_enum_type_def, pallet_name); - writeln!(output, "Usage:\n subxt explore {pallet_name} calls \n explore a specific call within this pallet\n\n{available_calls}")?; + writeln!(output, "{}", usage())?; return Ok(()); }; @@ -47,12 +60,11 @@ pub fn explore_calls( let Some(call) = calls_enum_type_def .variants .iter() - .find(|variant| variant.name.to_lowercase() == call_name.to_lowercase()) + .find(|variant| variant.name.eq_ignore_ascii_case(&call_name)) else { - let available_calls = print_available_calls(calls_enum_type_def, pallet_name); - let description = format!("Usage:\n subxt explore {pallet_name} calls \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}" + "\"{call_name}\" call not found in \"{pallet_name}\" pallet!\n\n{}", + usage() )); }; @@ -61,38 +73,49 @@ pub fn explore_calls( // 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.types())?; - type_description = with_indent(type_description, 4); - let mut type_examples = print_type_examples(&call.fields, metadata.types(), "SCALE_VALUE")?; - type_examples = with_indent(type_examples, 4); - writeln!(output, "Usage:")?; - writeln!( - output, - " subxt explore {pallet_name} calls {call_name} " - )?; - writeln!( - output, - " construct the call by providing a valid argument\n" - )?; - writeln!( - output, - "The call expect expects a 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..])?; + 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 = "".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 = 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 = 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())); - writeln!(output, "Encoded call data:\n {hex_bytes}")?; + writedoc! {output, " + Encoded call data: + {hex_bytes} + "}?; + Ok(()) } -fn print_available_calls(pallet_calls: &TypeDefVariant, pallet_name: &str) -> String { +fn calls_to_string(pallet_calls: &TypeDefVariant, pallet_name: &str) -> String { if pallet_calls.variants.is_empty() { return format!("No 's available in the \"{pallet_name}\" pallet."); } diff --git a/cli/src/commands/explore/pallets/constants.rs b/cli/src/commands/explore/pallets/constants.rs new file mode 100644 index 0000000000..dd5e5faddc --- /dev/null +++ b/cli/src/commands/explore/pallets/constants.rs @@ -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::{types::PalletMetadata, Metadata}; + +use crate::utils::{first_paragraph_of_docs, format_scale_value, Indent, SyntaxHighlight}; + +#[derive(Debug, Clone, Args)] +pub struct ConstantsSubcommand { + constant: Option, +} + +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 + 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 's available in the \"{pallet_name}\" pallet."); + } + let mut output = format!("Available '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 +} diff --git a/cli/src/commands/explore/pallets/events.rs b/cli/src/commands/explore/pallets/events.rs new file mode 100644 index 0000000000..c27264310c --- /dev/null +++ b/cli/src/commands/explore/pallets/events.rs @@ -0,0 +1,88 @@ +use clap::Args; +use color_eyre::eyre::eyre; +use indoc::{formatdoc, writedoc}; +use scale_info::{form::PortableForm, Variant}; +use subxt::metadata::{types::PalletMetadata, Metadata}; + +use crate::utils::{fields_description, first_paragraph_of_docs, Indent}; + +#[derive(Debug, Clone, Args)] +pub struct EventsSubcommand { + event: Option, +} + +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 + 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], pallet_name: &str) -> String { + if event_variants.is_empty() { + return format!("No 's available in the \"{pallet_name}\" pallet."); + } + let mut output = format!("Available '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 +} diff --git a/cli/src/commands/explore/pallets/mod.rs b/cli/src/commands/explore/pallets/mod.rs new file mode 100644 index 0000000000..5bbdbfffbf --- /dev/null +++ b/cli/src/commands/explore/pallets/mod.rs @@ -0,0 +1,80 @@ +use clap::Subcommand; + +use indoc::writedoc; +use subxt::Metadata; +use subxt_metadata::PalletMetadata; + +use crate::utils::{first_paragraph_of_docs, FileOrUrl, Indent}; + +use self::{ + calls::CallsSubcommand, + constants::ConstantsSubcommand, + events::{explore_events, EventsSubcommand}, + 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, + 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) + } + } +} diff --git a/cli/src/commands/explore/pallets/storage.rs b/cli/src/commands/explore/pallets/storage.rs new file mode 100644 index 0000000000..0c932c8f1a --- /dev/null +++ b/cli/src/commands/explore/pallets/storage.rs @@ -0,0 +1,222 @@ +use clap::Args; +use color_eyre::{ + eyre::{bail, eyre}, + 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::{ + ext::scale_encode::EncodeAsType, + metadata::{ + types::{PalletMetadata, StorageEntryType, StorageMetadata}, + Metadata, + }, +}; + +use crate::utils::{ + create_client, first_paragraph_of_docs, parse_string_into_scale_value, type_example, FileOrUrl, + Indent, SyntaxHighlight, +}; + +#[derive(Debug, Clone, Args)] +pub struct StorageSubcommand { + storage_entry: Option, + #[clap(long, short, action)] + execute: bool, + #[clap(required = false)] + trailing_args: Vec, +} + +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.join(" "); + let trailing_args = trailing_args.trim(); + + let Some(storage_metadata) = pallet_metadata.storage() else { + writeln!( + output, + "The \"{pallet_name}\" pallet has no storage entries." + )?; + return Ok(()); + }; + + let storage_entry_placeholder = "".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, key_ty_id) = match storage.entry_type() { + StorageEntryType::Plain(value) => (*value, None), + StorageEntryType::Map { + value_ty, key_ty, .. + } => (*value_ty, Some(*key_ty)), + }; + + let key_value_placeholder = "".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: + if let Some(key_ty_id) = key_ty_id { + let key_ty_description = type_description(key_ty_id, metadata.types(), true) + .expect("No type Description") + .indent(4) + .highlight(); + + let key_ty_example = type_example(key_ty_id, metadata.types()) + .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 = match (trailing_args.is_empty(), key_ty_id) { + (false, None) => { + let warning = format!("Warning: You submitted a key, but no key is needed: \"{trailing_args}\". To access the storage value, please do not provide any key."); + writeln!(output, "{}", warning.yellow())?; + return Ok(()); + } + (true, Some(_)) => { + // just return. The user was instructed above how to provide a value if they want to. + return Ok(()); + } + (true, None) => vec![], + (false, Some(type_id)) => { + let value = parse_string_into_scale_value(trailing_args)?; + let value_str = value.indent(4); + writedoc! {output, " + + You submitted the following {key_value_placeholder}: + {value_str} + "}?; + + let key_bytes = value.encode_as_type(type_id, metadata.types())?; + let bytes_composite = Value::from_bytes(key_bytes); + vec![bytes_composite] + } + }; + + // construct the client: + let client = create_client(&file_or_url).await?; + + let storage_query = subxt::dynamic::storage(pallet_name, storage.name(), storage_entry_keys); + let decoded_value_thunk_or_none = 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()?.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 = "".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:", + 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 + } +} diff --git a/cli/src/commands/explore/runtime_apis/mod.rs b/cli/src/commands/explore/runtime_apis/mod.rs new file mode 100644 index 0000000000..cdd5681e26 --- /dev/null +++ b/cli/src/commands/explore/runtime_apis/mod.rs @@ -0,0 +1,203 @@ +use crate::utils::{ + create_client, fields_composite_example, fields_description, first_paragraph_of_docs, + parse_string_into_scale_value, FileOrUrl, Indent, SyntaxHighlight, +}; + +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::{ + ext::{scale_decode::DecodeAsType, scale_encode::EncodeAsType}, + Metadata, +}; +use subxt_metadata::RuntimeApiMetadata; + +/// Runs for a specified runtime API trait. +/// Cases to consider: +/// ```norun +/// method is: +/// None => Show pallet docs + available methods +/// Some (invalid) => Show Error + available methods +/// Some (valid) => Show method docs + output type description +/// exectute 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 witht the provided args. +/// response is: +/// Err => Show Error +/// Ok => Show the result +/// ``` +pub async fn run<'a>( + method: Option, + execute: bool, + trailing_args: Vec, + 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} + 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 = "".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.as_str()), f.ty)) + .collect(); + let fields_description = + fields_description(&fields, method.name(), metadata.types()).indent(4); + + let fields_example = + fields_composite_example(method.inputs().map(|e| e.ty), 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 = 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.ty, metadata.types())?; + let value = Value::decode_as_type(&mut &bytes[..], ty.ty, metadata.types())? + .map_context(|_| ()); + Ok(value) + }) + .collect::>>()?; + + let method_call = subxt::dynamic::runtime_api_call(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_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 's available for the \"{api_name}\" runtime api."); + } + + let mut output = format!("Available '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 +} diff --git a/cli/src/commands/explore/storage.rs b/cli/src/commands/explore/storage.rs deleted file mode 100644 index 11acf4c2d9..0000000000 --- a/cli/src/commands/explore/storage.rs +++ /dev/null @@ -1,192 +0,0 @@ -use clap::Args; -use color_eyre::eyre::eyre; -use std::fmt::Write; -use std::write; - -use subxt::OnlineClient; -use subxt::{ - config::SubstrateConfig, - metadata::{ - types::{PalletMetadata, StorageEntryType, StorageMetadata}, - Metadata, - }, -}; - -use crate::utils::type_description::print_type_description; -use crate::utils::type_example::print_type_examples; -use crate::utils::{print_first_paragraph_with_indent, with_indent}; - -#[derive(Debug, Clone, Args)] -pub struct StorageSubcommand { - storage_entry: Option, - #[clap(required = false)] - trailing_args: Vec, -} - -pub async fn explore_storage( - command: StorageSubcommand, - metadata: &Metadata, - pallet_metadata: PalletMetadata<'_>, - custom_online_client_url: Option, - output: &mut impl std::io::Write, -) -> color_eyre::Result<()> { - let pallet_name = pallet_metadata.name(); - let trailing_args = command.trailing_args.join(" "); - let trailing_args = trailing_args.trim(); - - let Some(storage_metadata) = pallet_metadata.storage() else { - writeln!( - output, - "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); - writeln!(output, "Usage:\n subxt explore {pallet_name} storage \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 \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.entry_type() { - StorageEntryType::Plain(value) => (*value, None), - StorageEntryType::Map { - value_ty, key_ty, .. - } => (*value_ty, Some(*key_ty)), - }; - - // only inform user about usage if a key can be provided: - if key_ty_id.is_some() && trailing_args.is_empty() { - writeln!(output, "Usage:")?; - writeln!( - output, - " subxt explore {pallet_name} storage {entry_name} \n" - )?; - } - - let docs_string = print_first_paragraph_with_indent(storage.docs(), 4); - if !docs_string.is_empty() { - writeln!(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_ty_examples = with_indent(key_ty_examples, 4); - writeln!( - output, - "\nThe has the following shape:\n {key_ty_description}\n" - )?; - writeln!(output, "{}", &key_ty_examples[4..])?; - } else { - writeln!( - output, - "The 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 - }; - writeln!( - output, - "\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))?; - writeln!( - 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 = 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() { - writeln!(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)?; - } - - // 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::::new().await?, - Some(url) => OnlineClient::::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); - writeln!( - output, - "\nThe value of the storage entry is:\n{value_string}" - )?; - } - - Ok(()) -} - -fn print_available_storage_entries( - storage_metadata: &StorageMetadata, - pallet_name: &str, -) -> String { - if storage_metadata.entries().is_empty() { - format!("No 's available in the \"{pallet_name}\" pallet.") - } else { - let mut output = format!( - "Available '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 - } -} diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 24cf6756a2..624d5256ae 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -3,16 +3,19 @@ // see LICENSE for license details. use clap::Args; -use color_eyre::eyre::bail; - +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 subxt_codegen::fetch_metadata::{fetch_metadata_from_url, MetadataVersion, Url}; -pub mod type_description; -pub mod type_example; - /// The source of the metadata. #[derive(Debug, Args, Clone)] pub struct FileOrUrl { @@ -125,7 +128,68 @@ impl FileOrUrl { } } -pub fn print_first_paragraph_with_indent(docs: &[String], indent: usize) -> String { +/// 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, + types: &PortableRegistry, +) -> Value { + let examples: Vec = 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::>() + .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(value: &Value) -> 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. let docs_str = docs .iter() @@ -133,15 +197,113 @@ pub fn print_first_paragraph_with_indent(docs: &[String], indent: usize) -> Stri .take_while(|e| !e.is_empty()) .collect::>() .join("\n"); - with_indent(docs_str, indent) + docs_str } -pub fn with_indent(s: String, indent: usize) -> String { - let indent_str = " ".repeat(indent); - s.lines() - .map(|line| format!("{indent_str}{line}")) - .collect::>() - .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::>() + .join("\n") + } +} + +impl Indent for T {} + +pub async fn create_client( + file_or_url: &FileOrUrl, +) -> color_eyre::Result> { + let client = match &file_or_url.url { + Some(url) => OnlineClient::::from_url(url).await?, + None => OnlineClient::::new().await?, + }; + Ok(client) +} + +pub fn parse_string_into_scale_value(str: &str) -> color_eyre::Result { + 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> 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 = 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<()> { diff --git a/cli/src/utils/type_description.rs b/cli/src/utils/type_description.rs deleted file mode 100644 index 65ac52d341..0000000000 --- a/cli/src/utils/type_description.rs +++ /dev/null @@ -1,303 +0,0 @@ -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(ty: &T, registry: &PortableRegistry) -> color_eyre::Result -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; -} - -impl TypeDescription for u32 { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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) -> Option<&str> { - match type_def { - TypeDef::Composite(_) => Some("struct"), - TypeDef::Variant(_) => Some("enum"), - _ => None, - } -} - -impl TypeDescription for TypeDef { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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 { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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 { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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 { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - let type_description = self.type_param.id.type_description(registry)?; - Ok(format!("Sequence({type_description})")) - } -} - -impl TypeDescription for TypeDefCompact { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - let type_description = self.type_param.id.type_description(registry)?; - Ok(format!("Compact({type_description})")) - } -} - -impl TypeDescription for TypeDefArray { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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 { - 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 { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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() { - variants_string.push(','); - } - } - variants_string.push('}'); - Ok(variants_string) - } -} - -impl TypeDescription for Variant { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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> { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - if self.is_empty() { - return Ok("()".to_string()); - } - - 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() { - fields_string.push(',') - } - } - fields_string.push(brackets.1); - Ok(fields_string) - } -} - -impl TypeDescription for Field { - fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result { - 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 -} - -#[cfg(test)] -mod test { - use crate::utils::type_description::print_type_description; - use scale_info::scale::{Decode, Encode}; - use scale_info::TypeInfo; - use std::fmt::Write; - use std::write; - - #[derive(Encode, Decode, Debug, Clone, TypeInfo)] - pub struct Foo { - hello: String, - num: i32, - } - - /// Given a type definition, return type ID and registry representing it. - fn make_type() -> (u32, scale_info::PortableRegistry) { - let m = scale_info::MetaType::new::(); - let mut types = scale_info::Registry::new(); - let id = types.register_type(&m); - let portable_registry: scale_info::PortableRegistry = types.into(); - (id.id, portable_registry) - } - - #[test] - fn test_type_description() { - let (foo_type_id, foo_registry) = make_type::(); - let description = print_type_description(&foo_type_id, &foo_registry).unwrap(); - let mut output = String::new(); - writeln!(output, "struct Foo {{").unwrap(); - writeln!(output, " hello: String,").unwrap(); - writeln!(output, " num: i32").unwrap(); - write!(output, "}}").unwrap(); - assert_eq!(description, output); - } -} diff --git a/cli/src/utils/type_example.rs b/cli/src/utils/type_example.rs deleted file mode 100644 index f14e1f7cf0..0000000000 --- a/cli/src/utils/type_example.rs +++ /dev/null @@ -1,380 +0,0 @@ -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( - ty: &T, - registry: &PortableRegistry, - type_placeholder: &str, -) -> color_eyre::Result -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 = ::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>; - 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> { - 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 { - type Value = scale_value::Variant<()>; - - fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result> { - let mut examples: Vec> = 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 { - type Value = scale_value::Composite<()>; - - fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result> { - // 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 { - type Value = scale_value::Composite<()>; - - fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result> { - // create unnamed fields to use the same logic already used for struct example generation - let fields_vector: Vec> = 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> { - type Value = scale_value::Composite<()>; - - fn type_example(&self, registry: &PortableRegistry) -> color_eyre::Result> { - 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, Vec)> = 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, scale_value::Value)>> = - Vec::new(); - for _ in 0..n { - let composite_example: Vec<(&Field, 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> { - 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: (), - } - } -}