Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI subxt explore commands #950

Merged
merged 21 commits into from
May 23, 2023

Conversation

tadeohepperle
Copy link
Contributor

@tadeohepperle tadeohepperle commented May 10, 2023

fixes #583
fixes #621
fixes #620

The CLI tool has now a command explore which has a few functionalities to explore pallets, calls, call parameters, storage entries and constants of a node. You can also create (unsigned) extrinsics from a SCALE-value from the command line.
The subxt explore command has a tree like structure that guides you through the process of accessing different pallets and their calls, constants and storage entries.

Here are some commands that can get you started:

subxt explore or subxt explore --file=./artifacts/polkadot_metadata.scale

The --file or --url flag is optional, if no is submitted it looks for a local node at localhost://9933 as a default

Usage:
    subxt explore <PALLET>
        explore a specific pallet

Available <PALLET> values are:
    Alliance
    AllianceMotion
    AssetRate
    AssetTxPayment
    Assets
    ...

subxt explore Balances

(should also show docs for the pallet to help people understand what the pallet does. But currently there are no docs in the metadata)

Usage:
    subxt explore Balances calls
        explore the calls that can be made into this pallet
    subxt explore Balances constants
        explore the constants held in this pallet
    subxt explore Balances storage
        explore the storage values held in this pallet

CALLS

subxt explore Balances calls

Usage:
    subxt explore Balances calls <CALL>
        explore a specific call within this pallet

Available <CALL>'s in the "Balances" pallet:
    force_set_balance
    force_transfer
    force_unreserve
    set_balance_deprecated
    transfer
    transfer_all
    transfer_allow_death
    transfer_keep_alive
    upgrade_accounts

subxt explore Grandpa calls note_stalled

Usage:
    subxt explore Grandpa calls note_stalled <SCALE_VALUE>
        construct the call by providing a valid argument

The call expect expects a <SCALE_VALUE> with this shape:
    {
        delay: u32,
        best_finalized_block_number: u32
    }

Here are 3 examples of a SCALE_VALUE matching this shape:
    { "delay": 4294967295, "best_finalized_block_number": 4294967295 }
    { "delay": 99000, "best_finalized_block_number": 99000 }
    { "delay": 0, "best_finalized_block_number": 0 }

You may need to surround the value in single quotes when providing it as an argument.

subxt explore Grandpa calls note_stalled { "delay": 99000, "best_finalized_block_number": 99000 }

Encoded call data:
    0x2c041102b8820100b8820100

CONSTANTS

subxt explore Balances constants

Usage:
    subxt explore Balances constants <CONSTANT>
        explore a specific call within this pallet

Available <CONSTANT>'s in the "Balances" pallet:
    ExistentialDeposit
    MaxFreezes
    MaxHolds
    MaxLocks
    MaxReserves

subxt explore Balances constants ExistentialDeposit

Description:
    The minimum amount required to keep an account open. MUST BE GREATER THAN ZERO!

The constant has the following shape:
    u128

The value of the constant is:
    100000000000000

STORAGE

subxt explore Grandpa storage

Usage:
    subxt explore Grandpa storage <STORAGE_ENTRY>
        view details for a specific storage entry

Available <STORAGE_ENTRY>'s in the "Grandpa" pallet:
    CurrentSetId
    NextForced
    PendingChange
    SetIdSession
    Stalled
    State

subxt explore System storage Digest

this is a storage entry that does not need a key, so the value is fetched directly:

Description:
    Digest of the current block, also part of the block header.

The constant can be accessed without providing a key.

The storage entry has the following shape: 
    struct Digest {
        logs: Sequence(enum DigestItem {
            PreRuntime([u8; 4],
            Sequence(u8)), Consensus([u8; 4],
            Sequence(u8)), Seal([u8; 4],
            Sequence(u8)), Other(Sequence(u8)), RuntimeEnvironmentUpdated()
        })
    }

The value of the storage entry is:
    { "logs": (v"PreRuntime"((66, 65, 66, 69), (1, 0, 0, 0, 0, 50, 215, 118, 33, 0, 0, 0, 0, 108, 74, 64, 87, 52, 3, 27, 254, 40, 100, 83, 93, 220, 242, 5, 38, 82, 114, 206, 85, 240, 234, 214, 63, 190, 96, 71, 60, 227, 197, 40, 50, 189, 210, 252, 176, 104, 198, 136, 178, 221, 248, 239, 59, 218, 147, 23, 31, 48, 202, 12, 57, 66, 85, 112, 113, 179, 38, 123, 232, 187, 156, 165, 1, 15, 204, 242, 22, 216, 108, 35, 245, 72, 84, 127, 51, 250, 177, 60, 226, 28, 186, 135, 68, 186, 8, 47, 200, 147, 33, 248, 195, 237, 23, 75, 8))) }

subxt explore System storage Account

This storage call needs an account id as a key, the output tells you that as well:

Usage:
    subxt explore System storage Account <KEY_VALUE>

Description:
    The full account information for a particular account ID.

The <KEY_VALUE> has the following shape:
        struct AccountId32 ([u8; 32])

Here is an example of a <KEY_VALUE> matching this shape:
    ((255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255))

The storage entry has the following shape: 
    struct AccountInfo {
        nonce: u32,
        consumers: u32,
        providers: u32,
        sufficients: u32,
        data: struct AccountData {
            free: u128,
            reserved: u128,
            frozen: u128,
            flags: struct ExtraFlags (u128)
        }
    }

I guess here I still need to add the custom stringify for account adresses in the type example, such that it is not such an inconvenient byte array.

If we know the Account address of Alice, we can get here account details:

subxt explore System storage Account "((212,53,147,199,21,253,211,28,97,20,26,189,4,169,159,214,130,44,133,88,133,76,205,227,154,86,132,231,165,109,162,125))"

Description:
    The full account information for a particular account ID.

The <KEY_VALUE> has the following shape:
        struct AccountId32 ([u8; 32])

Here is an example of a <KEY_VALUE> matching this shape:
    ((255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255))

The storage entry has the following shape: 
    struct AccountInfo {
        nonce: u32,
        consumers: u32,
        providers: u32,
        sufficients: u32,
        data: struct AccountData {
            free: u128,
            reserved: u128,
            frozen: u128,
            flags: struct ExtraFlags (u128)
        }
    }

You submitted the following value as a key:
    ((212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125))

The value of the storage entry is:
    { "nonce": 0, "consumers": 0, "providers": 1, "sufficients": 0, "data": { "free": 1000000000000000000000, "reserved": 0, "frozen": 0, "flags": (170141183460469231731687303715884105728) } }

tadeohepperle and others added 4 commits May 10, 2023 12:31
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.160 to 1.0.162.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](serde-rs/serde@v1.0.160...1.0.162)

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

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

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

* extrinsics: Add extrinsics client

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

* extrinsics: Decode extrinsics

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

* subxt: Add extrinsic error

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

* blocks: Expose extrinsics

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

* examples: Fetch and decode block extrinsics

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

* Fix clippy

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

* extrinsics: Fetch pallet and variant index

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

* subxt: Move extrinsics on the subxt::blocks

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

* example: Adjust example

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

* metadata: Collect ExtrinsicMetadata

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

* subxt: Implement StaticExtrinsic for the calls

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

* Adjust examples

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

* codegen: Add root level Call enum

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

* Adjust testing

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

* subxt: Add new decode interface

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

* subxt: Merge ExtrinsicError with BlockError

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

* examples: Find first extrinsic

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

* Move code to extrinsic_types

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

* Add Extrinsic struct

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

* Adjust examples

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

* test: Decode extinsics

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

* extrinsics/test: Add fake metadata for static decoding

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

* extrinsics/test: Decode from insufficient bytes

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

* extrinsics/test: Check unsupported versions

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

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

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

* extrinsics/tests: Remove clones

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

* blocks: Fetch block body inline

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

* blocks: Rename ExtrinsicIds to ExtrinsicPartTypeIds

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

* extrinsics/test: Check decode as_extrinsic

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

* blocks: Remove InsufficientData error

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

* blocks: Return error from extrinsic_metadata

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

* extrinsics: Postpone decoding of call bytes

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

* metadata_type: Rename variables

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

* Adjust calls path for example and tests

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

* examples: Remove traces

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

* book: Add extrinsics documentation

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

* book: Improve extrinsics docs

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

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: James Wilson <james@jsdw.me>
@tadeohepperle tadeohepperle requested a review from a team as a code owner May 10, 2023 11:02
@tadeohepperle tadeohepperle self-assigned this May 10, 2023
@ascjones
Copy link
Contributor

ascjones commented May 10, 2023

Looking for suggestions on the name! I don't know which one of subxt tx, subxt show, subxt explore, subxt see, etc...
is best to describe this functionality

subxt inspect?

@tadeohepperle
Copy link
Contributor Author

subxt inspect?
@ascjones good idea! I am open to anything that is not too long.

@tadeohepperle tadeohepperle marked this pull request as draft May 11, 2023 11:46
@tadeohepperle tadeohepperle changed the title Cli tool explore node and create unsigned extrinsic CLI subxt explore commands May 15, 2023
@tadeohepperle tadeohepperle marked this pull request as ready for review May 15, 2023 16:02
@jsdw
Copy link
Collaborator

jsdw commented May 16, 2023

This has been awesome to play with; great work!

Some notes and nitpicks (mainly design based) from playing with the CLI tool to follow!

When we first explore, I'd suggest:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale
Usage:
    subxt explore <PALLET>
        explore a specific pallet

Available <PALLET> values are:
    System
    Scheduler
    Preimage
    Babe
    Timestamp
    Indices
    Balances
    TransactionPayment
    Authorship
    Staking
    ... lots of others

ie no -, indent by 4 spaces (to better separate things; more below) and "usage" clear at top.

When selecting a pallet, could we support case insensitive pallet names too?

When you select a pallet, we can follow this same "Usage:" pattern, and provide a little info on each with clear indent like this:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale Balances
Usage:
    subxt explore Balances calls
        explore the calls that can be made into this pallet
    subxt explore Balances constants
        explore the constants held in this pallet
    subxt explore Balances storage
        explore the storage values held in this pallet

When we select calls I'd probably go with:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale Balances calls
Usage:
    subxt explore Balances calls <CALL>
        explore a specific call within this pallet

Available <CALL>'s in the Balances pallet:
    transfer_allow_death
    set_balance_deprecated
    force_transfer
    transfer_keep_alive
    transfer_all
    force_unreserve
    upgrade_accounts
    transfer
    force_set_balance

ie more indenting and less '-'s to make it a little prettier (imo) :)

When we select a specific call:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale Balances calls transfer
Usage:
    subxt explore Balances calls transfer <SCALE_VALUE>
        construct the call by providing a valid argument

The call expects a <SCALE_VALUE> with this shape:
    {
        dest: enum MultiAddress {
            Id(struct AccountId32 ([u8; 32])),
            Index(Compact(())),
            Raw(Sequence(u8)),
            Address32([u8; 32]),
            Address20([u8; 20])
        },
        value: Compact(u128)
    }

Here are 3 examples of a <SCALE_VALUE> matching this shape:
    { "dest": v"Address20"((255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255)), "value": 340282366920938463463374607431768211455 }
    { "dest": v"Address32"((255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255)), "value": 99000 }
    { "dest": v"Raw"((255, 255, 255)), "value": 0 }

You may need to surround the value in single quotes when providing it as an argument.

I don't think we need the type name offhand, but in any case fewer '-'s and clear shape and type printout for prettiness :)

Try updating scale-value to latest too to get the nicer SCALE_VALUE outputs (this can be a small separate PR though; no biggie)

Finally, when we provide a valid call (Just suggesting a slight indent here to match the style above):

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale Balances calls transfer '{ "dest": v"Raw"((255, 255, 255)), "value": 0 }'
Encoded call data:
    0x24040507020cffffff00

When you ask for constants I'd also suggest breaking them down into individual constants and following a similar design to above. Why; because some constant outputs are more verbose (try ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale System constants for an example ;)). Something like:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale System constants
Usage:
    subxt explore System constants <CONSTANT>
        print details about a specific constant

Available <CONSTANT>'s in the System pallet:
    BlockLength
    BlockHashCount
    DbWeight
    Version

And then:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale System constants Version
Description:
    Get the chain's current version.

This constant has the following shape:
    struct RuntimeVersion {
        spec_name: String,
        impl_name: String,
        authoring_version: u32,
        spec_version: u32,
        impl_version: u32,
        apis: struct Cow (Sequence(([u8; 8], u32))),
        transaction_version: u32,
        state_version: u8
    }

The value of the constant is:
    { "spec_name": "polkadot", "impl_name": "parity-polkadot", "authoring_version": 0, "spec_version": 9410, "impl_version": 0, "apis": ((((223, 106, 203, 104, 153, 7, 96, 155), 4), ((55, 227, 151, 252, 124, 145, 245, 228), 2), ((64, 254, 58, 212, 1, 248, 149, 154), 6), ((23, 166, 188, 13, 0, 98, 174, 179), 1), ((24, 239, 88, 163, 182, 123, 167, 112), 1), ((210, 188, 152, 151, 238, 208, 143, 21), 3), ((247, 139, 39, 139, 229, 63, 69, 76), 2), ((175, 44, 2, 151, 162, 62, 109, 61), 4), ((73, 234, 175, 27, 84, 138, 12, 176), 2), ((145, 213, 223, 24, 176, 210, 207, 88), 2), ((237, 153, 197, 172, 178, 94, 237, 245), 3), ((203, 202, 37, 227, 159, 20, 35, 135), 2), ((104, 122, 212, 74, 211, 127, 3, 194), 1), ((171, 60, 5, 114, 41, 31, 235, 139), 1), ((188, 157, 137, 144, 79, 91, 146, 63), 1), ((55, 200, 187, 19, 80, 169, 162, 168), 4), ((243, 255, 20, 213, 171, 82, 112, 89), 3))), "transaction_version": 22, "state_version": 0 }

And then for storage I'd go with:

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale System storage
Usage:
    subxt explore System storage <STORAGE_ENTRY>
        view details for a specific storage entry

Available <STORAGE_ENTRY>'s in the System pallet:
    Account
    ExtrinsicCount
    BlockWeight
    AllExtrinsicsLen
    BlockHash
    ExtrinsicData
    Number
    ParentHash
    Digest
    Events
    EventCount
    EventTopics
    LastRuntimeUpgrade
    UpgradedToU32RefCount
    UpgradedToTripleRefCount
    ExecutionPhase

And then, if we are connected to a node, it'd be awesome if we could actually let the user provide values and fetch the entry

% ./target/debug/subxt explore --file ./artifacts/polkadot_metadata_full.scale System storage Account
Usage:
    subxt explore System storage Account <KEY_VALUE1>

Description:
    The full account information for a particular account ID.

The <KEY_VALUE1> has the following shape:
   ...

Here are 3 examples of a <KEY_VALUE1> matching this shape:
   ...

Ie a similar display to submitting extrinsics. Some storage entries will take multiple keys (maybe you've thought about this already) so they will either need to be one VALUE that takes each arg as you did for extrinsics, or a separate arg per value if that's not possible. (we could use a (key1, key2) style tuple if multiple keys are needed and we just want one value I guess?)

I think that about covers it! Summary of thoughts so far:
0. This is great stuff and really fun to play with so far!

  1. I suggested some design changes primarily, ie generally less '-', usage first with standard output everywhere, 4 space indents everywhere to separate things nicely
  2. Can be accept case insensitive values where applicable?
  3. Can we sort values alphabetically where applicable?
  4. newer scale-value will make printed output nicer. custom printed output might be even nicer (both of these could def be a followup PR)
  5. From a quick skim of the code, perhaps it makes sense to have an explore folder and each sub command is a file inside that to make it easier to find each bit, and then the mod.rs will delegate to each subcommand as needed.

@tadeohepperle
Copy link
Contributor Author

@jsdw Thanks for the detailed feedback! I will go ahead and implement the design suggestions, and I think things like case insensitive input should be possible.

And then, if we are connected to a node, it'd be awesome if we could actually let the user provide values and fetch the entry

This is already working (e.g. see last example in PR description), multiple keys are represented as an unnamed composite scale value. I have to try some calls with multiple keys to see if the encoding works properly there.

@chevdor
Copy link

chevdor commented May 17, 2023

I can't dislike the functionnality :)
Note that subwasm metadata (as well the WIP show command) do something similar while also being able to do it offline, off of a wasm file.

let mut strings: Vec<_> = pallet_metadata.constants.iter().map(|c| &c.name).collect();
strings.sort();
for constant in strings {
write!(output, "\n {}", constant).unwrap();
Copy link
Member

@niklasad1 niklasad1 May 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write! is a bit clunky and having to unwrap I guess you could do:

output.push_str("\n    ");
ouput.push_str(&constant);

let mut strings: Vec<_> = pallet_calls.variants.iter().map(|c| &c.name).collect();
strings.sort();
for variant in strings {
write!(output, "\n {}", variant).unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should infallible but could re-written with push_str ^^

Ok((calls_enum_type_def, calls_enum_type))
}

fn new_offline_client(metadata: Metadata) -> OfflineClient<SubstrateConfig> {
Copy link
Member

@niklasad1 niklasad1 May 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow why these hardcoded values are needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured they are needed to create an OfflineClient. Is there a quicker way to just create an OfflineClient? We don't need an OnlineClient here yet. I just took the hardcoded values from an example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit hard to follow why creating an offline_client with hard coded values here which is not obvious to me at least :)

I would prefer if you could rename this function to mocked_offline_client and add a comment why/where it's used.

cli/src/utils.rs Outdated Show resolved Hide resolved
Copy link
Member

@niklasad1 niklasad1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, looks good but I added some suggestions/questions but I would want some tests for this...

Perhaps it possible to add CLI tests and provide a small metadata file as input?

@tadeohepperle
Copy link
Contributor Author

tadeohepperle commented May 19, 2023

Perhaps it possible to add CLI tests and provide a small metadata file as input?

@niklasad1 Thanks for your review! We could do that, but it comes at the cost that every time we change a minor thing in formatting the style of the CLI output we would have to adjust all the tests. I could do that in a separate PR after this is merged.

@jsdw
Copy link
Collaborator

jsdw commented May 19, 2023

It is an interesting question how to test this; one approach would be to split the CLI crate into a lib and bin (living in the same folder for simplicity) and then we could integration test the lib by having each test create the CLI Opts struct (probably often by pointing at our artifacts/polkadot_metadata as the file we'll test against) and then checking that the output is what we expect in a few cases just to make sure we can indeex get call data or an extrinsic or whatever back.

The tests could also cover a couple of the existing commands if this restructuring was done, and the actual parsing that clap does would not be under test here since we construct the Opts ourselves. Rather than printing themselves, commands would return a string (maybe in an enum like Result<String,String> so that the CLI side will either println or eprintln and exit appropriately depending on whether an err or not)

Tests would involve a bunch of searching for things in strings and such though, which is the less ideal bit. I suppose it's not awful if we just check that the output string contains a specific value and ignore the rest of it though, so that the formatting that we go with isn't really being tested.

This could all be a separate PR, but curious to know what you guys think to the idea? Is it sane?

@tadeohepperle
Copy link
Contributor Author

@jsdw I think that is a good idea. What is the splitting between bin and lib about? Just so that we can have the lib expose a main cli function that takes the Opts struct?

@niklasad1
Copy link
Member

niklasad1 commented May 22, 2023

From my side, I just want some basic unit tests for the custom stuff introduced in this PR i.e, not relying on the clap parsing/output.

Thus, some basic unit tests for the helpers would do it I think + that the basic explore --help, explore PALLET storage, explore PALLET calls and explore PALLET constants returns exit code 0.

Manually parsing strings/CLI output should be avoidable if possible :)

Copy link
Member

@niklasad1 niklasad1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good stuff 👍

/// ## Pallets
///
/// Show the pallets that are available:
/// ```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: All doc examples with "" are assumed to be rust doc comments; I'd do "text".

I guess rustdoc isn't trying to interpret these or something because it's a binary and not library project; I'd have expected to see CI others ordinarily as it tried to run them.

I don't expect anybody will ever see these comments, so they are only really useful to us.

write!(output, "Description:\n{docs_string}")?;
}
write!(output, "Usage:")?;
write!(output, "\n subxt explore {pallet_name} calls\n explore the calls that can be made into this pallet")?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: as a general tidiness thing for everywhere you've printed things, I'd probably go with:

        writeln!(output, "Usage:")?;
        writeln!(output, "    subxt explore {pallet_name} calls");
        writeln!(output, "        explore the calls that can be made into this pallet")?;

ie separate lines for each printed line, so you can see super clearly in the code what it'll look like when printed.

// if no storage entry specified, show user the calls to choose from:
let Some(entry_name) = command.storage_entry else {
let storage_entries = print_available_storage_entries(storage_metadata, pallet_name);
println!("Usage:\n subxt explore {pallet_name} storage <STORAGE_ENTRY>\n view details for a specific storage entry\n\n{storage_entries}");
Copy link
Collaborator

@jsdw jsdw May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

        println!("Usage:");
        println!("    subxt explore {pallet_name} storage <STORAGE_ENTRY>");
        println!("        view details for a specific storage entry\n\n{storage_entries}");

just an example of another place multiple lines makes sense for me

Comment on lines +19 to +22
pub(crate) url: Option<Uri>,
/// The path to the encoded metadata file.
#[clap(long, value_parser)]
file: Option<PathBuf>,
pub(crate) file: Option<PathBuf>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: probably fine just being pub, ie no (crate) (this is a binary anyway atm and even if we make a bunch of it a lib for testing nobody should depend on it.

@@ -71,3 +75,22 @@ impl FileOrUrl {
}
}
}

pub(crate) fn print_docs_with_indent(docs: &[String], indent: usize) -> String {
// take at most the first paragraph of documentation, such that it does not get too long.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this hsould be reflected in the function name, since it would be easy to be confused at why docs weren't all being printed otherwise, eg print_first_paragraph_with_indent

with_indent(docs_str, indent)
}

pub(crate) fn with_indent(s: String, indent: usize) -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the (crate)'s :)

Comment on lines +134 to +135
const MIN_VARIANT_COUNT_FOR_TRAILING_COMMA: usize = 100;
let add_trailing_comma = self.variants.len() >= MIN_VARIANT_COUNT_FOR_TRAILING_COMMA;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we explicitly add a trailing comma if 100+ variants?

Comment on lines +144 to +146
if iter.peek().is_some() || add_trailing_comma {
variants_string.push(',');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is a hint to tweak the parsing of scale-value's to allow trailing commas :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I was not aware of the handling of trailing commas in scale-value, I just looked at the rust formatter for inspiration.

fn type_description(&self, registry: &PortableRegistry) -> color_eyre::Result<String>;
}

impl TypeDescription for u32 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this impl is a bit strange; the others are all about generating a description of the themselves, but this one uses self as the lookup into a type registry.

Personally I'd just make this be a standalong function so people can either print_type_description(typedef, registry) or print_type_description_of(u32, registry), but not a big deal really :)

}
}

fn format_type_description(input: &str) -> String {
Copy link
Collaborator

@jsdw jsdw May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I'd suggest indentation being a part of the TypeDescription trait method, so that things can say how to indent children etc, and the formatting logic for each item is all in one place, but I've no problem with this for now since it seems to work!

}

/// a trait for producing scale value examples for a type.
pub trait TypeExample {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ok for now, but I'm not super convinced that a trait is necessary here (or above). I'd think that in general if you want to give an example of some type, you could expose a single function, type_example(type_id: u32, registry: &PortableRegistry, indent: 32) -> String that would be the entire interface to this stuff, which would make it quite easy to change around the internals (eg stop relying on scale-value) or whatever without worrying about other code breaking. Then, you'd just have internal functions to handle the different types rather than trait impls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true, maybe I don't need to expose the trait. It just made it easier to reason about the examples for me. I can clean this up.

Copy link
Collaborator

@jsdw jsdw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the new output; looks great :)

I left various comments on the code but nothing blocking approval and things we can iterate on over time.

I do think we should add some unit tests for some of the different internal functions, but I also think we'll want to evolve some of the code anyway over time, like not using scale-value to print output (or improving how scale-value prints output), handling hex and ss58 addresses in the output etc. I don't mind merging this though as a good first step!

@tadeohepperle tadeohepperle merged commit f344d0d into master May 23, 2023
@tadeohepperle tadeohepperle deleted the tadeo-hepperle-cli-tool-create-extrinsics branch May 23, 2023 11:33
@jsdw jsdw mentioned this pull request Jun 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants