subxt-cli 0.50.1

Command line utilities for working with subxt codegen
use clap::Args;
use color_eyre::{eyre::bail, 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::metadata::{ArcMetadata, PalletMetadata, StorageMetadata};

use crate::utils::{
    FileOrUrl, Indent, SyntaxHighlight, create_client, first_paragraph_of_docs,
    parse_string_into_scale_value, type_example,
};

#[derive(Debug, Clone, Args)]
pub struct StorageSubcommand {
    storage_entry: Option<String>,
    #[clap(long, short, action)]
    execute: bool,
    #[clap(required = false)]
    trailing_args: Vec<String>,
}

pub async fn explore_storage(
    command: StorageSubcommand,
    pallet_metadata: PalletMetadata<'_>,
    metadata: ArcMetadata,
    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;

    let Some(storage_metadata) = pallet_metadata.storage() else {
        writeln!(
            output,
            "The \"{pallet_name}\" pallet has no storage entries."
        )?;
        return Ok(());
    };

    let storage_entry_placeholder = "<STORAGE_ENTRY>".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 = storage.value_ty();

    let key_value_placeholder = "<KEY_VALUE>".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:
    let storage_keys = storage.keys().collect::<Vec<_>>();
    if !storage_keys.is_empty() {
        let key_ty_description = format!(
            "({})",
            storage_keys
                .iter()
                .map(|key| type_description(key.key_id, metadata.types(), true)
                    .expect("No type Description"))
                .collect::<Vec<_>>()
                .join(", ")
        )
        .indent(4)
        .highlight();

        let key_ty_example = format!(
            "({})",
            storage_keys
                .iter()
                .map(|key| type_example(key.key_id, metadata.types()).to_string())
                .collect::<Vec<_>>()
                .join(", ")
        )
        .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<Value> = match (!trailing_args.is_empty(), !storage_keys.is_empty())
    {
        // keys provided, keys not needed.
        (true, false) => {
            let trailing_args_str = trailing_args.join(" ");
            let warning = format!(
                "Warning: You submitted one or more keys \"{trailing_args_str}\", but no key is needed. To access the storage value, please do not provide any keys."
            );
            writeln!(output, "{}", warning.yellow())?;
            return Ok(());
        }
        // Keys not provided, keys needed.
        (false, true) => {
            // just return. The user was instructed above how to provide a value if they want to.
            return Ok(());
        }
        // Keys not provided, keys not needed.
        (false, false) => vec![],
        // Keys provided, keys needed.
        (true, true) => {
            // Each trailing arg is parsed into its own value, to be provided as a separate storage key.
            let values = trailing_args
                .iter()
                .map(|arg| parse_string_into_scale_value(arg))
                .collect::<color_eyre::Result<Vec<_>>>()?;

            // We do this just to print them out.
            let values_str = values
                .iter()
                .map(|v| v.to_string().highlight())
                .collect::<Vec<_>>()
                .join("\n");
            let value_str = values_str.indent(4);

            writedoc! {output, "

            You submitted the following {key_value_placeholder}:
            {value_str}
            "}?;

            values
        }
    };

    // construct the client:
    let client = create_client(&file_or_url).await?;

    // Fetch the value:
    let storage_value = client
        .at_current_block()
        .await?
        .storage()
        .fetch((pallet_name, storage.name()), storage_entry_keys)
        .await?
        .decode()?;

    let value = storage_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 = "<STORAGE_ENTRY>".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_name}\" pallet:");
        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
    }
}