diff --git a/.github/actions/run-e2e-test/action.yml b/.github/actions/run-e2e-test/action.yml index 870a248989..f066979c56 100644 --- a/.github/actions/run-e2e-test/action.yml +++ b/.github/actions/run-e2e-test/action.yml @@ -26,6 +26,10 @@ inputs: follow-up-finalization-check: description: 'Whether to run a follow-up finalization check.' required: false + deploy-adder: + description: 'Whether to deploy the adder sample contract to the node.' + required: false + default: 'false' runs: using: 'composite' @@ -84,6 +88,14 @@ runs: ) fi + DEPLOY_ADDER="${{ inputs.deploy-adder }}" + + if [[ "${DEPLOY_ADDER}" = "true" ]]; then + pushd contracts/adder + export ADDER=$(./deploy.sh) + popd + fi + ./.github/scripts/run_e2e_test.sh "${ARGS[@]}" - name: Run finalization e2e test diff --git a/.github/scripts/run_e2e_test.sh b/.github/scripts/run_e2e_test.sh index bbe31c5d98..47dcdfa0d3 100755 --- a/.github/scripts/run_e2e_test.sh +++ b/.github/scripts/run_e2e_test.sh @@ -111,6 +111,11 @@ if [[ -n "${ONLY_LEGACY:-}" ]]; then ARGS+=(-e ONLY_LEGACY) fi -docker run -v $(pwd)/docker/data:/data "${ARGS[@]}" aleph-e2e-client:latest +if [[ -n "${ADDER:-}" ]]; then + ARGS+=(-e "ADDER=${ADDER}") + ARGS+=(-e "ADDER_METADATA=/contracts/adder/target/ink/metadata.json") +fi + +docker run -v "$(pwd)/contracts:/contracts" -v "$(pwd)/docker/data:/data" "${ARGS[@]}" aleph-e2e-client:latest exit $? diff --git a/.github/workflows/check-excluded-packages.yml b/.github/workflows/check-excluded-packages.yml index 6a3f6879f9..202fb804be 100644 --- a/.github/workflows/check-excluded-packages.yml +++ b/.github/workflows/check-excluded-packages.yml @@ -32,12 +32,6 @@ jobs: with: version: '3.6.1' - - name: Install clippy and fmt - run: rustup component add clippy rustfmt - - - name: Install WASM target - run: rustup target add wasm32-unknown-unknown - - name: Read excluded packages from Cargo.toml id: read_excluded uses: SebRollen/toml-action@v1.0.0 @@ -75,6 +69,8 @@ jobs: do echo "Checking package $p..." pushd "$p" + rustup component add clippy rustfmt + rustup target add wasm32-unknown-unknown cargo fmt --all --check cargo clippy --all-features -- --no-deps -D warnings popd diff --git a/.github/workflows/e2e-tests-main-devnet.yml b/.github/workflows/e2e-tests-main-devnet.yml index 63ee5476c7..839b64b956 100644 --- a/.github/workflows/e2e-tests-main-devnet.yml +++ b/.github/workflows/e2e-tests-main-devnet.yml @@ -552,6 +552,31 @@ jobs: UPGRADE_FINALIZATION_WAIT_SESSIONS: 2 timeout-minutes: 10 + run-e2e-adder-contract-test: + needs: [build-test-docker, build-test-client] + name: Run e2e adder contract test + runs-on: ubuntu-20.04 + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install cargo-contract + uses: baptiste0928/cargo-install@v1 + with: + crate: cargo-contract + version: "2.0.0-beta.1" + + - name: Install rust-src + working-directory: ./contracts + run: rustup component add rust-src + + - name: Run e2e test + uses: ./.github/actions/run-e2e-test + with: + deploy-adder: true + test-case: adder + timeout-minutes: 10 + # The tests below were written under the assumption that nonfinalized blocks are being produced, they need a rewrite before being reenabled. # TODO(A0-1644): Reenable these tests. # run-e2e-failing-version-upgrade: @@ -659,6 +684,7 @@ jobs: run-e2e-ban-threshold, run-e2e-version-upgrade, run-e2e-permissionless-ban, + run-e2e-adder-contract-test, # run-e2e-failing-version-upgrade, # run-e2e-version-upgrade-catchup, ] diff --git a/Cargo.toml b/Cargo.toml index 36b4491e69..822e5fe487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ exclude = [ "fork-off", "benches/payout-stakers", "bin/cliain", + "contracts/adder" ] [profile.release] diff --git a/aleph-client/src/connections.rs b/aleph-client/src/connections.rs index 2d8da43b4b..5b12262349 100644 --- a/aleph-client/src/connections.rs +++ b/aleph-client/src/connections.rs @@ -142,12 +142,14 @@ impl SignedConnection { if let Some(details) = tx.validation_details() { info!(target:"aleph-client", "Sending extrinsic {}.{} with params: {:?}", details.pallet_name, details.call_name, params); } + let progress = self .connection .client .tx() .sign_and_submit_then_watch(&tx, &self.signer, params) - .await?; + .await + .map_err(|e| anyhow!("Failed to submit transaction: {:?}", e))?; // In case of Submitted hash does not mean anything let hash = match status { diff --git a/aleph-client/src/contract/convertible_value.rs b/aleph-client/src/contract/convertible_value.rs new file mode 100644 index 0000000000..18397e3d47 --- /dev/null +++ b/aleph-client/src/contract/convertible_value.rs @@ -0,0 +1,195 @@ +use std::{fmt::Debug, ops::Deref, str::FromStr}; + +use anyhow::{anyhow, bail, Context, Result}; +use contract_transcode::Value; + +use crate::AccountId; + +/// Temporary wrapper for converting from [Value] to primitive types. +/// +/// ``` +/// # #![feature(assert_matches)] +/// # #![feature(type_ascription)] +/// # use std::assert_matches::assert_matches; +/// # use anyhow::{anyhow, Result}; +/// # use aleph_client::{AccountId, contract::ConvertibleValue}; +/// use contract_transcode::Value; +/// +/// assert_matches!(ConvertibleValue(Value::UInt(42)).try_into(), Ok(42u128)); +/// assert_matches!(ConvertibleValue(Value::UInt(42)).try_into(), Ok(42u32)); +/// assert_matches!(ConvertibleValue(Value::UInt(u128::MAX)).try_into(): Result, Err(_)); +/// assert_matches!(ConvertibleValue(Value::Bool(true)).try_into(), Ok(true)); +/// assert_matches!( +/// ConvertibleValue(Value::Literal("5H8cjBBzCJrAvDn9LHZpzzJi2UKvEGC9VeVYzWX5TrwRyVCA".to_string())). +/// try_into(): Result, +/// Ok(_) +/// ); +/// assert_matches!( +/// ConvertibleValue(Value::String("not a number".to_string())).try_into(): Result, +/// Err(_) +/// ); +/// ``` +#[derive(Debug, Clone)] +pub struct ConvertibleValue(pub Value); + +impl Deref for ConvertibleValue { + type Target = Value; + + fn deref(&self) -> &Value { + &self.0 + } +} + +impl TryFrom for bool { + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result { + match value.0 { + Value::Bool(value) => Ok(value), + _ => bail!("Expected {:?} to be a boolean", value.0), + } + } +} + +impl TryFrom for u128 { + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result { + match value.0 { + Value::UInt(value) => Ok(value), + _ => bail!("Expected {:?} to be an integer", value.0), + } + } +} + +impl TryFrom for u32 { + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result { + match value.0 { + Value::UInt(value) => Ok(value.try_into()?), + _ => bail!("Expected {:?} to be an integer", value.0), + } + } +} + +impl TryFrom for AccountId { + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result { + match value.0 { + Value::Literal(value) => { + AccountId::from_str(&value).map_err(|_| anyhow!("Invalid account id")) + } + _ => bail!("Expected {:?} to be a string", value), + } + } +} + +impl TryFrom for Result +where + ConvertibleValue: TryInto, +{ + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result, Self::Error> { + if let Value::Tuple(tuple) = &value.0 { + match tuple.ident() { + Some(x) if x == "Ok" => { + if tuple.values().count() == 1 { + let item = + ConvertibleValue(tuple.values().next().unwrap().clone()).try_into()?; + return Ok(Ok(item)); + } else { + bail!("Unexpected number of elements in Ok variant: {:?}", &value); + } + } + Some(x) if x == "Err" => { + if tuple.values().count() == 1 { + return Ok(Err(anyhow!(value.to_string()))); + } else { + bail!("Unexpected number of elements in Err variant: {:?}", &value); + } + } + _ => (), + } + } + + bail!("Expected {:?} to be an Ok(_) or Err(_) tuple.", value); + } +} + +impl TryFrom for String { + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> std::result::Result { + let seq = match value.0 { + Value::Seq(seq) => seq, + _ => bail!("Failed parsing `ConvertibleValue` to `String`. Expected `Seq(Value::UInt)` but instead got: {:?}", value), + }; + + let mut bytes: Vec = Vec::with_capacity(seq.len()); + for el in seq.elems() { + if let Value::UInt(byte) = *el { + if byte > u8::MAX as u128 { + bail!("Expected number <= u8::MAX but instead got: {:?}", byte) + } + bytes.push(byte as u8); + } else { + bail!("Failed parsing `ConvertibleValue` to `String`. Expected `Value::UInt` but instead got: {:?}", el); + } + } + String::from_utf8(bytes).context("Failed parsing bytes to UTF-8 String.") + } +} + +auto trait NotEq {} +// We're basically telling the compiler that there is no instance of NotEq for `(X,X)` tuple. +// Or put differently - that you can't implement `NotEq` for `(X,X)`. +impl !NotEq for (X, X) {} + +impl TryFrom for Option +where + T: TryFrom + Debug, + // We will derive this impl only when `T != ConvertibleValue`. + // Otherwise we will get a conflict with generic impl in the rust `core` crate. + (ConvertibleValue, T): NotEq, +{ + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> std::result::Result, Self::Error> { + let tuple = match &value.0 { + Value::Tuple(tuple) => tuple, + _ => bail!("Expected {:?} to be a Some(_) or None Tuple.", &value), + }; + + match tuple.ident() { + Some(x) if x == "Some" => { + if tuple.values().count() == 1 { + let item = + ConvertibleValue(tuple.values().next().unwrap().clone()).try_into()?; + Ok(Some(item)) + } else { + bail!( + "Unexpected number of elements in Some(_) variant: {:?}. Expected one.", + &value + ); + } + } + Some(x) if x == "None" => { + if tuple.values().count() == 0 { + Ok(None) + } else { + bail!( + "Unexpected number of elements in None variant: {:?}. Expected zero.", + &value + ); + } + } + _ => bail!( + "Expected `.ident()` to be `Some` or `None`, got: {:?}", + &tuple + ), + } + } +} diff --git a/aleph-client/src/contract/mod.rs b/aleph-client/src/contract/mod.rs index 8591e4ee0b..d40c662050 100644 --- a/aleph-client/src/contract/mod.rs +++ b/aleph-client/src/contract/mod.rs @@ -5,10 +5,9 @@ //! //! ```no_run //! # use anyhow::{Result, Context}; -//! # use sp_core::crypto::AccountId32; -//! # use aleph_client::{AccountId, Connection, SignedConnection}; +//! # use aleph_client::AccountId; +//! # use aleph_client::{Connection, SignedConnection}; //! # use aleph_client::contract::ContractInstance; -//! # use aleph_client::contract::util::to_u128; //! # //! #[derive(Debug)] //! struct PSP22TokenInstance { @@ -25,39 +24,34 @@ //! }) //! } //! -//! fn transfer(&self, conn: &SignedConnection, to: AccountId, amount: u128) -> Result<()> { +//! async fn transfer(&self, conn: &SignedConnection, to: AccountId, amount: u128) -> Result<()> { //! self.contract.contract_exec( //! conn, //! "PSP22::transfer", //! vec![to.to_string().as_str(), amount.to_string().as_str(), "0x00"].as_slice(), -//! ) +//! ).await //! } //! -//! fn balance_of(&self, conn: &Connection, account: AccountId) -> Result { -//! to_u128(self.contract.contract_read( +//! async fn balance_of(&self, conn: &Connection, account: AccountId) -> Result { +//! self.contract.contract_read( //! conn, //! "PSP22::balance_of", //! &vec![account.to_string().as_str()], -//! )?) +//! ).await? //! } //! } //! ``` -pub mod util; +mod convertible_value; -use std::{ - fmt::{Debug, Formatter}, - fs::File, -}; +use std::fmt::{Debug, Formatter}; -use anyhow::{Context, Result}; -use codec::{Compact, Decode}; -use contract_metadata::ContractMetadata; +use anyhow::{anyhow, Context, Result}; use contract_transcode::ContractMessageTranscoder; -use ink_metadata::InkProject; -use serde_json::{from_reader, from_value}; +pub use convertible_value::ConvertibleValue; use crate::{ + contract_transcode::Value, pallets::contract::{ContractCallArgs, ContractRpc, ContractsUserApi}, sp_weights::weight_v2::Weight, AccountId, Connection, SignedConnection, TxStatus, @@ -66,22 +60,17 @@ use crate::{ /// Represents a contract instantiated on the chain. pub struct ContractInstance { address: AccountId, - ink_project: InkProject, transcoder: ContractMessageTranscoder, } impl ContractInstance { - const MAX_READ_GAS: u64 = 500000000000u64; const MAX_GAS: u64 = 10000000000u64; - const PAYABLE_VALUE: u64 = 0u64; - const STORAGE_FEE_LIMIT: Option> = None; /// Creates a new contract instance under `address` with metadata read from `metadata_path`. pub fn new(address: AccountId, metadata_path: &str) -> Result { Ok(Self { address, - ink_project: load_metadata(metadata_path)?, - transcoder: ContractMessageTranscoder::new(load_metadata(metadata_path)?), + transcoder: ContractMessageTranscoder::load(metadata_path)?, }) } @@ -90,93 +79,108 @@ impl ContractInstance { &self.address } - /// The metadata of this contract instance. - pub fn ink_project(&self) -> &InkProject { - &self.ink_project - } - /// Reads the value of a read-only, 0-argument call via RPC. - pub async fn contract_read0(&self, conn: &Connection, message: &str) -> Result { - self.contract_read(conn, message, &[]).await + pub async fn contract_read0>( + &self, + conn: &Connection, + message: &str, + ) -> Result { + self.contract_read::(conn, message, &[]).await } /// Reads the value of a read-only call via RPC. - pub async fn contract_read( + pub async fn contract_read< + S: AsRef + Debug, + T: TryFrom, + >( &self, conn: &Connection, message: &str, - args: &[&str], + args: &[S], ) -> Result { let payload = self.encode(message, args)?; let args = ContractCallArgs { origin: self.address.clone(), dest: self.address.clone(), value: 0, - gas_limit: Weight { - ref_time: Self::MAX_READ_GAS, - proof_size: u64::MAX, - }, + gas_limit: None, input_data: payload, storage_deposit_limit: None, }; - conn.call_and_get(args) + + let result = conn + .call_and_get(args) .await - .context("RPC request error - there may be more info in node logs.") + .context("RPC request error - there may be more info in node logs.")? + .result + .map_err(|e| anyhow!("Contract exec failed {:?}", e))?; + let decoded = self.decode(message, result.data)?; + ConvertibleValue(decoded).try_into()? } /// Executes a 0-argument contract call. pub async fn contract_exec0(&self, conn: &SignedConnection, message: &str) -> Result<()> { - self.contract_exec(conn, message, &[]).await + self.contract_exec::(conn, message, &[]).await } /// Executes a contract call. - pub async fn contract_exec( + pub async fn contract_exec + Debug>( &self, conn: &SignedConnection, message: &str, - args: &[&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: &SignedConnection, + message: &str, + value: u128, + ) -> 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: &SignedConnection, + message: &str, + args: &[S], + value: u128, ) -> Result<()> { let data = self.encode(message, args)?; conn.call( self.address.clone(), - Self::PAYABLE_VALUE as u128, + value, Weight { ref_time: Self::MAX_GAS, - proof_size: u64::MAX, + proof_size: Self::MAX_GAS, }, - Self::STORAGE_FEE_LIMIT, + None, data, TxStatus::InBlock, ) .await - .context("Failed to exec contract message")?; - - Ok(()) + .map(|_| ()) } - fn encode(&self, message: &str, args: &[&str]) -> 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 { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ContractInstance") .field("address", &self.address) - .field("ink_project", &self.ink_project) .finish() } } - -/// Helper for loading contract metadata from a file. -/// -/// The contract-metadata lib contains a similar function starting with version 0.2. It seems that -/// version conflicts with some of our other dependencies, however, if we upgrade in the future we -/// can drop this function in favour of their implementation. -fn load_metadata(path: &str) -> Result { - let file = File::open(path)?; - let metadata: ContractMetadata = from_reader(file)?; - let ink_metadata = from_value(serde_json::Value::Object(metadata.abi))?; - - Ok(ink_metadata) -} diff --git a/aleph-client/src/lib.rs b/aleph-client/src/lib.rs index 3acc5db42d..f6ccaceeec 100644 --- a/aleph-client/src/lib.rs +++ b/aleph-client/src/lib.rs @@ -1,5 +1,9 @@ +#![feature(auto_traits)] +#![feature(negative_impls)] + extern crate core; +pub use contract_transcode; pub use subxt::ext::sp_core::Pair; use subxt::{ ext::sp_core::{ed25519, sr25519, H256}, diff --git a/aleph-client/src/pallets/contract.rs b/aleph-client/src/pallets/contract.rs index 0612114b4b..8ea602df63 100644 --- a/aleph-client/src/pallets/contract.rs +++ b/aleph-client/src/pallets/contract.rs @@ -1,10 +1,7 @@ -use codec::{Compact, Decode, Encode}; +use codec::{Compact, Encode}; use pallet_contracts_primitives::ContractExecResult; use primitives::Balance; -use subxt::{ - ext::{sp_core::Bytes, sp_runtime::MultiAddress}, - rpc_params, -}; +use subxt::{ext::sp_core::Bytes, rpc_params}; use crate::{ api, pallet_contracts::wasm::OwnerInfo, sp_weights::weight_v2::Weight, AccountId, BlockHash, @@ -16,7 +13,7 @@ pub struct ContractCallArgs { pub origin: AccountId, pub dest: AccountId, pub value: Balance, - pub gas_limit: Weight, + pub gas_limit: Option, pub storage_deposit_limit: Option, pub input_data: Vec, } @@ -78,7 +75,10 @@ pub trait ContractsUserApi { #[async_trait::async_trait] pub trait ContractRpc { - async fn call_and_get(&self, args: ContractCallArgs) -> anyhow::Result; + async fn call_and_get( + &self, + args: ContractCallArgs, + ) -> anyhow::Result>; } #[async_trait::async_trait] @@ -160,13 +160,10 @@ impl ContractsUserApi for SignedConnection { data: Vec, status: TxStatus, ) -> anyhow::Result { - let tx = api::tx().contracts().call( - MultiAddress::Id(destination), - balance, - gas_limit, - storage_limit, - data, - ); + let tx = + api::tx() + .contracts() + .call(destination.into(), balance, gas_limit, storage_limit, data); self.send_tx(tx, status).await } @@ -183,19 +180,11 @@ impl ContractsUserApi for SignedConnection { #[async_trait::async_trait] impl ContractRpc for Connection { - async fn call_and_get(&self, args: ContractCallArgs) -> anyhow::Result { + async fn call_and_get( + &self, + args: ContractCallArgs, + ) -> anyhow::Result> { let params = rpc_params!["ContractsApi_call", Bytes(args.encode())]; - - let res: ContractExecResult = - self.rpc_call("state_call".to_string(), params).await?; - let res = T::decode( - &mut (res - .result - .expect("Failed to decode execution result of the wasm code!") - .data - .as_slice()), - )?; - - Ok(res) + self.rpc_call("state_call".to_string(), params).await } } diff --git a/contracts/adder/.gitignore b/contracts/adder/.gitignore new file mode 100755 index 0000000000..8de8f877e4 --- /dev/null +++ b/contracts/adder/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/contracts/adder/Cargo.toml b/contracts/adder/Cargo.toml new file mode 100755 index 0000000000..b55dd2fac2 --- /dev/null +++ b/contracts/adder/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "adder" +version = "0.1.0" +authors = ["[your_name] <[your_email]>"] +edition = "2021" + +[dependencies] +anyhow = "*" +ink = { version = "4.0.0-beta", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "adder" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", +] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/contracts/adder/deploy.sh b/contracts/adder/deploy.sh new file mode 100755 index 0000000000..5acd85cbe3 --- /dev/null +++ b/contracts/adder/deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +NODE_URL="${NODE_URL:-ws://localhost:9944}" +AUTHORITY="${AUTHORITY:-//Alice}" + +cargo contract build --release --quiet 1>&2 +cargo contract upload --url "$NODE_URL" --suri "$AUTHORITY" --quiet 1>&2 + +export ADDER + +ADDER=$( + cargo contract instantiate --url "$NODE_URL" --suri "$AUTHORITY" --skip-confirm --output-json \ + | jq -r ".contract" +) +echo "$ADDER" diff --git a/contracts/adder/lib.rs b/contracts/adder/lib.rs new file mode 100755 index 0000000000..7e86049c66 --- /dev/null +++ b/contracts/adder/lib.rs @@ -0,0 +1,65 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! This is a simple example contract for use with e2e tests of the aleph-client contract interaction. + +#[ink::contract] +mod adder { + #[ink(storage)] + pub struct Adder { + name: Option<[u8; 20]>, + value: u32, + } + + #[ink(event)] + pub struct ValueChanged { + new_value: u32, + } + + #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + Overflow, + } + + impl Adder { + #[ink(constructor)] + pub fn new() -> Self { + Self { + value: 0, + name: None, + } + } + + #[ink(message)] + pub fn add(&mut self, value: u32) -> Result<(), Error> { + self.value = self.value.checked_add(value).ok_or(Error::Overflow)?; + + Self::env().emit_event(ValueChanged { + new_value: self.value, + }); + + Ok(()) + } + + #[ink(message)] + pub fn get(&self) -> u32 { + self.value + } + + #[ink(message)] + pub fn set_name(&mut self, name: Option<[u8; 20]>) { + self.name = name; + } + + #[ink(message)] + pub fn get_name(&self) -> Option<[u8; 20]> { + self.name + } + } + + impl Default for Adder { + fn default() -> Self { + Self::new() + } + } +} diff --git a/contracts/rust-toolchain b/contracts/rust-toolchain new file mode 100644 index 0000000000..902c74186f --- /dev/null +++ b/contracts/rust-toolchain @@ -0,0 +1 @@ +1.65.0 diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index e379e2513c..91ed27a6a8 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -53,12 +53,14 @@ version = "0.10.0" dependencies = [ "aleph_client", "anyhow", + "assert2", "clap", - "env_logger 0.8.4", + "env_logger", "frame-support", "frame-system", "futures", "hex", + "itertools", "log", "once_cell", "pallet-balances", @@ -66,6 +68,7 @@ dependencies = [ "pallet-staking", "parity-scale-codec", "primitives", + "rand 0.8.5", "rayon", "serde_json", "sp-core 6.0.0", @@ -154,6 +157,29 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "assert2" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1b167af16149cd41ff2b784bf511bb4208b21c3b05f3f61e30823ce3986361" +dependencies = [ + "assert2-macros", + "atty", + "yansi", +] + +[[package]] +name = "assert2-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ac27dd1c8f16b282d1c22a8a5ae17119acc757101dec79054458fef62c447e" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "async-lock" version = "2.6.0" @@ -435,9 +461,9 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "contract-metadata" -version = "2.0.0-beta" +version = "2.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40248c6a648b3679ea52bb83d8921246d0347644eaf7b0eec4c5601fa8feb651" +checksum = "7bff7703529b16e9d8ba0d54e842b2051691772a822eb9bc130a91183ff9f6a6" dependencies = [ "anyhow", "impl-serde", @@ -449,13 +475,12 @@ dependencies = [ [[package]] name = "contract-transcode" -version = "2.0.0-beta" +version = "2.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61159f8e266d4892be25f2b1e7ff2c4c6dd4338ca26498d907e5532a52a28e5f" +checksum = "d25e184ac4c29748e28c026f4c443fb94c002ad0877f0b190dccd624df8f893f" dependencies = [ "anyhow", "contract-metadata", - "env_logger 0.9.3", "escape8259", "hex", "indexmap", @@ -850,19 +875,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "environmental" version = "1.1.4" @@ -2774,6 +2786,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.20.7" @@ -3009,18 +3030,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.148" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e" dependencies = [ "proc-macro2", "quote", @@ -4799,6 +4820,12 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yap" version = "0.7.2" diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 851f5f1e1e..0cb8cb0be4 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -13,6 +13,9 @@ log = "0.4" serde_json = "1.0" codec = { package = 'parity-scale-codec', version = "3.0", default-features = false, features = ['derive'] } rayon = "1.5" +rand = "0.8" +itertools = "0.10" +assert2 = "0.3" tokio = { version = "1.21.2", features = ["full"] } futures = "0.3.25" once_cell = "1.16" @@ -33,5 +36,5 @@ default = ["std"] std = [ "pallet-staking/std", "pallet-balances/std", - "primitives/std" + "primitives/std", ] diff --git a/e2e-tests/src/config.rs b/e2e-tests/src/config.rs index 0fbdfe5c75..3352ea35f1 100644 --- a/e2e-tests/src/config.rs +++ b/e2e-tests/src/config.rs @@ -13,11 +13,6 @@ static GLOBAL_CONFIG: Lazy = Lazy::new(|| { .ok() .map(|s| s.split(',').map(|s| s.to_string()).collect()); let sudo_seed = get_env("SUDO_SEED").unwrap_or_else(|| "//Alice".to_string()); - let reserved_seats = get_env("RESERVED_SEATS"); - let non_reserved_seats = get_env("NON_RESERVED_SEATS"); - let upgrade_to_version = get_env("UPGRADE_VERSION"); - let upgrade_session = get_env("UPGRADE_SESSION"); - let upgrade_finalization_wait_sessions = get_env("UPGRADE_FINALIZATION_WAIT_SESSIONS"); Config { node, @@ -25,11 +20,13 @@ static GLOBAL_CONFIG: Lazy = Lazy::new(|| { validators_seeds, sudo_seed, test_case_params: TestCaseParams { - reserved_seats, - non_reserved_seats, - upgrade_to_version, - upgrade_session, - upgrade_finalization_wait_sessions, + reserved_seats: get_env("RESERVED_SEATS"), + non_reserved_seats: get_env("NON_RESERVED_SEATS"), + upgrade_to_version: get_env("UPGRADE_VERSION"), + upgrade_session: get_env("UPGRADE_SESSION"), + upgrade_finalization_wait_sessions: get_env("UPGRADE_FINALIZATION_WAIT_SESSIONS"), + adder: get_env("ADDER"), + adder_metadata: get_env("ADDER_METADATA"), }, } }); @@ -113,4 +110,10 @@ pub struct TestCaseParams { /// How many sessions we should wait after upgrade in VersionUpgrade test. pub upgrade_finalization_wait_sessions: Option, + + /// Adder contract address. + pub adder: Option, + + /// Adder contract metadata. + pub adder_metadata: Option, } diff --git a/e2e-tests/src/test/adder.rs b/e2e-tests/src/test/adder.rs new file mode 100644 index 0000000000..3b32450795 --- /dev/null +++ b/e2e-tests/src/test/adder.rs @@ -0,0 +1,95 @@ +use std::str::FromStr; + +use aleph_client::{contract::ContractInstance, AccountId, Connection, SignedConnection}; +use anyhow::{Context, Result}; +use assert2::assert; + +use crate::{config::setup_test, test::helpers::basic_test_context}; + +/// This test exercises the aleph-client code for interacting with contracts by testing a simple contract that maintains +/// some state and publishes some events. +#[tokio::test] +pub async fn adder() -> 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 increment = 10; + let before = contract.get(&conn).await?; + contract.add(&account.sign(&conn), increment).await?; + let after = contract.get(&conn).await?; + assert!(after == before + increment); + + let new_name = "test"; + contract.set_name(&account.sign(&conn), None).await?; + assert!(contract.get_name(&conn).await?.is_none()); + contract + .set_name(&account.sign(&conn), Some(new_name)) + .await?; + assert!(contract.get_name(&conn).await? == Some(new_name.to_string())); + + Ok(()) +} + +pub(super) struct AdderInstance { + contract: ContractInstance, +} + +impl<'a> From<&'a AdderInstance> for &'a ContractInstance { + fn from(instance: &'a AdderInstance) -> Self { + &instance.contract + } +} + +impl<'a> From<&'a AdderInstance> for AccountId { + fn from(instance: &'a AdderInstance) -> Self { + instance.contract.address().clone() + } +} + +impl AdderInstance { + pub fn new(address: &Option, metadata_path: &Option) -> Result { + let address = address.as_ref().context("Adder contract address not set")?; + let metadata_path = metadata_path + .as_ref() + .context("Adder contract metadata not set")?; + + let address = AccountId::from_str(address) + .ok() + .with_context(|| format!("Failed to parse address: {}", address))?; + let contract = ContractInstance::new(address, metadata_path)?; + Ok(Self { contract }) + } + + pub async fn get(&self, conn: &Connection) -> Result { + self.contract.contract_read0(conn, "get").await + } + + pub async fn add(&self, conn: &SignedConnection, value: u32) -> Result<()> { + self.contract + .contract_exec(conn, "add", &[value.to_string()]) + .await + } + + pub async fn set_name(&self, conn: &SignedConnection, name: Option<&str>) -> Result<()> { + let name = name.map_or_else( + || "None".to_string(), + |name| { + let mut bytes = name.bytes().take(20).collect::>(); + bytes.extend(std::iter::repeat(0).take(20 - bytes.len())); + format!("Some({:?})", bytes) + }, + ); + + self.contract.contract_exec(conn, "set_name", &[name]).await + } + + pub async fn get_name(&self, conn: &Connection) -> Result> { + let res: Option = self.contract.contract_read0(conn, "get_name").await?; + Ok(res.map(|name| name.replace("\0", ""))) + } +} diff --git a/e2e-tests/src/test/helpers.rs b/e2e-tests/src/test/helpers.rs new file mode 100644 index 0000000000..a235fde8fe --- /dev/null +++ b/e2e-tests/src/test/helpers.rs @@ -0,0 +1,80 @@ +use std::ops::Deref; + +use aleph_client::{ + pallets::balances::BalanceUserApi, AccountId, Connection, KeyPair, Pair, SignedConnection, + TxStatus, +}; +use anyhow::Result; +use primitives::Balance; +use rand::Rng; + +use crate::config::Config; + +/// A wrapper around a KeyPair for purposes of converting to an account id in tests. +pub struct KeyPairWrapper(KeyPair); + +impl KeyPairWrapper { + /// Creates a copy of the `connection` signed by `signer` + pub fn sign(&self, conn: &Connection) -> SignedConnection { + SignedConnection::from_connection(conn.clone(), self.clone().0) + } +} + +impl Clone for KeyPairWrapper { + fn clone(&self) -> Self { + Self(KeyPair::new(self.0.signer().clone())) + } +} + +impl Deref for KeyPairWrapper { + type Target = KeyPair; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&KeyPairWrapper> for AccountId { + fn from(keypair: &KeyPairWrapper) -> Self { + keypair.signer().public().into() + } +} + +impl From for AccountId { + fn from(keypair: KeyPairWrapper) -> Self { + (&keypair).into() + } +} + +/// Derives a test account based on a randomized string +pub fn random_account() -> KeyPairWrapper { + KeyPairWrapper(aleph_client::keypair_from_string(&format!( + "//TestAccount/{}", + rand::thread_rng().gen::() + ))) +} + +/// Transfer `amount` from `from` to `to` +pub async fn transfer(conn: &SignedConnection, to: &KeyPair, amount: Balance) -> Result<()> { + conn.transfer(to.signer().public().into(), amount, TxStatus::InBlock) + .await + .map(|_| ()) +} + +/// Returns a number representing the given amount of alephs (adding decimals) +pub fn alephs(basic_unit_amount: Balance) -> Balance { + basic_unit_amount * 1_000_000_000_000 +} + +/// Prepares a `(conn, authority, account)` triple with some money in `account` for fees. +pub async fn basic_test_context( + config: &Config, +) -> Result<(Connection, KeyPairWrapper, KeyPairWrapper)> { + let conn = config.get_first_signed_connection().await; + let authority = KeyPairWrapper(aleph_client::keypair_from_string(&config.sudo_seed)); + let account = random_account(); + + transfer(&conn, &account, alephs(100)).await?; + + Ok((conn.connection, authority, account)) +} diff --git a/e2e-tests/src/test/mod.rs b/e2e-tests/src/test/mod.rs index 392a56f4e5..afadd91b9f 100644 --- a/e2e-tests/src/test/mod.rs +++ b/e2e-tests/src/test/mod.rs @@ -18,12 +18,14 @@ pub use version_upgrade::{ schedule_doomed_version_change_and_verify_finalization_stopped, schedule_version_change, }; +mod adder; mod ban; mod electing_validators; mod era_payout; mod era_validators; mod fee; mod finalization; +mod helpers; mod rewards; mod staking; mod transfer;