From 99cc979c425a83aae49ccb18e78b115de9535f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Gawrya=C5=82?= Date: Fri, 26 Apr 2024 13:25:30 +0200 Subject: [PATCH] Cherry-pick --- aleph-client/Cargo.lock | 2 +- aleph-client/Cargo.toml | 2 +- aleph-client/src/contract/event.rs | 6 +- aleph-client/src/contract/mod.rs | 207 +++++++++++--------- aleph-client/src/pallets/contract.rs | 7 +- e2e-tests/Cargo.lock | 2 +- e2e-tests/src/test/adder.rs | 123 +++++++++++- e2e-tests/src/test/button_game/contracts.rs | 82 +++++--- 8 files changed, 302 insertions(+), 129 deletions(-) diff --git a/aleph-client/Cargo.lock b/aleph-client/Cargo.lock index 19548299e3..e6a456593b 100644 --- a/aleph-client/Cargo.lock +++ b/aleph-client/Cargo.lock @@ -106,7 +106,7 @@ dependencies = [ [[package]] name = "aleph_client" -version = "3.7.3" +version = "3.8.0" dependencies = [ "anyhow", "async-trait", diff --git a/aleph-client/Cargo.toml b/aleph-client/Cargo.toml index 02848e9845..6c86c14c4a 100644 --- a/aleph-client/Cargo.toml +++ b/aleph-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aleph_client" -version = "3.7.3" +version = "3.8.0" edition = "2021" authors = ["Cardinal"] documentation = "https://docs.rs/aleph_client" diff --git a/aleph-client/src/contract/event.rs b/aleph-client/src/contract/event.rs index 2577b092ab..2651d294a6 100644 --- a/aleph-client/src/contract/event.rs +++ b/aleph-client/src/contract/event.rs @@ -56,7 +56,7 @@ pub struct ContractEvent { /// # async fn example(conn: Connection, signed_conn: SignedConnection, address: AccountId, path: &str) -> Result<()> { /// let contract = ContractInstance::new(address, path)?; /// -/// let tx_info = contract.contract_exec0(&signed_conn, "some_method").await?; +/// let tx_info = contract.exec0(&signed_conn, "some_method", Default::default()).await?; /// /// println!("Received events {:?}", get_contract_events(&conn, &contract, tx_info).await); /// @@ -110,8 +110,8 @@ pub async fn get_contract_events( /// }; /// let join = tokio::spawn(listen()); /// -/// contract1.contract_exec0(&signed_conn, "some_method").await?; -/// contract2.contract_exec0(&signed_conn, "some_other_method").await?; +/// contract1.exec0(&signed_conn, "some_method", Default::default()).await?; +/// contract2.exec0(&signed_conn, "some_other_method", Default::default()).await?; /// /// println!("Received event {:?}", rx.next().await); /// diff --git a/aleph-client/src/contract/mod.rs b/aleph-client/src/contract/mod.rs index 0930e322ae..c9c1bce412 100644 --- a/aleph-client/src/contract/mod.rs +++ b/aleph-client/src/contract/mod.rs @@ -1,6 +1,6 @@ //! Contains types and functions simplifying common contract-related operations. //! -//! For example, you could write this wrapper around (some of) the functionality of openbrush PSP22 +//! For example, you could write this wrapper around (some of) the functionality of PSP22 //! contracts using the building blocks provided by this module: //! //! ```no_run @@ -25,18 +25,20 @@ //! } //! //! async fn transfer(&self, conn: &SignedConnection, to: AccountId, amount: Balance) -> Result { -//! self.contract.contract_exec( +//! self.contract.exec( //! conn, //! "PSP22::transfer", //! vec![to.to_string().as_str(), amount.to_string().as_str(), "0x00"].as_slice(), +//! Default::default() //! ).await //! } //! //! async fn balance_of(&self, conn: &Connection, account: AccountId) -> Result { -//! self.contract.contract_read( +//! self.contract.read( //! conn, //! "PSP22::balance_of", //! &vec![account.to_string().as_str()], +//! Default::default() //! ).await? //! } //! } @@ -52,27 +54,71 @@ use contract_transcode::ContractMessageTranscoder; pub use convertible_value::ConvertibleValue; use log::info; use pallet_contracts_primitives::ContractExecResult; +use serde::__private::Clone; use crate::{ connections::TxInfo, contract_transcode::Value, pallets::contract::{ContractCallArgs, ContractRpc, ContractsUserApi, EventRecord}, sp_weights::weight_v2::Weight, - AccountId, Balance, ConnectionApi, SignedConnectionApi, TxStatus, + AccountId, Balance, BlockHash, ConnectionApi, SignedConnectionApi, TxStatus, }; -/// Default gas limit, which allows up to 25% of block time (62.5% of the actual block capacity). -pub const DEFAULT_MAX_GAS: u64 = 250_000_000_000u64; -/// Default proof size limit, which allows up to 25% of block time (62.5% of the actual block -/// capacity). -pub const DEFAULT_MAX_PROOF_SIZE: u64 = 250_000_000_000u64; - /// Represents a contract instantiated on the chain. pub struct ContractInstance { address: AccountId, transcoder: ContractMessageTranscoder, - max_gas_override: Option, - max_proof_size_override: Option, +} + +/// Builder for read only contract call +#[derive(Debug, Clone, Default)] +pub struct ReadonlyCallParams { + at: Option, + sender: Option, +} + +impl ReadonlyCallParams { + /// Creates a new instance of `ReadonlyCallParams`. + pub fn new() -> Self { + Default::default() + } + /// Sets the block hash to execute the call at. If not set, by default the latest block is used. + pub fn at(mut self, at: BlockHash) -> Self { + self.at = Some(at); + self + } + + /// Overriders `sender` of the contract call as if it was executed by them. If not set, + /// by default the contract address is used. + pub fn sender(mut self, sender: AccountId) -> Self { + self.sender = Some(sender); + self + } +} + +/// Builder for a contract call that will be submitted to chain +#[derive(Debug, Clone, Default)] +pub struct ExecCallParams { + value: Balance, + max_gas: Option, +} + +impl ExecCallParams { + /// Creates a new instance of `ExecCallParams`. + pub fn new() -> Self { + Default::default() + } + /// Sets the `value` balance to send with the call. + pub fn value(mut self, value: Balance) -> Self { + self.value = value; + self + } + + /// Sets the `gas_limit` in the call. + pub fn gas_limit(mut self, max_gas: Weight) -> Self { + self.max_gas = Some(max_gas); + self + } } impl ContractInstance { @@ -81,42 +127,26 @@ impl ContractInstance { Ok(Self { address, transcoder: ContractMessageTranscoder::load(metadata_path)?, - max_gas_override: None, - max_proof_size_override: None, }) } - /// From now on, the contract instance will use `limit_override` as the gas limit for all - /// contract calls. If `limit_override` is `None`, then [DEFAULT_MAX_GAS] will be used. - pub fn override_gas_limit(&mut self, limit_override: Option) { - self.max_gas_override = limit_override; - } - - /// From now on, the contract instance will use `limit_override` as the proof size limit for all - /// contract calls. If `limit_override` is `None`, then [DEFAULT_MAX_PROOF_SIZE] will be used. - pub fn override_proof_size_limit(&mut self, limit_override: Option) { - self.max_proof_size_override = limit_override; - } - /// The address of this contract instance. pub fn address(&self) -> &AccountId { &self.address } /// Reads the value of a read-only, 0-argument call via RPC. - pub async fn contract_read0< - T: TryFrom, - C: ConnectionApi, - >( + pub async fn read0, C: ConnectionApi>( &self, conn: &C, message: &str, + params: ReadonlyCallParams, ) -> Result { - self.contract_read::(conn, message, &[]).await + self.read::(conn, message, &[], params).await } /// Reads the value of a read-only call via RPC. - pub async fn contract_read< + pub async fn read< S: AsRef + Debug, T: TryFrom, C: ConnectionApi, @@ -125,25 +155,12 @@ impl ContractInstance { conn: &C, message: &str, args: &[S], + params: ReadonlyCallParams, ) -> Result { - self.contract_read_as(conn, message, args, self.address.clone()) - .await - } + let sender = params.sender.unwrap_or(self.address.clone()); - /// Reads the value of a contract call via RPC as if it was executed by `sender`. - pub async fn contract_read_as< - S: AsRef + Debug, - T: TryFrom, - C: ConnectionApi, - >( - &self, - conn: &C, - message: &str, - args: &[S], - sender: AccountId, - ) -> Result { let result = self - .dry_run(conn, message, args, sender, 0) + .dry_run_any(conn, message, args, sender, 0, None, params.at) .await? .result .map_err(|e| anyhow!("Contract exec failed {:?}", e))?; @@ -152,57 +169,42 @@ impl ContractInstance { ConvertibleValue(decoded).try_into()? } - /// Executes a 0-argument contract call. - pub async fn contract_exec0( + /// Executes a 0-argument contract call sending with a given params. + pub async fn exec0( &self, conn: &C, message: &str, + params: ExecCallParams, ) -> Result { - self.contract_exec::(conn, message, &[]).await + self.exec::(conn, message, &[], params).await } /// Executes a contract call. - pub async fn contract_exec + Debug>( + pub async fn exec + Debug>( &self, conn: &C, message: &str, args: &[S], - ) -> Result { - self.contract_exec_value::(conn, message, args, 0) - .await - } - - /// Executes a 0-argument contract call sending the given amount of value with it. - pub async fn contract_exec_value0( - &self, - conn: &C, - message: &str, - value: Balance, - ) -> Result { - self.contract_exec_value::(conn, message, &[], value) - .await - } - - /// Executes a contract call sending the given amount of value with it. - pub async fn contract_exec_value + Debug>( - &self, - conn: &C, - message: &str, - args: &[S], - value: Balance, + params: ExecCallParams, ) -> Result { let dry_run_result = self - .dry_run(conn, message, args, conn.account_id().clone(), value) + .exec_dry_run( + conn, + conn.account_id().clone(), + message, + args, + params.clone(), + ) .await?; let data = self.encode(message, args)?; conn.call( self.address.clone(), - value, - Weight { - ref_time: dry_run_result.gas_required.ref_time(), - proof_size: dry_run_result.gas_required.proof_size(), - }, + params.value, + params.max_gas.unwrap_or(Weight::new( + dry_run_result.gas_required.ref_time(), + dry_run_result.gas_required.proof_size(), + )), None, data, TxStatus::Finalized, @@ -210,34 +212,51 @@ impl ContractInstance { .await } - fn encode + Debug>(&self, message: &str, args: &[S]) -> Result> { - self.transcoder.encode(message, args) - } - - fn decode(&self, message: &str, data: Vec) -> Result { - self.transcoder.decode_return(message, &mut data.as_slice()) + /// Dry-runs contract call with the given params. Useful to measure gas or to check if + /// the call will likely fail or not. + pub async fn exec_dry_run + Debug>( + &self, + conn: &C, + sender: AccountId, + message: &str, + args: &[S], + params: ExecCallParams, + ) -> Result> { + self.dry_run_any( + conn, + message, + args, + sender, + params.value, + params.max_gas, + None, + ) + .await } - async fn dry_run + Debug, C: ConnectionApi>( + #[allow(clippy::too_many_arguments)] + async fn dry_run_any + Debug, C: ConnectionApi>( &self, conn: &C, message: &str, args: &[S], sender: AccountId, value: Balance, + gas_limit: Option, + at: Option, ) -> Result> { let payload = self.encode(message, args)?; let args = ContractCallArgs { origin: sender, dest: self.address.clone(), value, - gas_limit: None, + gas_limit, input_data: payload, storage_deposit_limit: None, }; let contract_read_result = conn - .call_and_get(args) + .call_and_get(args, at) .await .context("RPC request error - there may be more info in node logs.")?; @@ -266,6 +285,14 @@ impl ContractInstance { Ok(contract_read_result) } + + fn encode + Debug>(&self, message: &str, args: &[S]) -> Result> { + self.transcoder.encode(message, args) + } + + fn decode(&self, message: &str, data: Vec) -> Result { + self.transcoder.decode_return(message, &mut data.as_slice()) + } } impl Debug for ContractInstance { diff --git a/aleph-client/src/pallets/contract.rs b/aleph-client/src/pallets/contract.rs index b62b680970..abe4385f40 100644 --- a/aleph-client/src/pallets/contract.rs +++ b/aleph-client/src/pallets/contract.rs @@ -97,9 +97,13 @@ pub trait ContractsUserApi { #[async_trait::async_trait] pub trait ContractRpc { /// API for [`call`](https://paritytech.github.io/substrate/master/pallet_contracts/trait.ContractsApi.html#method.call) call. + /// * `args` - Arguments for the call. + /// * `at` - Optional hash of a block, the state of which should be used. + /// If `None`, state associated with the best block is queried. async fn call_and_get( &self, args: ContractCallArgs, + at: Option, ) -> anyhow::Result>; } @@ -203,8 +207,9 @@ impl ContractRpc for C { async fn call_and_get( &self, args: ContractCallArgs, + block_hash: Option, ) -> anyhow::Result> { - let params = rpc_params!["ContractsApi_call", Bytes(args.encode())]; + let params = rpc_params!["ContractsApi_call", Bytes(args.encode()), block_hash]; self.rpc_call("state_call".to_string(), params).await } } diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index 0c30393546..5e1946496f 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -138,7 +138,7 @@ dependencies = [ [[package]] name = "aleph_client" -version = "3.7.3" +version = "3.8.0" dependencies = [ "anyhow", "async-trait", diff --git a/e2e-tests/src/test/adder.rs b/e2e-tests/src/test/adder.rs index d14a11aa50..cb6bb8c7f6 100644 --- a/e2e-tests/src/test/adder.rs +++ b/e2e-tests/src/test/adder.rs @@ -1,17 +1,18 @@ -use std::{fmt::Debug, str::FromStr, sync::Arc}; - use aleph_client::{ contract::{ event::{get_contract_events, listen_contract_events}, - ContractInstance, + ContractInstance, ExecCallParams, ReadonlyCallParams, }, contract_transcode::Value, pallets::system::SystemApi, - AccountId, ConnectionApi, SignedConnectionApi, TxInfo, + sp_weights::weight_v2::Weight, + utility::BlocksApi, + AccountId, BlockHash, ConnectionApi, SignedConnectionApi, TxInfo, }; use anyhow::{anyhow, Context, Result}; use assert2::assert; use futures::{channel::mpsc::unbounded, StreamExt}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use crate::{config::setup_test, test::helpers::basic_test_context}; @@ -143,6 +144,92 @@ pub async fn adder_dry_run_failure() -> Result<()> { struct AdderInstance { contract: ContractInstance, } +/// Test read only contract calls. +#[tokio::test] +pub async fn adder_readonly_calls() -> Result<()> { + let config = setup_test(); + + let (conn, _authority, account) = basic_test_context(config).await?; + + let contract = AdderInstance::new( + &config.test_case_params.adder, + &config.test_case_params.adder_metadata, + )?; + + let base = contract.get(&conn).await?; + let block_with_state_0 = conn + .get_block_hash(conn.get_best_block().await?.unwrap()) + .await + .unwrap() + .unwrap(); + let block_with_state_1 = contract.add(&account.sign(&conn), 1).await?.block_hash; + let block_with_state_2 = contract.add(&account.sign(&conn), 1).await?.block_hash; + + assert_eq!(contract.get_at(&conn, block_with_state_0).await?, base); + assert_eq!(contract.get_at(&conn, block_with_state_1).await?, base + 1); + assert_eq!(contract.get_at(&conn, block_with_state_2).await?, base + 2); + assert_eq!(contract.get(&conn).await?, base + 2); + + Ok(()) +} + +/// Test setting gas limits for contract calls. +#[tokio::test] +pub async fn adder_setting_gas_limits() -> Result<()> { + let config = setup_test(); + let (conn, _authority, account) = basic_test_context(config).await?; + let contract = AdderInstance::new( + &config.test_case_params.adder, + &config.test_case_params.adder_metadata, + )?; + + let dry_run_result = contract + .contract + .exec_dry_run( + &conn, + account.account_id().clone(), + "add", + &["1"], + Default::default(), + ) + .await?; + let gas_required = dry_run_result.gas_required; + + assert!(contract + .add_with_params( + &account.sign(&conn), + 1, + ExecCallParams::new().gas_limit(Weight::new( + gas_required.ref_time() - 1, + gas_required.proof_size() + )) + ) + .await + .is_err()); + assert!(contract + .add_with_params( + &account.sign(&conn), + 1, + ExecCallParams::new().gas_limit(Weight::new( + gas_required.ref_time(), + gas_required.proof_size() - 1 + )) + ) + .await + .is_err()); + assert!(contract + .add_with_params( + &account.sign(&conn), + 1, + ExecCallParams::new().gas_limit(Weight::new( + gas_required.ref_time(), + gas_required.proof_size() + )) + ) + .await + .is_ok()); + Ok(()) +} impl<'a> From<&'a AdderInstance> for &'a ContractInstance { fn from(instance: &'a AdderInstance) -> Self { @@ -171,12 +258,27 @@ impl AdderInstance { } pub async fn get(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "get").await + self.contract.read0(conn, "get", Default::default()).await + } + + pub async fn get_at(&self, conn: &C, at: BlockHash) -> Result { + self.contract + .read0(conn, "get", ReadonlyCallParams::new().at(at)) + .await } pub async fn add(&self, conn: &S, value: u32) -> Result { + self.add_with_params(conn, value, Default::default()).await + } + + pub async fn add_with_params( + &self, + conn: &S, + value: u32, + params: ExecCallParams, + ) -> Result { self.contract - .contract_exec(conn, "add", &[value.to_string()]) + .exec(conn, "add", &[value.to_string()], params) .await } @@ -194,11 +296,16 @@ impl AdderInstance { }, ); - self.contract.contract_exec(conn, "set_name", &[name]).await + self.contract + .exec(conn, "set_name", &[name], Default::default()) + .await } pub async fn get_name(&self, conn: &C) -> Result> { - let res: Option = self.contract.contract_read0(conn, "get_name").await?; + let res: Option = self + .contract + .read0(conn, "get_name", Default::default()) + .await?; Ok(res.map(|name| name.replace('\0', ""))) } } diff --git a/e2e-tests/src/test/button_game/contracts.rs b/e2e-tests/src/test/button_game/contracts.rs index ffd6421a31..11642126eb 100644 --- a/e2e-tests/src/test/button_game/contracts.rs +++ b/e2e-tests/src/test/button_game/contracts.rs @@ -1,7 +1,8 @@ use std::{fmt::Debug, str::FromStr}; use aleph_client::{ - contract::ContractInstance, AccountId, Connection, ConnectionApi, SignedConnection, TxInfo, + contract::{ContractInstance, ExecCallParams}, + AccountId, Connection, ConnectionApi, SignedConnection, TxInfo, }; use anyhow::{Context, Result}; use primitives::Balance; @@ -56,7 +57,12 @@ impl SimpleDexInstance { to: AccountId, ) -> Result { self.contract - .contract_exec(conn, "add_swap_pair", &[from.to_string(), to.to_string()]) + .exec( + conn, + "add_swap_pair", + &[from.to_string(), to.to_string()], + Default::default(), + ) .await } @@ -67,10 +73,11 @@ impl SimpleDexInstance { to: AccountId, ) -> Result { self.contract - .contract_exec( + .exec( conn, "remove_swap_pair", &[&from.to_string(), &to.to_string()], + Default::default(), ) .await } @@ -86,7 +93,7 @@ impl SimpleDexInstance { let token_out: AccountId = token_out.into(); self.contract - .contract_read( + .read( conn, "out_given_in", &[ @@ -94,6 +101,7 @@ impl SimpleDexInstance { token_out.to_string(), amount_token_in.to_string(), ], + Default::default(), ) .await? } @@ -110,7 +118,7 @@ impl SimpleDexInstance { let token_out: AccountId = token_out.into(); self.contract - .contract_exec( + .exec( conn, "swap", &[ @@ -119,6 +127,7 @@ impl SimpleDexInstance { amount_token_in.to_string(), min_amount_token_out.to_string(), ], + Default::default(), ) .await } @@ -151,39 +160,51 @@ impl ButtonInstance { } pub async fn round(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "round").await + self.contract.read0(conn, "round", Default::default()).await } pub async fn deadline(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "deadline").await + self.contract + .read0(conn, "deadline", Default::default()) + .await } pub async fn is_dead(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "is_dead").await + self.contract + .read0(conn, "is_dead", Default::default()) + .await } pub async fn ticket_token(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "ticket_token").await + self.contract + .read0(conn, "ticket_token", Default::default()) + .await } pub async fn reward_token(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "reward_token").await + self.contract + .read0(conn, "reward_token", Default::default()) + .await } pub async fn last_presser(&self, conn: &C) -> Result> { - self.contract.contract_read0(conn, "last_presser").await + self.contract + .read0(conn, "last_presser", Default::default()) + .await } pub async fn marketplace(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "marketplace").await + self.contract + .read0(conn, "marketplace", Default::default()) + .await } pub async fn press(&self, conn: &SignedConnection) -> Result { - self.contract.contract_exec0(conn, "press").await + self.contract.exec0(conn, "press", Default::default()).await } pub async fn reset(&self, conn: &SignedConnection) -> Result { - self.contract.contract_exec0(conn, "reset").await + self.contract.exec0(conn, "reset", Default::default()).await } } @@ -224,10 +245,11 @@ impl PSP22TokenInstance { amount: Balance, ) -> Result { self.contract - .contract_exec( + .exec( conn, "PSP22::transfer", &[to.to_string(), amount.to_string(), "0x00".to_string()], + Default::default(), ) .await } @@ -239,10 +261,11 @@ impl PSP22TokenInstance { amount: Balance, ) -> Result { self.contract - .contract_exec( + .exec( conn, "PSP22Mintable::mint", &[to.to_string(), amount.to_string()], + Default::default(), ) .await } @@ -254,17 +277,23 @@ impl PSP22TokenInstance { value: Balance, ) -> Result { self.contract - .contract_exec( + .exec( conn, "PSP22::approve", &[spender.to_string(), value.to_string()], + Default::default(), ) .await } pub async fn balance_of(&self, conn: &Connection, account: &AccountId) -> Result { self.contract - .contract_read(conn, "PSP22::balance_of", &[account.to_string()]) + .read( + conn, + "PSP22::balance_of", + &[account.to_string()], + Default::default(), + ) .await } } @@ -302,19 +331,19 @@ impl MarketplaceInstance { } pub async fn reset(&self, conn: &SignedConnection) -> Result { - self.contract.contract_exec0(conn, "reset").await + self.contract.exec0(conn, "reset", Default::default()).await } pub async fn buy(&self, conn: &SignedConnection, max_price: Option) -> Result { let max_price = max_price.map_or_else(|| "None".to_string(), |x| format!("Some({x})")); self.contract - .contract_exec(conn, "buy", &[max_price.as_str()]) + .exec(conn, "buy", &[max_price.as_str()], Default::default()) .await } pub async fn price(&self, conn: &C) -> Result { - self.contract.contract_read0(conn, "price").await + self.contract.read0(conn, "price", Default::default()).await } } @@ -358,13 +387,13 @@ impl WAzeroInstance { pub async fn wrap(&self, conn: &SignedConnection, value: Balance) -> Result { self.contract - .contract_exec_value0(conn, "wrap", value) + .exec0(conn, "wrap", ExecCallParams::new().value(value)) .await } pub async fn unwrap(&self, conn: &SignedConnection, amount: Balance) -> Result { self.contract - .contract_exec(conn, "unwrap", &[amount.to_string()]) + .exec(conn, "unwrap", &[amount.to_string()], Default::default()) .await } @@ -374,7 +403,12 @@ impl WAzeroInstance { account: &AccountId, ) -> Result { self.contract - .contract_read(conn, "PSP22::balance_of", &[account.to_string()]) + .read( + conn, + "PSP22::balance_of", + &[account.to_string()], + Default::default(), + ) .await } }