diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ef098d3..eb5463941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `subxt` to `0.32.0` - [#1352](https://github.com/paritytech/cargo-contract/pull/1352) - Remove check for compatible `scale` and `scale-info` versions - [#1370](https://github.com/paritytech/cargo-contract/pull/1370) - Add workspace support -[#1358](https://github.com/paritytech/cargo-contract/pull/1358) +- Add `Storage Total Deposit` to `info` command output - [#1347](https://github.com/paritytech/cargo-contract/pull/1347) ## [4.0.0-alpha] diff --git a/crates/cargo-contract/src/cmd/info.rs b/crates/cargo-contract/src/cmd/info.rs index 5d165d878..492b621a3 100644 --- a/crates/cargo-contract/src/cmd/info.rs +++ b/crates/cargo-contract/src/cmd/info.rs @@ -105,12 +105,7 @@ impl InfoCommand { .as_ref() .expect("Contract argument was not provided"); - let info_to_json = fetch_contract_info(contract, &rpc, &client) - .await? - .ok_or(anyhow!( - "No contract information was found for account id {}", - contract - ))?; + let info_to_json = fetch_contract_info(contract, &rpc, &client).await?; let wasm_code = fetch_wasm_code(&client, &rpc, info_to_json.code_hash()) .await? @@ -154,7 +149,8 @@ pub struct ExtendedContractInfo { pub trie_id: String, pub code_hash: CodeHash, pub storage_items: u32, - pub storage_item_deposit: Balance, + pub storage_items_deposit: Balance, + pub storage_total_deposit: Balance, pub source_language: String, } @@ -168,7 +164,8 @@ impl ExtendedContractInfo { trie_id: contract_info.trie_id().to_string(), code_hash: *contract_info.code_hash(), storage_items: contract_info.storage_items(), - storage_item_deposit: contract_info.storage_item_deposit(), + storage_items_deposit: contract_info.storage_items_deposit(), + storage_total_deposit: contract_info.storage_total_deposit(), source_language: language, } } diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index aa1078f95..4b7f2d738 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -121,7 +121,7 @@ impl CLIExtrinsicOpts { } } -const STORAGE_DEPOSIT_KEY: &str = "Storage Deposit"; +const STORAGE_DEPOSIT_KEY: &str = "Storage Total Deposit"; pub const MAX_KEY_COL_WIDTH: usize = STORAGE_DEPOSIT_KEY.len() + 1; /// Print to stdout the fields of the result of a `instantiate` or `call` dry-run via RPC. @@ -230,8 +230,13 @@ pub fn basic_display_format_extended_contract_info(info: &ExtendedContractInfo) MAX_KEY_COL_WIDTH ); name_value_println!( - "Storage Deposit", - format!("{:?}", info.storage_item_deposit), + "Storage Items Deposit", + format!("{:?}", info.storage_items_deposit), + MAX_KEY_COL_WIDTH + ); + name_value_println!( + STORAGE_DEPOSIT_KEY, + format!("{:?}", info.storage_total_deposit), MAX_KEY_COL_WIDTH ); name_value_println!( diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 405dd6d04..6294e5fd9 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -32,45 +32,142 @@ use scale::Decode; use std::option::Option; use subxt::{ backend::legacy::LegacyRpcMethods, + dynamic::DecodedValueThunk, + ext::{ + scale_decode::DecodeAsType, + scale_value::Value, + }, + storage::dynamic, utils::AccountId32, }; +/// Return the account data for an account ID. +async fn get_account_balance( + account: &AccountId32, + rpc: &LegacyRpcMethods, + client: &Client, +) -> Result { + let storage_query = + subxt::dynamic::storage("System", "Account", vec![Value::from_bytes(account)]); + let best_block = get_best_block(rpc).await?; + + let account = client + .storage() + .at(best_block) + .fetch(&storage_query) + .await? + .ok_or_else(|| anyhow::anyhow!("Failed to fetch account data"))?; + + let data = account.as_type::()?.data; + Ok(data) +} + /// Fetch the contract info from the storage using the provided client. pub async fn fetch_contract_info( contract: &AccountId32, rpc: &LegacyRpcMethods, client: &Client, -) -> Result> { - let info_contract_call = api::storage().contracts().contract_info_of(contract); - +) -> Result { let best_block = get_best_block(rpc).await?; - let contract_info_of = client + let contract_info_address = dynamic( + "Contracts", + "ContractInfoOf", + vec![Value::from_bytes(contract)], + ); + let contract_info_value = client .storage() .at(best_block) - .fetch(&info_contract_call) - .await?; + .fetch(&contract_info_address) + .await? + .ok_or_else(|| { + anyhow!( + "No contract information was found for account id {}", + contract + ) + })?; + + let contract_info_raw = ContractInfoRaw::new(contract.clone(), contract_info_value)?; + let deposit_account = contract_info_raw.get_deposit_account(); + + let deposit_account_data = get_account_balance(deposit_account, rpc, client).await?; + Ok(contract_info_raw.into_contract_info(deposit_account_data)) +} - match contract_info_of { - Some(info_result) => { - let convert_trie_id = hex::encode(info_result.trie_id.0); - Ok(Some(ContractInfo { - trie_id: convert_trie_id, - code_hash: info_result.code_hash, - storage_items: info_result.storage_items, - storage_item_deposit: info_result.storage_item_deposit, - })) +/// Struct representing contract info, supporting deposit on either the main or secondary +/// account. +struct ContractInfoRaw { + deposit_account: AccountId32, + contract_info: ContractInfoOf, + deposit_on_main_account: bool, +} + +impl ContractInfoRaw { + /// Create a new instance of `ContractInfoRaw` based on the provided contract and + /// contract info value. Determines whether it's a main or secondary account deposit. + pub fn new( + contract_account: AccountId32, + contract_info_value: DecodedValueThunk, + ) -> Result { + let contract_info = contract_info_value.as_type::()?; + // Pallet-contracts [>=10, <15] store the contract's deposit as a free balance + // in a secondary account (deposit account). Other versions store it as + // reserved balance on the main contract's account. If the + // `deposit_account` field is present in a contract info structure, + // the contract's deposit is in this account. + match Self::get_deposit_account_id(&contract_info_value) { + Ok(deposit_account) => { + Ok(Self { + deposit_account, + contract_info, + deposit_on_main_account: false, + }) + } + Err(_) => { + Ok(Self { + deposit_account: contract_account, + contract_info, + deposit_on_main_account: true, + }) + } + } + } + + pub fn get_deposit_account(&self) -> &AccountId32 { + &self.deposit_account + } + + /// Convert `ContractInfoRaw` to `ContractInfo` + pub fn into_contract_info(self, deposit: AccountData) -> ContractInfo { + let total_deposit = if self.deposit_on_main_account { + deposit.reserved + } else { + deposit.free + }; + + ContractInfo { + trie_id: hex::encode(&self.contract_info.trie_id.0), + code_hash: self.contract_info.code_hash, + storage_items: self.contract_info.storage_items, + storage_items_deposit: self.contract_info.storage_item_deposit, + storage_total_deposit: total_deposit, } - None => Ok(None), + } + + /// Decode the deposit account from the contract info + fn get_deposit_account_id(contract_info: &DecodedValueThunk) -> Result { + let account = contract_info.as_type::()?; + Ok(account.deposit_account) } } -#[derive(serde::Serialize)] +#[derive(Debug, PartialEq, serde::Serialize)] pub struct ContractInfo { trie_id: String, code_hash: CodeHash, storage_items: u32, - storage_item_deposit: Balance, + storage_items_deposit: Balance, + storage_total_deposit: Balance, } impl ContractInfo { @@ -95,8 +192,13 @@ impl ContractInfo { } /// Return the storage item deposit of the contract. - pub fn storage_item_deposit(&self) -> Balance { - self.storage_item_deposit + pub fn storage_items_deposit(&self) -> Balance { + self.storage_items_deposit + } + + /// Return the storage item deposit of the contract. + pub fn storage_total_deposit(&self) -> Balance { + self.storage_total_deposit } } @@ -161,3 +263,207 @@ pub async fn fetch_all_contracts( Ok(contract_accounts) } + +/// A struct used in the storage reads to access account info. +#[derive(DecodeAsType, Debug)] +#[decode_as_type(crate_path = "subxt::ext::scale_decode")] +struct AccountInfo { + data: AccountData, +} + +/// A struct used in the storage reads to access account data. +#[derive(Clone, Debug, DecodeAsType)] +#[decode_as_type(crate_path = "subxt::ext::scale_decode")] +struct AccountData { + free: Balance, + reserved: Balance, +} + +/// A struct representing `Vec`` used in the storage reads. +#[derive(Debug, DecodeAsType)] +#[decode_as_type(crate_path = "subxt::ext::scale_decode")] +struct BoundedVec(pub ::std::vec::Vec); + +/// A struct used in the storage reads to access contract info. +#[derive(Debug, DecodeAsType)] +#[decode_as_type(crate_path = "subxt::ext::scale_decode")] +struct ContractInfoOf { + trie_id: BoundedVec, + code_hash: CodeHash, + storage_items: u32, + storage_item_deposit: Balance, +} + +/// A struct used in storage reads to access the deposit account from contract info. +#[derive(Debug, DecodeAsType)] +#[decode_as_type(crate_path = "subxt::ext::scale_decode")] +struct DepositAccount { + deposit_account: AccountId32, +} + +#[cfg(test)] +mod tests { + use super::*; + use scale::Encode; + use scale_info::{ + IntoPortable, + Path, + }; + use subxt::metadata::{ + types::Metadata, + DecodeWithMetadata, + }; + + // Find the type index in the metadata. + fn get_metadata_type_index( + ident: &'static str, + module_path: &'static str, + metadata: &Metadata, + ) -> Result { + let contract_info_path = + Path::new(ident, module_path).into_portable(&mut Default::default()); + + metadata + .types() + .types + .iter() + .enumerate() + .find_map(|(i, t)| { + if t.ty.path == contract_info_path { + Some(i) + } else { + None + } + }) + .ok_or(anyhow!("Type not found")) + } + + #[test] + fn contract_info_v11_decode_works() { + // This version of metadata includes the deposit_account field in ContractInfo + #[subxt::subxt(runtime_metadata_path = "src/runtime_api/metadata_v11.scale")] + mod api_v11 {} + + use api_v11::runtime_types::{ + bounded_collections::bounded_vec::BoundedVec, + pallet_contracts::storage::{ + ContractInfo as ContractInfoV11, + DepositAccount, + }, + }; + + let metadata_bytes = std::fs::read("src/runtime_api/metadata_v11.scale") + .expect("the metadata must be present"); + let metadata = + Metadata::decode(&mut &*metadata_bytes).expect("the metadata must decode"); + let contract_info_type_id = get_metadata_type_index( + "ContractInfo", + "pallet_contracts::storage", + &metadata, + ) + .expect("the contract info type must be present in the metadata"); + + let contract_info_v11 = ContractInfoV11 { + trie_id: BoundedVec(vec![]), + deposit_account: DepositAccount(AccountId32([7u8; 32])), + code_hash: Default::default(), + storage_bytes: 1, + storage_items: 1, + storage_byte_deposit: 1, + storage_item_deposit: 1, + storage_base_deposit: 1, + }; + + let contract_info_thunk = DecodedValueThunk::decode_with_metadata( + &mut &*contract_info_v11.encode(), + contract_info_type_id as u32, + &metadata.into(), + ) + .expect("the contract info must be decoded"); + + let contract = AccountId32([0u8; 32]); + let contract_info_raw = ContractInfoRaw::new(contract, contract_info_thunk) + .expect("the conatract info raw must be created"); + let account_data = AccountData { + free: 1, + reserved: 10, + }; + + let contract_info = contract_info_raw.into_contract_info(account_data.clone()); + assert_eq!( + contract_info, + ContractInfo { + trie_id: hex::encode(contract_info_v11.trie_id.0), + code_hash: contract_info_v11.code_hash, + storage_items: contract_info_v11.storage_items, + storage_items_deposit: contract_info_v11.storage_item_deposit, + storage_total_deposit: account_data.free, + } + ); + } + + #[test] + fn contract_info_v15_decode_works() { + // This version of metadata does not include the deposit_account field in + // ContractInfo + #[subxt::subxt(runtime_metadata_path = "src/runtime_api/metadata.scale")] + mod api_v15 {} + + use api_v15::runtime_types::{ + bounded_collections::{ + bounded_btree_map::BoundedBTreeMap, + bounded_vec::BoundedVec, + }, + pallet_contracts::storage::ContractInfo as ContractInfoV15, + }; + + let metadata_bytes = std::fs::read("src/runtime_api/metadata.scale") + .expect("the metadata must be present"); + let metadata = + Metadata::decode(&mut &*metadata_bytes).expect("the metadata must decode"); + let contract_info_type_id = get_metadata_type_index( + "ContractInfo", + "pallet_contracts::storage", + &metadata, + ) + .expect("the contract info type must be present in the metadata"); + + let contract_info_v15 = ContractInfoV15 { + trie_id: BoundedVec(vec![]), + code_hash: Default::default(), + storage_bytes: 1, + storage_items: 1, + storage_byte_deposit: 1, + storage_item_deposit: 1, + storage_base_deposit: 1, + delegate_dependencies: BoundedBTreeMap(vec![]), + }; + + let contract_info_thunk = DecodedValueThunk::decode_with_metadata( + &mut &*contract_info_v15.encode(), + contract_info_type_id as u32, + &metadata.into(), + ) + .expect("the contract info must be decoded"); + + let contract = AccountId32([0u8; 32]); + let contract_info_raw = ContractInfoRaw::new(contract, contract_info_thunk) + .expect("the conatract info raw must be created"); + let account_data = AccountData { + free: 1, + reserved: 10, + }; + + let contract_info = contract_info_raw.into_contract_info(account_data.clone()); + assert_eq!( + contract_info, + ContractInfo { + trie_id: hex::encode(contract_info_v15.trie_id.0), + code_hash: contract_info_v15.code_hash, + storage_items: contract_info_v15.storage_items, + storage_items_deposit: contract_info_v15.storage_item_deposit, + storage_total_deposit: account_data.reserved, + } + ); + } +} diff --git a/crates/extrinsics/src/runtime_api/metadata_v11.scale b/crates/extrinsics/src/runtime_api/metadata_v11.scale new file mode 100644 index 000000000..d61d093ba Binary files /dev/null and b/crates/extrinsics/src/runtime_api/metadata_v11.scale differ