diff --git a/Cargo.lock b/Cargo.lock index 3e01a9c8622..bda8e9987c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4671,9 +4671,11 @@ dependencies = [ "gsdk", "gsdk-codegen", "hex", + "indexmap 2.0.0", "jsonrpsee", "log", "parity-scale-codec", + "parking_lot 0.12.1", "rand 0.8.5", "scale-decode", "scale-value", diff --git a/gclient/src/api/calls.rs b/gclient/src/api/calls.rs index 69e93065b9e..5932d0e0acf 100644 --- a/gclient/src/api/calls.rs +++ b/gclient/src/api/calls.rs @@ -210,7 +210,7 @@ impl GearApi { let amount = calls.len(); - let tx = self.0.force_batch(calls).await?; + let tx = self.0.calls.force_batch(calls).await?; let mut res = Vec::with_capacity(amount); for event in tx.wait_for_success().await?.iter() { @@ -677,7 +677,7 @@ impl GearApi { let amount = calls.len(); - let tx = self.0.force_batch(calls).await?; + let tx = self.0.calls.force_batch(calls).await?; let mut res = Vec::with_capacity(amount); for event in tx.wait_for_success().await?.iter() { @@ -773,7 +773,7 @@ impl GearApi { let amount = calls.len(); - let tx = self.0.force_batch(calls).await?; + let tx = self.0.calls.force_batch(calls).await?; let mut res = Vec::with_capacity(amount); for event in tx.wait_for_success().await?.iter() { @@ -913,7 +913,7 @@ impl GearApi { let amount = calls.len(); - let tx = self.0.force_batch(calls).await?; + let tx = self.0.calls.force_batch(calls).await?; let mut res = Vec::with_capacity(amount); for event in tx.wait_for_success().await?.iter() { @@ -1012,7 +1012,7 @@ impl GearApi { let amount = calls.len(); - let tx = self.0.force_batch(calls).await?; + let tx = self.0.calls.force_batch(calls).await?; let mut res = Vec::with_capacity(amount); for event in tx.wait_for_success().await?.iter() { @@ -1143,7 +1143,7 @@ impl GearApi { let amount = calls.len(); - let tx = self.0.force_batch(calls).await?; + let tx = self.0.calls.force_batch(calls).await?; let mut res = Vec::with_capacity(amount); for event in tx.wait_for_success().await?.iter() { @@ -1251,6 +1251,7 @@ impl GearApi { pub async fn set_code(&self, code: impl AsRef<[u8]>) -> Result { let events = self .0 + .calls .sudo_unchecked_weight( RuntimeCall::System(SystemCall::set_code { code: code.as_ref().to_vec(), @@ -1283,6 +1284,7 @@ impl GearApi { pub async fn set_code_without_checks(&self, code: impl AsRef<[u8]>) -> Result { let events = self .0 + .calls .sudo_unchecked_weight( RuntimeCall::System(SystemCall::set_code_without_checks { code: code.as_ref().to_vec(), @@ -1318,6 +1320,7 @@ impl GearApi { ) -> Result { let events = self .0 + .calls .sudo_unchecked_weight( RuntimeCall::Balances(BalancesCall::set_balance { who: to.into().convert(), diff --git a/gsdk/Cargo.toml b/gsdk/Cargo.toml index 34946780c3d..9ecf26935dc 100644 --- a/gsdk/Cargo.toml +++ b/gsdk/Cargo.toml @@ -18,6 +18,7 @@ futures.workspace = true gear-core = { workspace = true, features = [ "std" ] } gear-core-errors.workspace = true hex.workspace = true +indexmap.workspace = true jsonrpsee = { workspace = true, features = [ "http-client", "ws-client" ] } log.workspace = true scale-value.workspace = true @@ -28,6 +29,7 @@ thiserror.workspace = true sp-runtime = { workspace = true, features = [ "std" ] } sp-core.workspace = true gsdk-codegen = { path = "codegen" } +parking_lot.workspace = true # Importing these two libraries for trimming # the the size of the generated file. diff --git a/gsdk/src/backtrace.rs b/gsdk/src/backtrace.rs new file mode 100644 index 00000000000..0478eb07ee8 --- /dev/null +++ b/gsdk/src/backtrace.rs @@ -0,0 +1,101 @@ +// This file is part of Gear. +// +// Copyright (C) 2021-2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +//! Backtrace support for `gsdk` + +use crate::TxStatus; +use indexmap::IndexMap; +use parking_lot::Mutex; +use sp_core::H256; +use std::{collections::BTreeMap, sync::Arc, time::SystemTime}; + +/// Transaction Status for Backtrace +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BacktraceStatus { + Future, + Ready, + Broadcast(Vec), + InBlock { + block_hash: H256, + extrinsic_hash: H256, + }, + Retracted { + block_hash: H256, + }, + FinalityTimeout { + block_hash: H256, + }, + Finalized { + block_hash: H256, + extrinsic_hash: H256, + }, + Usurped { + extrinsic_hash: H256, + }, + Dropped, + Invalid, +} + +impl<'s> From<&'s TxStatus> for BacktraceStatus { + fn from(status: &'s TxStatus) -> BacktraceStatus { + match status { + TxStatus::Future => BacktraceStatus::Future, + TxStatus::Ready => BacktraceStatus::Ready, + TxStatus::Broadcast(v) => BacktraceStatus::Broadcast(v.clone()), + TxStatus::InBlock(b) => BacktraceStatus::InBlock { + block_hash: b.block_hash(), + extrinsic_hash: b.extrinsic_hash(), + }, + TxStatus::Retracted(h) => BacktraceStatus::Retracted { block_hash: *h }, + TxStatus::FinalityTimeout(h) => BacktraceStatus::FinalityTimeout { block_hash: *h }, + TxStatus::Finalized(b) => BacktraceStatus::Finalized { + block_hash: b.block_hash(), + extrinsic_hash: b.extrinsic_hash(), + }, + TxStatus::Usurped(h) => BacktraceStatus::Usurped { extrinsic_hash: *h }, + TxStatus::Dropped => BacktraceStatus::Dropped, + TxStatus::Invalid => BacktraceStatus::Invalid, + } + } +} + +/// Backtrace support for transactions +#[derive(Clone, Debug, Default)] +pub struct Backtrace { + inner: Arc>>>, +} + +impl Backtrace { + /// Append status to transaction + pub fn append(&self, tx: H256, status: impl Into) { + let mut inner = self.inner.lock(); + + if let Some(map) = inner.get_mut(&tx) { + map.insert(SystemTime::now(), status.into()); + } else { + let mut map: BTreeMap = Default::default(); + map.insert(SystemTime::now(), status.into()); + inner.insert(tx, map); + }; + } + + /// Get backtrace of transaction + pub fn get(&self, tx: H256) -> Option> { + self.inner.lock().get(&tx).cloned() + } +} diff --git a/gsdk/src/lib.rs b/gsdk/src/lib.rs index ea229d6adf6..3eff02f20a5 100644 --- a/gsdk/src/lib.rs +++ b/gsdk/src/lib.rs @@ -43,6 +43,7 @@ use subxt::{ }; mod api; +pub mod backtrace; mod client; pub mod config; mod constants; diff --git a/gsdk/src/signer/calls.rs b/gsdk/src/signer/calls.rs index 2dc3d2bece5..3ab66b71e3c 100644 --- a/gsdk/src/signer/calls.rs +++ b/gsdk/src/signer/calls.rs @@ -17,56 +17,26 @@ // along with this program. If not, see . //! gear api calls -use super::SignerInner; +use super::Inner; use crate::{ config::GearConfig, metadata::{ calls::{BalancesCall, GearCall, SudoCall, UtilityCall}, gear_runtime::RuntimeCall, - runtime_types::{ - frame_system::pallet::Call, - gear_common::{ActiveProgram, Program}, - gear_core::code::InstrumentedCode, - pallet_gear_bank::pallet::BankAccount, - sp_weights::weight_v2::Weight, - }, - storage::{GearBankStorage, GearGasStorage, GearProgramStorage}, - sudo::Event as SudoEvent, - Event, + runtime_types::sp_weights::weight_v2::Weight, }, - signer::SignerRpc, - utils::storage_address_bytes, - Api, BlockNumber, Error, GearGasNode, GearGasNodeId, GearPages, Result, TxInBlock, TxStatus, + Error, Result, TxInBlock, }; -use anyhow::anyhow; -use gear_core::{ - ids::*, - memory::{PageBuf, PageBufInner}, -}; -use hex::ToHex; -use parity_scale_codec::Encode; +use gear_core::ids::*; use sp_runtime::AccountId32; use std::sync::Arc; -use subxt::{ - blocks::ExtrinsicEvents, - dynamic::Value, - metadata::EncodeWithMetadata, - storage::StorageAddress, - tx::{DynamicPayload, TxProgress}, - utils::Static, - Error as SubxtError, OnlineClient, -}; +use subxt::{blocks::ExtrinsicEvents, dynamic::Value}; -type TxProgressT = TxProgress>; type EventsResult = Result, Error>; /// Implementation of calls to programs/other users for [`Signer`]. #[derive(Clone)] -pub struct SignerCalls(pub(crate) Arc); - -/// Implementation of storage calls for [`Signer`]. -#[derive(Clone)] -pub struct SignerStorage(pub(crate) Arc); +pub struct SignerCalls(pub(crate) Arc); // pallet-balances impl SignerCalls { @@ -194,257 +164,34 @@ impl SignerCalls { } // pallet-utility -impl SignerInner { +impl SignerCalls { /// `pallet_utility::force_batch` pub async fn force_batch(&self, calls: Vec) -> Result { - self.run_tx( - UtilityCall::ForceBatch, - vec![calls.into_iter().map(Value::from).collect::>()], - ) - .await + self.0 + .run_tx( + UtilityCall::ForceBatch, + vec![calls.into_iter().map(Value::from).collect::>()], + ) + .await } } // pallet-sudo -impl SignerInner { - pub async fn process_sudo(&self, tx: DynamicPayload) -> EventsResult { - let tx = self.process(tx).await?; - let events = tx.wait_for_success().await?; - for event in events.iter() { - let event = event?.as_root_event::()?; - if let Event::Sudo(SudoEvent::Sudid { - sudo_result: Err(err), - }) = event - { - return Err(self.api().decode_error(err).into()); - } - } - - Ok(events) - } - +impl SignerCalls { /// `pallet_sudo::sudo_unchecked_weight` pub async fn sudo_unchecked_weight(&self, call: RuntimeCall, weight: Weight) -> EventsResult { - self.sudo_run_tx( - SudoCall::SudoUncheckedWeight, - // As `call` implements conversion to `Value`. - vec![ - call.into(), - Value::named_composite([ - ("ref_time", Value::u128(weight.ref_time as u128)), - ("proof_size", Value::u128(weight.proof_size as u128)), - ]), - ], - ) - .await - } - - /// `pallet_sudo::sudo` - pub async fn sudo(&self, call: RuntimeCall) -> EventsResult { - self.sudo_run_tx(SudoCall::Sudo, vec![Value::from(call)]) - .await - } -} - -// pallet-system -impl SignerStorage { - /// Sets storage values via calling sudo pallet - pub async fn set_storage(&self, items: &[(impl StorageAddress, impl Encode)]) -> EventsResult { - let metadata = self.0.api().metadata(); - let mut items_to_set = Vec::with_capacity(items.len()); - for item in items { - let item_key = storage_address_bytes(&item.0, &metadata)?; - let mut item_value_bytes = Vec::new(); - let item_value_type_id = crate::storage::storage_type_id(&metadata, &item.0)?; - Static(&item.1).encode_with_metadata( - item_value_type_id, - &metadata, - &mut item_value_bytes, - )?; - items_to_set.push((item_key, item_value_bytes)); - } - self.0 - .sudo(RuntimeCall::System(Call::set_storage { - items: items_to_set, - })) - .await - } -} - -// pallet-gas -impl SignerStorage { - /// Writes gas total issuance into storage. - pub async fn set_total_issuance(&self, value: u64) -> EventsResult { - let addr = Api::storage_root(GearGasStorage::TotalIssuance); - self.set_storage(&[(addr, value)]).await - } - - /// Writes Gear gas nodes into storage at their ids. - pub async fn set_gas_nodes( - &self, - gas_nodes: &impl AsRef<[(GearGasNodeId, GearGasNode)]>, - ) -> EventsResult { - let gas_nodes = gas_nodes.as_ref(); - let mut gas_nodes_to_set = Vec::with_capacity(gas_nodes.len()); - for gas_node in gas_nodes { - let addr = Api::storage(GearGasStorage::GasNodes, vec![Static(gas_node.0)]); - gas_nodes_to_set.push((addr, &gas_node.1)); - } - self.set_storage(&gas_nodes_to_set).await - } -} - -// pallet-gear-bank -impl SignerStorage { - /// Writes given BankAccount info into storage at `AccountId32`. - pub async fn set_bank_account_storage( - &self, - dest: impl Into, - value: BankAccount, - ) -> EventsResult { - let addr = Api::storage(GearBankStorage::Bank, vec![Value::from_bytes(dest.into())]); - self.set_storage(&[(addr, value)]).await - } -} - -// pallet-gear-program -impl SignerStorage { - /// Writes `InstrumentedCode` length into storage at `CodeId` - pub async fn set_code_len_storage(&self, code_id: CodeId, code_len: u32) -> EventsResult { - let addr = Api::storage( - GearProgramStorage::CodeLenStorage, - vec![Value::from_bytes(code_id)], - ); - self.set_storage(&[(addr, code_len)]).await - } - - /// Writes `InstrumentedCode` into storage at `CodeId` - pub async fn set_code_storage(&self, code_id: CodeId, code: &InstrumentedCode) -> EventsResult { - let addr = Api::storage( - GearProgramStorage::CodeStorage, - vec![Value::from_bytes(code_id)], - ); - self.set_storage(&[(addr, code)]).await - } - - /// Writes `GearPages` into storage at `program_id` - pub async fn set_gpages( - &self, - program_id: ProgramId, - program_pages: &GearPages, - ) -> EventsResult { - let mut program_pages_to_set = Vec::with_capacity(program_pages.len()); - for program_page in program_pages { - let addr = Api::storage( - GearProgramStorage::MemoryPageStorage, + .sudo_run_tx( + SudoCall::SudoUncheckedWeight, + // As `call` implements conversion to `Value`. vec![ - subxt::dynamic::Value::from_bytes(program_id), - subxt::dynamic::Value::u128(*program_page.0 as u128), + call.into(), + Value::named_composite([ + ("ref_time", Value::u128(weight.ref_time as u128)), + ("proof_size", Value::u128(weight.proof_size as u128)), + ]), ], - ); - let page_buf_inner = PageBufInner::try_from(program_page.1.clone()) - .map_err(|_| Error::PageInvalid(*program_page.0, program_id.encode_hex()))?; - let value = PageBuf::from_inner(page_buf_inner); - program_pages_to_set.push((addr, value)); - } - self.set_storage(&program_pages_to_set).await - } - - /// Writes `ActiveProgram` into storage at `program_id` - pub async fn set_gprog( - &self, - program_id: ProgramId, - program: ActiveProgram, - ) -> EventsResult { - let addr = Api::storage( - GearProgramStorage::ProgramStorage, - vec![Value::from_bytes(program_id)], - ); - self.set_storage(&[(addr, &Program::Active(program))]).await - } -} - -// Singer utils -impl SignerInner { - /// Propagates log::info for given status. - pub(crate) fn log_status(&self, status: &TxStatus) { - match status { - TxStatus::Future => log::info!(" Status: Future"), - TxStatus::Ready => log::info!(" Status: Ready"), - TxStatus::Broadcast(v) => log::info!(" Status: Broadcast( {v:?} )"), - TxStatus::InBlock(b) => log::info!( - " Status: InBlock( block hash: {}, extrinsic hash: {} )", - b.block_hash(), - b.extrinsic_hash() - ), - TxStatus::Retracted(h) => log::warn!(" Status: Retracted( {h} )"), - TxStatus::FinalityTimeout(h) => log::error!(" Status: FinalityTimeout( {h} )"), - TxStatus::Finalized(b) => log::info!( - " Status: Finalized( block hash: {}, extrinsic hash: {} )", - b.block_hash(), - b.extrinsic_hash() - ), - TxStatus::Usurped(h) => log::error!(" Status: Usurped( {h} )"), - TxStatus::Dropped => log::error!(" Status: Dropped"), - TxStatus::Invalid => log::error!(" Status: Invalid"), - } - } - - /// Wrapper for submit and watch with nonce. - async fn sign_and_submit_then_watch<'a>( - &self, - tx: &DynamicPayload, - ) -> Result { - if let Some(nonce) = self.nonce { - self.api - .tx() - .create_signed_with_nonce(tx, &self.signer, nonce, Default::default())? - .submit_and_watch() - .await - } else { - self.api - .tx() - .sign_and_submit_then_watch_default(tx, &self.signer) - .await - } - } - - /// Listen transaction process and print logs. - pub async fn process<'a>(&self, tx: DynamicPayload) -> Result { - use subxt::tx::TxStatus::*; - - let signer_rpc = SignerRpc(Arc::new(self.clone())); - let before = signer_rpc.get_balance().await?; - - let mut process = self.sign_and_submit_then_watch(&tx).await?; - let (pallet, name) = (tx.pallet_name(), tx.call_name()); - - log::info!("Submitted extrinsic {}::{}", pallet, name); - - while let Some(status) = process.next_item().await { - let status = status?; - self.log_status(&status); - match status { - Future | Ready | Broadcast(_) | InBlock(_) | Retracted(_) => (), - Finalized(b) => { - log::info!( - "Successfully submitted call {}::{} {} at {}!", - pallet, - name, - b.extrinsic_hash(), - b.block_hash() - ); - self.log_balance_spent(before).await?; - return Ok(b); - } - _ => { - self.log_balance_spent(before).await?; - return Err(status.into()); - } - } - } - - Err(anyhow!("Transaction wasn't found").into()) + ) + .await } } diff --git a/gsdk/src/signer/mod.rs b/gsdk/src/signer/mod.rs index d8e1f1161b5..e1136977ba8 100644 --- a/gsdk/src/signer/mod.rs +++ b/gsdk/src/signer/mod.rs @@ -19,31 +19,34 @@ //! Gear api with signer use crate::{ + backtrace::Backtrace, config::GearConfig, result::{Error, Result}, Api, }; -use calls::{SignerCalls, SignerStorage}; +use calls::SignerCalls; use core::ops::Deref; pub use pair_signer::PairSigner; use rpc::SignerRpc; use sp_core::{crypto::Ss58Codec, sr25519::Pair, Pair as PairT}; use sp_runtime::AccountId32; use std::sync::Arc; +use storage::SignerStorage; mod calls; mod pair_signer; mod rpc; +mod storage; mod utils; /// Signer representation that provides access to gear API. -/// Implements low-level methods such as [`run_tx`](`SignerInner::run_tx`) -/// and [`force_batch`](`SignerInner::force_batch`). +/// Implements low-level methods such as [`run_tx`](`Inner::run_tx`) +/// and [`force_batch`](`Signer.calls()::force_batch`). /// Other higher-level calls are provided by [`Signer::storage`], /// [`Signer::calls`], [`Signer::rpc`]. #[derive(Clone)] pub struct Signer { - signer: Arc, + signer: Arc, /// Calls that get or set storage. pub storage: SignerStorage, /// Calls for interaction with on-chain programs. @@ -54,28 +57,35 @@ pub struct Signer { /// Implementation of low-level calls for [`Signer`]. #[derive(Clone)] -pub struct SignerInner { +pub struct Inner { api: Api, /// Current signer. signer: PairSigner, nonce: Option, + backtrace: Backtrace, } impl Signer { + /// Get backtrace of the signer. + pub fn backtrace(&self) -> Backtrace { + self.calls.0.backtrace.clone() + } + /// New signer api. pub fn new(api: Api, suri: &str, passwd: Option<&str>) -> Result { - let signer = SignerInner { + let signer = Inner { api, signer: PairSigner::new( Pair::from_string(suri, passwd).map_err(|_| Error::InvalidSecret)?, ), nonce: None, + backtrace: Default::default(), }; Ok(Self::from_inner(signer)) } - fn from_inner(signer: SignerInner) -> Self { + fn from_inner(signer: Inner) -> Self { let signer = Arc::new(signer); Self { @@ -86,8 +96,10 @@ impl Signer { } } - #[deny(unused_variables)] - fn replace_inner(&mut self, inner: SignerInner) { + fn replace_inner(&mut self, mut inner: Inner) { + let backtrace = self.backtrace(); + inner.backtrace = backtrace; + let Signer { signer, storage, @@ -106,7 +118,7 @@ impl Signer { let signer = PairSigner::new(Pair::from_string(suri, passwd).map_err(|_| Error::InvalidSecret)?); - self.replace_inner(SignerInner { + self.replace_inner(Inner { signer, ..self.signer.as_ref().clone() }); @@ -116,14 +128,14 @@ impl Signer { /// Set nonce of the signer pub fn set_nonce(&mut self, nonce: u32) { - self.replace_inner(SignerInner { + self.replace_inner(Inner { nonce: Some(nonce), ..self.signer.as_ref().clone() }); } } -impl SignerInner { +impl Inner { /// Get address of the current signer pub fn address(&self) -> String { self.account_id().to_ss58check() @@ -142,10 +154,11 @@ impl SignerInner { impl From<(Api, PairSigner)> for Signer { fn from((api, signer): (Api, PairSigner)) -> Self { - let signer = SignerInner { + let signer = Inner { api, signer, nonce: None, + backtrace: Backtrace::default(), }; Self::from_inner(signer) @@ -153,9 +166,9 @@ impl From<(Api, PairSigner)> for Signer { } impl Deref for Signer { - type Target = SignerInner; + type Target = Inner; - fn deref(&self) -> &SignerInner { + fn deref(&self) -> &Inner { self.signer.as_ref() } } diff --git a/gsdk/src/signer/rpc.rs b/gsdk/src/signer/rpc.rs index 912531fd809..4a9e5413ba5 100644 --- a/gsdk/src/signer/rpc.rs +++ b/gsdk/src/signer/rpc.rs @@ -18,14 +18,14 @@ //! RPC calls with signer -use crate::{result::Result, signer::SignerInner, GasInfo}; +use crate::{result::Result, signer::Inner, GasInfo}; use gear_core::ids::{CodeId, MessageId, ProgramId}; use sp_core::H256; use std::sync::Arc; /// Implementation of calls to node RPC for [`Signer`]. #[derive(Clone)] -pub struct SignerRpc(pub(crate) Arc); +pub struct SignerRpc(pub(crate) Arc); impl SignerRpc { /// public key of the signer in H256 diff --git a/gsdk/src/signer/storage.rs b/gsdk/src/signer/storage.rs new file mode 100644 index 00000000000..25628995559 --- /dev/null +++ b/gsdk/src/signer/storage.rs @@ -0,0 +1,172 @@ +// This file is part of Gear. +// +// Copyright (C) 2021-2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Storage interfaces +use crate::{ + config::GearConfig, + metadata::{ + gear_runtime::RuntimeCall, + runtime_types::{ + frame_system::pallet::Call, + gear_common::{ActiveProgram, Program}, + gear_core::code::InstrumentedCode, + pallet_gear_bank::pallet::BankAccount, + }, + storage::{GearBankStorage, GearGasStorage, GearProgramStorage}, + }, + signer::Inner, + utils::storage_address_bytes, + Api, BlockNumber, Error, GearGasNode, GearGasNodeId, GearPages, Result, +}; +use gear_core::{ + ids::*, + memory::{PageBuf, PageBufInner}, +}; +use hex::ToHex; +use parity_scale_codec::Encode; +use sp_runtime::AccountId32; +use std::sync::Arc; +use subxt::{ + blocks::ExtrinsicEvents, dynamic::Value, metadata::EncodeWithMetadata, storage::StorageAddress, + utils::Static, +}; + +type EventsResult = Result, Error>; + +/// Implementation of storage calls for [`Signer`]. +#[derive(Clone)] +pub struct SignerStorage(pub(crate) Arc); + +// pallet-system +impl SignerStorage { + /// Sets storage values via calling sudo pallet + pub async fn set_storage(&self, items: &[(impl StorageAddress, impl Encode)]) -> EventsResult { + let metadata = self.0.api().metadata(); + let mut items_to_set = Vec::with_capacity(items.len()); + for item in items { + let item_key = storage_address_bytes(&item.0, &metadata)?; + let mut item_value_bytes = Vec::new(); + let item_value_type_id = crate::storage::storage_type_id(&metadata, &item.0)?; + Static(&item.1).encode_with_metadata( + item_value_type_id, + &metadata, + &mut item_value_bytes, + )?; + items_to_set.push((item_key, item_value_bytes)); + } + + self.0 + .sudo(RuntimeCall::System(Call::set_storage { + items: items_to_set, + })) + .await + } +} + +// pallet-gas +impl SignerStorage { + /// Writes gas total issuance into storage. + pub async fn set_total_issuance(&self, value: u64) -> EventsResult { + let addr = Api::storage_root(GearGasStorage::TotalIssuance); + self.set_storage(&[(addr, value)]).await + } + + /// Writes Gear gas nodes into storage at their ids. + pub async fn set_gas_nodes( + &self, + gas_nodes: &impl AsRef<[(GearGasNodeId, GearGasNode)]>, + ) -> EventsResult { + let gas_nodes = gas_nodes.as_ref(); + let mut gas_nodes_to_set = Vec::with_capacity(gas_nodes.len()); + for gas_node in gas_nodes { + let addr = Api::storage(GearGasStorage::GasNodes, vec![Static(gas_node.0)]); + gas_nodes_to_set.push((addr, &gas_node.1)); + } + self.set_storage(&gas_nodes_to_set).await + } +} + +// pallet-gear-bank +impl SignerStorage { + /// Writes given BankAccount info into storage at `AccountId32`. + pub async fn set_bank_account_storage( + &self, + dest: impl Into, + value: BankAccount, + ) -> EventsResult { + let addr = Api::storage(GearBankStorage::Bank, vec![Value::from_bytes(dest.into())]); + self.set_storage(&[(addr, value)]).await + } +} + +// pallet-gear-program +impl SignerStorage { + /// Writes `InstrumentedCode` length into storage at `CodeId` + pub async fn set_code_len_storage(&self, code_id: CodeId, code_len: u32) -> EventsResult { + let addr = Api::storage( + GearProgramStorage::CodeLenStorage, + vec![Value::from_bytes(code_id)], + ); + self.set_storage(&[(addr, code_len)]).await + } + + /// Writes `InstrumentedCode` into storage at `CodeId` + pub async fn set_code_storage(&self, code_id: CodeId, code: &InstrumentedCode) -> EventsResult { + let addr = Api::storage( + GearProgramStorage::CodeStorage, + vec![Value::from_bytes(code_id)], + ); + self.set_storage(&[(addr, code)]).await + } + + /// Writes `GearPages` into storage at `program_id` + pub async fn set_gpages( + &self, + program_id: ProgramId, + program_pages: &GearPages, + ) -> EventsResult { + let mut program_pages_to_set = Vec::with_capacity(program_pages.len()); + for program_page in program_pages { + let addr = Api::storage( + GearProgramStorage::MemoryPageStorage, + vec![ + subxt::dynamic::Value::from_bytes(program_id), + subxt::dynamic::Value::u128(*program_page.0 as u128), + ], + ); + let page_buf_inner = PageBufInner::try_from(program_page.1.clone()) + .map_err(|_| Error::PageInvalid(*program_page.0, program_id.encode_hex()))?; + let value = PageBuf::from_inner(page_buf_inner); + program_pages_to_set.push((addr, value)); + } + self.set_storage(&program_pages_to_set).await + } + + /// Writes `ActiveProgram` into storage at `program_id` + pub async fn set_gprog( + &self, + program_id: ProgramId, + program: ActiveProgram, + ) -> EventsResult { + let addr = Api::storage( + GearProgramStorage::ProgramStorage, + vec![Value::from_bytes(program_id)], + ); + self.set_storage(&[(addr, &Program::Active(program))]).await + } +} diff --git a/gsdk/src/signer/utils.rs b/gsdk/src/signer/utils.rs index f4d88a5f4a5..24b56bef50b 100644 --- a/gsdk/src/signer/utils.rs +++ b/gsdk/src/signer/utils.rs @@ -18,18 +18,32 @@ //! Utils -use std::sync::Arc; - -use super::SignerInner; +use super::Inner; use crate::{ - config::GearConfig, metadata::CallInfo, result::Result, signer::SignerRpc, Error, TxInBlock, + backtrace::BacktraceStatus, + config::GearConfig, + metadata::{ + calls::SudoCall, gear_runtime::RuntimeCall, sudo::Event as SudoEvent, CallInfo, Event, + }, + result::Result, + signer::SignerRpc, + Error, TxInBlock, TxStatus, }; +use anyhow::anyhow; use scale_value::Composite; -use subxt::blocks::ExtrinsicEvents; +use sp_core::H256; +use std::sync::Arc; +use subxt::{ + blocks::ExtrinsicEvents, + dynamic::Value, + tx::{DynamicPayload, TxProgress}, + Error as SubxtError, OnlineClient, +}; +type TxProgressT = TxProgress>; type EventsResult = Result, Error>; -impl SignerInner { +impl Inner { /// Logging balance spent pub async fn log_balance_spent(&self, before: u128) -> Result<()> { let signer_rpc = SignerRpc(Arc::new(self.clone())); @@ -39,6 +53,107 @@ impl SignerInner { Ok(()) } + /// Propagates log::info for given status. + pub(crate) fn log_status(status: &TxStatus) { + match status { + TxStatus::Future => log::info!(" Status: Future"), + TxStatus::Ready => log::info!(" Status: Ready"), + TxStatus::Broadcast(v) => log::info!(" Status: Broadcast( {v:?} )"), + TxStatus::InBlock(b) => log::info!( + " Status: InBlock( block hash: {}, extrinsic hash: {} )", + b.block_hash(), + b.extrinsic_hash() + ), + TxStatus::Retracted(h) => log::warn!(" Status: Retracted( {h} )"), + TxStatus::FinalityTimeout(h) => log::error!(" Status: FinalityTimeout( {h} )"), + TxStatus::Finalized(b) => log::info!( + " Status: Finalized( block hash: {}, extrinsic hash: {} )", + b.block_hash(), + b.extrinsic_hash() + ), + TxStatus::Usurped(h) => log::error!(" Status: Usurped( {h} )"), + TxStatus::Dropped => log::error!(" Status: Dropped"), + TxStatus::Invalid => log::error!(" Status: Invalid"), + } + } + + /// Listen transaction process and print logs. + pub async fn process<'a>(&self, tx: DynamicPayload) -> Result { + use subxt::tx::TxStatus::*; + + let signer_rpc = SignerRpc(Arc::new(self.clone())); + let before = signer_rpc.get_balance().await?; + + let mut process = self.sign_and_submit_then_watch(&tx).await?; + let (pallet, name) = (tx.pallet_name(), tx.call_name()); + + log::info!("Submitted extrinsic {}::{}", pallet, name); + + let mut queue: Vec = Default::default(); + let mut hash: Option = None; + + while let Some(status) = process.next_item().await { + let status = status?; + Self::log_status(&status); + + if let Some(h) = &hash { + self.backtrace + .clone() + .append(*h, BacktraceStatus::from(&status)); + } else { + queue.push((&status).into()); + } + + match status { + Future | Ready | Broadcast(_) | Retracted(_) => (), + InBlock(b) => { + hash = Some(b.extrinsic_hash()); + self.backtrace.append( + b.extrinsic_hash(), + BacktraceStatus::InBlock { + block_hash: b.block_hash(), + extrinsic_hash: b.extrinsic_hash(), + }, + ); + } + Finalized(b) => { + log::info!( + "Successfully submitted call {}::{} {} at {}!", + pallet, + name, + b.extrinsic_hash(), + b.block_hash() + ); + self.log_balance_spent(before).await?; + return Ok(b); + } + _ => { + self.log_balance_spent(before).await?; + return Err(status.into()); + } + } + } + + Err(anyhow!("Transaction wasn't found").into()) + } + + /// Process sudo transaction. + pub async fn process_sudo(&self, tx: DynamicPayload) -> EventsResult { + let tx = self.process(tx).await?; + let events = tx.wait_for_success().await?; + for event in events.iter() { + let event = event?.as_root_event::()?; + if let Event::Sudo(SudoEvent::Sudid { + sudo_result: Err(err), + }) = event + { + return Err(self.api().decode_error(err).into()); + } + } + + Ok(events) + } + /// Run transaction. /// /// This function allows us to execute any transactions in gear. @@ -98,4 +213,29 @@ impl SignerInner { self.process_sudo(tx).await } + + /// `pallet_sudo::sudo` + pub async fn sudo(&self, call: RuntimeCall) -> EventsResult { + self.sudo_run_tx(SudoCall::Sudo, vec![Value::from(call)]) + .await + } + + /// Wrapper for submit and watch with nonce. + async fn sign_and_submit_then_watch<'a>( + &self, + tx: &DynamicPayload, + ) -> Result { + if let Some(nonce) = self.nonce { + self.api + .tx() + .create_signed_with_nonce(tx, &self.signer, nonce, Default::default())? + .submit_and_watch() + .await + } else { + self.api + .tx() + .sign_and_submit_then_watch_default(tx, &self.signer) + .await + } + } } diff --git a/gsdk/tests/backtrace.rs b/gsdk/tests/backtrace.rs new file mode 100644 index 00000000000..2907371c0ac --- /dev/null +++ b/gsdk/tests/backtrace.rs @@ -0,0 +1,45 @@ +// This file is part of Gear. +// +// Copyright (C) 2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use gsdk::{backtrace::BacktraceStatus, signer::Signer, Api, Result}; +use utils::{alice_account_id, dev_node, node_uri}; + +mod utils; + +#[tokio::test] +async fn transfer_backtrace() -> Result<()> { + let node = dev_node(); + let api = Api::new(Some(&node_uri(&node))).await?; + let signer = Signer::new(api, "//Alice", None)?; + let alice: [u8; 32] = *alice_account_id().as_ref(); + + let tx = signer.calls.transfer(alice, 42).await?; + let backtrace = signer + .backtrace() + .get(tx.extrinsic_hash()) + .expect("Failed to get backtrace of transfer"); + + assert!(matches!( + backtrace.values().collect::>()[..], + [ + BacktraceStatus::InBlock { .. }, + BacktraceStatus::Finalized { .. }, + ] + )); + Ok(()) +} diff --git a/gsdk/tests/rpc.rs b/gsdk/tests/rpc.rs index 376af7741de..8aef161cd86 100644 --- a/gsdk/tests/rpc.rs +++ b/gsdk/tests/rpc.rs @@ -19,42 +19,14 @@ //! Requires node to be built in release mode use gear_core::ids::{CodeId, ProgramId}; -use gsdk::{ - ext::{sp_core::crypto::Ss58Codec, sp_runtime::AccountId32}, - testing::Node, - Api, Error, Result, -}; +use gsdk::{Api, Error, Result}; use jsonrpsee::types::error::{CallError, ErrorObject}; use parity_scale_codec::Encode; use std::{borrow::Cow, process::Command, str::FromStr}; use subxt::{config::Header, error::RpcError, Error as SubxtError}; +use utils::{alice_account_id, dev_node, node_uri}; -fn dev_node() -> Node { - // Use release build because of performance reasons. - let bin_path = env!("CARGO_MANIFEST_DIR").to_owned() + "/../target/release/gear"; - - #[cfg(not(feature = "vara-testing"))] - let args = vec!["--tmp", "--dev"]; - #[cfg(feature = "vara-testing")] - let args = vec![ - "--tmp", - "--chain=vara-dev", - "--alice", - "--validator", - "--reserved-only", - ]; - - Node::try_from_path(bin_path, args) - .expect("Failed to start node: Maybe it isn't built with --release flag?") -} - -fn node_uri(node: &Node) -> String { - format!("ws://{}", &node.address()) -} - -fn alice_account_id() -> AccountId32 { - AccountId32::from_ss58check("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY").unwrap() -} +mod utils; #[tokio::test] async fn test_calculate_upload_gas() -> Result<()> { diff --git a/gsdk/tests/utils/mod.rs b/gsdk/tests/utils/mod.rs new file mode 100644 index 00000000000..6c0696c2b28 --- /dev/null +++ b/gsdk/tests/utils/mod.rs @@ -0,0 +1,49 @@ +// This file is part of Gear. +// +// Copyright (C) 2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use gsdk::{ + ext::{sp_core::crypto::Ss58Codec, sp_runtime::AccountId32}, + testing::Node, +}; + +pub fn dev_node() -> Node { + // Use release build because of performance reasons. + let bin_path = env!("CARGO_MANIFEST_DIR").to_owned() + "/../target/release/gear"; + + #[cfg(not(feature = "vara-testing"))] + let args = vec!["--tmp", "--dev"]; + #[cfg(feature = "vara-testing")] + let args = vec![ + "--tmp", + "--chain=vara-dev", + "--alice", + "--validator", + "--reserved-only", + ]; + + Node::try_from_path(bin_path, args) + .expect("Failed to start node: Maybe it isn't built with --release flag?") +} + +pub fn node_uri(node: &Node) -> String { + format!("ws://{}", &node.address()) +} + +pub fn alice_account_id() -> AccountId32 { + AccountId32::from_ss58check("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY").unwrap() +}