From 6dadad4b64e00831fe099835fe23208b884cc5b9 Mon Sep 17 00:00:00 2001 From: MedovTimur <62596970+MedovTimur@users.noreply.github.com> Date: Sun, 7 Apr 2024 02:03:01 +0300 Subject: [PATCH] Upgrade event leaderboard (#317) --- contracts/auto-changed-nft/src/lib.rs | 4 +- contracts/battleship/io/src/lib.rs | 57 +++- contracts/battleship/src/contract.rs | 197 ++++++------ contracts/battleship/tests/test.rs | 169 ++++++++-- contracts/battleship/tests/test_node.rs | 88 +++--- contracts/car-races/io/src/lib.rs | 24 +- contracts/car-races/src/lib.rs | 290 +++++++++++------- contracts/car-races/tests/test.rs | 32 +- contracts/galactic-express/io/src/lib.rs | 2 +- contracts/gear-lib/src/tokens.rs | 2 +- .../nft-marketplace/tests/utils/nftoken.rs | 1 + .../nft-pixelboard/tests/utils/nftoken.rs | 4 +- contracts/ping/src/lib.rs | 6 - 13 files changed, 545 insertions(+), 331 deletions(-) diff --git a/contracts/auto-changed-nft/src/lib.rs b/contracts/auto-changed-nft/src/lib.rs index de456ff4c..f76b271b3 100644 --- a/contracts/auto-changed-nft/src/lib.rs +++ b/contracts/auto-changed-nft/src/lib.rs @@ -211,7 +211,7 @@ unsafe extern fn handle() { let gas_available = exec::gas_available(); gstd::debug!("Update. gas_available: {}", gas_available); if gas_available <= GAS_FOR_UPDATE { - let reservations = unsafe { &mut RESERVATION }; + let reservations: &mut Vec = unsafe { RESERVATION.as_mut() }; let reservation_id = reservations.pop().expect("Need more gas"); send_delayed_from_reservation( reservation_id, @@ -337,7 +337,7 @@ impl AutoChangedNft { } } fn reserve_gas(&self) { - let reservations = unsafe { &mut RESERVATION }; + let reservations: &mut Vec = unsafe { RESERVATION.as_mut() }; let reservation_id = ReservationId::reserve(RESERVATION_AMOUNT, 600).expect("reservation across executions"); reservations.push(reservation_id); diff --git a/contracts/battleship/io/src/lib.rs b/contracts/battleship/io/src/lib.rs index debe66810..12fcc7938 100644 --- a/contracts/battleship/io/src/lib.rs +++ b/contracts/battleship/io/src/lib.rs @@ -14,7 +14,7 @@ pub struct BattleshipMetadata; impl Metadata for BattleshipMetadata { type Init = In; - type Handle = InOut; + type Handle = InOut>; type Others = (); type Reply = (); type Signal = (); @@ -130,12 +130,36 @@ pub struct Config { } #[derive(Debug, Clone, Encode, Decode, TypeInfo)] pub enum BattleshipReply { + GameFinished(BattleshipParticipants), MessageSentToBot, - EndGame(BattleshipParticipants), BotChanged(ActorId), SessionCreated, SessionDeleted, ConfigUpdated, + StateCleared, + GameDeleted, +} + +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +pub enum BattleshipError { + GameIsAlreadyStarted, + GameIsNotStarted, + IncorrectLocationShips, + OutOfBounds, + GameIsAlreadyOver, + ThisCellAlreadyKnown, + BotDidNotInitializeBoard, + NotYourTurn, + NotAdmin, + WrongLength, + AccessDenied, + AlreadyHaveActiveSession, + NoMessagesForApprovalWerePassed, + DurationIsSmall, + HasNotValidSession, + SessionHasAlreadyExpired, + MessageIsNotAllowed, + NotApproved, } #[derive(Debug, Clone, Encode, Decode, TypeInfo)] @@ -169,7 +193,7 @@ pub struct Game { impl Game { pub fn start_bot(&mut self, mut ships: Ships) { - let bot_board = ships.get_field(); + let bot_board = ships.get_field().unwrap(); self.bot_board = bot_board; ships.sort_by_length(); self.bot_ships = ships; @@ -332,20 +356,19 @@ impl Ships { let has_non_empty = !vectors.iter().any(|vector: &&Vec| !vector.is_empty()); has_non_empty } - pub fn get_field(&self) -> Vec { + pub fn get_field(&self) -> Result, BattleshipError> { let mut board = vec![Entity::Empty; 25]; for position in self.iter() { - assert!( - board[*position as usize] != Entity::Ship, - "Incorrect location of ships" - ); + if board[*position as usize] == Entity::Ship { + return Err(BattleshipError::IncorrectLocationShips); + } board[*position as usize] = Entity::Ship; } - board + Ok(board) } - pub fn check_correct_location(&self) -> bool { + pub fn check_correct_location(&self) -> Result<(), BattleshipError> { if self.iter().any(|&position| position > 24) { - return false; + return Err(BattleshipError::OutOfBounds); } // ship size check let mut vec_len = vec![ @@ -356,10 +379,10 @@ impl Ships { ]; vec_len.sort(); if vec_len != vec![1, 2, 2, 3] { - return false; + return Err(BattleshipError::WrongLength); } - let mut field = self.get_field(); - let mut ships = vec![ + let mut field = self.get_field()?; + let mut ships = [ self.ship_1.clone(), self.ship_2.clone(), self.ship_3.clone(), @@ -372,13 +395,13 @@ impl Ships { match (ship.len(), distance) { (1, 0) | (2, 1) | (2, 5) => (), (3, 2) | (3, 10) if (ship[2] + ship[0]) % ship[1] == 0 => (), - _ => return false, + _ => return Err(BattleshipError::IncorrectLocationShips), } // checking the distance between ships let mut occupy_cells = vec![]; for position in ship { if field[*position as usize] == Entity::Occupied { - return false; + return Err(BattleshipError::IncorrectLocationShips); } let cells = match *position { 0 => vec![1, 5, 6], @@ -403,7 +426,7 @@ impl Ships { } } - true + Ok(()) } pub fn count_alive_ships(&self) -> Vec<(u8, u8)> { diff --git a/contracts/battleship/src/contract.rs b/contracts/battleship/src/contract.rs index 1015f9544..672fca5f2 100644 --- a/contracts/battleship/src/contract.rs +++ b/contracts/battleship/src/contract.rs @@ -25,11 +25,10 @@ impl Battleship { key: &ActorId, duration: u64, allowed_actions: Vec, - ) { - assert!( - duration >= MINIMUM_SESSION_SURATION_MS, - "Duration is too small" - ); + ) -> Result { + if duration < MINIMUM_SESSION_SURATION_MS { + return Err(BattleshipError::DurationIsSmall); + } let msg_source = msg::source(); let block_timestamp = exec::block_timestamp(); @@ -40,7 +39,7 @@ impl Battleship { }) = self.sessions.get(&msg_source) { if *expires > block_timestamp { - panic!("You already have an active session. If you want to create a new one, please delete this one.") + return Err(BattleshipError::AlreadyHaveActiveSession); } } @@ -49,10 +48,9 @@ impl Battleship { let number_of_blocks = u32::try_from(duration.div_ceil(self.config.block_duration_ms)) .expect("Duration is too large"); - assert!( - !allowed_actions.is_empty(), - "No messages for approval were passed." - ); + if allowed_actions.is_empty() { + return Err(BattleshipError::NoMessagesForApprovalWerePassed); + } self.sessions.entry(msg_source).insert(Session { key: *key, @@ -71,44 +69,46 @@ impl Battleship { ) .expect("Error in sending a delayed msg"); - msg::reply(BattleshipReply::SessionCreated, 0).expect("Error in sending a reply"); + Ok(BattleshipReply::SessionCreated) } - fn delete_session_from_program(&mut self, session_for_account: &ActorId) { - assert_eq!( - exec::program_id(), - msg::source(), - "The msg source must be the program" - ); + fn delete_session_from_program( + &mut self, + session_for_account: &ActorId, + ) -> Result { + if exec::program_id() != msg::source() { + return Err(BattleshipError::AccessDenied); + } if let Some(session) = self.sessions.remove(session_for_account) { - assert!( - session.expires <= exec::block_timestamp(), - "Too early to delete session" - ); + if session.expires > exec::block_timestamp() { + return Err(BattleshipError::AccessDenied); + } } - - msg::reply(BattleshipReply::SessionDeleted, 0).expect("Error in sending a reply"); + Ok(BattleshipReply::SessionDeleted) } - fn delete_session_from_account(&mut self) { - assert!(self.sessions.remove(&msg::source()).is_some(), "No session"); - - msg::reply(BattleshipReply::SessionDeleted, 0).expect("Error in sending a reply"); + fn delete_session_from_account(&mut self) -> Result { + self.sessions.remove(&msg::source()); + Ok(BattleshipReply::SessionDeleted) } - fn start_game(&mut self, mut ships: Ships, session_for_account: Option) { - let player = self.get_player(&session_for_account, ActionsForSession::StartGame); + fn start_game( + &mut self, + mut ships: Ships, + session_for_account: Option, + ) -> Result { + let player = self.get_player(&session_for_account, ActionsForSession::StartGame)?; + if let Some(game) = self.games.get(&player) { - assert!(game.game_over, "Please finish the previous game"); + if !game.game_over { + return Err(BattleshipError::GameIsAlreadyStarted); + } } - assert!( - ships.check_correct_location(), - "Incorrect location of ships" - ); + ships.check_correct_location()?; + let player_board = ships.get_field()?; - let player_board = ships.get_field(); ships.sort_by_length(); let game_instance = Game { player_board, @@ -131,32 +131,38 @@ impl Battleship { .expect("Error in sending a message"); self.msg_id_to_game_id.insert(msg_id, player); - msg::reply(BattleshipReply::MessageSentToBot, 0).expect("Error in sending a reply"); + Ok(BattleshipReply::MessageSentToBot) } - fn player_move(&mut self, step: u8, session_for_account: Option) { - let player = self.get_player(&session_for_account, ActionsForSession::Turn); - assert!(step < 25, "Step must be less than 24"); - - let game = self - .games - .get_mut(&player) - .expect("The player has no game, please start the game"); + fn player_move( + &mut self, + step: u8, + session_for_account: Option, + ) -> Result { + let player = self.get_player(&session_for_account, ActionsForSession::Turn)?; + if step > 24 { + return Err(BattleshipError::OutOfBounds); + } - assert!(!game.game_over, "Game is already over"); + let Some(game) = self.games.get_mut(&player) else { + return Err(BattleshipError::GameIsNotStarted); + }; + if game.game_over { + return Err(BattleshipError::GameIsAlreadyOver); + } if game.bot_board.is_empty() { - panic!("The bot did not initialize the board"); + return Err(BattleshipError::BotDidNotInitializeBoard); } if game.turn != Some(BattleshipParticipants::Player) { - panic!("Please wait your turn"); + return Err(BattleshipError::NotYourTurn); } if game.bot_board[step as usize] != Entity::Empty && game.bot_board[step as usize] != Entity::Ship { - panic!("The value of this cell is already known"); + return Err(BattleshipError::ThisCellAlreadyKnown); } let res = game.bot_ships.bang(step); @@ -166,13 +172,14 @@ impl Battleship { Step::Killed => game.dead_ship(step, 1), } game.total_shots += 1; + if game.bot_ships.check_end_game() { game.game_over = true; game.game_result = Some(BattleshipParticipants::Player); game.end_time = exec::block_timestamp(); - msg::reply(BattleshipReply::EndGame(BattleshipParticipants::Player), 0) - .expect("Error in sending a reply"); - return; + return Ok(BattleshipReply::GameFinished( + BattleshipParticipants::Player, + )); } game.turn = Some(BattleshipParticipants::Bot); @@ -186,67 +193,70 @@ impl Battleship { .expect("Error in sending a message"); self.msg_id_to_game_id.insert(msg_id, player); - msg::reply(BattleshipReply::MessageSentToBot, 0).expect("Error in sending a reply"); + Ok(BattleshipReply::MessageSentToBot) } - fn change_bot(&mut self, bot: ActorId) { - assert!( - msg::source() == self.admin, - "Only the admin can change the bot's contract address" - ); + fn change_bot(&mut self, bot: ActorId) -> Result { + if msg::source() != self.admin { + return Err(BattleshipError::NotAdmin); + } self.bot_address = bot; - msg::reply(BattleshipReply::BotChanged(bot), 0).expect("Error in sending a reply"); + Ok(BattleshipReply::BotChanged(bot)) } - fn clear_state(&mut self, leave_active_games: bool) { - assert!( - msg::source() == self.admin, - "Only the admin can change the contract state" - ); + fn clear_state( + &mut self, + leave_active_games: bool, + ) -> Result { + if msg::source() != self.admin { + return Err(BattleshipError::NotAdmin); + } if leave_active_games { self.games.retain(|_actor_id, game| !game.game_over); } else { self.games.clear(); } + Ok(BattleshipReply::StateCleared) } - fn delete_game(&mut self, player_address: ActorId) { - assert!( - msg::source() == self.admin, - "Only the admin can change the contract state" - ); + fn delete_game(&mut self, player_address: ActorId) -> Result { + if msg::source() != self.admin { + return Err(BattleshipError::NotAdmin); + } self.games.remove(&player_address); + Ok(BattleshipReply::GameDeleted) } fn get_player( &self, session_for_account: &Option, actions_for_session: ActionsForSession, - ) -> ActorId { + ) -> Result { let msg_source = msg::source(); let player = match session_for_account { Some(account) => { let session = self .sessions .get(account) - .expect("This account has no valid session"); - assert!( - session.expires > exec::block_timestamp(), - "The session has already expired" - ); - assert!( - session.allowed_actions.contains(&actions_for_session), - "This message is not allowed" - ); - assert_eq!( - session.key, msg_source, - "The account is not approved for this session" - ); + .ok_or(BattleshipError::HasNotValidSession)?; + + if session.expires <= exec::block_timestamp() { + return Err(BattleshipError::SessionHasAlreadyExpired); + } + if !session.allowed_actions.contains(&actions_for_session) { + return Err(BattleshipError::MessageIsNotAllowed); + } + if session.expires <= exec::block_timestamp() { + return Err(BattleshipError::SessionHasAlreadyExpired); + } + if session.key != msg_source { + return Err(BattleshipError::NotApproved); + } *account } None => msg_source, }; - player + Ok(player) } fn update_config( @@ -255,12 +265,10 @@ impl Battleship { gas_for_move: Option, gas_to_delete_session: Option, block_duration_ms: Option, - ) { - assert_eq!( - msg::source(), - self.admin, - "Only admin can change configurable parameters" - ); + ) -> Result { + if msg::source() != self.admin { + return Err(BattleshipError::NotAdmin); + } if let Some(gas_for_start) = gas_for_start { self.config.gas_for_start = gas_for_start; } @@ -276,8 +284,7 @@ impl Battleship { if let Some(block_duration_ms) = block_duration_ms { self.config.block_duration_ms = block_duration_ms; } - - msg::reply(BattleshipReply::ConfigUpdated, 0).expect("Error in sending a reply"); + Ok(BattleshipReply::ConfigUpdated) } } @@ -306,7 +313,7 @@ extern fn handle() { }; let action: BattleshipAction = msg::load().expect("Failed to decode `BattleshipAction` message."); - match action { + let reply = match action { BattleshipAction::StartGame { ships, session_for_account, @@ -340,7 +347,9 @@ extern fn handle() { gas_to_delete_session, block_duration_ms, ), - } + }; + msg::reply(reply, 0) + .expect("Failed to encode or reply with `Result`."); } #[no_mangle] @@ -376,7 +385,7 @@ extern fn handle_reply() { game.end_time = exec::block_timestamp(); msg::send( game_id, - BattleshipReply::EndGame(BattleshipParticipants::Bot), + BattleshipReply::GameFinished(BattleshipParticipants::Bot), 0, ) .expect("Unable to send the message about game over"); diff --git a/contracts/battleship/tests/test.rs b/contracts/battleship/tests/test.rs index 8c89bf44a..1a0cd86cb 100644 --- a/contracts/battleship/tests/test.rs +++ b/contracts/battleship/tests/test.rs @@ -1,6 +1,7 @@ use battleship_io::{ - ActionsForSession, BattleshipAction, BattleshipInit, BattleshipParticipants, BattleshipReply, - Config, Entity, GameState, Session, Ships, StateQuery, StateReply, MINIMUM_SESSION_SURATION_MS, + ActionsForSession, BattleshipAction, BattleshipError, BattleshipInit, BattleshipParticipants, + BattleshipReply, Config, Entity, GameState, Session, Ships, StateQuery, StateReply, + MINIMUM_SESSION_SURATION_MS, }; use gstd::prelude::*; use gtest::{Program, System}; @@ -51,7 +52,11 @@ fn failures_location_ships() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::OutOfBounds).encode() + ))); // wrong ship size let ships = Ships { ship_1: vec![19], @@ -66,7 +71,11 @@ fn failures_location_ships() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::WrongLength).encode() + ))); // ship crossing let ships = Ships { ship_1: vec![1], @@ -81,7 +90,11 @@ fn failures_location_ships() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::IncorrectLocationShips).encode() + ))); // the ship isn't solid let ships = Ships { ship_1: vec![19], @@ -96,7 +109,11 @@ fn failures_location_ships() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::IncorrectLocationShips).encode() + ))); // the distance between the ships is not maintained let ships = Ships { ship_1: vec![5], @@ -111,7 +128,11 @@ fn failures_location_ships() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::IncorrectLocationShips).encode() + ))); } #[test] @@ -129,7 +150,11 @@ fn failures_test() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::GameIsNotStarted).encode() + ))); let ships = Ships { ship_1: vec![19], @@ -159,7 +184,11 @@ fn failures_test() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::GameIsAlreadyStarted).encode() + ))); // outfield let res = battleship.send( 3, @@ -168,11 +197,19 @@ fn failures_test() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::OutOfBounds).encode() + ))); // only the admin can change the bot's contract address let res = battleship.send(4, BattleshipAction::ChangeBot { bot: 8.into() }); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 4, + Err::(BattleshipError::NotAdmin).encode() + ))); let steps: Vec = (0..25).collect(); for step in steps { @@ -201,7 +238,12 @@ fn failures_test() { session_for_account: None, }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + 3, + Err::(BattleshipError::GameIsAlreadyOver) + .encode() + ))); } } } @@ -280,7 +322,10 @@ fn create_session_success() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); check_session_in_state(&battleship, main_account, Some(session)); } @@ -329,7 +374,11 @@ fn create_session_failures() { }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + main_account, + Err::(BattleshipError::DurationIsSmall).encode() + ))); // there are no allowed actions (empty array of allowed_actions). let duration = MINIMUM_SESSION_SURATION_MS; @@ -344,7 +393,12 @@ fn create_session_failures() { }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + main_account, + Err::(BattleshipError::NoMessagesForApprovalWerePassed) + .encode() + ))); // The user already has a current active session. let allowed_actions = vec![ActionsForSession::StartGame, ActionsForSession::Turn]; @@ -358,7 +412,10 @@ fn create_session_failures() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); let res = battleship.send( main_account, BattleshipAction::CreateSession { @@ -367,7 +424,12 @@ fn create_session_failures() { allowed_actions, }, ); - assert!(res.main_failed()); + + assert!(!res.main_failed()); + assert!(res.contains(&( + main_account, + Err::(BattleshipError::AlreadyHaveActiveSession).encode() + ))); } // This function tests the mechanism where, upon creating a session, a delayed message is sent. @@ -399,7 +461,10 @@ fn session_deletion_on_expiration() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); system.spend_blocks((number_of_blocks as u32) + 1); @@ -436,7 +501,10 @@ fn disallow_game_without_required_actions() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); check_session_in_state(&battleship, main_account, Some(session)); @@ -456,11 +524,18 @@ fn disallow_game_without_required_actions() { }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + proxy_account, + Err::(BattleshipError::MessageIsNotAllowed).encode() + ))); // delete session and create a new one let res = battleship.send(main_account, BattleshipAction::DeleteSessionFromAccount); - assert!(res.contains(&(main_account, BattleshipReply::SessionDeleted.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionDeleted).encode() + ))); check_session_in_state(&battleship, main_account, None); @@ -479,7 +554,10 @@ fn disallow_game_without_required_actions() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); check_session_in_state(&battleship, main_account, Some(session)); @@ -492,7 +570,10 @@ fn disallow_game_without_required_actions() { }, ); - assert!(res.contains(&(proxy_account, BattleshipReply::MessageSentToBot.encode()))); + assert!(res.contains(&( + proxy_account, + Ok::(BattleshipReply::MessageSentToBot).encode() + ))); // must fail since `Turn` wasn't indicated in the `allowed_actions` let steps: Vec = (0..25).collect(); @@ -509,7 +590,12 @@ fn disallow_game_without_required_actions() { session_for_account: Some(main_account.into()), }, ); - assert!(res.main_failed()); + assert!(!res.main_failed()); + assert!(res.contains(&( + proxy_account, + Err::(BattleshipError::MessageIsNotAllowed) + .encode() + ))); } } } @@ -542,7 +628,10 @@ fn complete_session_game() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); check_session_in_state(&battleship, main_account, Some(session)); @@ -562,7 +651,10 @@ fn complete_session_game() { }, ); - assert!(res.contains(&(proxy_account, BattleshipReply::MessageSentToBot.encode()))); + assert!(res.contains(&( + proxy_account, + Ok::(BattleshipReply::MessageSentToBot).encode() + ))); let steps: Vec = (0..25).collect(); for step in steps { @@ -582,10 +674,17 @@ fn complete_session_game() { if game.game_over { assert!(res.contains(&( proxy_account, - BattleshipReply::EndGame(BattleshipParticipants::Player).encode() + Ok::(BattleshipReply::GameFinished( + BattleshipParticipants::Player + )) + .encode() ))); } else { - assert!(res.contains(&(proxy_account, BattleshipReply::MessageSentToBot.encode()))); + assert!(res.contains(&( + proxy_account, + Ok::(BattleshipReply::MessageSentToBot) + .encode() + ))); } } } @@ -620,19 +719,21 @@ fn premature_session_deletion_by_user() { }, ); - assert!(res.contains(&(main_account, BattleshipReply::SessionCreated.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionCreated).encode() + ))); check_session_in_state(&battleship, main_account, Some(session)); // delete session let res = battleship.send(main_account, BattleshipAction::DeleteSessionFromAccount); - assert!(res.contains(&(main_account, BattleshipReply::SessionDeleted.encode()))); + assert!(res.contains(&( + main_account, + Ok::(BattleshipReply::SessionDeleted).encode() + ))); check_session_in_state(&battleship, main_account, None); - - // fails since a user is trying to delete a non-existent session - let res = battleship.send(main_account, BattleshipAction::DeleteSessionFromAccount); - assert!(res.main_failed()); } fn check_session_in_state(battleship: &Program<'_>, account: u64, session: Option) { diff --git a/contracts/battleship/tests/test_node.rs b/contracts/battleship/tests/test_node.rs index 43ae13f17..fcbd9dd3e 100644 --- a/contracts/battleship/tests/test_node.rs +++ b/contracts/battleship/tests/test_node.rs @@ -134,7 +134,6 @@ async fn gclient_turn_test() -> Result<()> { 0, ) .await?; - let init_battleship = BattleshipInit { bot_address: bot_actor_id.into(), config: Config { @@ -156,7 +155,6 @@ async fn gclient_turn_test() -> Result<()> { true, ) .await?; - let (message_id, program_id, _hash) = api .upload_program_bytes( gclient::code_from_os(path)?, @@ -169,50 +167,48 @@ async fn gclient_turn_test() -> Result<()> { assert!(listener.message_processed(message_id).await?.succeed()); - for _i in 1..3 { - let ships = Ships { - ship_1: vec![19], - ship_2: vec![0, 1, 2], - ship_3: vec![4, 9], - ship_4: vec![16, 21], - }; - let start_payload = BattleshipAction::StartGame { - ships, - session_for_account: None, - }; - - let gas_info = api - .calculate_handle_gas(None, program_id, start_payload.encode(), 0, true) - .await?; - - let (message_id, _) = api - .send_message(program_id, start_payload, gas_info.min_limit, 0) - .await?; - - assert!(listener.message_processed(message_id).await?.succeed()); - assert!(listener.blocks_running().await?); - let steps: Vec = (0..25).collect(); - for step in steps { - let state = get_all_state(&api, &program_id) - .await - .expect("Unexpected invalid state."); - if (state.games[0].1.bot_board[step as usize] == Entity::Empty - || state.games[0].1.bot_board[step as usize] == Entity::Ship) - && !state.games[0].1.game_over - { - let turn_payload = BattleshipAction::Turn { - step, - session_for_account: None, - }; - let gas_info = api - .calculate_handle_gas(None, program_id, turn_payload.encode(), 0, true) - .await?; - let (message_id, _) = api - .send_message(program_id, turn_payload, gas_info.min_limit, 0) - .await?; - assert!(listener.message_processed(message_id).await?.succeed()); - assert!(listener.blocks_running().await?); - } + let ships = Ships { + ship_1: vec![19], + ship_2: vec![0, 1, 2], + ship_3: vec![4, 9], + ship_4: vec![16, 21], + }; + let start_payload = BattleshipAction::StartGame { + ships, + session_for_account: None, + }; + + let gas_info = api + .calculate_handle_gas(None, program_id, start_payload.encode(), 0, true) + .await?; + + let (message_id, _) = api + .send_message(program_id, start_payload, gas_info.min_limit, 0) + .await?; + + assert!(listener.message_processed(message_id).await?.succeed()); + assert!(listener.blocks_running().await?); + let steps: Vec = (0..25).collect(); + for step in steps { + let state = get_all_state(&api, &program_id) + .await + .expect("Unexpected invalid state."); + if (state.games[0].1.bot_board[step as usize] == Entity::Empty + || state.games[0].1.bot_board[step as usize] == Entity::Ship) + && !state.games[0].1.game_over + { + let turn_payload = BattleshipAction::Turn { + step, + session_for_account: None, + }; + let gas_info = api + .calculate_handle_gas(None, program_id, turn_payload.encode(), 0, true) + .await?; + let (message_id, _) = api + .send_message(program_id, turn_payload, gas_info.min_limit, 0) + .await?; + assert!(listener.message_processed(message_id).await?.succeed()); + assert!(listener.blocks_running().await?); } } diff --git a/contracts/car-races/io/src/lib.rs b/contracts/car-races/io/src/lib.rs index 90e8892c0..2e53e8a8d 100644 --- a/contracts/car-races/io/src/lib.rs +++ b/contracts/car-races/io/src/lib.rs @@ -54,7 +54,7 @@ impl Metadata for ContractMetadata { /// /// We use the [`GameAction`] type for incoming and [`GameReply`] for outgoing /// messages. - type Handle = InOut; + type Handle = InOut>; /// Asynchronous handle message type. /// /// Describes incoming/outgoing types for the `main()` function in case of @@ -200,12 +200,26 @@ pub enum CarAction { #[derive(Encode, Decode, TypeInfo, PartialEq, Eq)] pub enum GameReply { - GameStarted, - NotEnoughGas, GameFinished, - GasReserved, + GameStarted, StrategyAdded, - PlayersMove, + MoveMade, + GameInstanceRemoved, + InstancesRemoved, + AdminAdded, + AdminRemoved, + ConfigUpdated, + StatusMessagesUpdated, +} + +#[derive(Debug, Clone, Encode, Decode, TypeInfo)] +pub enum GameError { + NotAdmin, + MustBeTwoStrategies, + GameAlreadyStarted, + NotPlayerTurn, + NotProgram, + MessageProcessingSuspended, } impl Game { diff --git a/contracts/car-races/src/lib.rs b/contracts/car-races/src/lib.rs index d76b7a454..4684a02a9 100644 --- a/contracts/car-races/src/lib.rs +++ b/contracts/car-races/src/lib.rs @@ -21,34 +21,40 @@ pub struct Contract { } impl Contract { - fn add_strategy_ids(&mut self, car_ids: Vec) { - assert!(self.admins.contains(&msg::source()), "You are not admin"); - - assert!(car_ids.len() == 2, "There must be 2 strategies of cars"); + fn add_strategy_ids( + &mut self, + msg_src: &ActorId, + car_ids: Vec, + ) -> Result { + if !self.admins.contains(msg_src) { + return Err(GameError::NotAdmin); + } + if car_ids.len() != 2 { + return Err(GameError::MustBeTwoStrategies); + } self.strategy_ids = car_ids; + Ok(GameReply::StrategyAdded) } - fn start_game(&mut self) { - let player = msg::source(); - + fn start_game(&mut self, msg_src: &ActorId) -> Result { let last_time_step = exec::block_timestamp(); - let game = if let Some(game) = self.games.get_mut(&player) { + let game = if let Some(game) = self.games.get_mut(msg_src) { if game.state != GameState::Finished { - panic!("Please complete the game"); + return Err(GameError::GameAlreadyStarted); } game.current_round = 0; game.result = None; game.last_time_step = last_time_step; game } else { - self.games.entry(player).or_insert_with(|| Game { + self.games.entry(*msg_src).or_insert_with(|| Game { last_time_step, ..Default::default() }) }; - game.car_ids = vec![player, self.strategy_ids[0], self.strategy_ids[1]]; + game.car_ids = vec![*msg_src, self.strategy_ids[0], self.strategy_ids[1]]; let initial_state = Car { position: 0, speed: self.config.initial_speed, @@ -56,24 +62,25 @@ impl Contract { round_result: None, }; - game.cars.insert(player, initial_state.clone()); + game.cars.insert(*msg_src, initial_state.clone()); game.cars .insert(self.strategy_ids[0], initial_state.clone()); game.cars.insert(self.strategy_ids[1], initial_state); game.state = GameState::PlayerAction; - msg::reply(GameReply::GameStarted, 0).expect("Error during reply"); + Ok(GameReply::GameStarted) } - fn player_move(&mut self, strategy_move: StrategyAction) { - let player = msg::source(); - let game = self.get_game(&player); + fn player_move( + &mut self, + msg_src: &ActorId, + strategy_move: StrategyAction, + ) -> Result { + let game = self.get_game(msg_src); - assert_eq!( - game.state, - GameState::PlayerAction, - "Not time for the player" - ); + if game.state != GameState::PlayerAction { + return Err(GameError::NotPlayerTurn); + } match strategy_move { StrategyAction::BuyAcceleration => { game.buy_acceleration(); @@ -100,15 +107,14 @@ impl Contract { ) .expect("Error in sending a message"); - self.msg_id_to_game_id.insert(msg_id, player); + self.msg_id_to_game_id.insert(msg_id, *msg_src); + Ok(GameReply::MoveMade) } - fn play(&mut self, account: &ActorId) { - assert_eq!( - msg::source(), - exec::program_id(), - "Only program can send this message" - ); + fn play(&mut self, msg_src: &ActorId, account: &ActorId) -> Result { + if *msg_src != exec::program_id() { + return Err(GameError::NotProgram); + } let game = self.get_game(account); @@ -118,7 +124,7 @@ impl Contract { let car_ids = game.car_ids.clone(); self.send_messages(account); send_message_round_info(&car_ids[0], &cars, &result); - return; + return Ok(GameReply::GameFinished); } if game.current_turn == 0 { game.state = GameState::PlayerAction; @@ -126,7 +132,7 @@ impl Contract { let cars = game.cars.clone(); let car_ids = game.car_ids.clone(); send_message_round_info(&car_ids[0], &cars, &result); - return; + return Ok(GameReply::MoveMade); } let car_id = game.get_current_car_id(); @@ -135,6 +141,7 @@ impl Contract { .expect("Error in sending a message"); self.msg_id_to_game_id.insert(msg_id, *account); + Ok(GameReply::MoveMade) } fn get_game(&mut self, account: &ActorId) -> &mut Game { @@ -154,12 +161,14 @@ impl Contract { .expect("Error in sending message"); } - fn remove_game_instance(&mut self, account: &ActorId) { - assert_eq!( - msg::source(), - exec::program_id(), - "This message can be sent only by the program" - ); + fn remove_game_instance( + &mut self, + msg_src: &ActorId, + account: &ActorId, + ) -> Result { + if *msg_src != exec::program_id() { + return Err(GameError::NotProgram); + } let game = self .games .get(account) @@ -168,13 +177,17 @@ impl Contract { if game.state == GameState::Finished { self.games.remove(account); } + Ok(GameReply::GameInstanceRemoved) } - fn remove_instances(&mut self, player_ids: Option>) { - assert!( - self.admins.contains(&msg::source()), - "Only admin can send this message" - ); + fn remove_instances( + &mut self, + msg_src: &ActorId, + player_ids: Option>, + ) -> Result { + if !self.admins.contains(msg_src) { + return Err(GameError::NotAdmin); + } match player_ids { Some(player_ids) => { for player_id in player_ids { @@ -188,6 +201,88 @@ impl Contract { }); } } + Ok(GameReply::InstancesRemoved) + } + fn add_admin(&mut self, msg_src: &ActorId, admin: ActorId) -> Result { + if !self.admins.contains(msg_src) { + return Err(GameError::NotAdmin); + } + self.admins.push(admin); + Ok(GameReply::AdminAdded) + } + fn remove_admin(&mut self, msg_src: &ActorId, admin: ActorId) -> Result { + if !self.admins.contains(msg_src) { + return Err(GameError::NotAdmin); + } + self.admins.retain(|id| *id != admin); + Ok(GameReply::AdminRemoved) + } + #[allow(clippy::too_many_arguments)] + fn update_config( + &mut self, + msg_src: &ActorId, + gas_to_remove_game: Option, + initial_speed: Option, + min_speed: Option, + max_speed: Option, + gas_for_round: Option, + time_interval: Option, + max_distance: Option, + time: Option, + time_for_game_storage: Option, + ) -> Result { + if !self.admins.contains(msg_src) { + return Err(GameError::NotAdmin); + } + + if let Some(gas_to_remove_game) = gas_to_remove_game { + self.config.gas_to_remove_game = gas_to_remove_game; + } + if let Some(initial_speed) = initial_speed { + self.config.initial_speed = initial_speed; + } + + if let Some(min_speed) = min_speed { + self.config.min_speed = min_speed; + } + + if let Some(max_speed) = max_speed { + self.config.max_speed = max_speed; + } + + if let Some(gas_for_round) = gas_for_round { + self.config.gas_for_round = gas_for_round; + } + if let Some(time_interval) = time_interval { + self.config.time_interval = time_interval; + } + + if let Some(max_distance) = max_distance { + self.config.max_distance = max_distance; + } + + if let Some(max_speed) = max_speed { + self.config.max_speed = max_speed; + } + + if let Some(time) = time { + self.config.time = time; + } + if let Some(time_for_game_storage) = time_for_game_storage { + self.config.time_for_game_storage = time_for_game_storage; + } + Ok(GameReply::ConfigUpdated) + } + fn allow_messages( + &mut self, + msg_src: &ActorId, + messages_allowed: bool, + ) -> Result { + if !self.admins.contains(msg_src) { + return Err(GameError::NotAdmin); + } + self.messages_allowed = messages_allowed; + Ok(GameReply::StatusMessagesUpdated) } } @@ -195,43 +290,32 @@ impl Contract { extern fn handle() { let action: GameAction = msg::load().expect("Unable to decode the message"); let contract = unsafe { CONTRACT.as_mut().expect("The game is not initialized") }; + let msg_src = msg::source(); - if let GameAction::AllowMessages(messages_allowed) = action { - assert!( - contract.admins.contains(&msg::source()), - "Only an admin can send this message" - ); - contract.messages_allowed = messages_allowed; + if !contract.messages_allowed && !contract.admins.contains(&msg_src) { + msg::reply( + Err::(GameError::MessageProcessingSuspended), + 0, + ) + .expect("Failed to encode or reply with `Result`."); return; } - assert!( - contract.messages_allowed, - "Message processing has been suspended for some time" - ); - - match action { - GameAction::AddStrategyIds { car_ids } => contract.add_strategy_ids(car_ids), - GameAction::StartGame => contract.start_game(), - GameAction::Play { account } => contract.play(&account), - GameAction::PlayerMove { strategy_action } => contract.player_move(strategy_action), - GameAction::RemoveGameInstance { account_id } => contract.remove_game_instance(&account_id), - GameAction::RemoveGameInstances { players_ids } => contract.remove_instances(players_ids), - GameAction::AddAdmin(admin) => { - assert!( - contract.admins.contains(&msg::source()), - "You are not admin" - ); - contract.admins.push(admin); - } - GameAction::RemoveAdmin(admin) => { - assert!( - contract.admins.contains(&msg::source()), - "You are not admin" - ); - - contract.admins.retain(|id| *id != admin); + let reply = match action { + GameAction::AddStrategyIds { car_ids } => contract.add_strategy_ids(&msg_src, car_ids), + GameAction::StartGame => contract.start_game(&msg_src), + GameAction::Play { account } => contract.play(&msg_src, &account), + GameAction::PlayerMove { strategy_action } => { + contract.player_move(&msg_src, strategy_action) } + GameAction::RemoveGameInstance { account_id } => { + contract.remove_game_instance(&msg_src, &account_id) + } + GameAction::RemoveGameInstances { players_ids } => { + contract.remove_instances(&msg_src, players_ids) + } + GameAction::AddAdmin(admin) => contract.add_admin(&msg_src, admin), + GameAction::RemoveAdmin(admin) => contract.remove_admin(&msg_src, admin), GameAction::UpdateConfig { gas_to_remove_game, initial_speed, @@ -242,51 +326,23 @@ extern fn handle() { max_distance, time, time_for_game_storage, - } => { - assert!( - contract.admins.contains(&msg::source()), - "You are not admin" - ); - - if let Some(gas_to_remove_game) = gas_to_remove_game { - contract.config.gas_to_remove_game = gas_to_remove_game; - } - if let Some(initial_speed) = initial_speed { - contract.config.initial_speed = initial_speed; - } - - if let Some(min_speed) = min_speed { - contract.config.min_speed = min_speed; - } - - if let Some(max_speed) = max_speed { - contract.config.max_speed = max_speed; - } - - if let Some(gas_for_round) = gas_for_round { - contract.config.gas_for_round = gas_for_round; - } - if let Some(time_interval) = time_interval { - contract.config.time_interval = time_interval; - } - - if let Some(max_distance) = max_distance { - contract.config.max_distance = max_distance; - } - - if let Some(max_speed) = max_speed { - contract.config.max_speed = max_speed; - } - - if let Some(time) = time { - contract.config.time = time; - } - if let Some(time_for_game_storage) = time_for_game_storage { - contract.config.time_for_game_storage = time_for_game_storage; - } + } => contract.update_config( + &msg_src, + gas_to_remove_game, + initial_speed, + min_speed, + max_speed, + gas_for_round, + time_interval, + max_distance, + time, + time_for_game_storage, + ), + GameAction::AllowMessages(messages_allowed) => { + contract.allow_messages(&msg_src, messages_allowed) } - _ => {} - } + }; + msg::reply(reply, 0).expect("Failed to encode or reply with `Result`."); } #[no_mangle] diff --git a/contracts/car-races/tests/test.rs b/contracts/car-races/tests/test.rs index 74eeebf34..e72ffa599 100644 --- a/contracts/car-races/tests/test.rs +++ b/contracts/car-races/tests/test.rs @@ -147,14 +147,22 @@ fn failures_test() { assert!(!game_init_result.main_failed()); // AllowMessages not true - let run_result = game.send(ADMIN, GameAction::AddAdmin(1.into())); - assert!(run_result.main_failed()); + let run_result = game.send(PLAYERS[0], GameAction::StartGame); + assert!(!run_result.main_failed()); + assert!(run_result.contains(&( + PLAYERS[0], + Err::(GameError::MessageProcessingSuspended).encode() + ))); let run_result = game.send(ADMIN, GameAction::AllowMessages(true)); assert!(!run_result.main_failed()); // not admin let run_result = game.send(PLAYERS[0], GameAction::AddAdmin(2.into())); - assert!(run_result.main_failed()); + assert!(!run_result.main_failed()); + assert!(run_result.contains(&( + PLAYERS[0], + Err::(GameError::NotAdmin).encode() + ))); let car_1 = Program::from_file( &system, @@ -178,7 +186,11 @@ fn failures_test() { // There must be 2 strategies of cars let car_ids: Vec = vec![1.into(), 2.into(), 3.into()]; let run_result = game.send(ADMIN, GameAction::AddStrategyIds { car_ids }); - assert!(run_result.main_failed()); + assert!(!run_result.main_failed()); + assert!(run_result.contains(&( + ADMIN, + Err::(GameError::MustBeTwoStrategies).encode() + ))); let car_ids: Vec = vec![2.into(), 3.into()]; let run_result = game.send(ADMIN, GameAction::AddStrategyIds { car_ids }); @@ -188,7 +200,11 @@ fn failures_test() { assert!(!run_result.main_failed()); // The game has already started let run_result = game.send(ADMIN, GameAction::StartGame); - assert!(run_result.main_failed()); + assert!(!run_result.main_failed()); + assert!(run_result.contains(&( + ADMIN, + Err::(GameError::GameAlreadyStarted).encode() + ))); for _i in 0..40 { let run_result = game.send( @@ -218,5 +234,9 @@ fn failures_test() { strategy_action: StrategyAction::BuyShell, }, ); - assert!(run_result.main_failed()); + assert!(!run_result.main_failed()); + assert!(run_result.contains(&( + ADMIN, + Err::(GameError::NotPlayerTurn).encode() + ))); } diff --git a/contracts/galactic-express/io/src/lib.rs b/contracts/galactic-express/io/src/lib.rs index caf4686fa..7554a08d9 100644 --- a/contracts/galactic-express/io/src/lib.rs +++ b/contracts/galactic-express/io/src/lib.rs @@ -96,6 +96,7 @@ pub enum Action { #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)] pub enum Event { + GameFinished(Results), AdminChanged(ActorId, ActorId), NewSessionCreated { altitude: u16, @@ -109,7 +110,6 @@ pub enum Event { player_id: ActorId, }, GameCanceled, - GameFinished(Results), GameLeft, } diff --git a/contracts/gear-lib/src/tokens.rs b/contracts/gear-lib/src/tokens.rs index e00a39121..91f77f626 100644 --- a/contracts/gear-lib/src/tokens.rs +++ b/contracts/gear-lib/src/tokens.rs @@ -13,7 +13,7 @@ mod test_helper { use std::thread_local; thread_local! { - static SOURCE: Cell = Cell::new(ActorId::zero()); + static SOURCE: Cell = const{ Cell::new(ActorId::zero())}; } pub mod msg { diff --git a/contracts/nft-marketplace/tests/utils/nftoken.rs b/contracts/nft-marketplace/tests/utils/nftoken.rs index c5c830da8..d09898c6a 100644 --- a/contracts/nft-marketplace/tests/utils/nftoken.rs +++ b/contracts/nft-marketplace/tests/utils/nftoken.rs @@ -92,6 +92,7 @@ impl<'a> NonFungibleToken<'a> { } } +#[allow(dead_code)] pub struct NonFungibleTokenMetaState<'a>(&'a InnerProgram<'a>); impl NonFungibleTokenMetaState<'_> { diff --git a/contracts/nft-pixelboard/tests/utils/nftoken.rs b/contracts/nft-pixelboard/tests/utils/nftoken.rs index 32045e658..b0f44538a 100644 --- a/contracts/nft-pixelboard/tests/utils/nftoken.rs +++ b/contracts/nft-pixelboard/tests/utils/nftoken.rs @@ -5,7 +5,7 @@ use gtest::{Program as InnerProgram, System}; use non_fungible_token_io::{Collection, Config, InitNFT}; use std::fs; -pub struct NonFungibleToken<'a>(InnerProgram<'a>, u64); +pub struct NonFungibleToken<'a>(InnerProgram<'a>); impl Program for NonFungibleToken<'_> { fn inner_program(&self) -> &InnerProgram<'_> { @@ -31,7 +31,7 @@ impl<'a> NonFungibleToken<'a> { ) .main_failed()); - Self(program, 0) + Self(program) } pub fn meta_state(&self) -> NonFungibleTokenMetaState<'_> { diff --git a/contracts/ping/src/lib.rs b/contracts/ping/src/lib.rs index df15dcbca..68a1b0bac 100644 --- a/contracts/ping/src/lib.rs +++ b/contracts/ping/src/lib.rs @@ -14,12 +14,6 @@ extern fn handle() { unsafe { MESSAGE_LOG.push(new_msg); - - debug!("{:?} total message(s) stored: ", MESSAGE_LOG.len()); - - for log in &MESSAGE_LOG { - debug!("{log:?}"); - } } }