diff --git a/.github/workflows/contracts-e2e-tests-and-deploy.yaml b/.github/workflows/contracts-e2e-tests-and-deploy.yaml index 7c9fca11ec..86ab4c3ef8 100644 --- a/.github/workflows/contracts-e2e-tests-and-deploy.yaml +++ b/.github/workflows/contracts-e2e-tests-and-deploy.yaml @@ -60,7 +60,15 @@ jobs: target-key: e2e-contracts cargo-key: e2e-contracts cache-version: v3 - cargo-targets: e2e-tests-contracts/target/ + cargo-targets: | + e2e-tests/target/ + contracts/access_control/target/ + contracts/button/target/ + contracts/game_token/target/ + contracts/marketplace/target/ + contracts/simple_dex/target/ + contracts/ticket_token/target/ + contracts/wrapped_azero/target/ - name: Install cargo-contract run: | diff --git a/Cargo.lock b/Cargo.lock index c186149454..a00bab5732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2513,7 +2513,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -5358,7 +5358,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -8559,7 +8559,7 @@ dependencies = [ "pbkdf2 0.11.0", "rand 0.8.5", "rustc-hash", - "sha2 0.10.2", + "sha2 0.10.6", "thiserror", "unicode-normalization", "wasm-bindgen", diff --git a/aleph-client/Cargo.lock b/aleph-client/Cargo.lock index 2bb0e93a5d..f617e7681d 100644 --- a/aleph-client/Cargo.lock +++ b/aleph-client/Cargo.lock @@ -93,7 +93,7 @@ dependencies = [ [[package]] name = "aleph_client" -version = "1.9.0" +version = "1.10.0" dependencies = [ "ac-node-api", "ac-primitives", diff --git a/aleph-client/Cargo.toml b/aleph-client/Cargo.toml index 4c07db35f8..c8c3b33799 100644 --- a/aleph-client/Cargo.toml +++ b/aleph-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aleph_client" -version = "1.9.0" +version = "1.10.0" edition = "2021" license = "Apache 2.0" diff --git a/aleph-client/src/contract/convertible_value.rs b/aleph-client/src/contract/convertible_value.rs new file mode 100644 index 0000000000..7cbcd58bbe --- /dev/null +++ b/aleph-client/src/contract/convertible_value.rs @@ -0,0 +1,73 @@ +use std::ops::Deref; + +use anyhow::{bail, Result}; +use contract_transcode::Value; +use sp_core::crypto::Ss58Codec; + +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(42)); +/// 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 AccountId { + type Error = anyhow::Error; + + fn try_from(value: ConvertibleValue) -> Result { + match value.0 { + Value::Literal(value) => Ok(AccountId::from_ss58check(&value)?), + _ => bail!("Expected {:?} to be a string", value), + } + } +} diff --git a/aleph-client/src/contract/mod.rs b/aleph-client/src/contract/mod.rs index 55beef23ce..4dd3cbdfe5 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; //! # use aleph_client::{Connection, SignedConnection}; //! # use aleph_client::contract::ContractInstance; -//! # use aleph_client::contract::util::to_u128; //! # //! #[derive(Debug)] //! struct PSP22TokenInstance { @@ -16,7 +15,7 @@ //! } //! //! impl PSP22TokenInstance { -//! fn new(address: AccountId32, metadata_path: &Option) -> Result { +//! fn new(address: AccountId, metadata_path: &Option) -> Result { //! let metadata_path = metadata_path //! .as_ref() //! .context("PSP22Token metadata not set.")?; @@ -25,7 +24,7 @@ //! }) //! } //! -//! fn transfer(&self, conn: &SignedConnection, to: AccountId32, amount: u128) -> Result<()> { +//! fn transfer(&self, conn: &SignedConnection, to: AccountId, amount: u128) -> Result<()> { //! self.contract.contract_exec( //! conn, //! "PSP22::transfer", @@ -33,18 +32,18 @@ //! ) //! } //! -//! fn balance_of(&self, conn: &Connection, account: AccountId32) -> Result { -//! to_u128(self.contract.contract_read( +//! fn balance_of(&self, conn: &Connection, account: AccountId) -> Result { +//! self.contract.contract_read( //! conn, //! "PSP22::balance_of", //! &vec![account.to_string().as_str()], -//! )?) +//! )?.try_into() //! } //! } //! ``` +mod convertible_value; pub mod event; -pub mod util; use std::{ fmt::{Debug, Formatter}, @@ -54,28 +53,29 @@ use std::{ use ac_primitives::ExtrinsicParams; use anyhow::{anyhow, Context, Result}; use contract_metadata::ContractMetadata; -use contract_transcode::{ContractMessageTranscoder, Value}; +use contract_transcode::ContractMessageTranscoder; +pub use convertible_value::ConvertibleValue; use ink_metadata::{InkProject, MetadataVersioned}; use serde_json::{from_reader, from_str, from_value, json}; -use sp_core::{crypto::AccountId32, Pair}; +use sp_core::Pair; use substrate_api_client::{compose_extrinsic, GenericAddress, XtStatus}; -use crate::{try_send_xt, AnyConnection, SignedConnection}; +use crate::{try_send_xt, AccountId, AnyConnection, SignedConnection}; /// Represents a contract instantiated on the chain. pub struct ContractInstance { - address: AccountId32, + address: AccountId, ink_project: InkProject, } impl ContractInstance { const MAX_READ_GAS: u64 = 500000000000u64; - const MAX_GAS: u64 = 10000000000u64; + const MAX_GAS: u64 = 100000000000u64; 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: AccountId32, metadata_path: &str) -> Result { + pub fn new(address: AccountId, metadata_path: &str) -> Result { Ok(Self { address, ink_project: load_metadata(metadata_path)?, @@ -83,7 +83,7 @@ impl ContractInstance { } /// The address of this contract instance. - pub fn address(&self) -> &AccountId32 { + pub fn address(&self) -> &AccountId { &self.address } @@ -93,7 +93,11 @@ impl ContractInstance { } /// Reads the value of a read-only, 0-argument call via RPC. - pub fn contract_read0(&self, conn: &C, message: &str) -> Result { + pub fn contract_read0( + &self, + conn: &C, + message: &str, + ) -> Result { self.contract_read(conn, message, &[]) } @@ -103,7 +107,7 @@ impl ContractInstance { conn: &C, message: &str, args: &[&str], - ) -> Result { + ) -> Result { let payload = self.encode(message, args)?; let request = self.contract_read_request(&payload); let response = conn @@ -166,10 +170,12 @@ impl ContractInstance { ContractMessageTranscoder::new(&self.ink_project).encode(message, args) } - fn decode_response(&self, from: &str, contract_response: &str) -> Result { + fn decode_response(&self, from: &str, contract_response: &str) -> Result { let contract_response = contract_response.trim_start_matches("0x"); let bytes = hex::decode(contract_response)?; - ContractMessageTranscoder::new(&self.ink_project).decode_return(from, &mut bytes.as_slice()) + ContractMessageTranscoder::new(&self.ink_project) + .decode_return(from, &mut bytes.as_slice()) + .map(ConvertibleValue) } } diff --git a/aleph-client/src/contract/util.rs b/aleph-client/src/contract/util.rs deleted file mode 100644 index d925e7e386..0000000000 --- a/aleph-client/src/contract/util.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Utilities for writing contract wrappers. - -use anyhow::{anyhow, Result}; -use contract_transcode::Value; -use sp_core::crypto::Ss58Codec; - -use crate::AccountId; - -/// Returns `Ok(u128)` if the given `Value` represents one, or `Err(_)` otherwise. -/// -/// ``` -/// # #![feature(assert_matches)] -/// # use std::assert_matches::assert_matches; -/// # use anyhow::anyhow; -/// # use aleph_client::contract::util::to_u128; -/// use contract_transcode::Value; -/// -/// assert_matches!(to_u128(Value::UInt(42)), Ok(42)); -/// assert_matches!(to_u128(Value::String("not a number".to_string())), Err(_)); -/// ``` -pub fn to_u128(value: Value) -> Result { - match value { - Value::UInt(value) => Ok(value), - _ => Err(anyhow!("Expected {:?} to be an integer", value)), - } -} - -/// Returns `Ok(AccountId)` if the given `Value` represents one, or `Err(_)` otherwise. -/// -/// ``` -/// # #![feature(assert_matches)] -/// # use std::assert_matches::assert_matches; -/// # use anyhow::anyhow; -/// # use aleph_client::contract::util::to_account_id; -/// use contract_transcode::Value; -/// -/// assert_matches!( -/// to_account_id(Value::Literal("5H8cjBBzCJrAvDn9LHZpzzJi2UKvEGC9VeVYzWX5TrwRyVCA".to_string())), -/// Ok(_) -/// ); -/// assert_matches!(to_account_id(Value::UInt(42)), Err(_)); -/// ``` -pub fn to_account_id(value: Value) -> Result { - match value { - Value::Literal(value) => Ok(AccountId::from_ss58check(&value)?), - _ => Err(anyhow!("Expected {:?} to be a string", value)), - } -} diff --git a/aleph-client/src/lib.rs b/aleph-client/src/lib.rs index 52275b752e..f6cef09d7e 100644 --- a/aleph-client/src/lib.rs +++ b/aleph-client/src/lib.rs @@ -4,6 +4,7 @@ use ac_primitives::{PlainTipExtrinsicParamsBuilder, SubstrateDefaultSignedExtra} pub use account::{get_free_balance, locks}; pub use balances::total_issuance; use codec::{Decode, Encode}; +pub use contract_transcode; pub use debug::print_storages; pub use elections::{ get_committee_seats, get_current_era_non_reserved_validators, diff --git a/bin/cliain/src/commands.rs b/bin/cliain/src/commands.rs index adeea04533..63ac4a22ec 100644 --- a/bin/cliain/src/commands.rs +++ b/bin/cliain/src/commands.rs @@ -16,7 +16,7 @@ pub struct ContractOptions { #[clap(long, default_value = "0")] pub balance: u128, /// The gas limit enforced when executing the constructor - #[clap(long, default_value = "1_000_000_000")] + #[clap(long, default_value = "1000000000")] pub gas_limit: u64, /// The maximum amount of balance that can be charged/reserved from the caller to pay for the storage consumed #[clap(long)] diff --git a/contracts/button/lib.rs b/contracts/button/lib.rs index a6f78d4e5b..745bfda9e6 100644 --- a/contracts/button/lib.rs +++ b/contracts/button/lib.rs @@ -5,7 +5,7 @@ mod errors; use ink_lang as ink; #[ink::contract] -mod button_game { +pub mod button_game { use access_control::{roles::Role, traits::AccessControlled, ACCESS_CONTROL_PUBKEY}; use game_token::MINT_SELECTOR; use ink_env::{ diff --git a/contracts/env/dev b/contracts/env/dev index a50c1b38cf..acb2ca9f90 100644 --- a/contracts/env/dev +++ b/contracts/env/dev @@ -8,7 +8,10 @@ export AUTHORITY_SEED=//Alice export LIFETIME=20 # mint this many ticket tokens -export TICKET_BALANCE=100 +export TICKET_BALANCE=100000000 # initial price of ticket on the marketplace export INITIAL_PRICE=69 + +# use dev-only hacks, like allowing the authority to mint game tokens on demand +export ENV_NAME=dev diff --git a/contracts/env/fe-benjamin b/contracts/env/fe-benjamin index e1d2f72a3c..17183c6504 100644 --- a/contracts/env/fe-benjamin +++ b/contracts/env/fe-benjamin @@ -12,3 +12,6 @@ export TICKET_BALANCE=100 # initial price of ticket on the marketplace export INITIAL_PRICE=69 + +# serious environment with no hacks +ENV_NAME=prod diff --git a/contracts/scripts/deploy.sh b/contracts/scripts/deploy.sh index 7f6e25e5b1..ea59c266fb 100755 --- a/contracts/scripts/deploy.sh +++ b/contracts/scripts/deploy.sh @@ -114,6 +114,9 @@ function deploy_button_game { cd "$CONTRACTS_PATH"/access_control cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$AUTHORITY" 'Owner('"$contract_address"')' --suri "$AUTHORITY_SEED" --skip-confirm + if [ "$ENV_NAME" = "dev" ]; then + cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$AUTHORITY" 'Minter('"$game_token"')' --suri "$AUTHORITY_SEED" --skip-confirm + fi cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$contract_address" 'Admin('"$marketplace"')' --suri "$AUTHORITY_SEED" --skip-confirm cargo contract call --url "$NODE" --contract "$ACCESS_CONTROL" --message grant_role --args "$contract_address" 'Minter('"$game_token"')' --suri "$AUTHORITY_SEED" --skip-confirm diff --git a/contracts/scripts/test.sh b/contracts/scripts/test.sh index 0da59d04f2..8bfc27de82 100755 --- a/contracts/scripts/test.sh +++ b/contracts/scripts/test.sh @@ -2,77 +2,26 @@ set -euo pipefail -# --- FUNCTIONS - -function play { - - local contract_name=$1 - local contract_address=$(cat "$CONTRACTS_PATH"/addresses.json | jq --raw-output ".$contract_name") - local ticket_address=$(cat "$CONTRACTS_PATH"/addresses.json | jq --raw-output ".${contract_name}_ticket") - - # airdrop initial tickets - - cd "$CONTRACTS_PATH"/ticket_token - - echo "sending ticket token" ${contract_name}_ticket "["$ticket_address"]" "to " $PLAYER1 - - cargo contract call --url $NODE --contract $ticket_address --message PSP22::transfer --args $PLAYER1 1 "[0]" --suri $AUTHORITY_SEED --skip-confirm - - echo "sending ticket token" ${contract_name}_ticket "["$ticket_address"]" "to " $PLAYER2 - - cargo contract call --url $NODE --contract $ticket_address --message PSP22::transfer --args $PLAYER2 1 "[0]" --suri $AUTHORITY_SEED --skip-confirm - - # give allowance for spending tickets to the game contract - - echo "allowing" $contract_name "["$contract_address"]" "to spend up to" $TICKET_BALANCE "of" ${contract_name}_ticket "["$ticket_address"]" "on behalf of" $PLAYER1 - - cargo contract call --url $NODE --contract $ticket_address --message PSP22::approve --args $contract_address $TICKET_BALANCE --suri $PLAYER1_SEED --skip-confirm - - echo "allowing" $contract_name "["$contract_address"]" "to spend up to" $TICKET_BALANCE "of" ${contract_name}_ticket "["$ticket_address"]" "on behalf of" $PLAYER2 - - cargo contract call --url $NODE --contract $ticket_address --message PSP22::approve --args $contract_address $TICKET_BALANCE --suri $PLAYER2_SEED --skip-confirm - -# TODO: uncomment when cargo contract doesn't break on parsing "foreign" events -# -# # play the game -# -# cd "$CONTRACTS_PATH"/button -# -# echo "calling press for" $contract_name "["$contract_address"]" "by" $PLAYER1_SEED -# -# cargo contract call --url $NODE --contract $contract_address --message press --suri $PLAYER1_SEED --skip-confirm -# -# sleep 1 -# -# echo "calling press for" $contract_name "["$contract_address "]" "by" $PLAYER2_SEED -# -# cargo contract call --url $NODE --contract $contract_address --message press --suri $PLAYER2_SEED --skip-confirm -# -# # --- WAIT FOR THE BUTTON DEATH -# -# sleep $(($LIFETIME + 1)) -# -# # --- TRIGGER GAME RESET -# -# cargo contract call --url $NODE --contract $contract_address --message reset --suri $AUTHORITY_SEED --skip-confirm -# -# echo "Done playing" $contract_name -} - -# --- ARGUMENTS - +E2E_PATH=$(pwd)/e2e-tests CONTRACTS_PATH=$(pwd)/contracts - -PLAYER1=5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH -PLAYER1_SEED=//0 -PLAYER2=5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY -PLAYER2_SEED=//Alice - -GAMES=(early_bird_special back_to_the_future the_pressiah_cometh) -for GAME in "${GAMES[@]}"; do - ( - play $GAME - )& -done +EARLY_BIRD_SPECIAL=$(jq --raw-output ".early_bird_special" < "$CONTRACTS_PATH"/addresses.json) +THE_PRESSIAH_COMETH=$(jq --raw-output ".the_pressiah_cometh" < "$CONTRACTS_PATH"/addresses.json) +BACK_TO_THE_FUTURE=$(jq --raw-output ".back_to_the_future" < "$CONTRACTS_PATH"/addresses.json) + +pushd "$E2E_PATH" + +RUST_LOG="aleph_e2e_client=info" cargo run --release -- \ + --test-cases marketplace \ + --test-cases button_game_reset \ + --test-cases early_bird_special \ + --test-cases the_pressiah_cometh \ + --test-cases back_to_the_future \ + --early-bird-special "$EARLY_BIRD_SPECIAL" \ + --the-pressiah-cometh "$THE_PRESSIAH_COMETH" \ + --back-to-the-future "$BACK_TO_THE_FUTURE" \ + --button-game-metadata ../contracts/button/target/ink/metadata.json \ + --ticket-token-metadata ../contracts/ticket_token/target/ink/metadata.json \ + --reward-token-metadata ../contracts/game_token/target/ink/metadata.json \ + --marketplace-metadata ../contracts/marketplace/target/ink/metadata.json exit $? diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index 6f0228c566..a52f2c8f65 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -97,17 +97,20 @@ version = "0.4.0" dependencies = [ "aleph_client", "anyhow", + "assert2", "clap", "env_logger 0.8.4", "frame-support", "frame-system", "hex", + "itertools", "log", "pallet-balances", "pallet-elections", "pallet-staking", "parity-scale-codec", "primitives", + "rand 0.8.5", "rayon", "serde_json", "sp-core 6.0.0 (git+https://github.com/Cardinal-Cryptography/substrate.git?branch=aleph-v0.9.26)", @@ -116,7 +119,7 @@ dependencies = [ [[package]] name = "aleph_client" -version = "1.9.0" +version = "1.10.0" dependencies = [ "ac-node-api", "ac-primitives", @@ -204,6 +207,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-trait" version = "0.1.58" @@ -2668,6 +2694,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 = "rustversion" version = "1.0.9" @@ -4334,6 +4369,12 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zeroize" version = "1.5.7" diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index d05960eaef..87c7fe086e 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" sp-core = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.26", default-features = false, features = ["full_crypto"] } sp-runtime = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.26", default-features = false } @@ -30,5 +33,5 @@ default = ["std"] std = [ "pallet-staking/std", "pallet-balances/std", - "primitives/std" + "primitives/std", ] diff --git a/e2e-tests/src/cases.rs b/e2e-tests/src/cases.rs index 0a1c55fc4c..eaf1f52c35 100644 --- a/e2e-tests/src/cases.rs +++ b/e2e-tests/src/cases.rs @@ -2,16 +2,19 @@ use crate::{ config::Config, test::{ authorities_are_staking as test_authorities_are_staking, - batch_transactions as test_batch_transactions, + back_to_the_future as test_back_to_the_future, + batch_transactions as test_batch_transactions, button_game_reset as test_button_game_reset, change_stake_and_force_new_era as test_change_stake_and_force_new_era, change_validators as test_change_validators, channeling_fee_and_tip as test_channeling_fee_and_tip, disable_node as test_disable_node, + early_bird_special as test_early_bird_special, era_payouts_calculated_correctly as test_era_payout, era_validators as test_era_validators, fee_calculation as test_fee_calculation, finalization as test_finalization, - force_new_era as test_force_new_era, points_basic as test_points_basic, - points_stake_change as test_points_stake_change, + force_new_era as test_force_new_era, marketplace as test_marketplace, + points_basic as test_points_basic, points_stake_change as test_points_stake_change, staking_era_payouts as test_staking_era_payouts, - staking_new_validator as test_staking_new_validator, token_transfer as test_token_transfer, + staking_new_validator as test_staking_new_validator, + the_pressiah_cometh as test_the_pressiah_cometh, token_transfer as test_token_transfer, treasury_access as test_treasury_access, validators_rotate as test_validators_rotate, }, }; @@ -55,5 +58,10 @@ pub fn possible_test_cases() -> PossibleTestCases { "authorities_are_staking", test_authorities_are_staking as TestCase, ), + ("button_game_reset", test_button_game_reset as TestCase), + ("early_bird_special", test_early_bird_special as TestCase), + ("back_to_the_future", test_back_to_the_future as TestCase), + ("the_pressiah_cometh", test_the_pressiah_cometh as TestCase), + ("marketplace", test_marketplace as TestCase), ] } diff --git a/e2e-tests/src/config.rs b/e2e-tests/src/config.rs index 451e8300a8..f69aae60e4 100644 --- a/e2e-tests/src/config.rs +++ b/e2e-tests/src/config.rs @@ -62,19 +62,37 @@ impl Config { pub struct TestCaseParams { /// Desired number of reserved seats for validators, may be set within the test. #[clap(long)] - reserved_seats: Option, + pub reserved_seats: Option, /// Desired number of non-reserved seats for validators, may be set within the test. #[clap(long)] - non_reserved_seats: Option, -} + pub non_reserved_seats: Option, -impl TestCaseParams { - pub fn reserved_seats(&self) -> Option { - self.reserved_seats - } + /// Address of the Early Bird Special game contract, only used by button game tests. + #[clap(long)] + pub early_bird_special: Option, - pub fn non_reserved_seats(&self) -> Option { - self.non_reserved_seats - } + /// Address of the Back to the Future game contract, only used by button game tests. + #[clap(long)] + pub back_to_the_future: Option, + + /// Address of the The Pressiah Cometh game contract, only used by button game tests. + #[clap(long)] + pub the_pressiah_cometh: Option, + + /// Path to the button game metadata file. Only used by button tests. + #[clap(long)] + pub button_game_metadata: Option, + + /// Path to the ticket token metadata file. Only used by button tests. + #[clap(long)] + pub ticket_token_metadata: Option, + + /// Path to the reward token metadata file. Only used by button tests. + #[clap(long)] + pub reward_token_metadata: Option, + + /// Path to the marketplace metadata file. Only used by button tests. + #[clap(long)] + pub marketplace_metadata: Option, } diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 2ee3b4dc74..f4b24f6c7f 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -1,3 +1,7 @@ +#![feature(pattern)] + +extern crate core; + pub use cases::{possible_test_cases, PossibleTestCases}; pub use config::Config; diff --git a/e2e-tests/src/test/button_game/contracts.rs b/e2e-tests/src/test/button_game/contracts.rs new file mode 100644 index 0000000000..8bcde90b04 --- /dev/null +++ b/e2e-tests/src/test/button_game/contracts.rs @@ -0,0 +1,192 @@ +use aleph_client::{ + contract::ContractInstance, AnyConnection, Balance, Connection, SignedConnection, +}; +use anyhow::{Context, Result}; +use sp_core::crypto::{AccountId32 as AccountId, Ss58Codec}; + +use crate::Config; + +/// A wrapper around a button game contract. +/// +/// The methods on this type match contract methods. +#[derive(Debug)] +pub(super) struct ButtonInstance { + contract: ContractInstance, +} + +impl ButtonInstance { + pub fn new(config: &Config, button_address: &Option) -> Result { + let button_address = button_address + .clone() + .context("Button game address not set.")?; + let button_address = AccountId::from_string(&button_address)?; + let metadata_path = config + .test_case_params + .button_game_metadata + .clone() + .context("Button game metadata path not set.")?; + Ok(Self { + contract: ContractInstance::new(button_address, &metadata_path)?, + }) + } + + pub fn deadline(&self, conn: &C) -> Result { + self.contract.contract_read0(conn, "deadline")?.try_into() + } + + pub fn is_dead(&self, conn: &C) -> Result { + self.contract.contract_read0(conn, "is_dead")?.try_into() + } + + pub fn ticket_token(&self, conn: &C) -> Result { + self.contract + .contract_read0(conn, "ticket_token")? + .try_into() + } + + pub fn reward_token(&self, conn: &C) -> Result { + self.contract + .contract_read0(conn, "reward_token")? + .try_into() + } + + pub fn marketplace(&self, conn: &C) -> Result { + self.contract + .contract_read0(conn, "marketplace")? + .try_into() + } + + pub fn press(&self, conn: &SignedConnection) -> Result<()> { + self.contract.contract_exec0(conn, "press") + } + + pub fn reset(&self, conn: &SignedConnection) -> Result<()> { + self.contract.contract_exec0(conn, "reset") + } +} + +impl<'a> From<&'a ButtonInstance> for &'a ContractInstance { + fn from(button: &'a ButtonInstance) -> Self { + &button.contract + } +} + +impl From<&ButtonInstance> for AccountId { + fn from(button: &ButtonInstance) -> Self { + button.contract.address().clone() + } +} + +/// A wrapper around a PSP22 contract. +/// +/// The methods on this type match contract methods. +#[derive(Debug)] +pub(super) struct PSP22TokenInstance { + contract: ContractInstance, +} + +impl PSP22TokenInstance { + pub fn new(address: AccountId, metadata_path: &Option) -> Result { + let metadata_path = metadata_path + .as_ref() + .context("PSP22Token metadata not set.")?; + Ok(Self { + contract: ContractInstance::new(address, metadata_path)?, + }) + } + + pub fn transfer(&self, conn: &SignedConnection, to: &AccountId, amount: Balance) -> Result<()> { + self.contract.contract_exec( + conn, + "PSP22::transfer", + &[to.to_string().as_str(), amount.to_string().as_str(), "0x00"], + ) + } + + pub fn mint(&self, conn: &SignedConnection, to: &AccountId, amount: Balance) -> Result<()> { + self.contract.contract_exec( + conn, + "PSP22Mintable::mint", + &[to.to_string().as_str(), amount.to_string().as_str()], + ) + } + + pub fn approve( + &self, + conn: &SignedConnection, + spender: &AccountId, + value: Balance, + ) -> Result<()> { + self.contract.contract_exec( + conn, + "PSP22::approve", + &[spender.to_string().as_str(), value.to_string().as_str()], + ) + } + + pub fn balance_of(&self, conn: &Connection, account: &AccountId) -> Result { + self.contract + .contract_read(conn, "PSP22::balance_of", &[account.to_string().as_str()])? + .try_into() + } +} + +impl<'a> From<&'a PSP22TokenInstance> for &'a ContractInstance { + fn from(token: &'a PSP22TokenInstance) -> Self { + &token.contract + } +} + +impl From<&PSP22TokenInstance> for AccountId { + fn from(token: &PSP22TokenInstance) -> AccountId { + token.contract.address().clone() + } +} + +/// A wrapper around a marketplace contract instance. +/// +/// The methods on this type match contract methods. +#[derive(Debug)] +pub(super) struct MarketplaceInstance { + contract: ContractInstance, +} + +impl MarketplaceInstance { + pub fn new(address: AccountId, metadata_path: &Option) -> Result { + Ok(Self { + contract: ContractInstance::new( + address, + metadata_path + .as_ref() + .context("Marketplace metadata not set.")?, + )?, + }) + } + + pub fn reset(&self, conn: &SignedConnection) -> Result<()> { + self.contract.contract_exec0(conn, "reset") + } + + pub 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()]) + } + + pub fn price(&self, conn: &C) -> Result { + self.contract.contract_read0(conn, "price")?.try_into() + } +} + +impl<'a> From<&'a MarketplaceInstance> for &'a ContractInstance { + fn from(marketplace: &'a MarketplaceInstance) -> Self { + &marketplace.contract + } +} + +impl From<&MarketplaceInstance> for AccountId { + fn from(marketplace: &MarketplaceInstance) -> AccountId { + marketplace.contract.address().clone() + } +} diff --git a/e2e-tests/src/test/button_game/helpers.rs b/e2e-tests/src/test/button_game/helpers.rs new file mode 100644 index 0000000000..2ca2e0ae14 --- /dev/null +++ b/e2e-tests/src/test/button_game/helpers.rs @@ -0,0 +1,291 @@ +use std::{ + fmt::Debug, + ops::Deref, + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError}, + Arc, + }, + thread, + time::{Duration, Instant}, +}; + +use aleph_client::{ + contract::event::{listen_contract_events, subscribe_events, ContractEvent}, + AccountId, AnyConnection, Balance, Connection, KeyPair, SignedConnection, XtStatus, +}; +use anyhow::{bail, Result}; +use itertools::Itertools; +use log::{info, warn}; +use rand::Rng; +use sp_core::Pair; + +use super::contracts::{ButtonInstance, PSP22TokenInstance}; +use crate::{test::button_game::contracts::MarketplaceInstance, Config}; + +/// A wrapper around a KeyPair for purposes of converting to an account id in tests. +pub struct KeyPairWrapper(KeyPair); + +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.public().into() + } +} + +/// Creates a copy of the `connection` signed by `signer` +pub fn sign(conn: &C, signer: &KeyPair) -> SignedConnection { + SignedConnection::from_any_connection(conn, signer.clone()) +} + +/// Returns a ticket token instance for the given button instance +pub(super) fn ticket_token( + conn: &C, + button: &ButtonInstance, + config: &Config, +) -> Result { + PSP22TokenInstance::new( + button.ticket_token(conn)?, + &config.test_case_params.ticket_token_metadata, + ) +} + +/// Returns a reward token instance for the given button instance +pub(super) fn reward_token( + conn: &C, + button: &ButtonInstance, + config: &Config, +) -> Result { + PSP22TokenInstance::new( + button.reward_token(conn)?, + &config.test_case_params.reward_token_metadata, + ) +} + +/// Returns a marketplace instance for the given button instance +pub(super) fn marketplace( + conn: &C, + button: &ButtonInstance, + config: &Config, +) -> Result { + MarketplaceInstance::new( + button.marketplace(conn)?, + &config.test_case_params.marketplace_metadata, + ) +} + +/// 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 fn transfer(conn: &C, from: &KeyPair, to: &KeyPair, amount: Balance) { + aleph_client::balances_transfer( + &SignedConnection::from_any_connection(conn, from.clone()), + &to.public().into(), + amount, + XtStatus::InBlock, + ); +} + +/// 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 +} + +pub(super) struct ButtonTestContext { + pub button: Arc, + pub ticket_token: Arc, + pub reward_token: Arc, + pub marketplace: Arc, + pub conn: Connection, + /// A [BufferedReceiver] preconfigured to listen for events of `button`, `ticket_token`, `reward_token`, and + /// `marketplace`. + pub events: BufferedReceiver>, + /// The authority owning the initial supply of tickets and with the power to mint game tokens. + pub authority: KeyPairWrapper, + /// A random account with some money for transaction fees. + pub player: KeyPairWrapper, +} + +/// Sets up a number of objects commonly used in button game tests. +pub(super) fn setup_button_test( + config: &Config, + button_contract_address: &Option, +) -> Result { + let conn = config.get_first_signed_connection().as_connection(); + + let authority = KeyPairWrapper(aleph_client::keypair_from_string(&config.sudo_seed)); + let player = random_account(); + + let button = Arc::new(ButtonInstance::new(config, button_contract_address)?); + let ticket_token = Arc::new(ticket_token(&conn, &button, config)?); + let reward_token = Arc::new(reward_token(&conn, &button, config)?); + let marketplace = Arc::new(marketplace(&conn, &button, config)?); + + let c1 = button.clone(); + let c2 = ticket_token.clone(); + let c3 = reward_token.clone(); + let c4 = marketplace.clone(); + + let subscription = subscribe_events(&conn)?; + let (events_tx, events_rx) = channel(); + + thread::spawn(move || { + let contract_metadata = vec![ + c1.as_ref().into(), + c2.as_ref().into(), + c3.as_ref().into(), + c4.as_ref().into(), + ]; + + listen_contract_events(subscription, &contract_metadata, None, |event| { + let _ = events_tx.send(event); + }); + }); + + let events = BufferedReceiver::new(events_rx, Duration::from_secs(3)); + + transfer(&conn, &authority, &player, alephs(100)); + + Ok(ButtonTestContext { + button, + ticket_token, + reward_token, + marketplace, + conn, + events, + authority, + player, + }) +} + +/// A receiver where it's possible to wait for messages out of order. +pub struct BufferedReceiver { + buffer: Vec, + receiver: Receiver, + default_timeout: Duration, +} + +impl BufferedReceiver { + pub fn new(receiver: Receiver, default_timeout: Duration) -> Self { + Self { + buffer: Vec::new(), + receiver, + default_timeout, + } + } + + /// Receive a message satisfying `filter`. + /// + /// If such a message was received earlier and is waiting in the buffer, returns the message immediately and removes + /// it from the buffer. Otherwise, listens for messages for `default_timeout`, storing them in the buffer. If a + /// matching message is found during that time, it is returned. If not, `Err(RecvTimeoutError)` is returned. + pub fn recv_timeout bool>(&mut self, filter: F) -> Result { + match self.buffer.iter().find_position(|m| filter(m)) { + Some((i, _)) => Ok(self.buffer.remove(i)), + None => { + let mut timeout = self.default_timeout; + + while timeout > Duration::from_millis(0) { + let start = Instant::now(); + match self.receiver.recv_timeout(timeout) { + Ok(msg) => { + if filter(&msg) { + return Ok(msg); + } else { + self.buffer.push(msg); + timeout -= Instant::now().duration_since(start); + } + } + Err(_) => return Err(RecvTimeoutError::Timeout), + } + } + + Err(RecvTimeoutError::Timeout) + } + } + } +} + +/// Wait until `button` is dead. +/// +/// Returns `Err(_)` if the button doesn't die within 30 seconds. +pub(super) fn wait_for_death(conn: &C, button: &ButtonInstance) -> Result<()> { + info!("Waiting for button to die"); + assert_soon(|| button.is_dead(conn), Duration::from_secs(30)) +} + +/// Wait until `check` returns true. +/// +/// Repeatedly performs `check` (busy wait) until `timeout` elapses. Returns `Ok(())` if `check` returns true during +/// that time, `Err(_)` otherwise. +pub fn assert_soon Result>(check: F, timeout: Duration) -> Result<()> { + let start = Instant::now(); + while !check()? { + if Instant::now().duration_since(start) > timeout { + bail!("Condition not met within timeout") + } + } + Ok(()) +} + +/// Asserts that a message with `id` is received (within `events.default_timeout`) and returns it. +pub fn assert_recv_id( + events: &mut BufferedReceiver>, + id: &str, +) -> ContractEvent { + assert_recv( + events, + |event| event.ident == Some(id.to_string()), + &format!("Expected {:?} contract event", id), + ) +} + +/// Asserts that a message matching `filter` is received (within `events.default_timeout`) and returns it. +pub fn assert_recv bool>( + events: &mut BufferedReceiver>, + filter: F, + context: &str, +) -> T { + let event = recv_timeout_with_log(events, filter); + + assert!(event.is_ok(), "{}", context); + + event.unwrap() +} + +/// Asserts that a message with `id` is not received (within `events.default_timeout`). +pub fn refute_recv_id(events: &mut BufferedReceiver>, id: &str) { + if let Ok(event) = recv_timeout_with_log(events, |event| event.ident == Some(id.to_string())) { + panic!("Received unexpected event {:?}", event); + } +} + +fn recv_timeout_with_log bool>( + events: &mut BufferedReceiver>, + filter: F, +) -> Result { + match events.recv_timeout(|event_or_error| { + if event_or_error.is_ok() { + info!("Received contract event {:?}", event_or_error); + } else { + warn!("Contract event error {:?}", event_or_error); + } + + event_or_error.as_ref().map(&filter).unwrap_or(false) + }) { + Ok(event) => Ok(event.unwrap()), + Err(err) => bail!(err), + } +} diff --git a/e2e-tests/src/test/button_game/mod.rs b/e2e-tests/src/test/button_game/mod.rs new file mode 100644 index 0000000000..a2857c1667 --- /dev/null +++ b/e2e-tests/src/test/button_game/mod.rs @@ -0,0 +1,222 @@ +use std::{thread, time::Duration}; + +use aleph_client::{contract_transcode::Value, AccountId}; +use anyhow::Result; +use assert2::{assert, let_assert}; +use helpers::sign; +use log::info; + +use crate::{ + test::button_game::helpers::{ + assert_recv, assert_recv_id, refute_recv_id, setup_button_test, wait_for_death, + ButtonTestContext, + }, + Config, +}; + +mod contracts; +mod helpers; + +/// Tests trading on the marketplace. +/// +/// The scenario: +/// +/// 1. Buys a ticket without setting the max price (this should succeed). +/// 2. Tries to buy a ticket with setting the max price too low (this should fail). +/// 3. Tries to buy a ticket with setting the max price appropriately (this should succeed). +pub fn marketplace(config: &Config) -> Result<()> { + let ButtonTestContext { + conn, + authority, + player, + marketplace, + ticket_token, + reward_token, + mut events, + .. + } = setup_button_test(config, &config.test_case_params.early_bird_special)?; + let player = &player; + + marketplace.reset(&sign(&conn, &authority))?; + assert_recv_id(&mut events, "Reset"); + ticket_token.transfer(&sign(&conn, &authority), &marketplace.as_ref().into(), 2)?; + + let early_price = marketplace.price(&conn)?; + thread::sleep(Duration::from_secs(2)); + let later_price = marketplace.price(&conn)?; + assert!(later_price < early_price); + + let player_balance = 100 * later_price; + reward_token.mint(&sign(&conn, &authority), &player.into(), player_balance)?; + reward_token.approve( + &sign(&conn, player), + &marketplace.as_ref().into(), + later_price, + )?; + marketplace.buy(&sign(&conn, player), None)?; + + let event = assert_recv_id(&mut events, "Bought"); + let player_account: AccountId = player.into(); + assert!(event.contract == marketplace.as_ref().into()); + let_assert!(Some(&Value::UInt(price)) = event.data.get("price")); + assert!(price <= later_price); + let_assert!(Some(Value::Literal(acc_id)) = event.data.get("account_id")); + assert!(acc_id == &player_account.to_string()); + assert!(ticket_token.balance_of(&conn, &player.into())? == 1); + assert!(reward_token.balance_of(&conn, &player.into())? <= player_balance - price); + assert!(marketplace.price(&conn)? > price); + + let latest_price = marketplace.price(&conn)?; + + info!("Setting max price too low"); + marketplace.buy(&sign(&conn, player), Some(latest_price / 2))?; + refute_recv_id(&mut events, "Bought"); + assert!(ticket_token.balance_of(&conn, &player.into())? == 1); + + info!("Setting max price high enough"); + marketplace.buy(&sign(&conn, player), Some(latest_price * 2))?; + assert_recv_id(&mut events, "Bought"); + assert!(ticket_token.balance_of(&conn, &player.into())? == 2); + + Ok(()) +} + +/// Tests resetting the button game. +pub fn button_game_reset(config: &Config) -> Result<()> { + let ButtonTestContext { + conn, + button, + mut events, + authority, + marketplace, + ticket_token, + .. + } = setup_button_test(config, &config.test_case_params.early_bird_special)?; + + let deadline_old = button.deadline(&conn)?; + let marketplace_initial = ticket_token.balance_of(&conn, &marketplace.as_ref().into())?; + ticket_token.transfer(&sign(&conn, &authority), &button.as_ref().into(), 1)?; + + wait_for_death(&conn, &button)?; + button.reset(&sign(&conn, &authority))?; + + let _ = assert_recv( + &mut events, + |event| { + event.contract == button.as_ref().into() && event.ident == Some("GameReset".to_string()) + }, + "GameReset event", + ); + let _ = assert_recv( + &mut events, + |event| { + event.contract == marketplace.as_ref().into() + && event.ident == Some("Reset".to_string()) + }, + "Marketplace Reset event", + ); + let deadline_new = button.deadline(&conn)?; + assert!(deadline_new > deadline_old); + assert!( + ticket_token.balance_of(&conn, &marketplace.as_ref().into())? == marketplace_initial + 1 + ); + + Ok(()) +} + +pub fn early_bird_special(config: &Config) -> Result<()> { + button_game_play( + config, + &config.test_case_params.early_bird_special, + |early_presser_score, late_presser_score| { + assert!(early_presser_score > late_presser_score); + }, + ) +} + +pub fn back_to_the_future(config: &Config) -> Result<()> { + button_game_play( + config, + &config.test_case_params.back_to_the_future, + |early_presser_score, late_presser_score| { + assert!(early_presser_score < late_presser_score); + }, + ) +} + +pub fn the_pressiah_cometh(config: &Config) -> Result<()> { + button_game_play( + config, + &config.test_case_params.the_pressiah_cometh, + |early_presser_score, late_presser_score| { + assert!(early_presser_score == 1); + assert!(late_presser_score == 2); + }, + ) +} + +/// Tests a basic scenario of playing the game. +/// +/// The scenario: +/// +/// 1. Resets the button. +/// 2. Gives 2 tickets to the player. +/// 3. Presses the button. +/// 4. Waits a bit and presses the button again. +/// 5. Waits until the button dies and checks the pressiah's score. +/// +/// Passes the scores received by an early presser and late presser to `score_check` so that different scoring rules +/// can be tested generically. +fn button_game_play( + config: &Config, + button_contract_address: &Option, + score_check: F, +) -> Result<()> { + let ButtonTestContext { + conn, + button, + mut events, + authority, + ticket_token, + reward_token, + player, + .. + } = setup_button_test(config, button_contract_address)?; + let player = &player; + + ticket_token.transfer(&sign(&conn, &authority), &player.into(), 2)?; + wait_for_death(&conn, &button)?; + button.reset(&sign(&conn, &authority))?; + let old_button_balance = ticket_token.balance_of(&conn, &button.as_ref().into())?; + + ticket_token.approve(&sign(&conn, player), &button.as_ref().into(), 2)?; + button.press(&sign(&conn, player))?; + + let event = assert_recv_id(&mut events, "ButtonPressed"); + let player_account: AccountId = player.into(); + let_assert!(Some(&Value::UInt(early_presser_score)) = event.data.get("score")); + assert!(event.data.get("by") == Some(&Value::Literal(player_account.to_string()))); + assert!(reward_token.balance_of(&conn, &player.into())? == early_presser_score); + assert!(early_presser_score > 0); + assert!(ticket_token.balance_of(&conn, &player.into())? == 1); + assert!(ticket_token.balance_of(&conn, &button.as_ref().into())? == old_button_balance + 1); + + info!("Waiting before pressing again"); + thread::sleep(Duration::from_secs(5)); + + button.press(&sign(&conn, player))?; + let event = assert_recv_id(&mut events, "ButtonPressed"); + let_assert!(Some(&Value::UInt(late_presser_score)) = event.data.get("score")); + score_check(early_presser_score, late_presser_score); + let total_score = early_presser_score + late_presser_score; + assert!(reward_token.balance_of(&conn, &player.into())? == total_score); + + wait_for_death(&conn, &button)?; + button.reset(&sign(&conn, &authority))?; + assert_recv_id(&mut events, "Reset"); + + let pressiah_score = total_score / 4; + assert!(reward_token.balance_of(&conn, &player.into())? == total_score + pressiah_score); + + Ok(()) +} diff --git a/e2e-tests/src/test/electing_validators.rs b/e2e-tests/src/test/electing_validators.rs index 3f2e14b366..fa5f27164d 100644 --- a/e2e-tests/src/test/electing_validators.rs +++ b/e2e-tests/src/test/electing_validators.rs @@ -145,11 +145,11 @@ pub fn authorities_are_staking(config: &Config) -> anyhow::Result<()> { // `MinimumValidatorCount` from `pallet_staking`, set in chain spec. let min_validator_count = get_minimum_validator_count(&root_connection); - let reserved_seats = match config.test_case_params.reserved_seats() { + let reserved_seats = match config.test_case_params.reserved_seats { Some(seats) => seats, None => RESERVED_SEATS_DEFAULT, }; - let non_reserved_seats = match config.test_case_params.non_reserved_seats() { + let non_reserved_seats = match config.test_case_params.non_reserved_seats { Some(seats) => seats, None => NON_RESERVED_SEATS_DEFAULT, }; diff --git a/e2e-tests/src/test/mod.rs b/e2e-tests/src/test/mod.rs index 2be596662b..790666f615 100644 --- a/e2e-tests/src/test/mod.rs +++ b/e2e-tests/src/test/mod.rs @@ -1,3 +1,6 @@ +pub use button_game::{ + back_to_the_future, button_game_reset, early_bird_special, marketplace, the_pressiah_cometh, +}; pub use electing_validators::authorities_are_staking; pub use era_payout::era_payouts_calculated_correctly; pub use era_validators::era_validators; @@ -13,6 +16,7 @@ pub use utility::batch_transactions; pub use validators_change::change_validators; pub use validators_rotate::validators_rotate; +mod button_game; mod electing_validators; mod era_payout; mod era_validators;