From 0d8d6cd8ab2c79aebf16fd18b40081581ca921c3 Mon Sep 17 00:00:00 2001 From: igamigo Date: Mon, 4 Dec 2023 12:37:22 -0300 Subject: [PATCH] `account show` command (#21) * account show * remove unnecessary use * store and retrieve keys * add accounts show * change comment * Change back miden-base rev to main * Add table formatting --- Cargo.toml | 8 +- src/cli/account.rs | 131 +++++++++++++++++++++++++++++++-- src/errors.rs | 26 ++++++- src/lib.rs | 47 +++++++++++- src/store/mod.rs | 180 ++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 369 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 42229c366..76161c507 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,12 @@ clap = { version = "4.3" , features = ["derive"] } crypto = { package = "miden-crypto", git = "https://github.com/0xPolygonMiden/crypto", branch = "next", default-features = false } lazy_static = "1.4.0" objects = { package = "miden-objects", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", features = ["serde"] } -miden_lib = { package = "miden-lib", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", default-features = false } -mock = { package = "miden-mock", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", default-features = false, optional = true } +miden_lib = { package = "miden-lib", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main" } +mock = { package = "miden-mock", git = "https://github.com/0xPolygonMiden/miden-base", branch = "main", optional = true } rusqlite = { version = "0.29.0", features = ["bundled"] } rusqlite_migration = { version = "1.0" } -rand = { version="0.8.5" } -serde = {version="1.0", features = ["derive"]} +rand = { version = "0.8.5" } +serde = {version = "1.0", features = ["derive"]} serde_json = { version = "1.0", features = ["raw_value"] } comfy-table = "7.1.0" diff --git a/src/cli/account.rs b/src/cli/account.rs index a5cd2304d..37e43fc1f 100644 --- a/src/cli/account.rs +++ b/src/cli/account.rs @@ -1,9 +1,16 @@ use clap::Parser; use comfy_table::{presets, Attribute, Cell, ContentArrangement, Table}; -use crypto::{dsa::rpo_falcon512::KeyPair, Felt}; +use crypto::{ + dsa::rpo_falcon512::KeyPair, + utils::{bytes_to_hex_string, Serializable}, + Felt, +}; use miden_client::Client; use miden_lib::{faucets, AuthScheme}; -use objects::{accounts::AccountType, assets::TokenSymbol}; +use objects::{ + accounts::{AccountId, AccountType}, + assets::TokenSymbol, +}; use rand::Rng; // ACCOUNT COMMAND @@ -16,11 +23,20 @@ pub enum AccountCmd { #[clap(short_flag = 'l')] List, - /// View details of the account for the specified ID - #[clap(short_flag = 'v')] - View { + /// Show details of the account for the specified ID + #[clap(short_flag = 's')] + Show { + // TODO: We should create a value parser for catching input parsing errors earlier (ie AccountID) once complexity grows #[clap()] id: Option, + #[clap(short, long, default_value_t = false)] + keys: bool, + #[clap(short, long, default_value_t = false)] + vault: bool, + #[clap(short, long, default_value_t = false)] + storage: bool, + #[clap(short, long, default_value_t = false)] + code: bool, }, /// Create new account and store it locally @@ -64,7 +80,21 @@ impl AccountCmd { AccountCmd::New { template, deploy } => { new_account(client, template, *deploy)?; } - AccountCmd::View { id: _ } => todo!(), + AccountCmd::Show { id: None, .. } => { + todo!("Default accounts are not supported yet") + } + AccountCmd::Show { + id: Some(v), + keys, + vault, + storage, + code, + } => { + let account_id: AccountId = AccountId::from_hex(v) + .map_err(|_| "Input number was not a valid Account Id")?; + + show_account(client, account_id, *keys, *vault, *storage, *code)?; + } } Ok(()) } @@ -164,6 +194,7 @@ fn new_account( .and_then(|_| client.store().insert_account_storage(account.storage())) .and_then(|_| client.store().insert_account_vault(account.vault())) .and_then(|_| client.store().insert_account(&account)) + .and_then(|_| client.store().insert_account_keys(account.id(), &key_pair)) .map(|_| { println!( "Succesfully created and stored Account ID: {}", @@ -174,3 +205,91 @@ fn new_account( Ok(()) } + +pub fn show_account( + client: Client, + account_id: AccountId, + show_keys: bool, + show_vault: bool, + show_storage: bool, + show_code: bool, +) -> Result<(), String> { + let account = client + .get_account_by_id(account_id) + .map_err(|err| err.to_string())?; + + let mut table = Table::new(); + table + .load_preset(presets::UTF8_FULL) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .set_header(vec![ + Cell::new("account id").add_attribute(Attribute::Bold), + Cell::new("code root").add_attribute(Attribute::Bold), + Cell::new("vault root").add_attribute(Attribute::Bold), + Cell::new("storage root").add_attribute(Attribute::Bold), + Cell::new("nonce").add_attribute(Attribute::Bold), + ]); + + table.add_row(vec![ + account.id().to_string(), + account.code_root().to_string(), + account.vault_root().to_string(), + account.storage_root().to_string(), + account.nonce().to_string(), + ]); + + println!("{table}\n"); + + if show_keys { + let auth_info = client + .get_account_keys(account_id) + .map_err(|err| err.to_string())?; + + // TODO: Decide how we want to output and import auth info + + const KEY_PAIR_SIZE: usize = std::mem::size_of::(); + let auth_info: [u8; KEY_PAIR_SIZE] = auth_info + .to_bytes() + .try_into() + .expect("Array size is const and should always exactly fit KeyPair"); + println!("Key pair:\n0x{}", bytes_to_hex_string(auth_info)); + } + + if show_vault { + let assets = client + .get_vault_assets(account.vault_root()) + .map_err(|err| err.to_string())?; + + println!( + "Vault assets: {}\n", + serde_json::to_string(&assets).map_err(|_| "Error serializing account assets")? + ); + } + + if show_storage { + let account_storage = client + .get_account_storage(account.storage_root()) + .map_err(|err| err.to_string())?; + + println!( + "Storage: {}\n", + serde_json::to_string(&account_storage) + .map_err(|_| "Error serializing account storage")? + ); + } + + if show_code { + let (procedure_digests, module) = client + .get_account_code(account.code_root()) + .map_err(|err| err.to_string())?; + + println!( + "Procedure digests:\n{}\n", + serde_json::to_string(&procedure_digests) + .map_err(|_| "Error serializing account storage for display")? + ); + println!("Module AST:\n{}\n", module); + } + + Ok(()) +} diff --git a/src/errors.rs b/src/errors.rs index 53fcd0c0c..e28dd4eb0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,6 @@ use core::fmt; -use objects::{AccountError, Digest}; +use crypto::utils::DeserializationError; +use objects::{accounts::AccountId, AccountError, Digest}; // CLIENT ERROR // ================================================================================================ @@ -38,7 +39,12 @@ pub enum StoreError { ColumnParsingError(rusqlite::Error), QueryError(rusqlite::Error), InputSerializationError(serde_json::Error), - DataDeserializationError(serde_json::Error), + JsonDataDeserializationError(serde_json::Error), + DataDeserializationError(DeserializationError), + AccountDataNotFound(AccountId), + AccountStorageNotFound(Digest), + VaultDataNotFound(Digest), + AccountCodeDataNotFound(Digest), InputNoteNotFound(Digest), } @@ -55,10 +61,26 @@ impl fmt::Display for StoreError { InputSerializationError(err) => { write!(f, "error trying to serialize inputs for the store: {err}") } + JsonDataDeserializationError(err) => { + write!( + f, + "error deserializing data from JSON from the store: {err}" + ) + } DataDeserializationError(err) => { write!(f, "error deserializing data from the store: {err}") } + AccountDataNotFound(account_id) => { + write!(f, "Account data was not found for Account Id {account_id}") + } InputNoteNotFound(hash) => write!(f, "input note with hash {} not found", hash), + AccountStorageNotFound(root) => { + write!(f, "account storage data with root {} not found", root) + } + VaultDataNotFound(root) => write!(f, "account vault data for root {} not found", root), + AccountCodeDataNotFound(root) => { + write!(f, "account code data with root {} not found", root) + } } } } diff --git a/src/lib.rs b/src/lib.rs index 741599586..7367edaa2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,11 @@ use objects::{ accounts::{Account, AccountId, AccountStub}, + assembly::ModuleAst, + assets::Asset, + crypto::dsa::rpo_falcon512::KeyPair, notes::RecordedNote, - Digest, + utils::collections::BTreeMap, + Digest, Word, }; use std::path::PathBuf; @@ -61,6 +65,47 @@ impl Client { self.store.get_accounts().map_err(|err| err.into()) } + /// Returns summary info about the specified account. + pub fn get_account_by_id(&self, account_id: AccountId) -> Result { + self.store + .get_account_by_id(account_id) + .map_err(|err| err.into()) + } + + /// Returns key pair structure for an Account Id. + pub fn get_account_keys(&self, account_id: AccountId) -> Result { + self.store + .get_account_keys(account_id) + .map_err(|err| err.into()) + } + + /// Returns vault assets from a vault root. + pub fn get_vault_assets(&self, vault_root: Digest) -> Result, ClientError> { + self.store + .get_vault_assets(vault_root) + .map_err(|err| err.into()) + } + + /// Returns account code data from a root. + pub fn get_account_code( + &self, + code_root: Digest, + ) -> Result<(Vec, ModuleAst), ClientError> { + self.store + .get_account_code(code_root) + .map_err(|err| err.into()) + } + + /// Returns account storage data from a storage root. + pub fn get_account_storage( + &self, + storage_root: Digest, + ) -> Result, ClientError> { + self.store + .get_account_storage(storage_root) + .map_err(|err| err.into()) + } + /// Returns historical states for the account with the specified ID. /// /// TODO: wrap `Account` in a type with additional info. diff --git a/src/store/mod.rs b/src/store/mod.rs index 4b6aa47d0..35ae149a7 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,8 +1,13 @@ use super::{errors::StoreError, AccountStub, ClientConfig}; -use crypto::{utils::collections::BTreeMap, Word}; +use crypto::hash::rpo::RpoDigest; +use crypto::{ + dsa::rpo_falcon512::KeyPair, + utils::{collections::BTreeMap, Deserializable, Serializable}, + Word, +}; use objects::{ accounts::{Account, AccountCode, AccountId, AccountStorage, AccountVault}, - assembly::AstSerdeOptions, + assembly::{AstSerdeOptions, ModuleAst}, assets::Asset, notes::{Note, NoteMetadata, RecordedNote}, Digest, Felt, @@ -77,16 +82,155 @@ impl Store { .try_into() .expect("Conversion from stored AccountID should not panic"), (nonce as u64).into(), - serde_json::from_str(&vault_root).map_err(StoreError::DataDeserializationError)?, + serde_json::from_str(&vault_root) + .map_err(StoreError::JsonDataDeserializationError)?, serde_json::from_str(&storage_root) - .map_err(StoreError::DataDeserializationError)?, - serde_json::from_str(&code_root).map_err(StoreError::DataDeserializationError)?, + .map_err(StoreError::JsonDataDeserializationError)?, + serde_json::from_str(&code_root) + .map_err(StoreError::JsonDataDeserializationError)?, )); } Ok(result) } + pub fn get_account_by_id(&self, account_id: AccountId) -> Result { + let mut stmt = self + .db + .prepare( + "SELECT id, nonce, vault_root, storage_root, code_root FROM accounts WHERE id = ?", + ) + .map_err(StoreError::QueryError)?; + let account_id_int: u64 = account_id.into(); + + let mut rows = stmt + .query(params![account_id_int as i64]) + .map_err(StoreError::QueryError)?; + + if let Some(row) = rows.next().map_err(StoreError::QueryError)? { + let id: i64 = row.get(0).map_err(StoreError::QueryError)?; + let nonce: u64 = row.get(1).map_err(StoreError::QueryError)?; + let vault_root: String = row.get(2).map_err(StoreError::QueryError)?; + let storage_root: String = row.get(3).map_err(StoreError::QueryError)?; + let code_root: String = row.get(4).map_err(StoreError::QueryError)?; + + let account = AccountStub::new( + (id as u64) + .try_into() + .expect("Conversion from stored AccountID should not panic"), + nonce.into(), + serde_json::from_str(&vault_root) + .map_err(StoreError::JsonDataDeserializationError)?, + serde_json::from_str(&storage_root) + .map_err(StoreError::JsonDataDeserializationError)?, + serde_json::from_str(&code_root) + .map_err(StoreError::JsonDataDeserializationError)?, + ); + + Ok(account) + } else { + Err(StoreError::AccountDataNotFound(account_id)) + } + } + + /// Retrieve account keys data by Account Id + pub fn get_account_keys(&self, account_id: AccountId) -> Result { + let mut stmt = self + .db + .prepare("SELECT key_pair FROM account_keys WHERE account_id = ?") + .map_err(StoreError::QueryError)?; + let account_id_int: u64 = account_id.into(); + + let mut rows = stmt + .query(params![account_id_int as i64]) + .map_err(StoreError::QueryError)?; + + if let Some(row) = rows.next().map_err(StoreError::QueryError)? { + let key_pair_bytes: Vec = row.get(0).map_err(StoreError::QueryError)?; + let key_pair: KeyPair = KeyPair::read_from_bytes(&key_pair_bytes).unwrap(); + + Ok(key_pair) + } else { + Err(StoreError::AccountDataNotFound(account_id)) + } + } + + /// Retrieve account code-related data by code root + pub fn get_account_code( + &self, + root: Digest, + ) -> Result<(Vec, ModuleAst), StoreError> { + let root_serialized = + serde_json::to_string(&root).map_err(StoreError::InputSerializationError)?; + + let mut stmt = self + .db + .prepare("SELECT procedures, module FROM account_code WHERE root = ?") + .map_err(StoreError::QueryError)?; + let mut rows = stmt + .query(params![root_serialized]) + .map_err(StoreError::QueryError)?; + + if let Some(row) = rows.next().map_err(StoreError::QueryError)? { + let procedures: String = row.get(0).map_err(StoreError::QueryError)?; + let module: Vec = row.get(1).map_err(StoreError::QueryError)?; + + let procedures = serde_json::from_str(&procedures) + .map_err(StoreError::JsonDataDeserializationError)?; + let module = + ModuleAst::from_bytes(&module).map_err(StoreError::DataDeserializationError)?; + Ok((procedures, module)) + } else { + Err(StoreError::AccountCodeDataNotFound(root)) + } + } + + /// Retrieve account storage data by vault root + pub fn get_account_storage(&self, root: RpoDigest) -> Result, StoreError> { + let root_serialized = + serde_json::to_string(&root).map_err(StoreError::InputSerializationError)?; + + let mut stmt = self + .db + .prepare("SELECT slots FROM account_storage WHERE root = ?") + .map_err(StoreError::QueryError)?; + let mut rows = stmt + .query(params![root_serialized]) + .map_err(StoreError::QueryError)?; + + if let Some(row) = rows.next().map_err(StoreError::QueryError)? { + let slots: String = row.get(0).map_err(StoreError::QueryError)?; + let slots = + serde_json::from_str(&slots).map_err(StoreError::JsonDataDeserializationError)?; + Ok(slots) + } else { + Err(StoreError::AccountStorageNotFound(root)) + } + } + + /// Retrieve assets by vault root + pub fn get_vault_assets(&self, root: RpoDigest) -> Result, StoreError> { + let vault_root = + serde_json::to_string(&root).map_err(StoreError::InputSerializationError)?; + + let mut stmt = self + .db + .prepare("SELECT assets FROM account_vaults WHERE root = ?") + .map_err(StoreError::QueryError)?; + let mut rows = stmt + .query(params![vault_root]) + .map_err(StoreError::QueryError)?; + + if let Some(row) = rows.next().map_err(StoreError::QueryError)? { + let assets: String = row.get(0).map_err(StoreError::QueryError)?; + let assets = + serde_json::from_str(&assets).map_err(StoreError::JsonDataDeserializationError)?; + Ok(assets) + } else { + Err(StoreError::VaultDataNotFound(root)) + } + } + pub fn insert_account(&self, account: &Account) -> Result<(), StoreError> { let id: u64 = account.id().into(); let code_root = serde_json::to_string(&account.code().root()) @@ -165,6 +309,22 @@ impl Store { .map_err(StoreError::QueryError) } + pub fn insert_account_keys( + &self, + account_id: AccountId, + key_pair: &KeyPair, + ) -> Result<(), StoreError> { + let account_id: u64 = account_id.into(); + let key_pair = key_pair.to_bytes(); + self.db + .execute( + "INSERT INTO account_keys (account_id, key_pair) VALUES (?, ?)", + params![account_id as i64, key_pair], + ) + .map(|_| ()) + .map_err(StoreError::QueryError) + } + // NOTES // -------------------------------------------------------------------------------------------- @@ -299,11 +459,11 @@ fn parse_input_note( ) -> Result { let (script, inputs, vault, serial_num, sender_id, tag, num_assets, inclusion_proof) = serialized_input_note_parts; - let script = serde_json::from_str(&script).map_err(StoreError::DataDeserializationError)?; - let inputs = serde_json::from_str(&inputs).map_err(StoreError::DataDeserializationError)?; - let vault = serde_json::from_str(&vault).map_err(StoreError::DataDeserializationError)?; + let script = serde_json::from_str(&script).map_err(StoreError::JsonDataDeserializationError)?; + let inputs = serde_json::from_str(&inputs).map_err(StoreError::JsonDataDeserializationError)?; + let vault = serde_json::from_str(&vault).map_err(StoreError::JsonDataDeserializationError)?; let serial_num = - serde_json::from_str(&serial_num).map_err(StoreError::DataDeserializationError)?; + serde_json::from_str(&serial_num).map_err(StoreError::JsonDataDeserializationError)?; let note_metadata = NoteMetadata::new( AccountId::new_unchecked(Felt::new(sender_id)), Felt::new(tag), @@ -312,7 +472,7 @@ fn parse_input_note( let note = Note::from_parts(script, inputs, vault, serial_num, note_metadata); let inclusion_proof = - serde_json::from_str(&inclusion_proof).map_err(StoreError::DataDeserializationError)?; + serde_json::from_str(&inclusion_proof).map_err(StoreError::JsonDataDeserializationError)?; Ok(RecordedNote::new(note, inclusion_proof)) }