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())