diff --git a/Cargo.lock b/Cargo.lock index 0eb0f75962..4ee8487cb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3581,8 +3581,11 @@ dependencies = [ "hex", "jsonrpsee", "parity-scale-codec", + "scale-info", + "scale-value", "serde", "serde_json", + "subxt", "subxt-codegen", "subxt-metadata", "syn 2.0.15", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 718b3df7af..136cab5dba 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,6 +19,7 @@ path = "src/main.rs" [dependencies] subxt-codegen = { workspace = true } subxt-metadata = { workspace = true } +subxt = { workspace = true } clap = { workspace = true } serde = { workspace = true, features = ["derive"] } color-eyre = { workspace = true } @@ -26,6 +27,8 @@ serde_json = { workspace = true } hex = { workspace = true } frame-metadata = { workspace = true } codec = { package = "parity-scale-codec", workspace = true } +scale-info = { workspace = true } +scale-value = { workspace = true } syn = { workspace = true } jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport", "http-client"] } tokio = { workspace = true } diff --git a/cli/src/commands/explore/calls.rs b/cli/src/commands/explore/calls.rs new file mode 100644 index 0000000000..14e37f79fa --- /dev/null +++ b/cli/src/commands/explore/calls.rs @@ -0,0 +1,146 @@ +use crate::utils::type_description::print_type_description; +use crate::utils::type_example::print_type_examples; +use crate::utils::with_indent; +use clap::Args; + +use std::fmt::Write; +use std::str::FromStr; +use std::write; + +use color_eyre::eyre::eyre; +use frame_metadata::v15::PalletMetadata; + +use scale_info::form::PortableForm; +use scale_info::{PortableRegistry, Type, TypeDef, TypeDefVariant}; +use scale_value::{Composite, ValueDef}; + +use subxt::tx; +use subxt::utils::H256; +use subxt::{config::SubstrateConfig, Metadata, OfflineClient}; + +#[derive(Debug, Clone, Args)] +pub struct CallsSubcommand { + call: Option, + #[clap(required = false)] + trailing_args: Vec, +} + +pub(crate) fn explore_calls( + command: CallsSubcommand, + metadata: &Metadata, + pallet_metadata: &PalletMetadata, +) -> color_eyre::Result<()> { + let pallet_name = pallet_metadata.name.as_str(); + + // get the enum that stores the possible calls: + let (calls_enum_type_def, _calls_enum_type) = + get_calls_enum_type(pallet_metadata, &metadata.runtime_metadata().types)?; + + // if no call specified, show user the calls to choose from: + let Some(call_name) = command.call else { + let available_calls = print_available_calls(calls_enum_type_def, pallet_name); + println!("Usage:\n subxt explore {pallet_name} calls \n explore a specific call within this pallet\n\n{available_calls}", ); + return Ok(()); + }; + + // if specified call is wrong, show user the calls to choose from (but this time as an error): + let Some(call) = calls_enum_type_def.variants.iter().find(|variant| variant.name.to_lowercase() == call_name.to_lowercase()) else { + let available_calls = print_available_calls(calls_enum_type_def, pallet_name); + let description = format!("Usage:\n subxt explore {pallet_name} calls \n explore a specific call within this pallet\n\n{available_calls}", ); + return Err(eyre!("\"{call_name}\" call not found in \"{pallet_name}\" pallet!\n\n{description}")); + }; + + // collect all the trailing arguments into a single string that is later into a scale_value::Value + let trailing_args = command.trailing_args.join(" "); + + // if no trailing arguments specified show user the expected type of arguments with examples: + if trailing_args.is_empty() { + let mut type_description = + print_type_description(&call.fields, &metadata.runtime_metadata().types)?; + type_description = with_indent(type_description, 4); + let mut type_examples = print_type_examples( + &call.fields, + &metadata.runtime_metadata().types, + "SCALE_VALUE", + )?; + type_examples = with_indent(type_examples, 4); + let mut output = String::new(); + write!(output, "Usage:\n subxt explore {pallet_name} calls {call_name} \n construct the call by providing a valid argument\n\n")?; + write!( + 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..])?; + println!("{output}"); + return Ok(()); + } + + // parse scale_value from trailing arguments and try to create an unsigned extrinsic with it: + let value = scale_value::stringify::from_str(&trailing_args).0.map_err(|err| eyre!("scale_value::stringify::from_str led to a ParseError.\n\ntried parsing: \"{}\"\n\n{}", trailing_args, err))?; + let value_as_composite = value_into_composite(value); + let offline_client = mocked_offline_client(metadata.clone()); + let payload = tx::dynamic(pallet_name, call_name, value_as_composite); + let unsigned_extrinsic = offline_client.tx().create_unsigned(&payload)?; + let hex_bytes = format!("0x{}", hex::encode(unsigned_extrinsic.encoded())); + println!("Encoded call data:\n {hex_bytes}"); + + Ok(()) +} + +fn print_available_calls(pallet_calls: &TypeDefVariant, pallet_name: &str) -> String { + if pallet_calls.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<_> = pallet_calls.variants.iter().map(|c| &c.name).collect(); + strings.sort(); + for variant in strings { + output.push_str("\n "); + output.push_str(variant); + } + output +} + +fn get_calls_enum_type<'a>( + pallet: &'a frame_metadata::v15::PalletMetadata, + registry: &'a PortableRegistry, +) -> color_eyre::Result<(&'a TypeDefVariant, &'a Type)> { + let calls = pallet + .calls + .as_ref() + .ok_or(eyre!("The \"{}\" pallet has no calls.", pallet.name))?; + let calls_enum_type = registry + .resolve(calls.ty.id) + .ok_or(eyre!("calls type with id {} not found.", calls.ty.id))?; + // should always be a variant type, where each variant corresponds to one call. + let calls_enum_type_def = match &calls_enum_type.type_def { + TypeDef::Variant(variant) => variant, + _ => { + return Err(eyre!("calls type is not a variant")); + } + }; + Ok((calls_enum_type_def, calls_enum_type)) +} + +/// The specific values used for construction do not matter too much, we just need any OfflineClient to create unsigned extrinsics +fn mocked_offline_client(metadata: Metadata) -> OfflineClient { + let genesis_hash = + H256::from_str("91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3") + .expect("Valid hash; qed"); + + let runtime_version = subxt::rpc::types::RuntimeVersion { + spec_version: 9370, + transaction_version: 20, + other: Default::default(), + }; + + OfflineClient::::new(genesis_hash, runtime_version, metadata) +} + +/// composites stay composites, all other types are converted into a 1-fielded unnamed composite +fn value_into_composite(value: scale_value::Value) -> scale_value::Composite<()> { + match value.value { + ValueDef::Composite(composite) => composite, + _ => Composite::Unnamed(vec![value]), + } +} diff --git a/cli/src/commands/explore/constants.rs b/cli/src/commands/explore/constants.rs new file mode 100644 index 0000000000..36c3c32ea1 --- /dev/null +++ b/cli/src/commands/explore/constants.rs @@ -0,0 +1,86 @@ +use crate::utils::type_description::print_type_description; +use crate::utils::{print_docs_with_indent, with_indent}; +use clap::Args; + +use std::fmt::Write; +use std::write; + +use color_eyre::eyre::eyre; +use frame_metadata::v15::PalletMetadata; + +use scale_info::form::PortableForm; + +use subxt::Metadata; + +#[derive(Debug, Clone, Args)] +pub struct ConstantsSubcommand { + constant: Option, +} + +pub(crate) fn explore_constants( + command: ConstantsSubcommand, + metadata: &Metadata, + pallet_metadata: &PalletMetadata, +) -> color_eyre::Result<()> { + let pallet_name = pallet_metadata.name.as_str(); + let Some(constant_name) = command.constant else { + let available_constants = print_available_constants(pallet_metadata, pallet_name); + println!("Usage:\n subxt explore {pallet_name} constants \n explore a specific call within this pallet\n\n{available_constants}", ); + return Ok(()); + }; + + // if specified constant is wrong, show user the constants to choose from (but this time as an error): + let Some(constant) = pallet_metadata.constants.iter().find(|constant| constant.name.to_lowercase() == constant_name.to_lowercase()) else { + let available_constants = print_available_constants(pallet_metadata, pallet_name); + let description = format!("Usage:\n subxt explore {pallet_name} constants \n explore a specific call within this pallet\n\n{available_constants}", ); + let err = eyre!("constant \"{constant_name}\" not found in \"{pallet_name}\" pallet!\n\n{description}"); + return Err(err); + }; + + // docs + let mut output = String::new(); + let doc_string = print_docs_with_indent(&constant.docs, 4); + if !doc_string.is_empty() { + write!(output, "Description:\n{doc_string}")?; + } + + // shape + let mut type_description = print_type_description(&constant.ty.id, metadata.types())?; + type_description = with_indent(type_description, 4); + write!( + output, + "\n\nThe constant has the following shape:\n{type_description}" + )?; + + // value + let scale_val = scale_value::scale::decode_as_type( + &mut &constant.value[..], + constant.ty.id, + metadata.types(), + )?; + write!( + output, + "\n\nThe value of the constant is:\n {}", + scale_value::stringify::to_string(&scale_val) + )?; + + println!("{output}"); + Ok(()) +} + +fn print_available_constants( + pallet_metadata: &PalletMetadata, + pallet_name: &str, +) -> String { + if pallet_metadata.constants.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<_> = pallet_metadata.constants.iter().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 new file mode 100644 index 0000000000..c35cf01f89 --- /dev/null +++ b/cli/src/commands/explore/mod.rs @@ -0,0 +1,147 @@ +use crate::utils::{print_docs_with_indent, FileOrUrl}; +use clap::{Parser as ClapParser, Subcommand}; + +use std::fmt::Write; + +use std::write; + +use codec::Decode; +use color_eyre::eyre::eyre; +use frame_metadata::v15::RuntimeMetadataV15; +use frame_metadata::RuntimeMetadataPrefixed; + +use syn::__private::str; + +use crate::commands::explore::calls::{explore_calls, CallsSubcommand}; +use crate::commands::explore::constants::{explore_constants, ConstantsSubcommand}; +use crate::commands::explore::storage::{explore_storage, StorageSubcommand}; + +use subxt::Metadata; + +mod calls; +mod constants; +mod storage; + +/// Explore pallets, calls, call parameters, storage entries and constants. Also allows for creating (unsigned) extrinsics. +/// +/// # Example +/// +/// ## Pallets +/// +/// Show the pallets that are available: +/// ``` +/// subxt explore --file=polkadot_metadata.scale +/// ``` +/// +/// ## Calls +/// +/// Show the calls in a pallet: +/// ``` +/// subxt explore Balances calls +/// ``` +/// Show the call parameters a call expects: +/// ``` +/// subxt explore Balances calls transfer +/// ``` +/// Create an unsigned extrinsic from a scale value, validate it and output its hex representation +/// ``` +/// subxt explore Grandpa calls note_stalled { "delay": 5, "best_finalized_block_number": 5 } +/// # Encoded call data: +/// # 0x2c0411020500000005000000 +/// subxt explore Balances calls transfer "{ \"dest\": v\"Raw\"((255, 255, 255)), \"value\": 0 }" +/// # Encoded call data: +/// # 0x24040607020cffffff00 +/// ``` +/// ## Constants +/// +/// Show the constants in a pallet: +/// ``` +/// subxt explore Balances constants +/// ``` +/// ## Storage +/// +/// Show the storage entries in a pallet +/// ``` +/// subxt explore Alliance storage +/// ``` +/// Show the types and value of a specific storage entry +/// ``` +/// subxt explore Alliance storage Announcements [KEY_SCALE_VALUE] +/// ``` +/// +#[derive(Debug, ClapParser)] +pub struct Opts { + #[command(flatten)] + file_or_url: FileOrUrl, + pallet: Option, + #[command(subcommand)] + pallet_subcommand: Option, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum PalletSubcommand { + Calls(CallsSubcommand), + Constants(ConstantsSubcommand), + Storage(StorageSubcommand), +} + +/// cargo run -- explore --file=../artifacts/polkadot_metadata.scale +pub async fn run(opts: Opts) -> color_eyre::Result<()> { + // get the metadata + let bytes = opts.file_or_url.fetch().await?; + let metadata_prefixed = ::decode(&mut &bytes[..])?; + let metadata = Metadata::try_from(metadata_prefixed)?; + + // if no pallet specified, show user the pallets to choose from: + let Some(pallet_name) = opts.pallet else { + let available_pallets = print_available_pallets(metadata.runtime_metadata()); + println!("Usage:\n subxt explore \n explore a specific pallet\n\n{available_pallets}", ); + return Ok(()); + }; + + // if specified pallet is wrong, show user the pallets to choose from (but this time as an error): + let Some(pallet_metadata) = metadata.runtime_metadata().pallets.iter().find(|pallet| pallet.name.to_lowercase() == pallet_name.to_lowercase())else { + return Err(eyre!("pallet \"{}\" not found in metadata!\n{}", pallet_name, print_available_pallets(metadata.runtime_metadata()))); + }; + + // if correct pallet was specified but no subcommand, instruct the user how to proceed: + let Some(pallet_subcomand) = opts.pallet_subcommand else { + let docs_string = print_docs_with_indent(&pallet_metadata.docs, 4); + let mut output = String::new(); + if !docs_string.is_empty() { + write!(output, "Description:\n{docs_string}")?; + } + write!(output, "Usage:")?; + write!(output, "\n subxt explore {pallet_name} calls\n explore the calls that can be made into this pallet")?; + write!(output, "\n subxt explore {pallet_name} constants\n explore the constants held in this pallet")?; + write!(output, "\n subxt explore {pallet_name} storage\n explore the storage values held in this pallet")?; + println!("{output}"); + return Ok(()); + }; + + match pallet_subcomand { + PalletSubcommand::Calls(command) => explore_calls(command, &metadata, pallet_metadata), + PalletSubcommand::Constants(command) => { + explore_constants(command, &metadata, pallet_metadata) + } + PalletSubcommand::Storage(command) => { + // if the metadata came from some url, we use that same url to make storage calls against. + let node_url = opts.file_or_url.url.map(|url| url.to_string()); + explore_storage(command, &metadata, pallet_metadata, node_url).await + } + } +} + +fn print_available_pallets(metadata_v15: &RuntimeMetadataV15) -> String { + if metadata_v15.pallets.is_empty() { + "There are no values available.".to_string() + } else { + let mut output = "Available values are:".to_string(); + let mut strings: Vec<_> = metadata_v15.pallets.iter().map(|p| &p.name).collect(); + strings.sort(); + for pallet in strings { + write!(output, "\n {}", pallet).unwrap(); + } + output + } +} diff --git a/cli/src/commands/explore/storage.rs b/cli/src/commands/explore/storage.rs new file mode 100644 index 0000000000..e942e6d8e9 --- /dev/null +++ b/cli/src/commands/explore/storage.rs @@ -0,0 +1,176 @@ +use crate::utils::type_description::print_type_description; +use crate::utils::type_example::print_type_examples; +use crate::utils::{print_docs_with_indent, with_indent}; +use clap::Args; + +use std::fmt::Write; +use std::write; + +use color_eyre::eyre::eyre; +use frame_metadata::v15::{PalletMetadata, PalletStorageMetadata, StorageEntryType}; + +use scale_info::form::PortableForm; + +use subxt::OnlineClient; +use subxt::{config::SubstrateConfig, Metadata}; + +#[derive(Debug, Clone, Args)] +pub struct StorageSubcommand { + storage_entry: Option, + #[clap(required = false)] + trailing_args: Vec, +} + +pub(crate) async fn explore_storage( + command: StorageSubcommand, + metadata: &Metadata, + pallet_metadata: &PalletMetadata, + custom_online_client_url: Option, +) -> color_eyre::Result<()> { + let pallet_name = pallet_metadata.name.as_str(); + let trailing_args = command.trailing_args.join(" "); + let trailing_args = trailing_args.trim(); + + let Some(storage_metadata) = &pallet_metadata.storage else { + println!("The \"{pallet_name}\" pallet has no storage entries."); + return Ok(()); + }; + + // if no storage entry specified, show user the calls to choose from: + let Some(entry_name) = command.storage_entry else { + let storage_entries = print_available_storage_entries(storage_metadata, pallet_name); + println!("Usage:\n subxt explore {pallet_name} storage \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.ty { + StorageEntryType::Plain(value) => (value.id, None), + StorageEntryType::Map { value, key, .. } => (value.id, Some(key.id)), + }; + + // get the type and type description for the return and key type: + let mut output = String::new(); + + // only inform user about usage if a key can be provided: + if key_ty_id.is_some() && trailing_args.is_empty() { + write!( + output, + "Usage:\n subxt explore {pallet_name} storage {entry_name} \n\n" + )?; + } + + let docs_string = print_docs_with_indent(&storage.docs, 4); + if !docs_string.is_empty() { + write!(output, "Description:\n{docs_string}")?; + } + + // inform user about shape of key if it can be provided: + if let Some(key_ty_id) = key_ty_id { + let mut key_ty_description = print_type_description(&key_ty_id, metadata.types())?; + key_ty_description = with_indent(key_ty_description, 4); + let mut key_ty_examples = print_type_examples(&key_ty_id, metadata.types(), "")?; + key_ty_examples = with_indent(key_ty_examples, 4); + write!( + output, + "\n\nThe has the following shape:\n {key_ty_description}\n\n{}", + &key_ty_examples[4..] + )?; + } else { + write!( + output, + "\n\nThe constant can be accessed without providing a key." + )?; + } + + let mut return_ty_description = print_type_description(&return_ty_id, metadata.types())?; + return_ty_description = if return_ty_description.contains('\n') { + format!("\n{}", with_indent(return_ty_description, 4)) + } else { + return_ty_description + }; + write!( + output, + "\n\nThe storage entry has the following shape: {}", + return_ty_description + )?; + + // construct the vector of scale_values that should be used as a key to the storage (often empty) + + let key_scale_values = if let Some(key_ty_id) = key_ty_id.filter(|_| !trailing_args.is_empty()) + { + let key_scale_value = scale_value::stringify::from_str(trailing_args).0.map_err(|err| eyre!("scale_value::stringify::from_str led to a ParseError.\n\ntried parsing: \"{}\"\n\n{}", trailing_args, err))?; + write!( + output, + "\n\nYou submitted the following value as a key:\n{}", + with_indent(scale_value::stringify::to_string(&key_scale_value), 4) + )?; + let mut key_bytes: Vec = Vec::new(); + scale_value::scale::encode_as_type( + &key_scale_value, + key_ty_id, + metadata.types(), + &mut key_bytes, + )?; + let bytes_composite = scale_value::Value::from_bytes(&key_bytes); + vec![bytes_composite] + } else { + Vec::new() + }; + + if key_ty_id.is_none() && !trailing_args.is_empty() { + write!(output, "\n\nWarning: You submitted the following value as a key, but it will be ignored, because the storage entry does not require a key: \"{}\"", trailing_args)?; + } + println!("{output}"); + + // construct and submit the storage entry request if either no key is needed or som key was provided as a scale value + if key_ty_id.is_none() || !key_scale_values.is_empty() { + let online_client = match custom_online_client_url { + None => OnlineClient::::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); + println!("\nThe value of the storage entry is:\n{value_string}"); + } + + Ok(()) +} + +fn print_available_storage_entries( + storage_metadata: &PalletStorageMetadata, + 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/commands/mod.rs b/cli/src/commands/mod.rs index 15660ac122..69542ad40d 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -4,5 +4,6 @@ pub mod codegen; pub mod compatibility; +pub mod explore; pub mod metadata; pub mod version; diff --git a/cli/src/main.rs b/cli/src/main.rs index 4ad786f7b8..956fd5e0b7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -15,6 +15,7 @@ enum Command { Codegen(commands::codegen::Opts), Compatibility(commands::compatibility::Opts), Version(commands::version::Opts), + Explore(commands::explore::Opts), } #[tokio::main] @@ -27,5 +28,6 @@ async fn main() -> color_eyre::Result<()> { Command::Codegen(opts) => commands::codegen::run(opts).await, Command::Compatibility(opts) => commands::compatibility::run(opts).await, Command::Version(opts) => commands::version::run(opts), + Command::Explore(opts) => commands::explore::run(opts).await, } } diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 6d8d5a9788..ec2e224a08 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -4,18 +4,22 @@ use clap::Args; use color_eyre::eyre; + use std::{fs, io::Read, path::PathBuf}; use subxt_codegen::utils::{MetadataVersion, Uri}; +pub mod type_description; +pub mod type_example; + /// The source of the metadata. #[derive(Debug, Args)] pub struct FileOrUrl { /// The url of the substrate node to query for metadata for codegen. #[clap(long, value_parser)] - url: Option, + pub(crate) url: Option, /// The path to the encoded metadata file. #[clap(long, value_parser)] - file: Option, + pub(crate) file: Option, /// Specify the metadata version. /// /// - unstable: @@ -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. + let docs_str = docs + .iter() + .map(|e| e.trim()) + .take_while(|e| !e.is_empty()) + .collect::>() + .join("\n"); + with_indent(docs_str, indent) +} + +pub(crate) fn with_indent(s: String, indent: usize) -> String { + let indent_str = " ".repeat(indent); + s.lines() + .map(|line| format!("{indent_str}{line}")) + .collect::>() + .join("\n") +} diff --git a/cli/src/utils/type_description.rs b/cli/src/utils/type_description.rs new file mode 100644 index 0000000000..5575ab52b2 --- /dev/null +++ b/cli/src/utils/type_description.rs @@ -0,0 +1,273 @@ +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 { + const MIN_VARIANT_COUNT_FOR_TRAILING_COMMA: usize = 100; + let add_trailing_comma = self.variants.len() >= MIN_VARIANT_COUNT_FOR_TRAILING_COMMA; + + 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() || add_trailing_comma { + 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()); + } + + const MIN_FIELD_COUNT_FOR_TRAILING_COMMA: usize = 100; + let add_trailing_comma = self.len() >= MIN_FIELD_COUNT_FOR_TRAILING_COMMA; + + 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() || add_trailing_comma { + 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 +} diff --git a/cli/src/utils/type_example.rs b/cli/src/utils/type_example.rs new file mode 100644 index 0000000000..f14e1f7cf0 --- /dev/null +++ b/cli/src/utils/type_example.rs @@ -0,0 +1,380 @@ +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: (), + } + } +}