From 4136342dfae4452c4ff1fc1c7da9635128879cd0 Mon Sep 17 00:00:00 2001 From: mihaicalinluca Date: Fri, 6 Oct 2023 09:38:03 +0200 Subject: [PATCH 1/3] added tests and small fix in sendReward function --- contracts/mvx-game-sc/src/owner.rs | 2 +- .../tests/game_sc_blackbox_tests.rs | 479 +++++++++++++++++- 2 files changed, 456 insertions(+), 25 deletions(-) diff --git a/contracts/mvx-game-sc/src/owner.rs b/contracts/mvx-game-sc/src/owner.rs index 6d0f69eb..af025856 100644 --- a/contracts/mvx-game-sc/src/owner.rs +++ b/contracts/mvx-game-sc/src/owner.rs @@ -35,7 +35,7 @@ pub trait OwnerModule: crate::private::PrivateModule + crate::storage::StorageMo for (winner, percentage) in val.into_iter() { let reward_per_winner = - &BigUint::from(percentage) * &total_wager / &BigUint::from(100u64); + &BigUint::from(percentage) * &total_wager / &BigUint::from(10000u64); self.send() .direct(&winner, &token_id, 0u64, &reward_per_winner); } diff --git a/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs b/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs index 41c0b50e..98ab202d 100644 --- a/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs +++ b/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs @@ -1,11 +1,15 @@ use multiversx_sc::{ codec::multi_types::OptionalValue, storage::mappers::SingleValue, - types::{EgldOrEsdtTokenIdentifier, TokenIdentifier}, + types::{ + Address, BigUint, EgldOrEsdtTokenIdentifier, ManagedAddress, MultiValueEncoded, + TokenIdentifier, + }, }; -use multiversx_sc_scenario::{api::StaticApi, num_bigint::BigUint, scenario_model::*, *}; +use multiversx_sc_scenario::{api::StaticApi, scenario_model::*, *}; use mvx_game_sc::{ - storage::ProxyTrait, + owner::ProxyTrait, + storage::ProxyTrait as _, types::{GameSettings, Status}, ProxyTrait as _, }; @@ -17,6 +21,9 @@ const TOKEN_ID_BY: &[u8] = b"GAME-123456"; const STARTING_FEE: u64 = 20u64; const USER1_ADDR: &str = "address:user1"; const USER2_ADDR: &str = "address:user2"; +const USER3_ADDR: &str = "address:user3"; +const USER4_ADDR: &str = "address:user4"; +const USER5_ADDR: &str = "address:user5"; const OWNER_ADDR: &str = "address:owner"; const GAME_SC_ADDR: &str = "sc:mvx_game_sc"; @@ -30,9 +37,12 @@ fn world() -> ScenarioWorld { struct GameContractState { world: ScenarioWorld, - user1: AddressValue, - user2: AddressValue, - owner: AddressValue, + user1: Address, + user2: Address, + user3: Address, + user4: Address, + user5: Address, + owner: Address, } impl GameContractState { @@ -60,17 +70,44 @@ impl GameContractState { .nonce(3) .balance(BALANCE) .esdt_balance(TOKEN_ID, BALANCE), + ) + .put_account( + USER3_ADDR, + Account::new() + .nonce(4) + .balance(BALANCE) + .esdt_balance(TOKEN_ID, BALANCE), + ) + .put_account( + USER4_ADDR, + Account::new() + .nonce(5) + .balance(BALANCE) + .esdt_balance(TOKEN_ID, BALANCE), + ) + .put_account( + USER5_ADDR, + Account::new() + .nonce(6) + .balance(BALANCE) + .esdt_balance(TOKEN_ID, BALANCE), ), ); - let user1 = AddressValue::from(USER1_ADDR); - let user2 = AddressValue::from(USER2_ADDR); - let owner = AddressValue::from(OWNER_ADDR); + let user1 = AddressValue::from(USER1_ADDR).to_address(); + let user2 = AddressValue::from(USER2_ADDR).to_address(); + let user3 = AddressValue::from(USER3_ADDR).to_address(); + let user4 = AddressValue::from(USER4_ADDR).to_address(); + let user5 = AddressValue::from(USER5_ADDR).to_address(); + let owner = AddressValue::from(OWNER_ADDR).to_address(); Self { world, user1, user2, + user3, + user4, + user5, owner, } } @@ -102,7 +139,7 @@ impl GameContractState { waiting_time: u64, number_of_players_min: u64, number_of_players_max: u64, - wager: BigUint, + wager: BigUint, caller: &str, game_sc: &mut ContractInfo>, ) -> &mut Self { @@ -110,7 +147,7 @@ impl GameContractState { ScCallStep::new() .from(caller) .to(GAME_SC_ADDR) - .esdt_transfer(TOKEN_ID, 0u64, BigUint::from(STARTING_FEE)) + .esdt_transfer(TOKEN_ID, 0u64, BigUint::::from(STARTING_FEE)) .call(game_sc.create_game( waiting_time, number_of_players_min, @@ -127,17 +164,95 @@ impl GameContractState { &mut self, game_id: u64, caller: &str, - amount: &BigUint, + amount: BigUint, game_sc: &mut ContractInfo>, + expected_error: OptionalValue<(&str, &str)>, ) -> &mut Self { - self.world.sc_call( - ScCallStep::new() - .from(caller) - .to(GAME_SC_ADDR) - .esdt_transfer(TOKEN_ID, 0u64, amount) - .call(game_sc.join_game(game_id)) - .expect(TxExpect::ok().no_result()), - ); + match expected_error { + OptionalValue::Some(val) => { + self.world.sc_call( + ScCallStep::new() + .from(caller) + .to(GAME_SC_ADDR) + .esdt_transfer(TOKEN_ID, 0u64, amount) + .call(game_sc.join_game(game_id)) + .expect(TxExpect::err(val.0, val.1)), + ); + } + OptionalValue::None => { + self.world.sc_call( + ScCallStep::new() + .from(caller) + .to(GAME_SC_ADDR) + .esdt_transfer(TOKEN_ID, 0u64, amount) + .call(game_sc.join_game(game_id)) + .expect(TxExpect::ok().no_result()), + ); + } + } + + self + } + + fn claim_back_wager( + &mut self, + game_id: u64, + caller: &str, + game_sc: &mut ContractInfo>, + expected_error: OptionalValue<(&str, &str)>, + ) -> &mut Self { + match expected_error { + OptionalValue::Some(val) => { + self.world.sc_call( + ScCallStep::new() + .from(caller) + .to(GAME_SC_ADDR) + .call(game_sc.claim_back_wager(game_id)) + .expect(TxExpect::err(val.0, val.1)), + ); + } + OptionalValue::None => { + self.world.sc_call( + ScCallStep::new() + .from(caller) + .to(GAME_SC_ADDR) + .call(game_sc.claim_back_wager(game_id)) + .expect(TxExpect::ok().no_result()), + ); + } + } + + self + } + + fn send_reward( + &mut self, + game_id: u64, + game_sc: &mut ContractInfo>, + winners: OptionalValue, u64)>>, + expected_error: OptionalValue<(&str, &str)>, + ) -> &mut Self { + match expected_error { + OptionalValue::Some(val) => { + self.world.sc_call( + ScCallStep::new() + .from(OWNER_ADDR) + .to(GAME_SC_ADDR) + .call(game_sc.send_reward(game_id, winners)) + .expect(TxExpect::err(val.0, val.1)), + ); + } + OptionalValue::None => { + self.world.sc_call( + ScCallStep::new() + .from(OWNER_ADDR) + .to(GAME_SC_ADDR) + .call(game_sc.send_reward(game_id, winners)) + .expect(TxExpect::ok().no_result()), + ); + } + } + self } } @@ -159,9 +274,12 @@ fn game_sc_simple_game_flow() { let number_of_players_min = 1u64; let number_of_players_max = 4u64; let wager = BigUint::from(100u64); - let _ = state.user1; - let _ = state.user2; - let _ = state.owner; + let user1 = state.user1.clone(); + let user2 = state.user2.clone(); + let _ = state.user3; + let _ = state.user4; + let _ = state.user5; + let _ = state.owner.clone(); //deploy state.deploy(&mut game_sc); @@ -193,7 +311,13 @@ fn game_sc_simple_game_flow() { ); //user1 tries to join the game, timestamp is ok, max players not reached, should work - state.join_game(1u64, USER1_ADDR, &wager, &mut game_sc); + state.join_game( + 1u64, + USER1_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::None, + ); //min number of players reached, game should be valid let game_settings: SingleValue> = game_sc @@ -203,5 +327,312 @@ fn game_sc_simple_game_flow() { .execute(&mut state.world); assert_eq!(game_settings.into().status, Status::Valid); + + //user2 tries to join the game, shuld work + state.join_game(1u64, USER2_ADDR, wager, &mut game_sc, OptionalValue::None); + + //both users should be in players vec + let players: MultiValueEncoded> = game_sc + .players(1u64) + .into_vm_query() + .expect(TxExpect::ok()) + .execute(&mut state.world); + let players_vec = players.to_vec(); + + assert_eq!( + players_vec.contains(&ManagedAddress::from(user1.clone())), + true + ); + assert_eq!( + players_vec.contains(&ManagedAddress::from(user2.clone())), + true + ); + + //game should be in users'storage + let games_per_user1: MultiValueEncoded = game_sc + .games_per_user(&ManagedAddress::from(user1)) + .into_vm_query() + .expect(TxExpect::ok()) + .execute(&mut state.world); + let games_per_user1_vec = games_per_user1.to_vec(); + + let games_per_user2: MultiValueEncoded = game_sc + .games_per_user(&ManagedAddress::from(user2)) + .into_vm_query() + .expect(TxExpect::ok()) + .execute(&mut state.world); + let games_per_user2_vec = games_per_user2.to_vec(); + + assert_eq!( + games_per_user1_vec.contains(&1u64) && games_per_user2_vec.contains(&1u64), + true + ); +} + +#[test] +fn game_sc_complex_flow() { + let mut state = GameContractState::new(); + let mut game_sc = ContractInfo::>::new(GAME_SC_ADDR); + + //game settings + let waiting_time = 100u64; // => timestamp 102 should be out of waiting time + let number_of_players_min = 2u64; + let number_of_players_max = 4u64; + let wager = BigUint::from(100u64); + let diff_amount = BigUint::::from(5u64); + + //users + let _user1 = state.user1.clone(); + let _user2 = state.user2.clone(); + let _user3 = state.user3.clone(); + let _user4 = state.user4.clone(); + let _user5 = state.user5.clone(); + let _owner = state.owner.clone(); + + //deploy + state.deploy(&mut game_sc); + + //set now = 1 + state + .world + .set_state_step(SetStateStep::new().block_timestamp(1u64)); + + //create first game + state.create_game( + waiting_time, + number_of_players_min, + number_of_players_max, + wager.clone(), + OWNER_ADDR, + &mut game_sc, + ); + + //user1 joins + state.join_game( + 1u64, + USER1_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::None, + ); + + let game_settings: SingleValue> = game_sc + .game_settings(1u64) + .into_vm_query() + .expect(TxExpect::ok()) + .execute(&mut state.world); + + assert_eq!(game_settings.into().status, Status::Invalid); + + //user1 tries to claim back wager, should fail (waiting time not passed) + state.claim_back_wager( + 1u64, + USER1_ADDR, + &mut game_sc, + OptionalValue::Some(("4", "str:waiting time is not over yet")), + ); + + //user2 joins + state.join_game( + 1u64, + USER2_ADDR, + diff_amount, + &mut game_sc, + OptionalValue::Some(("4", "str:wrong amount paid")), + ); //wrong amount paid + + state.join_game( + 1u64, + USER2_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::None, + ); + state.join_game( + 1u64, + USER2_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::Some(("4", "str:user already joined this game")), + ); //user already joined + + let game_settings: SingleValue> = game_sc + .game_settings(1u64) + .into_vm_query() + .expect(TxExpect::ok()) + .execute(&mut state.world); + + assert_eq!(game_settings.into().status, Status::Valid); + + //user3 joins + state.join_game( + 1u64, + USER3_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::None, + ); + + //set timestamp after time limit + state + .world + .set_state_step(SetStateStep::new().block_timestamp(102u64)); + + //user4 joins, time has passed, fails + state.join_game( + 1u64, + USER4_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::Some(("4", "str:waiting time has passed")), + ); + + //user4 tries to claim back wager, fails + state.claim_back_wager( + 1u64, + USER4_ADDR, + &mut game_sc, + OptionalValue::Some(("4", "str:caller has not joined the game")), + ); + + //user1 tries to claim back wager, fails + state.claim_back_wager( + 1u64, + USER1_ADDR, + &mut game_sc, + OptionalValue::Some(( + "4", + "str:can manually claim back wager only if the game is invalid", + )), + ); + + //send tokens to sc + state.world.transfer_step( + TransferStep::new() + .esdt_transfer(TOKEN_ID, 0u64, BigUint::::from(10000u64)) + .from(OWNER_ADDR) + .to(GAME_SC_ADDR), + ); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + GAME_SC_ADDR, + CheckAccount::new().esdt_balance(TOKEN_ID, BigUint::::from(10320u64)), + )); + + //owner sends rewards + let mut winners = MultiValueEncoded::, u64)>::new(); + winners.push((ManagedAddress::from(_user1), 8000u64)); //80% + winners.push((ManagedAddress::from(_user2), 2000u64)); //20% + + state.send_reward( + 1u64, + &mut game_sc, + OptionalValue::Some(winners), + OptionalValue::None, + ); + + //user1 should receive 80% of the reward + //reward = 3 * wager = 300 => user1's reward = 240 + state + .world + .check_state_step(CheckStateStep::new().put_account( + USER1_ADDR, + CheckAccount::new().esdt_balance(TOKEN_ID, "100000140"), //balance - wager + 240 + )); + + //user2's reward = 60 + state + .world + .check_state_step(CheckStateStep::new().put_account( + USER2_ADDR, + CheckAccount::new().esdt_balance(TOKEN_ID, "99999960"), //balance - wager + 60 + )); } +#[test] +fn invalid_game_test() { + let mut state = GameContractState::new(); + let mut game_sc = ContractInfo::>::new(GAME_SC_ADDR); + + //game settings + let waiting_time = 100u64; // => timestamp 102 should be out of waiting time + let number_of_players_min = 3u64; + let number_of_players_max = 5u64; + let wager = BigUint::::from(100u64); + + //deploy + state.deploy(&mut game_sc); + + //set now = 1 + state + .world + .set_state_step(SetStateStep::new().block_timestamp(1u64)); + + //create game + state.create_game( + waiting_time, + number_of_players_min, + number_of_players_max, + wager.clone(), + OWNER_ADDR, + &mut game_sc, + ); + + //user1 joins + state.join_game( + 1u64, + USER1_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::None, + ); + + //user2 joins + state.join_game( + 1u64, + USER2_ADDR, + wager.clone(), + &mut game_sc, + OptionalValue::None, + ); + + //game is still invalid, min number of players not reached + let game_settings: SingleValue> = game_sc + .game_settings(1u64) + .into_vm_query() + .expect(TxExpect::ok()) + .execute(&mut state.world); + + assert_eq!(game_settings.into().status, Status::Invalid); + + //set now = 102, past waiting time + state + .world + .set_state_step(SetStateStep::new().block_timestamp(102u64)); + + //send reward, invalid game => players should receive back wager, creator should receive the creation fee back + state.send_reward(1u64, &mut game_sc, OptionalValue::None, OptionalValue::None); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + USER1_ADDR, + CheckAccount::new().esdt_balance(TOKEN_ID, "100000000"), + )); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + USER2_ADDR, + CheckAccount::new().esdt_balance(TOKEN_ID, "100000000"), + )); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + OWNER_ADDR, + CheckAccount::new().esdt_balance(TOKEN_ID, "100000000"), + )); +} From 3a937995467bc10abbb27cee5411eda0f0d9e31f Mon Sep 17 00:00:00 2001 From: mihaicalinluca Date: Fri, 6 Oct 2023 10:08:50 +0200 Subject: [PATCH 2/3] clippy --- .../tests/game_sc_blackbox_tests.rs | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs b/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs index 98ab202d..34267163 100644 --- a/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs +++ b/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs @@ -339,14 +339,8 @@ fn game_sc_simple_game_flow() { .execute(&mut state.world); let players_vec = players.to_vec(); - assert_eq!( - players_vec.contains(&ManagedAddress::from(user1.clone())), - true - ); - assert_eq!( - players_vec.contains(&ManagedAddress::from(user2.clone())), - true - ); + assert!(players_vec.contains(&ManagedAddress::from(user1.clone()))); + assert!(players_vec.contains(&ManagedAddress::from(user2.clone()))); //game should be in users'storage let games_per_user1: MultiValueEncoded = game_sc @@ -363,10 +357,7 @@ fn game_sc_simple_game_flow() { .execute(&mut state.world); let games_per_user2_vec = games_per_user2.to_vec(); - assert_eq!( - games_per_user1_vec.contains(&1u64) && games_per_user2_vec.contains(&1u64), - true - ); + assert!((games_per_user1_vec.contains(&1u64) && games_per_user2_vec.contains(&1u64))); } #[test] @@ -482,7 +473,7 @@ fn game_sc_complex_flow() { state.join_game( 1u64, USER4_ADDR, - wager.clone(), + wager, &mut game_sc, OptionalValue::Some(("4", "str:waiting time has passed")), ); @@ -590,13 +581,7 @@ fn invalid_game_test() { ); //user2 joins - state.join_game( - 1u64, - USER2_ADDR, - wager.clone(), - &mut game_sc, - OptionalValue::None, - ); + state.join_game(1u64, USER2_ADDR, wager, &mut game_sc, OptionalValue::None); //game is still invalid, min number of players not reached let game_settings: SingleValue> = game_sc From f6af8ceb3870562d0e82a3edc1121bdd18426a8c Mon Sep 17 00:00:00 2001 From: mihaicalinluca Date: Mon, 9 Oct 2023 13:38:18 +0200 Subject: [PATCH 3/3] fix after review --- contracts/mvx-game-sc/src/owner.rs | 4 ++- .../tests/game_sc_blackbox_tests.rs | 28 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/contracts/mvx-game-sc/src/owner.rs b/contracts/mvx-game-sc/src/owner.rs index af025856..13f94aaa 100644 --- a/contracts/mvx-game-sc/src/owner.rs +++ b/contracts/mvx-game-sc/src/owner.rs @@ -3,6 +3,8 @@ use crate::types::Status; multiversx_sc::imports!(); multiversx_sc::derive_imports!(); +const DENOM: u64 = 10_000u64; + #[multiversx_sc::module] pub trait OwnerModule: crate::private::PrivateModule + crate::storage::StorageModule { //u64 is percentage * 100 @@ -35,7 +37,7 @@ pub trait OwnerModule: crate::private::PrivateModule + crate::storage::StorageMo for (winner, percentage) in val.into_iter() { let reward_per_winner = - &BigUint::from(percentage) * &total_wager / &BigUint::from(10000u64); + &BigUint::from(percentage) * &total_wager / &BigUint::from(DENOM); self.send() .direct(&winner, &token_id, 0u64, &reward_per_winner); } diff --git a/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs b/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs index 34267163..83db959f 100644 --- a/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs +++ b/contracts/mvx-game-sc/tests/game_sc_blackbox_tests.rs @@ -2,7 +2,7 @@ use multiversx_sc::{ codec::multi_types::OptionalValue, storage::mappers::SingleValue, types::{ - Address, BigUint, EgldOrEsdtTokenIdentifier, ManagedAddress, MultiValueEncoded, + Address, EgldOrEsdtTokenIdentifier, ManagedAddress, MultiValueEncoded, TokenIdentifier, }, }; @@ -14,6 +14,8 @@ use mvx_game_sc::{ ProxyTrait as _, }; +use num_bigint::BigUint; + const GAME_SC_PATH: &str = "file:output/mvx-game-sc.wasm"; const BALANCE: u64 = 100_000_000u64; const TOKEN_ID: &str = "str:GAME-123456"; @@ -60,35 +62,35 @@ impl GameContractState { .put_account( USER1_ADDR, Account::new() - .nonce(2) + .nonce(1) .balance(BALANCE) .esdt_balance(TOKEN_ID, BALANCE), ) .put_account( USER2_ADDR, Account::new() - .nonce(3) + .nonce(1) .balance(BALANCE) .esdt_balance(TOKEN_ID, BALANCE), ) .put_account( USER3_ADDR, Account::new() - .nonce(4) + .nonce(1) .balance(BALANCE) .esdt_balance(TOKEN_ID, BALANCE), ) .put_account( USER4_ADDR, Account::new() - .nonce(5) + .nonce(1) .balance(BALANCE) .esdt_balance(TOKEN_ID, BALANCE), ) .put_account( USER5_ADDR, Account::new() - .nonce(6) + .nonce(1) .balance(BALANCE) .esdt_balance(TOKEN_ID, BALANCE), ), @@ -139,7 +141,7 @@ impl GameContractState { waiting_time: u64, number_of_players_min: u64, number_of_players_max: u64, - wager: BigUint, + wager: BigUint, caller: &str, game_sc: &mut ContractInfo>, ) -> &mut Self { @@ -147,7 +149,7 @@ impl GameContractState { ScCallStep::new() .from(caller) .to(GAME_SC_ADDR) - .esdt_transfer(TOKEN_ID, 0u64, BigUint::::from(STARTING_FEE)) + .esdt_transfer(TOKEN_ID, 0u64, BigUint::from(STARTING_FEE)) .call(game_sc.create_game( waiting_time, number_of_players_min, @@ -164,7 +166,7 @@ impl GameContractState { &mut self, game_id: u64, caller: &str, - amount: BigUint, + amount: BigUint, game_sc: &mut ContractInfo>, expected_error: OptionalValue<(&str, &str)>, ) -> &mut Self { @@ -370,7 +372,7 @@ fn game_sc_complex_flow() { let number_of_players_min = 2u64; let number_of_players_max = 4u64; let wager = BigUint::from(100u64); - let diff_amount = BigUint::::from(5u64); + let diff_amount = BigUint::from(5u64); //users let _user1 = state.user1.clone(); @@ -500,7 +502,7 @@ fn game_sc_complex_flow() { //send tokens to sc state.world.transfer_step( TransferStep::new() - .esdt_transfer(TOKEN_ID, 0u64, BigUint::::from(10000u64)) + .esdt_transfer(TOKEN_ID, 0u64, BigUint::from(10_000u64)) .from(OWNER_ADDR) .to(GAME_SC_ADDR), ); @@ -509,7 +511,7 @@ fn game_sc_complex_flow() { .world .check_state_step(CheckStateStep::new().put_account( GAME_SC_ADDR, - CheckAccount::new().esdt_balance(TOKEN_ID, BigUint::::from(10320u64)), + CheckAccount::new().esdt_balance(TOKEN_ID, BigUint::from(10_320u64)), )); //owner sends rewards @@ -551,7 +553,7 @@ fn invalid_game_test() { let waiting_time = 100u64; // => timestamp 102 should be out of waiting time let number_of_players_min = 3u64; let number_of_players_max = 5u64; - let wager = BigUint::::from(100u64); + let wager = BigUint::from(100u64); //deploy state.deploy(&mut game_sc);