Skip to content

Commit

Permalink
Add user friendly view of storage data (#1414)
Browse files Browse the repository at this point in the history
Add a user-friendly view of contract storage data in the form of a table
  • Loading branch information
smiasojed authored Jan 9, 2024
1 parent 731056e commit 1990746
Show file tree
Hide file tree
Showing 7 changed files with 579 additions and 82 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/cargo-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
138 changes: 130 additions & 8 deletions crates/cargo-contract/src/cmd/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@
// along with cargo-contract. If not, see <http://www.gnu.org/licenses/>.

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;
Expand All @@ -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")]
Expand Down Expand Up @@ -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!(
Expand All @@ -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<Self> {
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!(
"{:<index_width$} | {:<key_width$} | {:<parent_width$} | {:<value_width$.value_width$}",
if i == 0 { index.to_string() } else { String::new() },
if i == 0 { key } else { "" },
if i == 0 { parent } else { "" },
value,
index_width = Self::INDEX_WIDTH,
key_width = Self::KEY_WIDTH,
parent_width = self.parent_width,
value_width = self.value_width,
);
}
}

fn display(&self) -> Result<()> {
// Print the table header
println!(
"{:<index_width$} | {:<key_width$} | {:<parent_width$} | {:<value_width$.value_width$}",
Self::INDEX_LABEL.bright_purple().bold(),
Self::KEY_LABEL.bright_purple().bold(),
Self::PARENT_LABEL.bright_purple().bold(),
Self::VALUE_LABEL.bright_purple().bold(),
index_width = Self::INDEX_WIDTH,
key_width = Self::KEY_WIDTH,
parent_width = self.parent_width,
value_width = self.value_width,
);

for (index, cell) in self.storage_layout.iter().enumerate() {
let formatted_cell = format!("{cell}");
let values = formatted_cell.split('\n');
for (i, v) in values.enumerate() {
self.table_row_println(
index + i,
cell.root_key().as_str(),
cell.parent().as_str(),
v,
);
}
}
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/extrinsics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ anyhow = "1.0.79"
blake2 = { version = "0.10.6", default-features = false }
clap = { version = "4.4.13", features = ["derive", "env"] }
futures = { version = "0.3.30", default-features = false, features = ["std"] }
itertools = { version = "0.12", default-features = false }
tracing = "0.1.40"
scale = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"] }
colored = "2.1.0"
Expand Down
Loading

0 comments on commit 1990746

Please sign in to comment.