contract_extrinsics/
contract_info.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17use super::get_best_block;
18use anyhow::{
19    anyhow,
20    Result,
21};
22use contract_metadata::byte_str::serialize_as_byte_str;
23use std::fmt::{
24    Display,
25    Formatter,
26};
27
28use ink_env::Environment;
29use scale::Decode;
30use std::option::Option;
31use subxt::{
32    backend::legacy::LegacyRpcMethods,
33    dynamic::DecodedValueThunk,
34    ext::{
35        scale_decode::{
36            DecodeAsType,
37            IntoVisitor,
38        },
39        scale_value::Value,
40    },
41    storage::dynamic,
42    Config,
43    OnlineClient,
44};
45
46/// Return the account data for an account ID.
47async fn get_account_balance<C: Config, E: Environment>(
48    account: &C::AccountId,
49    rpc: &LegacyRpcMethods<C>,
50    client: &OnlineClient<C>,
51) -> Result<AccountData<E::Balance>>
52where
53    C::AccountId: AsRef<[u8]>,
54    E::Balance: IntoVisitor,
55{
56    let storage_query =
57        subxt::dynamic::storage("System", "Account", vec![Value::from_bytes(account)]);
58    let best_block = get_best_block(rpc).await?;
59
60    let account = client
61        .storage()
62        .at(best_block)
63        .fetch(&storage_query)
64        .await?
65        .ok_or_else(|| anyhow::anyhow!("Failed to fetch account data"))?;
66
67    let data = account.as_type::<AccountInfo<E::Balance>>()?.data;
68    Ok(data)
69}
70
71/// Fetch the contract info from the storage using the provided client.
72pub async fn fetch_contract_info<C: Config, E: Environment>(
73    contract: &C::AccountId,
74    rpc: &LegacyRpcMethods<C>,
75    client: &OnlineClient<C>,
76) -> Result<ContractInfo<C::Hash, E::Balance>>
77where
78    C::AccountId: AsRef<[u8]> + Display + IntoVisitor,
79    C::Hash: IntoVisitor,
80    E::Balance: IntoVisitor,
81{
82    let best_block = get_best_block(rpc).await?;
83
84    let contract_info_address = dynamic(
85        "Contracts",
86        "ContractInfoOf",
87        vec![Value::from_bytes(contract)],
88    );
89    let contract_info_value = client
90        .storage()
91        .at(best_block)
92        .fetch(&contract_info_address)
93        .await?
94        .ok_or_else(|| {
95            anyhow!(
96                "No contract information was found for account id {}",
97                contract
98            )
99        })?;
100
101    let contract_info_raw =
102        ContractInfoRaw::<C, E>::new(contract.clone(), contract_info_value)?;
103    let deposit_account = contract_info_raw.get_deposit_account();
104
105    let deposit_account_data =
106        get_account_balance::<C, E>(deposit_account, rpc, client).await?;
107    Ok(contract_info_raw.into_contract_info(deposit_account_data))
108}
109
110/// Struct representing contract info, supporting deposit on either the main or secondary
111/// account.
112struct ContractInfoRaw<C: Config, E: Environment> {
113    deposit_account: C::AccountId,
114    contract_info: ContractInfoOf<C::Hash, E::Balance>,
115    deposit_on_main_account: bool,
116}
117
118impl<C: Config, E: Environment> ContractInfoRaw<C, E>
119where
120    C::AccountId: IntoVisitor,
121    C::Hash: IntoVisitor,
122    E::Balance: IntoVisitor,
123{
124    /// Create a new instance of `ContractInfoRaw` based on the provided contract and
125    /// contract info value. Determines whether it's a main or secondary account deposit.
126    pub fn new(
127        contract_account: C::AccountId,
128        contract_info_value: DecodedValueThunk,
129    ) -> Result<Self> {
130        let contract_info =
131            contract_info_value.as_type::<ContractInfoOf<C::Hash, E::Balance>>()?;
132        // Pallet-contracts [>=10, <15] store the contract's deposit as a free balance
133        // in a secondary account (deposit account). Other versions store it as
134        // reserved balance on the main contract's account. If the
135        // `deposit_account` field is present in a contract info structure,
136        // the contract's deposit is in this account.
137        match Self::get_deposit_account_id(&contract_info_value) {
138            Ok(deposit_account) => {
139                Ok(Self {
140                    deposit_account,
141                    contract_info,
142                    deposit_on_main_account: false,
143                })
144            }
145            Err(_) => {
146                Ok(Self {
147                    deposit_account: contract_account,
148                    contract_info,
149                    deposit_on_main_account: true,
150                })
151            }
152        }
153    }
154
155    pub fn get_deposit_account(&self) -> &C::AccountId {
156        &self.deposit_account
157    }
158
159    /// Convert `ContractInfoRaw` to `ContractInfo`
160    pub fn into_contract_info(
161        self,
162        deposit: AccountData<E::Balance>,
163    ) -> ContractInfo<C::Hash, E::Balance> {
164        let total_deposit = if self.deposit_on_main_account {
165            deposit.reserved
166        } else {
167            deposit.free
168        };
169
170        ContractInfo {
171            trie_id: self.contract_info.trie_id.0.into(),
172            code_hash: self.contract_info.code_hash,
173            storage_items: self.contract_info.storage_items,
174            storage_items_deposit: self.contract_info.storage_item_deposit,
175            storage_total_deposit: total_deposit,
176        }
177    }
178
179    /// Decode the deposit account from the contract info
180    fn get_deposit_account_id(contract_info: &DecodedValueThunk) -> Result<C::AccountId> {
181        let account = contract_info.as_type::<DepositAccount<C::AccountId>>()?;
182        Ok(account.deposit_account)
183    }
184}
185
186#[derive(Debug, PartialEq, serde::Serialize)]
187pub struct ContractInfo<Hash, Balance> {
188    trie_id: TrieId,
189    code_hash: Hash,
190    storage_items: u32,
191    storage_items_deposit: Balance,
192    storage_total_deposit: Balance,
193}
194
195impl<Hash, Balance> ContractInfo<Hash, Balance>
196where
197    Hash: serde::Serialize,
198    Balance: serde::Serialize + Copy,
199{
200    /// Convert and return contract info in JSON format.
201    pub fn to_json(&self) -> Result<String> {
202        Ok(serde_json::to_string_pretty(self)?)
203    }
204
205    /// Return the trie_id of the contract.
206    pub fn trie_id(&self) -> &TrieId {
207        &self.trie_id
208    }
209
210    /// Return the code_hash of the contract.
211    pub fn code_hash(&self) -> &Hash {
212        &self.code_hash
213    }
214
215    /// Return the number of storage items of the contract.
216    pub fn storage_items(&self) -> u32 {
217        self.storage_items
218    }
219
220    /// Return the storage item deposit of the contract.
221    pub fn storage_items_deposit(&self) -> Balance {
222        self.storage_items_deposit
223    }
224
225    /// Return the storage item deposit of the contract.
226    pub fn storage_total_deposit(&self) -> Balance {
227        self.storage_total_deposit
228    }
229}
230
231/// A contract's child trie id.
232#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
233pub struct TrieId(#[serde(serialize_with = "serialize_as_byte_str")] Vec<u8>);
234
235impl TrieId {
236    /// Encode the trie id as hex string.
237    pub fn to_hex(&self) -> String {
238        format!("0x{}", hex::encode(&self.0))
239    }
240}
241
242impl From<Vec<u8>> for TrieId {
243    fn from(raw: Vec<u8>) -> Self {
244        Self(raw)
245    }
246}
247
248impl AsRef<[u8]> for TrieId {
249    fn as_ref(&self) -> &[u8] {
250        &self.0
251    }
252}
253
254impl Display for TrieId {
255    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
256        write!(f, "{}", self.to_hex())
257    }
258}
259
260/// Fetch the contract wasm code from the storage using the provided client and code hash.
261pub async fn fetch_wasm_code<C: Config>(
262    client: &OnlineClient<C>,
263    rpc: &LegacyRpcMethods<C>,
264    hash: &C::Hash,
265) -> Result<Vec<u8>>
266where
267    C::Hash: AsRef<[u8]> + Display + IntoVisitor,
268{
269    let best_block = get_best_block(rpc).await?;
270
271    let pristine_code_address =
272        dynamic("Contracts", "PristineCode", vec![Value::from_bytes(hash)]);
273    let pristine_code = client
274        .storage()
275        .at(best_block)
276        .fetch(&pristine_code_address)
277        .await?
278        .ok_or_else(|| anyhow!("No WASM code was found for code hash {}", hash))?;
279    let pristine_code = pristine_code
280        .as_type::<BoundedVec<u8>>()
281        .map_err(|e| anyhow!("Contract wasm code could not be parsed: {e}"));
282    pristine_code.map(|v| v.0)
283}
284
285/// Parse a contract account address from a storage key. Returns error if a key is
286/// malformated.
287fn parse_contract_account_address<C: Config>(
288    storage_contract_account_key: &[u8],
289    storage_contract_root_key_len: usize,
290) -> Result<C::AccountId>
291where
292    C::AccountId: Decode,
293{
294    // storage_contract_account_key is a concatenation of contract_info_of root key and
295    // Twox64Concat(AccountId)
296    let mut account = storage_contract_account_key
297        .get(storage_contract_root_key_len + 8..)
298        .ok_or(anyhow!("Unexpected storage key size"))?;
299    Decode::decode(&mut account)
300        .map_err(|err| anyhow!("AccountId deserialization error: {}", err))
301}
302
303/// Fetch all contract addresses from the storage using the provided client.
304pub async fn fetch_all_contracts<C: Config>(
305    client: &OnlineClient<C>,
306    rpc: &LegacyRpcMethods<C>,
307) -> Result<Vec<C::AccountId>>
308where
309    C::AccountId: Decode,
310{
311    let best_block = get_best_block(rpc).await?;
312    let root_key =
313        subxt::dynamic::storage("Contracts", "ContractInfoOf", ()).to_root_bytes();
314    let mut keys = client
315        .storage()
316        .at(best_block)
317        .fetch_raw_keys(root_key.clone())
318        .await?;
319
320    let mut contract_accounts = Vec::new();
321    while let Some(result) = keys.next().await {
322        let key = result?;
323        let contract_account = parse_contract_account_address::<C>(&key, root_key.len())?;
324        contract_accounts.push(contract_account);
325    }
326
327    Ok(contract_accounts)
328}
329
330/// A struct used in the storage reads to access account info.
331#[derive(DecodeAsType, Debug)]
332#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
333struct AccountInfo<Balance> {
334    data: AccountData<Balance>,
335}
336
337/// A struct used in the storage reads to access account data.
338#[derive(Clone, Debug, DecodeAsType)]
339#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
340struct AccountData<Balance> {
341    free: Balance,
342    reserved: Balance,
343}
344
345/// A struct representing `Vec`` used in the storage reads.
346#[derive(Debug, DecodeAsType)]
347#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
348struct BoundedVec<T>(pub ::std::vec::Vec<T>);
349
350/// A struct used in the storage reads to access contract info.
351#[derive(Debug, DecodeAsType)]
352#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
353struct ContractInfoOf<Hash, Balance> {
354    trie_id: BoundedVec<u8>,
355    code_hash: Hash,
356    storage_items: u32,
357    storage_item_deposit: Balance,
358}
359
360/// A struct used in storage reads to access the deposit account from contract info.
361#[derive(Debug, DecodeAsType)]
362#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
363struct DepositAccount<AccountId> {
364    deposit_account: AccountId,
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use ink_env::DefaultEnvironment;
371    use scale::Encode;
372    use scale_info::{
373        IntoPortable,
374        Path,
375    };
376    use subxt::{
377        metadata::{
378            types::Metadata,
379            DecodeWithMetadata,
380        },
381        utils::AccountId32,
382        PolkadotConfig as DefaultConfig,
383    };
384
385    // Find the type index in the metadata.
386    fn get_metadata_type_index(
387        ident: &'static str,
388        module_path: &'static str,
389        metadata: &Metadata,
390    ) -> Result<usize> {
391        let contract_info_path =
392            Path::new(ident, module_path).into_portable(&mut Default::default());
393
394        metadata
395            .types()
396            .types
397            .iter()
398            .enumerate()
399            .find_map(|(i, t)| {
400                if t.ty.path == contract_info_path {
401                    Some(i)
402                } else {
403                    None
404                }
405            })
406            .ok_or(anyhow!("Type not found"))
407    }
408
409    #[test]
410    fn contract_info_v15_decode_works() {
411        // This version of metadata does not include the deposit_account field in
412        // ContractInfo
413        #[subxt::subxt(runtime_metadata_path = "src/test_runtime_api/metadata_v15.scale")]
414        mod api_v15 {}
415
416        use api_v15::runtime_types::{
417            bounded_collections::{
418                bounded_btree_map::BoundedBTreeMap,
419                bounded_vec::BoundedVec,
420            },
421            pallet_contracts::storage::ContractInfo as ContractInfoV15,
422        };
423
424        let metadata_bytes = std::fs::read("src/test_runtime_api/metadata_v15.scale")
425            .expect("the metadata must be present");
426        let metadata =
427            Metadata::decode(&mut &*metadata_bytes).expect("the metadata must decode");
428        let contract_info_type_id = get_metadata_type_index(
429            "ContractInfo",
430            "pallet_contracts::storage",
431            &metadata,
432        )
433        .expect("the contract info type must be present in the metadata");
434
435        let contract_info_v15 = ContractInfoV15 {
436            trie_id: BoundedVec(vec![]),
437            code_hash: Default::default(),
438            storage_bytes: 1,
439            storage_items: 1,
440            storage_byte_deposit: 1,
441            storage_item_deposit: 1,
442            storage_base_deposit: 1,
443            delegate_dependencies: BoundedBTreeMap(vec![]),
444        };
445
446        let contract_info_thunk = DecodedValueThunk::decode_with_metadata(
447            &mut &*contract_info_v15.encode(),
448            contract_info_type_id as u32,
449            &metadata.into(),
450        )
451        .expect("the contract info must be decoded");
452
453        let contract = AccountId32([0u8; 32]);
454        let contract_info_raw =
455            ContractInfoRaw::<DefaultConfig, DefaultEnvironment>::new(
456                contract,
457                contract_info_thunk,
458            )
459            .expect("the conatract info raw must be created");
460        let account_data = AccountData {
461            free: 1,
462            reserved: 10,
463        };
464
465        let contract_info = contract_info_raw.into_contract_info(account_data.clone());
466        assert_eq!(
467            contract_info,
468            ContractInfo {
469                trie_id: contract_info_v15.trie_id.0.into(),
470                code_hash: contract_info_v15.code_hash,
471                storage_items: contract_info_v15.storage_items,
472                storage_items_deposit: contract_info_v15.storage_item_deposit,
473                storage_total_deposit: account_data.reserved,
474            }
475        );
476    }
477}