diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bee58ae2..8a780ff9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Add a user-friendly view of contract storage data in the form of a table - [#1414](https://github.com/paritytech/cargo-contract/pull/1414) + ## [4.0.0-rc.1] ### Changed diff --git a/Cargo.lock b/Cargo.lock index e59ade5cd..a42c8a5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -787,6 +787,7 @@ dependencies = [ "contract-extrinsics", "contract-metadata", "contract-transcode", + "crossterm", "current_platform", "hex", "ink_metadata", @@ -1071,6 +1072,7 @@ dependencies = [ "futures", "hex", "ink_metadata", + "itertools 0.12.0", "pallet-contracts-primitives", "parity-scale-codec", "predicates", diff --git a/crates/cargo-contract/Cargo.toml b/crates/cargo-contract/Cargo.toml index 676f04919..71357f154 100644 --- a/crates/cargo-contract/Cargo.toml +++ b/crates/cargo-contract/Cargo.toml @@ -38,7 +38,7 @@ semver = "1.0" jsonschema = "0.17" schemars = "0.8" ink_metadata = "5.0.0-rc" - +crossterm = "0.27.0" # dependencies for extrinsics (deploying and calling a contract) tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index ba2ac22e2..944de691b 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -15,16 +15,21 @@ // along with cargo-contract. If not, see . use super::DefaultConfig; -use anyhow::Result; +use anyhow::{ + anyhow, + Result, +}; use colored::Colorize; use contract_extrinsics::{ ContractArtifacts, ContractStorage, + ContractStorageLayout, ContractStorageRpc, ErrorVariant, }; +use crossterm::terminal; use std::{ - fmt::Debug, + cmp, path::PathBuf, }; use subxt::Config; @@ -38,6 +43,9 @@ pub struct StorageCommand { /// Fetch the "raw" storage keys and values for the contract. #[clap(long)] raw: bool, + /// Export the instantiate output in JSON format. + #[clap(name = "output-json", long, conflicts_with = "raw")] + output_json: bool, /// Path to a contract build artifact file: a raw `.wasm` file, a `.contract` bundle, /// or a `.json` metadata file. #[clap(value_parser, conflicts_with = "manifest_path")] @@ -78,14 +86,19 @@ impl StorageCommand { match contract_artifacts { Ok(contract_artifacts) => { - let ink_metadata = contract_artifacts.ink_project_metadata()?; + let transcoder = contract_artifacts.contract_transcoder()?; let contract_storage = storage_layout - .load_contract_storage_with_layout(&ink_metadata, &self.contract) + .load_contract_storage_with_layout(&self.contract, &transcoder) .await?; - println!( - "{json}", - json = serde_json::to_string_pretty(&contract_storage)? - ); + if self.output_json { + println!( + "{json}", + json = serde_json::to_string_pretty(&contract_storage)? + ); + } else { + let table = StorageDisplayTable::new(&contract_storage)?; + table.display()?; + } } Err(_) => { eprintln!( @@ -106,3 +119,112 @@ impl StorageCommand { Ok(()) } } + +struct StorageDisplayTable<'a> { + storage_layout: &'a ContractStorageLayout, + parent_width: usize, + value_width: usize, +} + +impl<'a> StorageDisplayTable<'a> { + const KEY_WIDTH: usize = 8; + const INDEX_WIDTH: usize = 5; + const INDEX_LABEL: &'static str = "Index"; + const KEY_LABEL: &'static str = "Root Key"; + const PARENT_LABEL: &'static str = "Parent"; + const VALUE_LABEL: &'static str = "Value"; + + fn new(storage_layout: &'a ContractStorageLayout) -> Result { + let parent_len = storage_layout + .iter() + .map(|c| c.parent().len()) + .max() + .unwrap_or_default(); + let parent_width = cmp::max(parent_len, Self::PARENT_LABEL.len()); + let terminal_width = + terminal::size().expect("Failed to get terminal size").0 as usize; + + // There are tree separators in the table ' | ' + let value_width = terminal_width + .checked_sub(Self::KEY_WIDTH + Self::INDEX_WIDTH + 3 * 3 + parent_width) + .filter(|&w| w > Self::VALUE_LABEL.len()) + .ok_or(anyhow!( + "Terminal width to small to display the storage layout correctly" + ))?; + + Ok(Self { + storage_layout, + parent_width, + value_width, + }) + } + + fn table_row_println(&self, index: usize, key: &str, parent: &str, value: &str) { + let mut result = value.split_whitespace().fold( + (Vec::new(), String::new()), + |(mut result, mut current_line), word| { + if current_line.len() + word.len() + 1 > self.value_width { + if !current_line.is_empty() { + result.push(current_line.clone()); + current_line.clear(); + } + current_line.push_str(word); + (result, current_line) + } else { + if !current_line.is_empty() { + current_line.push(' '); + } + current_line.push_str(word); + (result, current_line) + } + }, + ); + + if !result.1.is_empty() { + result.0.push(result.1); + } + + for (i, value) in result.0.iter().enumerate() { + println!( + "{: Result<()> { + // Print the table header + println!( + "{:. -use anyhow::Result; -use ink_metadata::{ - layout::Layout, - InkProject, +use anyhow::{ + anyhow, + Result, +}; +use contract_transcode::{ + ContractMessageTranscoder, + Value, +}; +use ink_metadata::layout::{ + Layout, + StructLayout, +}; +use itertools::Itertools; +use scale::{ + Decode, + Encode, +}; +use scale_info::{ + form::PortableForm, + Type, +}; +use serde::{ + Serialize, + Serializer, +}; +use sp_core::{ + hexdisplay::AsBytesRef, + storage::ChildInfo, }; -use scale_info::form::PortableForm; -use serde::Serialize; -use sp_core::storage::ChildInfo; use std::{ collections::BTreeMap, - fmt::Display, + fmt, + fmt::{ + Display, + Formatter, + }, }; use subxt::{ backend::{ @@ -62,7 +87,6 @@ impl ContractStorage where C::AccountId: AsRef<[u8]> + Display + IntoVisitor, DecodeError: From<<::Visitor as Visitor>::Error>, - // BlockRef: From, { pub fn new(rpc: ContractStorageRpc) -> Self { Self { rpc } @@ -76,19 +100,35 @@ where let contract_info = self.rpc.fetch_contract_info(contract_account).await?; let trie_id = contract_info.trie_id(); - let storage_keys = self - .rpc - .fetch_storage_keys_paged(trie_id, None, 1000, None, None) // todo loop pages - .await?; - let storage_values = self - .rpc - .fetch_storage_entries(trie_id, &storage_keys, None) - .await?; - assert_eq!( - storage_keys.len(), - storage_values.len(), - "storage keys and values must be the same length" - ); + let mut storage_keys = Vec::new(); + let mut storage_values = Vec::new(); + const KEYS_COUNT: u32 = 1000; + loop { + let mut keys = self + .rpc + .fetch_storage_keys_paged( + trie_id, + None, + KEYS_COUNT, + storage_keys.last().map(|k: &Bytes| k.as_bytes_ref()), + None, + ) + .await?; + let keys_count = keys.len(); + let mut values = self.rpc.fetch_storage_entries(trie_id, &keys, None).await?; + assert_eq!( + keys_count, + values.len(), + "storage keys and values must be the same length" + ); + storage_keys.append(&mut keys); + storage_values.append(&mut values); + + if (keys_count as u32) < KEYS_COUNT { + break + } + } + let storage = storage_keys .into_iter() .zip(storage_values.into_iter()) @@ -101,84 +141,408 @@ where pub async fn load_contract_storage_with_layout( &self, - metadata: &InkProject, contract_account: &C::AccountId, + decoder: &ContractMessageTranscoder, ) -> Result { let data = self.load_contract_storage_data(contract_account).await?; - let layout = ContractStorageLayout::new(data, metadata.layout()); - Ok(layout) + ContractStorageLayout::new(data, decoder) } } /// Represents the raw key/value storage for the contract. -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct ContractStorageData(BTreeMap); -#[derive(Serialize)] +/// Represents the RootLayout storage entry for the contract. +#[derive(Serialize, Debug)] +pub struct RootKeyEntry { + #[serde(serialize_with = "RootKeyEntry::key_as_hex")] + pub root_key: u32, + pub path: Vec, + pub type_id: u32, +} + +impl RootKeyEntry { + fn key_as_hex(key: &u32, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(format!("0x{}", hex::encode(key.encode())).as_str()) + } +} + +#[derive(Serialize, Debug)] +pub struct Mapping { + #[serde(flatten)] + root: RootKeyEntry, + map: Vec<(Value, Value)>, +} + +impl Mapping { + // Create new `Mapping`. + pub fn new(root: RootKeyEntry, value: Vec<(Value, Value)>) -> Mapping { + Mapping { root, map: value } + } + + /// Return the root key entry of the `Mapping`. + pub fn root(&self) -> &RootKeyEntry { + &self.root + } + + /// Iterate all key-value pairs. + pub fn iter(&self) -> impl Iterator + DoubleEndedIterator { + self.map.iter() + } +} + +impl Display for Mapping { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let len = self.map.len(); + for (i, e) in self.map.iter().enumerate() { + write!(f, "Mapping {{ {} => {} }}", e.0, e.1)?; + if i + 1 < len { + writeln!(f)?; + } + } + Ok(()) + } +} + +#[derive(Serialize, Debug)] +pub struct Lazy { + #[serde(flatten)] + root: RootKeyEntry, + value: Value, +} + +impl Lazy { + /// Create new `Lazy` + pub fn new(root: RootKeyEntry, value: Value) -> Lazy { + Lazy { root, value } + } + + /// Return the root key entry of the `Lazy`. + pub fn root(&self) -> &RootKeyEntry { + &self.root + } + + /// Return the Lazy value. + pub fn value(&self) -> &Value { + &self.value + } +} + +impl Display for Lazy { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Lazy {{ {} }}", self.value) + } +} + +#[derive(Serialize, Debug)] +pub struct StorageVec { + #[serde(flatten)] + root: RootKeyEntry, + len: u32, + vec: Vec, +} + +impl StorageVec { + /// Create new `StorageVec`. + pub fn new(root: RootKeyEntry, len: u32, value: Vec) -> StorageVec { + StorageVec { + root, + len, + vec: value, + } + } + + /// Return the root key entry of the `StorageVec`. + pub fn root(&self) -> &RootKeyEntry { + &self.root + } + + // Return the len of the `StorageVec`. + pub fn len(&self) -> u32 { + self.len + } + + /// Return the iterator over the `StorageVec` values. + pub fn values(&self) -> impl Iterator { + self.vec.iter() + } +} + +impl Display for StorageVec { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for (i, v) in self.vec.iter().enumerate() { + write!(f, "StorageVec [{}] {{ [{}] => {} }}", self.len, i, v)?; + if i + 1 < self.len as usize { + writeln!(f)?; + } + } + Ok(()) + } +} + +#[derive(Serialize, Debug)] +pub struct Packed { + #[serde(flatten)] + root: RootKeyEntry, + value: Value, +} + +impl Packed { + /// Create new `Packed`. + pub fn new(root: RootKeyEntry, value: Value) -> Packed { + Packed { root, value } + } + + /// Return the root key entry of the `Packed`. + pub fn root(&self) -> &RootKeyEntry { + &self.root + } + + /// Return the Packed value. + pub fn value(&self) -> &Value { + &self.value + } +} + +impl Display for Packed { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +/// Represents the storage cell value. +#[derive(Serialize, Debug)] +pub enum ContractStorageCell { + Mapping(Mapping), + Lazy(Lazy), + StorageVec(StorageVec), + Packed(Packed), +} + +impl ContractStorageCell { + fn root(&self) -> &RootKeyEntry { + match self { + Self::Mapping(mapping) => mapping.root(), + Self::Lazy(lazy) => lazy.root(), + Self::StorageVec(storage_vec) => storage_vec.root(), + Self::Packed(packed) => packed.root(), + } + } + + /// Return the `RootKeyEntry` path as a string. + pub fn path(&self) -> String { + self.root().path.join("::") + } + + /// Return the parent. + pub fn parent(&self) -> String { + self.root().path.last().cloned().unwrap_or_default() + } + + /// Return the root_key as a hex-encoded string. + pub fn root_key(&self) -> String { + hex::encode(self.root().root_key.to_le_bytes()) + } +} + +impl Display for ContractStorageCell { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Mapping(mapping) => mapping.fmt(f), + Self::Lazy(lazy) => lazy.fmt(f), + Self::StorageVec(storage_vec) => storage_vec.fmt(f), + Self::Packed(value) => value.fmt(f), + } + } +} + +/// Represents storage cells containing values and type information for the contract. +#[derive(Serialize, Debug)] pub struct ContractStorageLayout { cells: Vec, } impl ContractStorageLayout { - pub fn new(data: ContractStorageData, layout: &Layout) -> Self { - let mut root_keys = Vec::new(); - Self::collect_root_keys("root".to_string(), layout, &mut root_keys); + /// Create a representation of contract storage based on raw storage entries and + /// metadata. + pub fn new( + data: ContractStorageData, + decoder: &ContractMessageTranscoder, + ) -> Result { + let layout = decoder.metadata().layout(); + let registry = decoder.metadata().registry(); + let mut path_stack = vec!["root".to_string()]; + let mut root_key_entries: Vec = Vec::new(); + Self::collect_root_key_entries(layout, &mut path_stack, &mut root_key_entries); - let cells = data + let mut cells = data .0 - .iter() - .filter_map(|(k, v)| { - let (root_key, mapping_key) = Self::key_parts(k); - let (label, _) = root_keys.iter().find(|(_, key)| *key == root_key)?; - - Some(ContractStorageCell { - key: k.clone(), - value: v.clone(), + .into_iter() + .map(|(key, value)| { + let (root_key, mapping_key) = Self::key_parts(&key); + (root_key, (mapping_key, value)) + }) + .into_group_map() + .into_iter() + .map(|(root_key, mut data)| { + let root_key_entry = root_key_entries + .iter() + .find(|e| e.root_key == root_key) + .ok_or(anyhow!( + "Root key {} not found for the RootLayout", + root_key + ))?; + let type_def = registry.resolve(root_key_entry.type_id).ok_or( + anyhow!("Type {} not found in the registry", root_key_entry.type_id), + )?; + let root = RootKeyEntry { + path: root_key_entry.path.clone(), + type_id: root_key_entry.type_id, root_key, - mapping_key, - label: label.clone(), - }) + }; + match type_def.path.to_string().as_str() { + "ink_storage::lazy::mapping::Mapping" => { + let key_type_id = Self::param_type_id(type_def, "K") + .ok_or(anyhow!("Param `K` not found in type registry"))?; + let value_type_id = Self::param_type_id(type_def, "V") + .ok_or(anyhow!("Param `V` not found in type registry"))?; + let value = Self::decode_to_mapping( + data, + key_type_id, + value_type_id, + decoder, + )?; + Ok(ContractStorageCell::Mapping(Mapping::new(root, value))) + } + "ink_storage::lazy::vec::StorageVec" => { + // Sort by the key to get the Vec in the right order. + data.sort_by(|a, b| a.0.cmp(&b.0)); + // First item is the `StorageVec` len. + let raw_len = data + .first() + .ok_or(anyhow!("Length of the StorageVec not found"))? + .1 + .clone(); + let len = u32::decode(&mut raw_len.as_bytes_ref())?; + let value_type_id = Self::param_type_id(type_def, "V") + .ok_or(anyhow!("Param `V` not found in type registry"))?; + let value = + Self::decode_to_vec(&data[1..], value_type_id, decoder)?; + Ok(ContractStorageCell::StorageVec(StorageVec::new( + root, len, value, + ))) + } + "ink_storage::lazy::Lazy" => { + let value_type_id = Self::param_type_id(type_def, "V") + .ok_or(anyhow!("Param `V` not found in type registry"))?; + let raw_value = + data.first().ok_or(anyhow!("Empty storage cell"))?.1.clone(); + let value = decoder + .decode(value_type_id, &mut raw_value.as_bytes_ref())?; + Ok(ContractStorageCell::Lazy(Lazy::new(root, value))) + } + _ => { + let raw_value = + data.first().ok_or(anyhow!("Empty storage cell"))?.1.clone(); + let value = decoder + .decode(root.type_id, &mut raw_value.as_bytes_ref())?; + Ok(ContractStorageCell::Packed(Packed::new(root, value))) + } + } }) - .collect(); + .collect::>>()?; + + cells.sort_by_key(|k| k.path()); - Self { cells } + Ok(Self { cells }) } - fn collect_root_keys( - label: String, + /// Return the iterator over the storage cells. + pub fn iter(&self) -> impl Iterator { + self.cells.iter() + } + + fn decode_to_mapping( + data: Vec<(Option, Bytes)>, + key_type_id: u32, + value_type_id: u32, + decoder: &ContractMessageTranscoder, + ) -> Result> { + data.into_iter() + .map(|(k, v)| { + let k = k.ok_or(anyhow!("The Mapping key is missing in the map"))?; + let key = decoder.decode(key_type_id, &mut k.as_bytes_ref())?; + let value = decoder.decode(value_type_id, &mut v.as_bytes_ref())?; + Ok((key, value)) + }) + .collect() + } + + fn decode_to_vec( + data: &[(Option, Bytes)], + value_type_id: u32, + decoder: &ContractMessageTranscoder, + ) -> Result> { + data.iter() + .map(|(_, v)| { + let value = decoder.decode(value_type_id, &mut v.as_bytes_ref())?; + Ok(value) + }) + .collect() + } + + fn collect_root_key_entries( layout: &Layout, - root_keys: &mut Vec<(String, u32)>, + path: &mut Vec, + entries: &mut Vec, ) { match layout { Layout::Root(root) => { - root_keys.push((label.clone(), *root.root_key().key())); - Self::collect_root_keys(label, root.layout(), root_keys) + entries.push(RootKeyEntry { + root_key: *root.root_key().key(), + path: path.clone(), + type_id: root.ty().id, + }); + Self::collect_root_key_entries(root.layout(), path, entries); } Layout::Struct(struct_layout) => { - for field in struct_layout.fields() { - let label = field.name().to_string(); - Self::collect_root_keys(label, field.layout(), root_keys); - } + Self::struct_entries(struct_layout, path, entries) } Layout::Enum(enum_layout) => { + path.push(enum_layout.name().to_string()); for (variant, struct_layout) in enum_layout.variants() { - for field in struct_layout.fields() { - let label = - format!("{}::{}", enum_layout.name(), variant.value()); - Self::collect_root_keys(label, field.layout(), root_keys); - } + path.push(variant.value().to_string()); + Self::struct_entries(struct_layout, path, entries); + path.pop(); } - } - Layout::Array(_) => { - todo!("Figure out what to do with an array layout") + path.pop(); } Layout::Hash(_) => { unimplemented!("Layout::Hash is not currently be constructed") } - Layout::Leaf(_) => {} + Layout::Array(_) | Layout::Leaf(_) => {} } } + fn struct_entries( + struct_layout: &StructLayout, + path: &mut Vec, + entries: &mut Vec, + ) { + let struct_label = struct_layout.name().to_string(); + path.push(struct_label); + for field in struct_layout.fields() { + path.push(field.name().to_string()); + Self::collect_root_key_entries(field.layout(), path, entries); + path.pop(); + } + path.pop(); + } + /// Split the key up /// /// 0x6a3fa479de3b1efe271333d8974501c8e7dc23266dd9bfa5543a94aad824cfb29396d200926d28223c57df8954cf0dc16812ea47 @@ -201,16 +565,18 @@ impl ContractStorageLayout { (root_key, mapping_key) } -} -#[derive(Serialize)] -pub struct ContractStorageCell { - key: Bytes, - value: Bytes, - label: String, - root_key: u32, - #[serde(skip_serializing_if = "Option::is_none")] - mapping_key: Option, + /// Get the type id of the parameter name from the type. + fn param_type_id(type_def: &Type, param_name: &str) -> Option { + Some( + type_def + .type_params + .iter() + .find(|&e| e.name == param_name)? + .ty? + .id, + ) + } } /// Methods for querying contracts over RPC. @@ -224,7 +590,6 @@ impl ContractStorageRpc where C::AccountId: AsRef<[u8]> + Display + IntoVisitor, DecodeError: From<<::Visitor as Visitor>::Error>, - // BlockRef: From, { /// Create a new instance of the ContractsRpc. pub async fn new(url: &url::Url) -> Result { @@ -266,6 +631,7 @@ where Ok(data) } + /// Fetch the keys of the contract storage. pub async fn fetch_storage_keys_paged( &self, trie_id: &TrieId, @@ -292,6 +658,7 @@ where Ok(data) } + /// Fetch the storage values for the given keys. pub async fn fetch_storage_entries( &self, trie_id: &TrieId, diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index d7299414b..590ba88f6 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -73,6 +73,8 @@ pub use contract_info::{ use contract_metadata::ContractMetadata; pub use contract_storage::{ ContractStorage, + ContractStorageCell, + ContractStorageLayout, ContractStorageRpc, }; pub use contract_transcode::ContractMessageTranscoder; @@ -176,7 +178,7 @@ where TxStatus::InBestBlock(tx_in_block) | TxStatus::InFinalizedBlock(tx_in_block) => { let events = tx_in_block.wait_for_success().await?; - return Ok(events); + return Ok(events) } TxStatus::Error { message } => { return Err(TransactionError::Error(message).into())