diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index b0006661c..000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/workflows/STG-tamagotchi-battle-new.yml b/.github/workflows/STG-tamagotchi-battle-new.yml index b7e60537a..3fa1ee256 100644 --- a/.github/workflows/STG-tamagotchi-battle-new.yml +++ b/.github/workflows/STG-tamagotchi-battle-new.yml @@ -5,7 +5,7 @@ on: push: branches: ["master", "main"] paths: - - frontend/apps/tamagotchi-battle-new/** + - frontend/apps/web3-warriors-battle/** - frontend/packages/** concurrency: @@ -71,7 +71,7 @@ jobs: - name: Build and push image uses: docker/build-push-action@v5 with: - file: frontend/apps/tamagotchi-battle-new/Dockerfile + file: frontend/apps/web3-warriors-battle/Dockerfile push: true tags: ${{ needs.prepair.outputs.image_name }} build-args: | diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4e52b6ca3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# misc +.DS_Store diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index e5f871b1f..7459fb5c3 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -459,7 +459,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.37", + "rustix 0.38.38", "slab", "tracing", "windows-sys 0.59.0", @@ -511,7 +511,7 @@ dependencies = [ "cfg-if", "event-listener 5.3.1", "futures-lite", - "rustix 0.38.37", + "rustix 0.38.38", "tracing", ] @@ -527,7 +527,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.37", + "rustix 0.38.38", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -2502,9 +2502,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" dependencies = [ "fastrand", "futures-core", @@ -2573,21 +2573,25 @@ dependencies = [ name = "galactic-express" version = "1.1.0" dependencies = [ - "galactic-express-io", + "galactic-express-app", "gear-wasm-builder", - "gstd", - "gtest", - "num-traits", + "sails-client-gen", + "sails-idl-gen", + "sails-rs", ] [[package]] -name = "galactic-express-io" +name = "galactic-express-app" version = "1.1.0" dependencies = [ - "gmeta", + "galactic-express", + "gclient", + "gear-core", "gstd", - "parity-scale-codec", - "scale-info", + "gtest", + "num-traits", + "sails-rs", + "tokio", ] [[package]] @@ -3920,7 +3924,7 @@ dependencies = [ "http 1.1.0", "jsonrpsee-core 0.23.2", "pin-project", - "rustls 0.23.15", + "rustls 0.23.16", "rustls-pki-types", "rustls-platform-verifier", "soketto 0.8.0", @@ -4185,9 +4189,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -4403,7 +4407,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix 0.38.37", + "rustix 0.38.38", ] [[package]] @@ -5115,7 +5119,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.37", + "rustix 0.38.38", "tracing", "windows-sys 0.59.0", ] @@ -5714,9 +5718,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -5753,9 +5757,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.15" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "log", "once_cell", @@ -5826,7 +5830,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.15", + "rustls 0.23.16", "rustls-native-certs 0.7.3", "rustls-platform-verifier-android", "rustls-webpki 0.102.8", @@ -6296,9 +6300,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.213" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -6325,9 +6329,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.213" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -7524,7 +7528,7 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix 0.38.37", + "rustix 0.38.38", "windows-sys 0.59.0", ] @@ -7745,7 +7749,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.15", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] @@ -8763,7 +8767,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.37", + "rustix 0.38.38", ] [[package]] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 093e9dd67..5721a5022 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -12,7 +12,7 @@ members = [ "car-races/car-3", "car-races/wasm", "concert/wasm", - "galactic-express", + "galactic-express/wasm", "multisig-wallet", "multisig-wallet/state", "oracle", @@ -70,7 +70,6 @@ tamagotchi-battle-state.path = "tamagotchi-battle/state" battleship-io.path = "battleship/io" car-races-io.path = "car-races/io" -galactic-express-io.path = "galactic-express/io" multi-token-io.path = "multi-token/io" multisig-wallet-io.path = "multisig-wallet/io" ping-io.path = "ping/io" diff --git a/contracts/battle/app/src/services/game/utils.rs b/contracts/battle/app/src/services/game/utils.rs index 41ed18ce3..4a15c9f06 100644 --- a/contracts/battle/app/src/services/game/utils.rs +++ b/contracts/battle/app/src/services/game/utils.rs @@ -117,24 +117,6 @@ impl Battle { } } - pub fn delete_players(&mut self, loser_1: &ActorId, loser_2: &ActorId, pair_id: u16) { - let player_loser_1 = self - .participants - .remove(loser_1) - .expect("The player must exist"); - let player_loser_2 = self - .participants - .remove(loser_2) - .expect("The player must exist"); - - self.defeated_participants.insert(*loser_1, player_loser_1); - self.defeated_participants.insert(*loser_2, player_loser_2); - - self.pairs.remove(&pair_id); - self.players_to_pairs.remove(loser_1); - self.players_to_pairs.remove(loser_2); - } - pub fn check_min_player_amount(&self) -> Result<(), BattleError> { if self.participants.len() <= 1 { return Err(BattleError::NotEnoughPlayers); @@ -183,6 +165,7 @@ impl Battle { self.pairs.insert(self.pair_id, pair); self.players_to_pairs.insert(player.owner, self.pair_id); self.waiting_player = Some((player.owner, self.pair_id)); + self.pair_id += 1; } } pub fn send_delayed_message_make_move_from_reservation(&mut self, time_for_move: u32) { diff --git a/contracts/battle/tests/gtest.rs b/contracts/battle/tests/gtest.rs index 28f2160ae..777d160b5 100644 --- a/contracts/battle/tests/gtest.rs +++ b/contracts/battle/tests/gtest.rs @@ -142,7 +142,7 @@ async fn test() { println!("\n RES {:?}", result); - make_move(&mut service_client, Move::Attack, USER_1, program_id) + make_move(&mut service_client, Move::Attack, USER_2, program_id) .await .unwrap(); @@ -170,7 +170,7 @@ async fn test() { service_client .start_next_fight() - .with_args(GTestArgs::new(USER_2.into())) + .with_args(GTestArgs::new(USER_3.into())) .send_recv(program_id) .await .unwrap(); diff --git a/contracts/galactic-express/.DS_Store b/contracts/galactic-express/.DS_Store deleted file mode 100644 index 866c1fe2f..000000000 Binary files a/contracts/galactic-express/.DS_Store and /dev/null differ diff --git a/contracts/galactic-express/.binpath b/contracts/galactic-express/.binpath deleted file mode 100644 index b4c198035..000000000 --- a/contracts/galactic-express/.binpath +++ /dev/null @@ -1 +0,0 @@ -../target/wasm32-unknown-unknown/release/galactic_express \ No newline at end of file diff --git a/contracts/galactic-express/Cargo.toml b/contracts/galactic-express/Cargo.toml deleted file mode 100644 index f73c4ed1c..000000000 --- a/contracts/galactic-express/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "galactic-express" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -gstd.workspace = true -galactic-express-io.workspace = true -num-traits.workspace = true - -[dev-dependencies] -gtest.workspace = true - -[build-dependencies] -gear-wasm-builder.workspace = true -galactic-express-io.workspace = true diff --git a/contracts/galactic-express/README.md b/contracts/galactic-express/README.md index 85949cbb0..f767052bb 100644 --- a/contracts/galactic-express/README.md +++ b/contracts/galactic-express/README.md @@ -1,6 +1,3 @@ -[![Open in Gitpod](https://img.shields.io/badge/Open_in-Gitpod-white?logo=gitpod)](https://gitpod.io/#FOLDER=galactic-express/https://github.com/gear-foundation/dapps) -[![Docs](https://img.shields.io/github/actions/workflow/status/gear-foundation/dapps/contracts.yml?logo=rust&label=docs)](https://dapps.gear.rs/galactic_express_io) - # [Galactic Express](https://wiki.gear-tech.io/docs/examples/Gaming/galactic-express) Galactic Express (GalEx) is a 100% on-chain PvE economic game. @@ -16,7 +13,7 @@ cargo b -r -p "galactic-express" ### ✅ Testing ```sh -cargo t -r -p "galactic-express" +cargo t -r -p "galactic-express-app" ``` ## Stages diff --git a/contracts/galactic-express/app/Cargo.toml b/contracts/galactic-express/app/Cargo.toml new file mode 100644 index 000000000..8ecca8b11 --- /dev/null +++ b/contracts/galactic-express/app/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "galactic-express-app" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +gstd = { workspace = true, features = ["debug"] } +sails-rs = { workspace = true, features = ["gtest"] } +num-traits.workspace = true + +[dev-dependencies] +gtest.workspace = true +gstd.workspace = true +gear-core.workspace = true +gclient.workspace = true +galactic-express = { path = "../wasm" } +tokio = "1" diff --git a/contracts/galactic-express/app/src/lib.rs b/contracts/galactic-express/app/src/lib.rs new file mode 100644 index 000000000..52bbb4705 --- /dev/null +++ b/contracts/galactic-express/app/src/lib.rs @@ -0,0 +1,19 @@ +#![no_std] +#![allow(clippy::new_without_default)] + +use sails_rs::prelude::*; +mod services; +use services::galactic_express::GameService; +pub struct Program(()); + +#[program] +impl Program { + pub async fn new(dns_id_and_name: Option<(ActorId, String)>) -> Self { + GameService::init(dns_id_and_name).await; + Self(()) + } + + pub fn galactic_express(&self) -> GameService { + GameService::new() + } +} diff --git a/contracts/galactic-express/app/src/services/galactic_express/funcs.rs b/contracts/galactic-express/app/src/services/galactic_express/funcs.rs new file mode 100644 index 000000000..bd6b6e3a8 --- /dev/null +++ b/contracts/galactic-express/app/src/services/galactic_express/funcs.rs @@ -0,0 +1,366 @@ +use crate::services::galactic_express::utils; +use crate::services::galactic_express::{ + Event, Game, GameError, HaltReason, Participant, Random, Results, Stage, Storage, Turn, + Weather, MAX_FUEL, MAX_PARTICIPANTS, MAX_PAYLOAD, PENALTY_LEVEL, REWARD, TURNS, TURN_ALTITUDE, +}; +use sails_rs::{collections::HashMap, gstd::msg, prelude::*}; + +pub fn create_new_session(storage: &mut Storage, name: String) -> Result { + let msg_src = msg::source(); + let msg_value = msg::value(); + + if storage.player_to_game_id.contains_key(&msg_src) { + return Err(GameError::SeveralRegistrations); + } + + let game = storage.games.entry(msg_src).or_insert_with(|| Game { + admin: msg_src, + admin_name: name, + bid: msg_value, + ..Default::default() + }); + + let stage = &mut game.stage; + + match stage { + Stage::Registration(participants) => { + participants.clear(); + } + Stage::Results { .. } => { + *stage = Stage::Registration(HashMap::::new()) + } + } + + let mut random = Random::new()?; + + game.weather = match random.next() % (Weather::Tornado as u8 + 1) { + 0 => Weather::Clear, + 1 => Weather::Cloudy, + 2 => Weather::Rainy, + 3 => Weather::Stormy, + 4 => Weather::Thunder, + 5 => Weather::Tornado, + _ => unreachable!(), + }; + game.altitude = random.generate(TURN_ALTITUDE.0, TURN_ALTITUDE.1) * TURNS as u16; + game.reward = random.generate(REWARD.0, REWARD.1); + storage.player_to_game_id.insert(msg_src, msg_src); + + Ok(Event::NewSessionCreated { + altitude: game.altitude, + weather: game.weather, + reward: game.reward, + bid: msg_value, + }) +} + +pub fn cancel_game(storage: &mut Storage) -> Result { + let msg_src = msg::source(); + let game = storage.games.get(&msg_src).ok_or(GameError::NoSuchGame)?; + + match &game.stage { + Stage::Registration(players) => { + players.iter().for_each(|(id, _)| { + send_value(*id, game.bid); + storage.player_to_game_id.remove(id); + }); + } + Stage::Results(results) => { + results.rankings.iter().for_each(|(id, _)| { + storage.player_to_game_id.remove(id); + }); + } + } + + storage.player_to_game_id.remove(&msg_src); + storage.games.remove(&msg_src); + Ok(Event::GameCanceled) +} + +pub fn leave_game(storage: &mut Storage) -> Result { + let msg_src = msg::source(); + storage.player_to_game_id.remove(&msg_src); + Ok(Event::GameLeft) +} + +pub fn register( + storage: &mut Storage, + creator: ActorId, + participant: Participant, +) -> Result { + let msg_source = msg::source(); + let msg_value = msg::value(); + let reply = register_for_session(storage, creator, participant, msg_source, msg_value); + if reply.is_err() { + send_value(msg_source, msg_value); + } + reply +} + +fn register_for_session( + storage: &mut Storage, + creator: ActorId, + participant: Participant, + msg_source: ActorId, + msg_value: u128, +) -> Result { + if storage.player_to_game_id.contains_key(&msg_source) { + return Err(GameError::SeveralRegistrations); + } + + if let Some(game) = storage.games.get_mut(&creator) { + if msg_value != game.bid { + return Err(GameError::WrongBid); + } + if let Stage::Results(_) = game.stage { + return Err(GameError::SessionEnded); + } + + let participants = game.stage.mut_participants()?; + + if participants.contains_key(&msg_source) { + return Err(GameError::AlreadyRegistered); + } + + if participants.len() >= MAX_PARTICIPANTS - 1 { + return Err(GameError::SessionFull); + } + + participant.check()?; + participants.insert(msg_source, participant.clone()); + storage.player_to_game_id.insert(msg_source, creator); + + Ok(Event::Registered(msg_source, participant)) + } else { + Err(GameError::NoSuchGame) + } +} + +pub fn cancel_register(storage: &mut Storage) -> Result { + let msg_source = msg::source(); + + let creator = storage + .player_to_game_id + .get(&msg_source) + .ok_or(GameError::Unregistered)?; + let game = storage + .games + .get_mut(creator) + .ok_or(GameError::NoSuchGame)?; + + if msg_source != game.admin { + let participants = game.stage.mut_participants()?; + if participants.contains_key(&msg_source) { + send_value(msg_source, game.bid); + participants.remove(&msg_source).expect("Critical error"); + storage.player_to_game_id.remove(&msg_source); + } else { + return Err(GameError::NoSuchPlayer); + } + Ok(Event::RegistrationCanceled) + } else { + Err(GameError::NotForAdmin) + } +} + +pub fn delete_player(storage: &mut Storage, player_id: ActorId) -> Result { + let msg_source = msg::source(); + + if let Some(game) = storage.games.get_mut(&msg_source) { + if let Stage::Results(_) = game.stage { + return Err(GameError::SessionEnded); + } + + let participants = game.stage.mut_participants()?; + + if participants.contains_key(&player_id) { + send_value(player_id, game.bid); + participants.remove(&player_id).expect("Critical error"); + storage.player_to_game_id.remove(&player_id); + } else { + return Err(GameError::NoSuchPlayer); + } + + Ok(Event::PlayerDeleted { player_id }) + } else { + Err(GameError::NoSuchGame) + } +} + +pub fn start_game( + storage: &mut Storage, + fuel_amount: u8, + payload_amount: u8, +) -> Result { + let msg_source = msg::source(); + + let game = storage + .games + .get_mut(&msg_source) + .ok_or(GameError::NoSuchGame)?; + + if fuel_amount > MAX_FUEL || payload_amount > MAX_PAYLOAD { + return Err(GameError::FuelOrPayloadOverload); + } + let participant = Participant { + id: msg_source, + name: game.admin_name.clone(), + fuel_amount, + payload_amount, + }; + + let participants = game.stage.mut_participants()?; + + if participants.is_empty() { + return Err(GameError::NotEnoughParticipants); + } + participants.insert(msg_source, participant); + + let mut random = Random::new()?; + let mut turns = HashMap::new(); + + for (actor, participant) in participants.into_iter() { + let mut actor_turns = Vec::with_capacity(TURNS); + let mut remaining_fuel = participant.fuel_amount; + + for turn_index in 0..TURNS { + match turn( + turn_index, + remaining_fuel, + &mut random, + game.weather, + participant.payload_amount, + ) { + Ok(fuel_left) => { + remaining_fuel = fuel_left; + + actor_turns.push(Turn::Alive { + fuel_left, + payload_amount: participant.payload_amount, + }); + } + Err(halt_reason) => { + actor_turns.push(Turn::Destroyed(halt_reason)); + + break; + } + } + } + + turns.insert(*actor, actor_turns); + } + + let mut scores: Vec<(ActorId, u128)> = turns + .iter() + .map(|(actor, turns)| { + let last_turn = turns.last().expect("there must be at least 1 turn"); + + ( + *actor, + match last_turn { + Turn::Alive { + fuel_left, + payload_amount, + } => (*payload_amount as u128 + *fuel_left as u128) * game.altitude as u128, + Turn::Destroyed(_) => 0, + }, + ) + }) + .collect(); + + scores.sort_by(|(_, score_a), (_, score_b)| score_a.cmp(score_b)); + + let mut io_turns: Vec> = vec![vec![]; TURNS]; + + for (i, io_turn) in io_turns.iter_mut().enumerate().take(TURNS) { + for (actor, actor_turns) in &turns { + let turn = actor_turns + .get(i) + .unwrap_or_else(|| actor_turns.last().expect("There must be at least 1 turn")); + io_turn.push((*actor, *turn)); + } + } + + let max_value = scores.iter().map(|(_, value)| value).max().unwrap(); + let winners: Vec<_> = scores + .iter() + .filter_map(|(actor_id, value)| { + if value == max_value { + Some(*actor_id) + } else { + None + } + }) + .collect(); + let prize = game.bid * scores.len() as u128 / winners.len() as u128; + + if game.bid != 0 { + winners.iter().for_each(|id| { + send_value(*id, prize); + }); + } + let participants = participants + .iter() + .map(|(id, participant)| (*id, participant.clone())) + .collect(); + + let results = Results { + turns: io_turns, + rankings: scores.clone(), + participants, + }; + game.stage = Stage::Results(results.clone()); + + Ok(Event::GameFinished(results)) +} + +fn turn( + turn: usize, + remaining_fuel: u8, + random: &mut Random, + weather: Weather, + payload: u8, +) -> Result { + let new_remaining_fuel = + match remaining_fuel.checked_sub((payload + 2 * weather as u8) / TURNS as u8) { + Some(actual_fuel) => actual_fuel, + None => return Err(HaltReason::FuelShortage), + }; + + match turn { + 0 => { + // values in "chance" are transmitted as percentages + if random.chance(3) { + return Err(HaltReason::EngineFailure); + } + // this trap for someone who specified a lot of fuel + if remaining_fuel >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) { + return Err(HaltReason::FuelOverload); + } + } + 1 => { + // this trap for someone who specified a lot of payload + if payload >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) { + return Err(HaltReason::PayloadOverload); + } + + if random.chance(5 + weather as u8) { + return Err(HaltReason::SeparationFailure); + } + } + 2 => { + if random.chance(10 + weather as u8) { + return Err(HaltReason::AsteroidCollision); + } + } + _ => unreachable!(), + } + + Ok(new_remaining_fuel) +} + +fn send_value(destination: ActorId, value: u128) { + if value != 0 { + msg::send_with_gas(destination, "", 0, value).expect("Error in sending value"); + } +} diff --git a/contracts/galactic-express/app/src/services/galactic_express/mod.rs b/contracts/galactic-express/app/src/services/galactic_express/mod.rs new file mode 100644 index 000000000..7d9483b7d --- /dev/null +++ b/contracts/galactic-express/app/src/services/galactic_express/mod.rs @@ -0,0 +1,194 @@ +use crate::services; +use gstd::{exec, msg}; +use sails_rs::{collections::HashMap, gstd::service, prelude::*}; +mod funcs; +pub mod utils; +use utils::*; + +#[derive(Default)] +pub struct Storage { + games: HashMap, + player_to_game_id: HashMap, + dns_info: Option<(ActorId, String)>, + admin: ActorId, +} + +#[derive(Default)] +pub struct Game { + admin: ActorId, + admin_name: String, + bid: u128, + altitude: u16, + weather: Weather, + reward: u128, + stage: Stage, +} + +static mut STORAGE: Option = None; + +#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Event { + GameFinished(Results), + NewSessionCreated { + altitude: u16, + weather: Weather, + reward: u128, + bid: u128, + }, + Registered(ActorId, Participant), + RegistrationCanceled, + PlayerDeleted { + player_id: ActorId, + }, + GameCanceled, + GameLeft, + AdminChanged { + new_admin: ActorId, + }, + Killed { + inheritor: ActorId, + }, +} + +#[derive(Clone)] +pub struct GameService(()); + +impl GameService { + pub async fn init(dns_id_and_name: Option<(ActorId, String)>) -> Self { + unsafe { + STORAGE = Some(Storage { + dns_info: dns_id_and_name.clone(), + admin: msg::source(), + ..Default::default() + }); + } + + if let Some((id, name)) = dns_id_and_name { + let request = [ + "Dns".encode(), + "AddNewProgram".to_string().encode(), + (name, exec::program_id()).encode(), + ] + .concat(); + + msg::send_bytes_with_gas_for_reply(id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } + Self(()) + } + pub fn get_mut(&mut self) -> &'static mut Storage { + unsafe { STORAGE.as_mut().expect("Storage is not initialized") } + } + pub fn get(&self) -> &'static Storage { + unsafe { STORAGE.as_ref().expect("Storage is not initialized") } + } +} + +#[service(events = Event)] +impl GameService { + pub fn new() -> Self { + Self(()) + } + pub fn create_new_session(&mut self, name: String) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::create_new_session(storage, name)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn cancel_game(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::cancel_game(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn leave_game(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::leave_game(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn register(&mut self, creator: ActorId, participant: Participant) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::register(storage, creator, participant)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn cancel_register(&mut self) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::cancel_register(storage)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn delete_player(&mut self, player_id: ActorId) { + let storage = self.get_mut(); + let event = services::utils::panicking(|| funcs::delete_player(storage, player_id)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn start_game(&mut self, fuel_amount: u8, payload_amount: u8) { + let storage = self.get_mut(); + let event = + services::utils::panicking(|| funcs::start_game(storage, fuel_amount, payload_amount)); + self.notify_on(event.clone()).expect("Notification Error"); + } + pub fn change_admin(&mut self, new_admin: ActorId) { + let storage = self.get_mut(); + let msg_source = msg::source(); + if storage.admin != msg_source { + services::utils::panic(GameError::DeniedAccess); + } + storage.admin = new_admin; + self.notify_on(Event::AdminChanged { new_admin }) + .expect("Notification Error"); + } + pub async fn kill(&mut self, inheritor: ActorId) { + let storage = self.get(); + if storage.admin != msg::source() { + services::utils::panic(GameError::DeniedAccess); + } + if let Some((id, _name)) = &storage.dns_info { + let request = ["Dns".encode(), "DeleteMe".to_string().encode(), ().encode()].concat(); + + msg::send_bytes_with_gas_for_reply(*id, request, 5_000_000_000, 0, 0) + .expect("Error in sending message") + .await + .expect("Error in `AddNewProgram`"); + } + + self.notify_on(Event::Killed { inheritor }) + .expect("Notification Error"); + exec::exit(inheritor); + } + pub fn get_game(&self, player_id: ActorId) -> Option { + let storage = self.get(); + storage + .player_to_game_id + .get(&player_id) + .and_then(|creator_id| storage.games.get(creator_id)) + .map(|game| { + let stage = match &game.stage { + Stage::Registration(participants_data) => { + StageState::Registration(participants_data.clone().into_iter().collect()) + } + Stage::Results(results) => StageState::Results(results.clone()), + }; + + GameState { + admin: game.admin, + admin_name: game.admin_name.clone(), + altitude: game.altitude, + weather: game.weather, + reward: game.reward, + stage, + bid: game.bid, + } + }) + } + pub fn all(&self) -> State { + self.get().into() + } + pub fn admin(&self) -> &'static ActorId { + &self.get().admin + } + pub fn dns_info(&self) -> &'static Option<(ActorId, String)> { + &self.get().dns_info + } +} diff --git a/contracts/galactic-express/app/src/services/galactic_express/utils.rs b/contracts/galactic-express/app/src/services/galactic_express/utils.rs new file mode 100644 index 000000000..788c81aed --- /dev/null +++ b/contracts/galactic-express/app/src/services/galactic_express/utils.rs @@ -0,0 +1,252 @@ +use crate::services::galactic_express::Storage; +use num_traits::FromBytes; +use sails_rs::{ + collections::HashMap, + errors::Error as GstdError, + gstd::exec, + ops::{Add, Rem, Sub}, + prelude::*, +}; + +pub const MAX_PARTICIPANTS: usize = 4; +pub const TURNS: usize = 3; + +/// Represents a range of the minimum & the maximum reward for a session. +pub const REWARD: (u128, u128) = (80, 360); +/// Represents a range of the minimum & the maximum turn altitude. +pub const TURN_ALTITUDE: (u16, u16) = (500, 1_000); +/// Dangerous level for high fuel and payload values +/// This is to account for the scenario where a player specifies a significant amount of fuel +/// or a large payload, resulting in a greater likelihood of mission failure. +pub const PENALTY_LEVEL: u8 = 80; +// maximum fuel value that can be entered by the user +pub const MAX_FUEL: u8 = 100; +// maximum payload value that can be entered by the user +pub const MAX_PAYLOAD: u8 = 100; + +pub struct Random { + index: usize, + random: [u8; 32], +} + +impl Random { + pub fn new() -> Result { + exec::random([0; 32]) + .map(|(random, _)| Self { index: 0, random }) + .map_err(|error| GstdError::from(error).into()) + } + + pub fn next(&mut self) -> u8 { + let next = *self + .random + .get(self.index) + .expect("index for the random array traversing must'n overflow"); + + self.index += 1; + + next + } + + pub fn generate(&mut self, min: T, max: T) -> T + where + T: FromBytes + + Add + + Sub + + Rem + + Copy, + { + min + T::from_le_bytes(&array::from_fn(|_| self.next())) % (max - min) + } + + pub fn chance(&mut self, probability: u8) -> bool { + assert!(probability < 101, "probability can't be more than 100"); + + self.next() % 100 < probability + } +} + +#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum GameError { + StateUninitaliazed, + GstdError(String), + SessionEnded, + FuelOrPayloadOverload, + SessionFull, + NotEnoughParticipants, + NoSuchGame, + WrongBid, + NoSuchPlayer, + Unregistered, + AlreadyRegistered, + SeveralRegistrations, + NotForAdmin, + DeniedAccess, +} + +impl From for GameError { + fn from(error: GstdError) -> Self { + GameError::GstdError(error.to_string()) + } +} + +#[derive(Encode, Decode, TypeInfo, Default, Clone, Copy, Debug, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Weather { + #[default] + Clear, + Cloudy, + Rainy, + Stormy, + Thunder, + Tornado, +} + +pub enum Stage { + Registration(HashMap), + Results(Results), +} + +impl Stage { + pub fn mut_participants(&mut self) -> Result<&mut HashMap, GameError> { + if let Stage::Registration(participants) = self { + Ok(participants) + } else { + Err(GameError::SessionEnded) + } + } +} + +impl Default for Stage { + fn default() -> Self { + Self::Results(Results::default()) + } +} + +#[derive(Encode, Decode, TypeInfo, Default, Clone, Debug, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Results { + pub turns: Vec>, + pub rankings: Vec<(ActorId, u128)>, + pub participants: Vec<(ActorId, Participant)>, +} + +#[derive(Encode, Decode, TypeInfo, Clone, Debug, Default, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct Participant { + pub id: ActorId, + pub name: String, + pub fuel_amount: u8, + pub payload_amount: u8, +} + +impl Participant { + pub fn check(&self) -> Result<(), GameError> { + if self.fuel_amount > MAX_FUEL || self.payload_amount > MAX_PAYLOAD { + Err(GameError::FuelOrPayloadOverload) + } else { + Ok(()) + } + } +} + +#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum Turn { + Alive { fuel_left: u8, payload_amount: u8 }, + Destroyed(HaltReason), +} + +#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum HaltReason { + PayloadOverload, + FuelOverload, + SeparationFailure, + AsteroidCollision, + FuelShortage, + EngineFailure, +} + +#[derive(Encode, Decode, TypeInfo, Debug)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct State { + pub games: Vec<(ActorId, GameState)>, + pub player_to_game_id: Vec<(ActorId, ActorId)>, + pub dns_info: Option<(ActorId, String)>, + pub admin: ActorId, +} + +#[derive(Encode, Decode, TypeInfo, Debug)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub struct GameState { + pub admin: ActorId, + pub admin_name: String, + pub altitude: u16, + pub weather: Weather, + pub reward: u128, + pub stage: StageState, + pub bid: u128, +} + +#[derive(Encode, Decode, TypeInfo, Debug)] +#[codec(crate = sails_rs::scale_codec)] +#[scale_info(crate = sails_rs::scale_info)] +pub enum StageState { + Registration(Vec<(ActorId, Participant)>), + Results(Results), +} + +impl From<&Storage> for State { + fn from(value: &Storage) -> Self { + let Storage { + games, + player_to_game_id, + dns_info, + admin, + } = value; + + let games = games + .iter() + .map(|(id, game)| { + let stage = match &game.stage { + Stage::Registration(participants_data) => StageState::Registration( + participants_data + .iter() + .map(|(actor_id, participant)| (*actor_id, participant.clone())) + .collect(), + ), + Stage::Results(results) => StageState::Results(results.clone()), + }; + + let game_state = GameState { + admin: game.admin, + admin_name: game.admin_name.clone(), + altitude: game.altitude, + weather: game.weather, + reward: game.reward, + stage, + bid: game.bid, + }; + (*id, game_state) + }) + .collect(); + + let player_to_game_id = player_to_game_id.iter().map(|(k, v)| (*k, *v)).collect(); + + Self { + games, + player_to_game_id, + dns_info: dns_info.clone(), + admin: *admin, + } + } +} diff --git a/contracts/galactic-express/app/src/services/mod.rs b/contracts/galactic-express/app/src/services/mod.rs new file mode 100644 index 000000000..a7aedd83a --- /dev/null +++ b/contracts/galactic-express/app/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod galactic_express; +pub mod utils; diff --git a/contracts/galactic-express/app/src/services/utils.rs b/contracts/galactic-express/app/src/services/utils.rs new file mode 100644 index 000000000..9dee576e1 --- /dev/null +++ b/contracts/galactic-express/app/src/services/utils.rs @@ -0,0 +1,13 @@ +use core::fmt::Debug; +use gstd::{ext, format}; + +pub fn panicking Result>(f: F) -> T { + match f() { + Ok(v) => v, + Err(e) => panic(e), + } +} + +pub fn panic(err: impl Debug) -> ! { + ext::panic(&format!("{err:?}")) +} diff --git a/contracts/galactic-express/app/tests/test.rs b/contracts/galactic-express/app/tests/test.rs new file mode 100644 index 000000000..aca592b07 --- /dev/null +++ b/contracts/galactic-express/app/tests/test.rs @@ -0,0 +1,325 @@ +use galactic_express::{ + traits::{GalacticExpress, GalacticExpressFactory}, + GalacticExpress as GalacticExpressClient, GalacticExpressFactory as Factory, Participant, + StageState, +}; +use gstd::errors::{ErrorReplyReason, SimpleExecutionError}; +use sails_rs::calls::*; +use sails_rs::errors::{Error, RtlError}; +use sails_rs::gtest::{calls::*, System}; + +pub const ADMIN: u64 = 10; +pub const PLAYERS: [u64; 3] = [12, 13, 14]; + +#[tokio::test] +async fn test_play_game() { + let system = System::new(); + system.init_logger(); + system.mint_to(ADMIN, 100_000_000_000_000); + system.mint_to(PLAYERS[0], 100_000_000_000_000); + system.mint_to(PLAYERS[1], 100_000_000_000_000); + system.mint_to(PLAYERS[2], 100_000_000_000_000); + let program_space = GTestRemoting::new(system, ADMIN.into()); + program_space.system().init_logger(); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/galactic_express.opt.wasm"); + + let galactic_express_factory = Factory::new(program_space.clone()); + let galactic_express_id = galactic_express_factory + .new(None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = GalacticExpressClient::new(program_space.clone()); + + let bid = 11_000_000_000_000; + program_space.system().mint_to(ADMIN, bid); + + // create_new_session + client + .create_new_session("Game".to_string()) + .with_value(bid) + .send_recv(galactic_express_id) + .await + .unwrap(); + // check game state + let state = client.all().recv(galactic_express_id).await.unwrap(); + assert!(!state.games.is_empty()); + assert!(!state.player_to_game_id.is_empty()); + + // register + for player_id in PLAYERS { + let player = Participant { + id: player_id.into(), + name: "player".to_string(), + fuel_amount: 42, + payload_amount: 20, + }; + program_space.system().mint_to(player_id, bid); + + client + .register(ADMIN.into(), player) + .with_value(bid) + .with_args(GTestArgs::new(player_id.into())) + .send_recv(galactic_express_id) + .await + .unwrap(); + } + // check game state + let state = client.all().recv(galactic_express_id).await.unwrap(); + assert_eq!(state.player_to_game_id.len(), 4); + if let StageState::Registration(participants) = &state.games[0].1.stage { + assert_eq!(participants.len(), 3); + } + + // start game + client + .start_game(42, 20) + .send_recv(galactic_express_id) + .await + .unwrap(); + + let state = client.all().recv(galactic_express_id).await.unwrap(); + if let StageState::Results(results) = &state.games[0].1.stage { + assert_eq!(results.rankings.len(), 4); + } +} + +#[tokio::test] +async fn cancel_register_and_delete_player() { + let system = System::new(); + system.init_logger(); + system.mint_to(ADMIN, 100_000_000_000_000); + system.mint_to(PLAYERS[0], 100_000_000_000_000); + system.mint_to(PLAYERS[1], 100_000_000_000_000); + system.mint_to(PLAYERS[2], 100_000_000_000_000); + let program_space = GTestRemoting::new(system, ADMIN.into()); + program_space.system().init_logger(); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/galactic_express.opt.wasm"); + + let galactic_express_factory = Factory::new(program_space.clone()); + let galactic_express_id = galactic_express_factory + .new(None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = GalacticExpressClient::new(program_space.clone()); + + let bid = 11_000_000_000_000; + program_space.system().mint_to(ADMIN, bid); + + // create_new_session + client + .create_new_session("Game".to_string()) + .with_value(bid) + .send_recv(galactic_express_id) + .await + .unwrap(); + // check game state + let state = client.all().recv(galactic_express_id).await.unwrap(); + assert!(!state.games.is_empty()); + assert!(!state.player_to_game_id.is_empty()); + + // register + for player_id in PLAYERS { + let player = Participant { + id: player_id.into(), + name: "player".to_string(), + fuel_amount: 42, + payload_amount: 20, + }; + program_space.system().mint_to(player_id, bid); + + client + .register(ADMIN.into(), player) + .with_value(bid) + .with_args(GTestArgs::new(player_id.into())) + .send_recv(galactic_express_id) + .await + .unwrap(); + } + // check game state + let state = client.all().recv(galactic_express_id).await.unwrap(); + assert_eq!(state.player_to_game_id.len(), 4); + if let StageState::Registration(participants) = &state.games[0].1.stage { + assert_eq!(participants.len(), 3); + } + + // cancel_register + client + .cancel_register() + .with_args(GTestArgs::new(PLAYERS[0].into())) + .send_recv(galactic_express_id) + .await + .unwrap(); + + // check game state + let state = client.all().recv(galactic_express_id).await.unwrap(); + if let StageState::Registration(participants) = &state.games[0].1.stage { + assert_eq!(participants.len(), 2); + } + assert_eq!(state.player_to_game_id.len(), 3); + + // delete_player + client + .delete_player(PLAYERS[1].into()) + .send_recv(galactic_express_id) + .await + .unwrap(); + + // check game state + let state = client.all().recv(galactic_express_id).await.unwrap(); + if let StageState::Registration(participants) = &state.games[0].1.stage { + assert_eq!(participants.len(), 1); + } + assert_eq!(state.player_to_game_id.len(), 2); +} + +#[tokio::test] +async fn errors() { + let system = System::new(); + system.init_logger(); + system.mint_to(ADMIN, 100_000_000_000_000); + system.mint_to(PLAYERS[0], 100_000_000_000_000); + system.mint_to(PLAYERS[1], 100_000_000_000_000); + system.mint_to(PLAYERS[2], 100_000_000_000_000); + let program_space = GTestRemoting::new(system, ADMIN.into()); + program_space.system().init_logger(); + let code_id = program_space + .system() + .submit_code_file("../../target/wasm32-unknown-unknown/release/galactic_express.opt.wasm"); + + let galactic_express_factory = Factory::new(program_space.clone()); + let galactic_express_id = galactic_express_factory + .new(None) + .send_recv(code_id, "123") + .await + .unwrap(); + + let mut client = GalacticExpressClient::new(program_space.clone()); + + let bid = 11_000_000_000_000; + program_space.system().mint_to(ADMIN, bid); + + let player = Participant { + id: ADMIN.into(), + name: "player".to_string(), + fuel_amount: 42, + payload_amount: 20, + }; + + let res = client + .register(ADMIN.into(), player) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "NoSuchGame".to_string()); + + client + .create_new_session("Game".to_string()) + .with_value(bid) + .send_recv(galactic_express_id) + .await + .unwrap(); + + let player = Participant { + id: ADMIN.into(), + name: "player".to_string(), + fuel_amount: 42, + payload_amount: 20, + }; + + let res = client + .register(ADMIN.into(), player) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "SeveralRegistrations".to_string()); + + let res = client + .start_game(42, 20) + .with_args(GTestArgs::new(PLAYERS[0].into())) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "NoSuchGame".to_string()); + + let res = client + .start_game(42, 20) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "NotEnoughParticipants".to_string()); + + // register + for player_id in PLAYERS { + let player = Participant { + id: player_id.into(), + name: "player".to_string(), + fuel_amount: 42, + payload_amount: 20, + }; + program_space.system().mint_to(player_id, bid); + + client + .register(ADMIN.into(), player) + .with_value(bid) + .with_args(GTestArgs::new(player_id.into())) + .send_recv(galactic_express_id) + .await + .unwrap(); + } + + let res = client + .start_game(101, 100) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "FuelOrPayloadOverload".to_string()); + + let res = client + .start_game(100, 101) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "FuelOrPayloadOverload".to_string()); + + let res = client + .start_game(101, 101) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "FuelOrPayloadOverload".to_string()); + + let player = Participant { + id: 100.into(), + name: "player".to_string(), + fuel_amount: 42, + payload_amount: 20, + }; + program_space.system().mint_to(100, 100_000_000_000_000); + + let res = client + .register(ADMIN.into(), player) + .with_value(bid) + .with_args(GTestArgs::new(100.into())) + .send_recv(galactic_express_id) + .await; + + assert_error(&res, "SessionFull".to_string()); +} + +fn assert_error(res: &Result<(), Error>, error: String) { + assert!(matches!( + res, + Err(sails_rs::errors::Error::Rtl(RtlError::ReplyHasError( + ErrorReplyReason::Execution(SimpleExecutionError::UserspacePanic), + message + ))) if *message == "Panic occurred: ".to_string() + &error + )); +} diff --git a/contracts/galactic-express/build.rs b/contracts/galactic-express/build.rs deleted file mode 100644 index 126c47cae..000000000 --- a/contracts/galactic-express/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -use galactic_express_io::ContractMetadata; - -fn main() { - gear_wasm_builder::build_with_metadata::(); -} diff --git a/contracts/galactic-express/io/Cargo.toml b/contracts/galactic-express/io/Cargo.toml deleted file mode 100644 index a0df31052..000000000 --- a/contracts/galactic-express/io/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "galactic-express-io" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -gmeta.workspace = true -gstd.workspace = true -parity-scale-codec.workspace = true -scale-info.workspace = true diff --git a/contracts/galactic-express/io/src/lib.rs b/contracts/galactic-express/io/src/lib.rs deleted file mode 100644 index 47f4ae9cc..000000000 --- a/contracts/galactic-express/io/src/lib.rs +++ /dev/null @@ -1,181 +0,0 @@ -#![no_std] - -use gmeta::{InOut, Metadata, Out}; -use gstd::{errors::Error as GstdError, prelude::*, ActorId}; - -pub struct ContractMetadata; - -impl Metadata for ContractMetadata { - type Init = Out>; - type Handle = InOut>; - type Reply = (); - type Others = (); - type Signal = (); - type State = InOut; -} - -pub const MAX_PARTICIPANTS: usize = 4; -pub const TURNS: usize = 3; - -/// Represents a range of the minimum & the maximum reward for a session. -pub const REWARD: (u128, u128) = (80, 360); -/// Represents a range of the minimum & the maximum turn altitude. -pub const TURN_ALTITUDE: (u16, u16) = (500, 1_000); -/// Dangerous level for high fuel and payload values -/// This is to account for the scenario where a player specifies a significant amount of fuel -/// or a large payload, resulting in a greater likelihood of mission failure. -pub const PENALTY_LEVEL: u8 = 80; -// maximum fuel value that can be entered by the user -pub const MAX_FUEL: u8 = 100; -// maximum payload value that can be entered by the user -pub const MAX_PAYLOAD: u8 = 100; - -#[derive(Encode, Decode, TypeInfo)] -pub enum StateQuery { - All, - GetGame { player_id: ActorId }, -} - -#[derive(Encode, Decode, TypeInfo)] -pub enum StateReply { - All(State), - Game(Option), -} - -#[derive(Encode, Decode, TypeInfo, Debug)] -pub struct State { - pub games: Vec<(ActorId, GameState)>, - pub player_to_game_id: Vec<(ActorId, ActorId)>, -} - -#[derive(Encode, Decode, TypeInfo, Debug)] -pub struct GameState { - pub admin: ActorId, - pub admin_name: String, - pub altitude: u16, - pub weather: Weather, - pub reward: u128, - pub stage: StageState, - pub bid: u128, -} - -#[derive(Encode, Decode, TypeInfo, Debug)] -pub enum StageState { - Registration(Vec<(ActorId, Participant)>), - Results(Results), -} - -#[derive(Encode, Decode, TypeInfo, Default, Clone, Debug, PartialEq, Eq)] -pub struct Results { - pub turns: Vec>, - pub rankings: Vec<(ActorId, u128)>, - pub participants: Vec<(ActorId, Participant)>, -} - -#[derive(Encode, Decode, TypeInfo)] -pub enum Action { - CreateNewSession { - name: String, - }, - Register { - creator: ActorId, - participant: Participant, - }, - CancelRegistration, - DeletePlayer { - player_id: ActorId, - }, - CancelGame, - LeaveGame, - StartGame { - fuel_amount: u8, - payload_amount: u8, - }, -} - -#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)] -pub enum Event { - GameFinished(Results), - AdminChanged(ActorId, ActorId), - NewSessionCreated { - altitude: u16, - weather: Weather, - reward: u128, - bid: u128, - }, - Registered(ActorId, Participant), - RegistrationCanceled, - PlayerDeleted { - player_id: ActorId, - }, - GameCanceled, - GameLeft, -} - -#[derive(Encode, Decode, TypeInfo, Clone, Debug, Default, PartialEq, Eq)] -pub struct Participant { - pub id: ActorId, - pub name: String, - pub fuel_amount: u8, - pub payload_amount: u8, -} - -impl Participant { - pub fn check(&self) -> Result<(), Error> { - if self.fuel_amount > MAX_FUEL || self.payload_amount > MAX_PAYLOAD { - Err(Error::FuelOrPayloadOverload) - } else { - Ok(()) - } - } -} - -#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)] -pub enum HaltReason { - PayloadOverload, - FuelOverload, - SeparationFailure, - AsteroidCollision, - FuelShortage, - EngineFailure, -} - -#[derive(Encode, Decode, TypeInfo, Clone, Copy, Debug, PartialEq, Eq)] -pub enum Turn { - Alive { fuel_left: u8, payload_amount: u8 }, - Destroyed(HaltReason), -} - -#[derive(Encode, Decode, TypeInfo, Default, Clone, Copy, Debug, PartialEq, Eq)] -pub enum Weather { - #[default] - Clear, - Cloudy, - Rainy, - Stormy, - Thunder, - Tornado, -} - -#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq)] -pub enum Error { - StateUninitaliazed, - GstdError(String), - SessionEnded, - FuelOrPayloadOverload, - SessionFull, - NotEnoughParticipants, - NoSuchGame, - WrongBid, - NoSuchPlayer, - Unregistered, - AlreadyRegistered, - SeveralRegistrations, - NotForAdmin, -} - -impl From for Error { - fn from(error: GstdError) -> Self { - Error::GstdError(error.to_string()) - } -} diff --git a/contracts/galactic-express/src/lib.rs b/contracts/galactic-express/src/lib.rs deleted file mode 100644 index 3350c60c3..000000000 --- a/contracts/galactic-express/src/lib.rs +++ /dev/null @@ -1,556 +0,0 @@ -#![no_std] - -use galactic_express_io::*; -use gstd::{ - collections::HashMap, - errors::Error as GstdError, - exec, msg, - ops::{Add, Rem, Sub}, - prelude::*, - ActorId, -}; -use num_traits::FromBytes; - -struct Random { - index: usize, - random: [u8; 32], -} - -impl Random { - fn new() -> Result { - exec::random([0; 32]) - .map(|(random, _)| Self { index: 0, random }) - .map_err(|error| GstdError::from(error).into()) - } - - fn next(&mut self) -> u8 { - let next = *self - .random - .get(self.index) - .expect("index for the random array traversing must'n overflow"); - - self.index += 1; - - next - } - - fn generate(&mut self, min: T, max: T) -> T - where - T: FromBytes - + Add - + Sub - + Rem - + Copy, - { - min + T::from_le_bytes(&array::from_fn(|_| self.next())) % (max - min) - } - - fn chance(&mut self, probability: u8) -> bool { - assert!(probability < 101, "probability can't be more than 100"); - - self.next() % 100 < probability - } -} - -static mut STATE: Option = None; - -fn state_mut() -> Result<&'static mut Contract, Error> { - unsafe { STATE.as_mut().ok_or(Error::StateUninitaliazed) } -} - -enum Stage { - Registration(HashMap), - Results(Results), -} - -impl Stage { - fn mut_participants(&mut self) -> Result<&mut HashMap, Error> { - if let Stage::Registration(participants) = self { - Ok(participants) - } else { - Err(Error::SessionEnded) - } - } -} - -impl Default for Stage { - fn default() -> Self { - Self::Results(Results::default()) - } -} - -#[derive(Default)] -struct Contract { - games: HashMap, - player_to_game_id: HashMap, -} - -#[derive(Default)] -pub struct Game { - admin: ActorId, - admin_name: String, - bid: u128, - altitude: u16, - weather: Weather, - reward: u128, - stage: Stage, -} - -impl Contract { - fn create_new_session(&mut self, name: String) -> Result { - let msg_src = msg::source(); - let msg_value = msg::value(); - - if self.player_to_game_id.contains_key(&msg_src) { - return Err(Error::SeveralRegistrations); - } - - let game = self.games.entry(msg_src).or_insert_with(|| Game { - admin: msg_src, - admin_name: name, - bid: msg_value, - ..Default::default() - }); - - let stage = &mut game.stage; - - match stage { - Stage::Registration(participants) => { - participants.clear(); - } - Stage::Results { .. } => *stage = Stage::Registration(HashMap::new()), - } - - let mut random = Random::new()?; - - game.weather = match random.next() % (Weather::Tornado as u8 + 1) { - 0 => Weather::Clear, - 1 => Weather::Cloudy, - 2 => Weather::Rainy, - 3 => Weather::Stormy, - 4 => Weather::Thunder, - 5 => Weather::Tornado, - _ => unreachable!(), - }; - game.altitude = random.generate(TURN_ALTITUDE.0, TURN_ALTITUDE.1) * TURNS as u16; - game.reward = random.generate(REWARD.0, REWARD.1); - self.player_to_game_id.insert(msg_src, msg_src); - - Ok(Event::NewSessionCreated { - altitude: game.altitude, - weather: game.weather, - reward: game.reward, - bid: msg_value, - }) - } - - fn cancel_game(&mut self) -> Result { - let msg_src = msg::source(); - let game = self.games.get(&msg_src).ok_or(Error::NoSuchGame)?; - - match &game.stage { - Stage::Registration(players) => { - players.iter().for_each(|(id, _)| { - send_value(*id, game.bid); - self.player_to_game_id.remove(id); - }); - } - Stage::Results(results) => { - results.rankings.iter().for_each(|(id, _)| { - self.player_to_game_id.remove(id); - }); - } - } - - self.player_to_game_id.remove(&msg_src); - self.games.remove(&msg_src); - Ok(Event::GameCanceled) - } - - fn leave_game(&mut self) -> Result { - let msg_src = msg::source(); - self.player_to_game_id.remove(&msg_src); - Ok(Event::GameLeft) - } - - fn register( - &mut self, - creator: ActorId, - participant: Participant, - msg_source: ActorId, - msg_value: u128, - ) -> Result { - if self.player_to_game_id.contains_key(&msg_source) { - return Err(Error::SeveralRegistrations); - } - - if let Some(game) = self.games.get_mut(&creator) { - if msg_value != game.bid { - return Err(Error::WrongBid); - } - if let Stage::Results(_) = game.stage { - return Err(Error::SessionEnded); - } - - let participants = game.stage.mut_participants()?; - - if participants.contains_key(&msg_source) { - return Err(Error::AlreadyRegistered); - } - - if participants.len() >= MAX_PARTICIPANTS - 1 { - return Err(Error::SessionFull); - } - - participant.check()?; - participants.insert(msg_source, participant.clone()); - self.player_to_game_id.insert(msg_source, creator); - - Ok(Event::Registered(msg_source, participant)) - } else { - Err(Error::NoSuchGame) - } - } - - fn cancel_register(&mut self) -> Result { - let msg_source = msg::source(); - - let creator = self - .player_to_game_id - .get(&msg_source) - .ok_or(Error::Unregistered)?; - let game = self.games.get_mut(creator).ok_or(Error::NoSuchGame)?; - - if msg_source != game.admin { - let participants = game.stage.mut_participants()?; - if participants.contains_key(&msg_source) { - send_value(msg_source, game.bid); - participants.remove(&msg_source).expect("Critical error"); - self.player_to_game_id.remove(&msg_source); - } else { - return Err(Error::NoSuchPlayer); - } - Ok(Event::RegistrationCanceled) - } else { - Err(Error::NotForAdmin) - } - } - fn delete_player(&mut self, player_id: ActorId) -> Result { - let msg_source = msg::source(); - - if let Some(game) = self.games.get_mut(&msg_source) { - if let Stage::Results(_) = game.stage { - return Err(Error::SessionEnded); - } - - let participants = game.stage.mut_participants()?; - - if participants.contains_key(&player_id) { - send_value(player_id, game.bid); - participants.remove(&player_id).expect("Critical error"); - self.player_to_game_id.remove(&player_id); - } else { - return Err(Error::NoSuchPlayer); - } - - Ok(Event::PlayerDeleted { player_id }) - } else { - Err(Error::NoSuchGame) - } - } - - async fn start_game(&mut self, fuel_amount: u8, payload_amount: u8) -> Result { - let msg_source = msg::source(); - - let game = self.games.get_mut(&msg_source).ok_or(Error::NoSuchGame)?; - - if fuel_amount > MAX_FUEL || payload_amount > MAX_PAYLOAD { - return Err(Error::FuelOrPayloadOverload); - } - let participant = Participant { - id: msg_source, - name: game.admin_name.clone(), - fuel_amount, - payload_amount, - }; - - let participants = game.stage.mut_participants()?; - - if participants.is_empty() { - return Err(Error::NotEnoughParticipants); - } - participants.insert(msg_source, participant); - - let mut random = Random::new()?; - let mut turns = HashMap::new(); - - for (actor, participant) in participants.into_iter() { - let mut actor_turns = Vec::with_capacity(TURNS); - let mut remaining_fuel = participant.fuel_amount; - - for turn_index in 0..TURNS { - match turn( - turn_index, - remaining_fuel, - &mut random, - game.weather, - participant.payload_amount, - ) { - Ok(fuel_left) => { - remaining_fuel = fuel_left; - - actor_turns.push(Turn::Alive { - fuel_left, - payload_amount: participant.payload_amount, - }); - } - Err(halt_reason) => { - actor_turns.push(Turn::Destroyed(halt_reason)); - - break; - } - } - } - - turns.insert(*actor, actor_turns); - } - - let mut scores: Vec<(ActorId, u128)> = turns - .iter() - .map(|(actor, turns)| { - let last_turn = turns.last().expect("there must be at least 1 turn"); - - ( - *actor, - match last_turn { - Turn::Alive { - fuel_left, - payload_amount, - } => (*payload_amount as u128 + *fuel_left as u128) * game.altitude as u128, - Turn::Destroyed(_) => 0, - }, - ) - }) - .collect(); - - scores.sort_by(|(_, score_a), (_, score_b)| score_a.cmp(score_b)); - - let mut io_turns: Vec> = vec![vec![]; TURNS]; - - for (i, io_turn) in io_turns.iter_mut().enumerate().take(TURNS) { - for (actor, actor_turns) in &turns { - let turn = actor_turns - .get(i) - .unwrap_or_else(|| actor_turns.last().expect("There must be at least 1 turn")); - io_turn.push((*actor, *turn)); - } - } - - let max_value = scores.iter().map(|(_, value)| value).max().unwrap(); - let winners: Vec<_> = scores - .iter() - .filter_map(|(actor_id, value)| { - if value == max_value { - Some(*actor_id) - } else { - None - } - }) - .collect(); - let prize = game.bid * scores.len() as u128 / winners.len() as u128; - - if game.bid != 0 { - winners.iter().for_each(|id| { - send_value(*id, prize); - }); - } - let participants = participants - .iter() - .map(|(id, participant)| (*id, participant.clone())) - .collect(); - - let results = Results { - turns: io_turns, - rankings: scores.clone(), - participants, - }; - game.stage = Stage::Results(results.clone()); - - Ok(Event::GameFinished(results)) - } -} - -fn turn( - turn: usize, - remaining_fuel: u8, - random: &mut Random, - weather: Weather, - payload: u8, -) -> Result { - let new_remaining_fuel = - match remaining_fuel.checked_sub((payload + 2 * weather as u8) / TURNS as u8) { - Some(actual_fuel) => actual_fuel, - None => return Err(HaltReason::FuelShortage), - }; - - match turn { - 0 => { - // values in "chance" are transmitted as percentages - if random.chance(3) { - return Err(HaltReason::EngineFailure); - } - // this trap for someone who specified a lot of fuel - if remaining_fuel >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) { - return Err(HaltReason::FuelOverload); - } - } - 1 => { - // this trap for someone who specified a lot of payload - if payload >= PENALTY_LEVEL - 2 * weather as u8 && random.chance(10) { - return Err(HaltReason::PayloadOverload); - } - - if random.chance(5 + weather as u8) { - return Err(HaltReason::SeparationFailure); - } - } - 2 => { - if random.chance(10 + weather as u8) { - return Err(HaltReason::AsteroidCollision); - } - } - _ => unreachable!(), - } - - Ok(new_remaining_fuel) -} - -fn send_value(destination: ActorId, value: u128) { - if value != 0 { - msg::send_with_gas(destination, "", 0, value).expect("Error in sending value"); - } -} - -#[no_mangle] -extern fn init() { - msg::reply(process_init(), 0).expect("failed to encode or reply from `main()`"); -} - -fn process_init() -> Result<(), Error> { - unsafe { - STATE = Some(Contract { - ..Default::default() - }); - } - - Ok(()) -} - -#[gstd::async_main] -async fn main() { - msg::reply(process_main().await, 0).expect("failed to encode or reply from `main()`"); -} - -async fn process_main() -> Result { - let action = msg::load()?; - let contract = state_mut()?; - - match action { - Action::CreateNewSession { name } => contract.create_new_session(name), - Action::Register { - creator, - participant, - } => { - let msg_source = msg::source(); - let msg_value = msg::value(); - let reply = contract.register(creator, participant, msg_source, msg_value); - if reply.is_err() { - send_value(msg_source, msg_value); - } - reply - } - Action::CancelRegistration => contract.cancel_register(), - Action::DeletePlayer { player_id } => contract.delete_player(player_id), - Action::CancelGame => contract.cancel_game(), - Action::LeaveGame => contract.leave_game(), - Action::StartGame { - fuel_amount, - payload_amount, - } => contract.start_game(fuel_amount, payload_amount).await, - } -} - -#[no_mangle] -extern fn state() { - let state = unsafe { STATE.take().expect("Unexpected error in taking state") }; - let query: StateQuery = msg::load().expect("Unable to load the state query"); - let reply = match query { - StateQuery::All => StateReply::All(state.into()), - StateQuery::GetGame { player_id } => { - let game_state = state - .player_to_game_id - .get(&player_id) - .and_then(|creator_id| state.games.get(creator_id)) - .map(|game| { - let stage = match &game.stage { - Stage::Registration(participants_data) => StageState::Registration( - participants_data.clone().into_iter().collect(), - ), - Stage::Results(results) => StageState::Results(results.clone()), - }; - - GameState { - admin: game.admin, - admin_name: game.admin_name.clone(), - altitude: game.altitude, - weather: game.weather, - reward: game.reward, - stage, - bid: game.bid, - } - }); - - StateReply::Game(game_state) - } - }; - msg::reply(reply, 0).expect("Unable to share the state"); -} - -impl From for State { - fn from(value: Contract) -> Self { - let Contract { - games, - player_to_game_id, - } = value; - - let games = games - .into_iter() - .map(|(id, game)| { - let stage = match game.stage { - Stage::Registration(participants_data) => { - StageState::Registration(participants_data.into_iter().collect()) - } - Stage::Results(results) => StageState::Results(results), - }; - - let game_state = GameState { - admin: game.admin, - admin_name: game.admin_name.clone(), - altitude: game.altitude, - weather: game.weather, - reward: game.reward, - stage, - bid: game.bid, - }; - (id, game_state) - }) - .collect(); - - let player_to_game_id = player_to_game_id.into_iter().collect(); - - Self { - games, - player_to_game_id, - } - } -} diff --git a/contracts/galactic-express/tests/test.rs b/contracts/galactic-express/tests/test.rs deleted file mode 100644 index 1b2b8c3ed..000000000 --- a/contracts/galactic-express/tests/test.rs +++ /dev/null @@ -1,142 +0,0 @@ -// use utils::prelude::*; - -// mod utils; - -// #[test] -// fn test() { -// let system = utils::initialize_system(); -// let mut rockets = GalEx::initialize(&system, ADMIN); - -// let bid = 11_000_000_000_000; -// system.mint_to(ADMIN, bid); -// rockets -// .create_new_session(ADMIN, "admin".to_string(), bid) -// .succeed(0, 0); - -// for player_id in PLAYERS { -// let player = Participant { -// id: player_id.into(), -// name: "player".to_string(), -// fuel_amount: 42, -// payload_amount: 20, -// }; -// system.mint_to(player_id, bid); -// rockets -// .register(player_id, ADMIN.into(), player.clone(), bid) -// .succeed((player_id, player), 0); -// } - -// let state = rockets.state().expect("Unexpected invalid state."); - -// if let StageState::Registration(participants) = &state.games[0].1.stage { -// assert_eq!(participants.len(), 3); -// } - -// rockets -// .start_game(ADMIN, 42, 20) -// .succeed(PLAYERS.into_iter().chain(iter::once(ADMIN)).collect(), 3); // 3 since three players win and msg::send_with_gas is sent to them - -// let state = rockets.state().expect("Unexpected invalid state."); - -// if let StageState::Results(results) = &state.games[0].1.stage { -// assert_eq!(results.rankings.len(), 4); -// } -// } - -// #[test] -// fn cancel_register_and_delete_player() { -// let system = utils::initialize_system(); -// let mut rockets = GalEx::initialize(&system, ADMIN); - -// let bid = 11_000_000_000_000; -// system.mint_to(ADMIN, bid); -// rockets -// .create_new_session(ADMIN, "admin".to_string(), bid) -// .succeed(0_u128, 0); - -// for player_id in PLAYERS { -// let player = Participant { -// id: player_id.into(), -// name: "player".to_string(), -// fuel_amount: 42, -// payload_amount: 20, -// }; -// system.mint_to(player_id, bid); -// rockets -// .register(player_id, ADMIN.into(), player.clone(), bid) -// .succeed((player_id, player), 0); -// } - -// let state = rockets.state().expect("Unexpected invalid state."); - -// if let StageState::Registration(participants) = &state.games[0].1.stage { -// assert_eq!(participants.len(), 3); -// } -// assert_eq!(state.player_to_game_id.len(), 4); - -// drop(rockets.cancel_register(PLAYERS[0])); - -// let state = rockets.state().expect("Unexpected invalid state."); - -// if let StageState::Registration(participants) = &state.games[0].1.stage { -// assert_eq!(participants.len(), 2); -// } -// assert_eq!(state.player_to_game_id.len(), 3); - -// drop(rockets.delete_player(ADMIN, PLAYERS[1].into())); - -// let state = rockets.state().expect("Unexpected invalid state."); - -// if let StageState::Registration(participants) = &state.games[0].1.stage { -// assert_eq!(participants.len(), 1); -// } -// assert_eq!(state.player_to_game_id.len(), 2); -// } - -// #[test] -// fn errors() { -// let system = utils::initialize_system(); - -// let mut rockets = GalEx::initialize(&system, ADMIN); - -// rockets -// .register(PLAYERS[0], ADMIN.into(), Default::default(), 0) -// .failed(Error::NoSuchGame, 0); - -// rockets -// .create_new_session(ADMIN, "admin".to_string(), 0) -// .succeed(0, 0); - -// rockets -// .register(ADMIN, ADMIN.into(), Default::default(), 0) -// .failed(Error::SeveralRegistrations, 0); - -// rockets -// .start_game(PLAYERS[0], 42, 20) -// .failed(Error::NoSuchGame, 0); - -// rockets -// .start_game(ADMIN, 42, 20) -// .failed(Error::NotEnoughParticipants, 0); - -// for player in PLAYERS { -// rockets -// .register(player, ADMIN.into(), Default::default(), 0) -// .succeed((player, Default::default()), 0); -// } - -// rockets -// .start_game(ADMIN, 101, 100) -// .failed(Error::FuelOrPayloadOverload, 0); - -// rockets -// .start_game(ADMIN, 100, 101) -// .failed(Error::FuelOrPayloadOverload, 0); -// rockets -// .start_game(ADMIN, 101, 101) -// .failed(Error::FuelOrPayloadOverload, 0); - -// rockets -// .register(FOREIGN_USER, ADMIN.into(), Default::default(), 0) -// .failed(Error::SessionFull, 0); -// } diff --git a/contracts/galactic-express/tests/utils/common.rs b/contracts/galactic-express/tests/utils/common.rs deleted file mode 100644 index 7064af081..000000000 --- a/contracts/galactic-express/tests/utils/common.rs +++ /dev/null @@ -1,88 +0,0 @@ -use gstd::{fmt::Debug, marker::PhantomData, prelude::*}; -use gtest::{Log, RunResult as InnerRunResult, System}; - -pub fn initialize_system() -> System { - let system = System::new(); - - system.init_logger(); - - system -} - -#[must_use] -pub struct RunResult { - pub result: InnerRunResult, - check: fn(Event, Check) -> CheckResult, - ghost_data: PhantomData<(Event, Error)>, -} - -impl - RunResult -{ - pub fn new(result: InnerRunResult, check: fn(Event, Check) -> CheckResult) -> Self { - Self { - result, - check, - ghost_data: PhantomData, - } - } - - #[track_caller] - pub fn failed(self, error: Error, index: usize) { - assert_eq!( - decode::>(&self.result, index).unwrap_err(), - error - ); - } - - #[track_caller] - pub fn succeed(self, value: Check, index: usize) -> CheckResult { - (self.check)( - decode::>(&self.result, index).unwrap(), - value, - ) - } -} - -#[must_use] -pub struct InitResult { - contract_instance: T, - pub result: InnerRunResult, - pub is_active: bool, - ghost_data: PhantomData, -} - -impl InitResult { - pub fn new(contract_instance: T, result: InnerRunResult, is_active: bool) -> Self { - Self { - contract_instance, - result, - is_active, - ghost_data: PhantomData, - } - } - - fn assert_contains(&self, payload: impl Encode) { - assert_contains(&self.result, payload); - } - - #[track_caller] - pub fn succeed(self) -> T { - assert!(self.is_active); - self.assert_contains(Ok::<_, E>(())); - - self.contract_instance - } -} - -#[track_caller] -fn assert_contains(result: &InnerRunResult, payload: impl Encode) { - assert!(result.contains(&Log::builder().payload(payload))); -} - -fn decode(result: &InnerRunResult, index: usize) -> T { - match T::decode(&mut result.log()[index].payload()) { - Ok(ok) => ok, - Err(_) => std::panic!("{}", String::from_utf8_lossy(result.log()[0].payload())), - } -} diff --git a/contracts/galactic-express/tests/utils/mod.rs b/contracts/galactic-express/tests/utils/mod.rs deleted file mode 100644 index da31903ea..000000000 --- a/contracts/galactic-express/tests/utils/mod.rs +++ /dev/null @@ -1,147 +0,0 @@ -use common::{InitResult, RunResult}; -use galactic_express_io::*; -use gstd::{collections::HashSet, prelude::*, ActorId}; -use gtest::{Program as InnerProgram, System}; - -mod common; - -pub mod prelude; - -pub use common::initialize_system; - -pub const FOREIGN_USER: u64 = 1029384756123; -pub const ADMIN: u64 = 10; -pub const PLAYERS: [u64; 3] = [12, 13, 14]; - -type GalExResult = RunResult; - -pub struct GalEx<'a>(InnerProgram<'a>); - -impl<'a> GalEx<'a> { - pub fn initialize(system: &'a System, from: u64) -> Self { - let program = InnerProgram::current(system); - - let result = program.send(from, 0); - let is_active = system.is_active_program(program.id()); - - InitResult::<_, Error>::new(Self(program), result, is_active).succeed() - } - - pub fn create_new_session( - &mut self, - from: u64, - name: String, - bid: u128, - ) -> GalExResult { - RunResult::new( - self.0 - .send_with_value(from, Action::CreateNewSession { name }, bid), - |event, _id| { - if let Event::NewSessionCreated { - altitude, reward, .. - } = event - { - assert!(((TURN_ALTITUDE.0 * (TURNS as u16)) - ..(TURN_ALTITUDE.1 * (TURNS as u16))) - .contains(&altitude)); - assert!((REWARD.0..REWARD.1).contains(&reward)); - reward - } else { - unreachable!() - } - }, - ) - } - - pub fn register( - &mut self, - from: u64, - creator: ActorId, - participant: Participant, - bid: u128, - ) -> GalExResult<(u64, Participant)> { - RunResult::new( - self.0.send_with_value( - from, - Action::Register { - creator, - participant, - }, - bid, - ), - |event, (actor, participant)| { - assert_eq!(Event::Registered(actor.into(), participant), event) - }, - ) - } - - pub fn cancel_register(&mut self, from: u64) -> GalExResult<(u64, Participant)> { - RunResult::new( - self.0.send(from, Action::CancelRegistration), - |event, (_actor, _participant)| assert_eq!(Event::RegistrationCanceled, event), - ) - } - - pub fn delete_player( - &mut self, - from: u64, - player_id: ActorId, - ) -> GalExResult<(u64, Participant)> { - RunResult::new( - self.0.send(from, Action::DeletePlayer { player_id }), - |_, _| {}, - ) - } - - pub fn start_game( - &mut self, - from: u64, - fuel_amount: u8, - payload_amount: u8, - ) -> GalExResult> { - RunResult::new( - self.0.send( - from, - Action::StartGame { - fuel_amount, - payload_amount, - }, - ), - |event, players| { - if let Event::GameFinished(results) = event { - assert!(results.turns.len() == TURNS); - assert!(results.rankings.len() == MAX_PARTICIPANTS); - assert!(results - .turns - .iter() - .all(|players| players.len() == MAX_PARTICIPANTS)); - - let players: HashSet = players.into_iter().map(|p| p.into()).collect(); - - assert!(results - .turns - .iter() - .map(|players| players - .iter() - .map(|(actor, _)| *actor) - .collect::>()) - .all(|true_players| true_players == players)); - } else { - unreachable!() - } - }, - ) - } - - pub fn state(&self) -> Option { - let reply = self - .0 - .read_state(StateQuery::All) - .expect("Unexpected invalid state."); - if let StateReply::All(state) = reply { - Some(state) - } else { - None - } - } -} diff --git a/contracts/galactic-express/tests/utils/prelude.rs b/contracts/galactic-express/tests/utils/prelude.rs deleted file mode 100644 index ed2994b73..000000000 --- a/contracts/galactic-express/tests/utils/prelude.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use super::{GalEx, ADMIN, FOREIGN_USER, PLAYERS}; -pub use galactic_express_io::*; -pub use gstd::prelude::*; diff --git a/contracts/galactic-express/wasm/Cargo.toml b/contracts/galactic-express/wasm/Cargo.toml new file mode 100644 index 000000000..eb0879e21 --- /dev/null +++ b/contracts/galactic-express/wasm/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "galactic-express" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +sails-rs.workspace = true +galactic-express-app = { path = "../app" } + +[build-dependencies] +gear-wasm-builder.workspace = true +sails-idl-gen.workspace = true +sails-client-gen.workspace = true +galactic-express-app = { path = "../app" } + +[lib] +crate-type = ["rlib"] +name = "galactic_express" diff --git a/contracts/galactic-express/wasm/build.rs b/contracts/galactic-express/wasm/build.rs new file mode 100644 index 000000000..511459d11 --- /dev/null +++ b/contracts/galactic-express/wasm/build.rs @@ -0,0 +1,20 @@ +use galactic_express_app::Program; +use sails_client_gen::ClientGenerator; +use sails_idl_gen::program; +use std::{env, fs::File, path::PathBuf}; + +fn main() { + gear_wasm_builder::build(); + + let manifest_dir_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let idl_file_path = manifest_dir_path.join("galactic-express.idl"); + + let idl_file = File::create(idl_file_path.clone()).unwrap(); + + program::generate_idl::(idl_file).unwrap(); + + ClientGenerator::from_idl_path(&idl_file_path) + .generate_to(PathBuf::from(env::var("OUT_DIR").unwrap()).join("galactic_express_client.rs")) + .unwrap(); +} diff --git a/contracts/galactic-express/wasm/galactic-express.idl b/contracts/galactic-express/wasm/galactic-express.idl new file mode 100644 index 000000000..f2449f14c --- /dev/null +++ b/contracts/galactic-express/wasm/galactic-express.idl @@ -0,0 +1,90 @@ +type Participant = struct { + id: actor_id, + name: str, + fuel_amount: u8, + payload_amount: u8, +}; + +type State = struct { + games: vec struct { actor_id, GameState }, + player_to_game_id: vec struct { actor_id, actor_id }, + dns_info: opt struct { actor_id, str }, + admin: actor_id, +}; + +type GameState = struct { + admin: actor_id, + admin_name: str, + altitude: u16, + weather: Weather, + reward: u128, + stage: StageState, + bid: u128, +}; + +type Weather = enum { + Clear, + Cloudy, + Rainy, + Stormy, + Thunder, + Tornado, +}; + +type StageState = enum { + Registration: vec struct { actor_id, Participant }, + Results: Results, +}; + +type Results = struct { + turns: vec vec struct { actor_id, Turn }, + rankings: vec struct { actor_id, u128 }, + participants: vec struct { actor_id, Participant }, +}; + +type Turn = enum { + Alive: struct { fuel_left: u8, payload_amount: u8 }, + Destroyed: HaltReason, +}; + +type HaltReason = enum { + PayloadOverload, + FuelOverload, + SeparationFailure, + AsteroidCollision, + FuelShortage, + EngineFailure, +}; + +constructor { + New : (dns_id_and_name: opt struct { actor_id, str }); +}; + +service GalacticExpress { + CancelGame : () -> null; + CancelRegister : () -> null; + ChangeAdmin : (new_admin: actor_id) -> null; + CreateNewSession : (name: str) -> null; + DeletePlayer : (player_id: actor_id) -> null; + Kill : (inheritor: actor_id) -> null; + LeaveGame : () -> null; + Register : (creator: actor_id, participant: Participant) -> null; + StartGame : (fuel_amount: u8, payload_amount: u8) -> null; + query Admin : () -> actor_id; + query All : () -> State; + query DnsInfo : () -> opt struct { actor_id, str }; + query GetGame : (player_id: actor_id) -> opt GameState; + + events { + GameFinished: Results; + NewSessionCreated: struct { altitude: u16, weather: Weather, reward: u128, bid: u128 }; + Registered: struct { actor_id, Participant }; + RegistrationCanceled; + PlayerDeleted: struct { player_id: actor_id }; + GameCanceled; + GameLeft; + AdminChanged: struct { new_admin: actor_id }; + Killed: struct { inheritor: actor_id }; + } +}; + diff --git a/contracts/galactic-express/wasm/src/lib.rs b/contracts/galactic-express/wasm/src/lib.rs new file mode 100644 index 000000000..f26bdedc7 --- /dev/null +++ b/contracts/galactic-express/wasm/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] +include!(concat!(env!("OUT_DIR"), "/galactic_express_client.rs")); +include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); + +#[cfg(target_arch = "wasm32")] +pub use galactic_express_app::wasm::*; diff --git a/contracts/tic-tac-toe/app/Cargo.toml b/contracts/tic-tac-toe/app/Cargo.toml index 8bf0aa440..19678eba7 100644 --- a/contracts/tic-tac-toe/app/Cargo.toml +++ b/contracts/tic-tac-toe/app/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true [dependencies] gstd = { workspace = true, features = ["debug"] } sails-rs = { workspace = true, features = ["gtest"] } - schnorrkel.workspace = true [dev-dependencies] diff --git a/contracts/tic-tac-toe/app/src/services/game/funcs.rs b/contracts/tic-tac-toe/app/src/services/game/funcs.rs index e647bb341..6eeb5335b 100644 --- a/contracts/tic-tac-toe/app/src/services/game/funcs.rs +++ b/contracts/tic-tac-toe/app/src/services/game/funcs.rs @@ -2,8 +2,7 @@ use crate::services::game::{ Config, Event, GameError, GameInstance, GameResult, Mark, Storage, VICTORIES, }; use crate::services::session::utils::{ActionsForSession, SessionData}; -use gstd::{collections::HashMap, exec, msg}; -use sails_rs::prelude::*; +use sails_rs::{prelude::*, collections::HashMap, gstd::{exec, msg}}; pub fn start_game( storage: &mut Storage, diff --git a/contracts/vara-man/app/src/services/game/funcs.rs b/contracts/vara-man/app/src/services/game/funcs.rs index e048cb7db..87eacc554 100644 --- a/contracts/vara-man/app/src/services/game/funcs.rs +++ b/contracts/vara-man/app/src/services/game/funcs.rs @@ -3,8 +3,7 @@ use crate::services::game::{ MAX_PARTICIPANTS, }; use crate::services::session::utils::{ActionsForSession, SessionData}; -use gstd::{collections::HashMap, exec, msg, prelude::*, ActorId}; -use sails_rs::U256; +use sails_rs::{U256, gstd::{exec, msg}, prelude::*, collections::HashMap, ActorId}; pub fn create_new_tournament( storage: &mut GameStorage, diff --git a/contracts/vara-man/app/src/services/game/mod.rs b/contracts/vara-man/app/src/services/game/mod.rs index c198a051d..2a1eeaa16 100644 --- a/contracts/vara-man/app/src/services/game/mod.rs +++ b/contracts/vara-man/app/src/services/game/mod.rs @@ -1,7 +1,6 @@ use super::session::Storage as SessionStorage; use crate::services; -use gstd::{collections::HashMap, exec, msg, String}; -use sails_rs::{gstd::service, prelude::*}; +use sails_rs::{gstd::{service, exec, msg}, prelude::*, collections::HashMap}; mod funcs; pub mod utils; use utils::*; diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx b/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx index 8ec7c1c37..383b7a7b7 100644 --- a/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx +++ b/frontend/apps/battleship-zk/src/features/multiplayer/components/create-game-form/create-game-form.tsx @@ -12,10 +12,6 @@ import { useMultiplayerGame } from '../../hooks'; import { useCreateGameMessage } from '../../sails/messages'; import styles from './CreateGameForm.module.scss'; -export interface ContractFormValues { - [key: string]: string; -} - type CreateFormValues = { fee: number; name: string; diff --git a/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx b/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx index 8e9741e64..2d2eacff5 100644 --- a/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx +++ b/frontend/apps/battleship-zk/src/features/multiplayer/components/join-game-form/join-game-form.tsx @@ -16,10 +16,6 @@ import { useJoinGameMessage } from '../../sails/messages'; import { useMultiGameQuery } from '../../sails/queries'; import styles from './JoinGameForm.module.scss'; -export interface ContractFormValues { - [key: string]: string; -} - type Props = { onCancel: () => void; }; diff --git a/frontend/apps/galactic-express/src/app/hooks/index.ts b/frontend/apps/galactic-express/src/app/hooks/index.ts new file mode 100644 index 000000000..e4b56d017 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/hooks/index.ts @@ -0,0 +1,2 @@ +export { usePending } from './use-pending'; +export * from './use-sign-and-send'; diff --git a/frontend/apps/galactic-express/src/app/hooks/use-pending.ts b/frontend/apps/galactic-express/src/app/hooks/use-pending.ts new file mode 100644 index 000000000..0ebee7ba0 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/hooks/use-pending.ts @@ -0,0 +1,8 @@ +import { useAtom } from 'jotai'; +import { IS_LOADING } from 'atoms'; + +export function usePending() { + const [pending, setPending] = useAtom(IS_LOADING); + + return { pending, setPending }; +} diff --git a/frontend/apps/galactic-express/src/app/hooks/use-sign-and-send.ts b/frontend/apps/galactic-express/src/app/hooks/use-sign-and-send.ts new file mode 100644 index 000000000..e7d779465 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/hooks/use-sign-and-send.ts @@ -0,0 +1,44 @@ +import { usePending } from './'; +import { useCheckBalance } from '@dapps-frontend/hooks'; +import { useAlert } from '@gear-js/react-hooks'; +import { GenericTransactionReturn, TransactionReturn } from '@gear-js/react-hooks/dist/esm/hooks/sails/types'; + +export type Options = { + onSuccess?: () => void; + onError?: (error?: Error) => void; +}; + +export const useSignAndSend = () => { + const { checkBalance } = useCheckBalance(); + const { setPending } = usePending(); + const alert = useAlert(); + + const signAndSend = async ( + transaction: TransactionReturn<() => GenericTransactionReturn>, + options?: Options, + ) => { + const { onSuccess, onError } = options || {}; + const calculatedGas = Number(transaction.extrinsic.args[2].toString()); + checkBalance( + calculatedGas, + async () => { + try { + const { response } = await transaction.signAndSend(); + await response(); + onSuccess?.(); + setPending(false); + } catch (e) { + onError?.(e as Error); + setPending(false); + console.error(e); + if (typeof e === 'string') { + alert.error(e); + } + } + }, + onError, + ); + }; + + return { signAndSend }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/index.ts b/frontend/apps/galactic-express/src/app/utils/index.ts new file mode 100644 index 000000000..15858e186 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/index.ts @@ -0,0 +1 @@ +export * from './sails'; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/events/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/events/index.ts new file mode 100644 index 000000000..2224d3720 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/events/index.ts @@ -0,0 +1,2 @@ +export { useEventGameCanceledSubscription } from './use-event-game-canceled-subscription'; +export { useEventPlayerDeletedSubscription } from './use-event-player-deleted-subscription'; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-game-canceled-subscription.ts b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-game-canceled-subscription.ts new file mode 100644 index 000000000..0c520a067 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-game-canceled-subscription.ts @@ -0,0 +1,22 @@ +import { useProgramEvent } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { REGISTRATION_STATUS } from 'atoms'; +import { useSetAtom } from 'jotai'; + +export function useEventGameCanceledSubscription(isUserAdmin: boolean) { + const program = useProgram(); + const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS); + + const onData = () => { + if (!isUserAdmin) { + setRegistrationStatus('GameCanceled'); + } + }; + + useProgramEvent({ + program, + serviceName: 'galacticExpress', + functionName: 'subscribeToGameCanceledEvent', + onData, + }); +} diff --git a/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-player-deleted-subscription.ts b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-player-deleted-subscription.ts new file mode 100644 index 000000000..402ae382d --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/events/use-event-player-deleted-subscription.ts @@ -0,0 +1,24 @@ +import { HexString } from '@gear-js/api'; +import { useProgramEvent, useAccount } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { REGISTRATION_STATUS } from 'atoms'; +import { useSetAtom } from 'jotai'; + +export function useEventPlayerDeletedSubscription() { + const program = useProgram(); + const { account } = useAccount(); + const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS); + + const onData = ({ player_id }: { player_id: HexString }) => { + if (account?.decodedAddress === player_id) { + setRegistrationStatus('PlayerRemoved'); + } + }; + + useProgramEvent({ + program, + serviceName: 'galacticExpress', + functionName: 'subscribeToPlayerDeletedEvent', + onData, + }); +} diff --git a/frontend/apps/galactic-express/src/app/utils/sails/galactic-express.idl b/frontend/apps/galactic-express/src/app/utils/sails/galactic-express.idl new file mode 100644 index 000000000..f2449f14c --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/galactic-express.idl @@ -0,0 +1,90 @@ +type Participant = struct { + id: actor_id, + name: str, + fuel_amount: u8, + payload_amount: u8, +}; + +type State = struct { + games: vec struct { actor_id, GameState }, + player_to_game_id: vec struct { actor_id, actor_id }, + dns_info: opt struct { actor_id, str }, + admin: actor_id, +}; + +type GameState = struct { + admin: actor_id, + admin_name: str, + altitude: u16, + weather: Weather, + reward: u128, + stage: StageState, + bid: u128, +}; + +type Weather = enum { + Clear, + Cloudy, + Rainy, + Stormy, + Thunder, + Tornado, +}; + +type StageState = enum { + Registration: vec struct { actor_id, Participant }, + Results: Results, +}; + +type Results = struct { + turns: vec vec struct { actor_id, Turn }, + rankings: vec struct { actor_id, u128 }, + participants: vec struct { actor_id, Participant }, +}; + +type Turn = enum { + Alive: struct { fuel_left: u8, payload_amount: u8 }, + Destroyed: HaltReason, +}; + +type HaltReason = enum { + PayloadOverload, + FuelOverload, + SeparationFailure, + AsteroidCollision, + FuelShortage, + EngineFailure, +}; + +constructor { + New : (dns_id_and_name: opt struct { actor_id, str }); +}; + +service GalacticExpress { + CancelGame : () -> null; + CancelRegister : () -> null; + ChangeAdmin : (new_admin: actor_id) -> null; + CreateNewSession : (name: str) -> null; + DeletePlayer : (player_id: actor_id) -> null; + Kill : (inheritor: actor_id) -> null; + LeaveGame : () -> null; + Register : (creator: actor_id, participant: Participant) -> null; + StartGame : (fuel_amount: u8, payload_amount: u8) -> null; + query Admin : () -> actor_id; + query All : () -> State; + query DnsInfo : () -> opt struct { actor_id, str }; + query GetGame : (player_id: actor_id) -> opt GameState; + + events { + GameFinished: Results; + NewSessionCreated: struct { altitude: u16, weather: Weather, reward: u128, bid: u128 }; + Registered: struct { actor_id, Participant }; + RegistrationCanceled; + PlayerDeleted: struct { player_id: actor_id }; + GameCanceled; + GameLeft; + AdminChanged: struct { new_admin: actor_id }; + Killed: struct { inheritor: actor_id }; + } +}; + diff --git a/frontend/apps/galactic-express/src/app/utils/sails/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/index.ts new file mode 100644 index 000000000..052b4eecf --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/index.ts @@ -0,0 +1,5 @@ +export * from './sails'; +export * from './lib'; +export * from './events'; +export * from './queries'; +export * from './messages'; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/lib.ts b/frontend/apps/galactic-express/src/app/utils/sails/lib.ts new file mode 100644 index 000000000..9ad8d332e --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/lib.ts @@ -0,0 +1,487 @@ +import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js'; +import { GearApi, HexString, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; + +type ActorId = HexString; + +export interface Participant { + id: ActorId; + name: string; + fuel_amount: number; + payload_amount: number; +} + +export interface State { + games: Array<[ActorId, GameState]>; + player_to_game_id: Array<[ActorId, ActorId]>; + dns_info: [ActorId, string] | null; + admin: ActorId; +} + +export interface GameState { + admin: ActorId; + admin_name: string; + altitude: number; + weather: Weather; + reward: number | string | bigint; + stage: StageState; + bid: number | string | bigint; +} + +export type Weather = 'clear' | 'cloudy' | 'rainy' | 'stormy' | 'thunder' | 'tornado'; + +export type StageState = { registration: Array<[ActorId, Participant]> } | { results: Results }; + +export interface Results { + turns: Array>; + rankings: Array<[ActorId, number | string | bigint]>; + participants: Array<[ActorId, Participant]>; +} + +export type Turn = { alive: { fuel_left: number; payload_amount: number } } | { destroyed: HaltReason }; + +export type HaltReason = + | 'payloadOverload' + | 'fuelOverload' + | 'separationFailure' + | 'asteroidCollision' + | 'fuelShortage' + | 'engineFailure'; + +export class Program { + public readonly registry: TypeRegistry; + public readonly galacticExpress: GalacticExpress; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types: Record = { + Participant: { id: '[u8;32]', name: 'String', fuel_amount: 'u8', payload_amount: 'u8' }, + State: { + games: 'Vec<([u8;32], GameState)>', + player_to_game_id: 'Vec<([u8;32], [u8;32])>', + dns_info: 'Option<([u8;32], String)>', + admin: '[u8;32]', + }, + GameState: { + admin: '[u8;32]', + admin_name: 'String', + altitude: 'u16', + weather: 'Weather', + reward: 'u128', + stage: 'StageState', + bid: 'u128', + }, + Weather: { _enum: ['Clear', 'Cloudy', 'Rainy', 'Stormy', 'Thunder', 'Tornado'] }, + StageState: { _enum: { Registration: 'Vec<([u8;32], Participant)>', Results: 'Results' } }, + Results: { + turns: 'Vec>', + rankings: 'Vec<([u8;32], u128)>', + participants: 'Vec<([u8;32], Participant)>', + }, + Turn: { _enum: { Alive: { fuel_left: 'u8', payload_amount: 'u8' }, Destroyed: 'HaltReason' } }, + HaltReason: { + _enum: [ + 'PayloadOverload', + 'FuelOverload', + 'SeparationFailure', + 'AsteroidCollision', + 'FuelShortage', + 'EngineFailure', + ], + }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.galacticExpress = new GalacticExpress(this); + } + + newCtorFromCode(code: Uint8Array | Buffer, dns_id_and_name: [ActorId, string] | null): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + ['New', dns_id_and_name], + '(String, Option<([u8;32], String)>)', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, dns_id_and_name: [ActorId, string] | null) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + ['New', dns_id_and_name], + '(String, Option<([u8;32], String)>)', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class GalacticExpress { + constructor(private _program: Program) {} + + public cancelGame(): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'CancelGame'], + '(String, String)', + 'Null', + this._program.programId, + ); + } + + public cancelRegister(): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'CancelRegister'], + '(String, String)', + 'Null', + this._program.programId, + ); + } + + public changeAdmin(new_admin: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'ChangeAdmin', new_admin], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public createNewSession(name: string): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'CreateNewSession', name], + '(String, String, String)', + 'Null', + this._program.programId, + ); + } + + public deletePlayer(player_id: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'DeletePlayer', player_id], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public kill(inheritor: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'Kill', inheritor], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public leaveGame(): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'LeaveGame'], + '(String, String)', + 'Null', + this._program.programId, + ); + } + + public register(creator: ActorId, participant: Participant): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'Register', creator, participant], + '(String, String, [u8;32], Participant)', + 'Null', + this._program.programId, + ); + } + + public startGame(fuel_amount: number, payload_amount: number): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['GalacticExpress', 'StartGame', fuel_amount, payload_amount], + '(String, String, u8, u8)', + 'Null', + this._program.programId, + ); + } + + public async admin( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['GalacticExpress', 'Admin']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, [u8;32])', reply.payload); + return result[2].toJSON() as unknown as ActorId; + } + + public async all(originAddress?: string, value?: number | string | bigint, atBlock?: `0x${string}`): Promise { + const payload = this._program.registry.createType('(String, String)', ['GalacticExpress', 'All']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, State)', reply.payload); + return result[2].toJSON() as unknown as State; + } + + public async dnsInfo( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise<[ActorId, string] | null> { + const payload = this._program.registry.createType('(String, String)', ['GalacticExpress', 'DnsInfo']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option<([u8;32], String)>)', reply.payload); + return result[2].toJSON() as unknown as [ActorId, string] | null; + } + + public async getGame( + player_id: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['GalacticExpress', 'GetGame', player_id]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as GameState | null; + } + + public subscribeToGameFinishedEvent(callback: (data: Results) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'GameFinished') { + callback( + this._program.registry + .createType('(String, String, Results)', message.payload)[2] + .toJSON() as unknown as Results, + ); + } + }); + } + + public subscribeToNewSessionCreatedEvent( + callback: (data: { + altitude: number; + weather: Weather; + reward: number | string | bigint; + bid: number | string | bigint; + }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'NewSessionCreated') { + callback( + this._program.registry + .createType( + '(String, String, {"altitude":"u16","weather":"Weather","reward":"u128","bid":"u128"})', + message.payload, + )[2] + .toJSON() as unknown as { + altitude: number; + weather: Weather; + reward: number | string | bigint; + bid: number | string | bigint; + }, + ); + } + }); + } + + public subscribeToRegisteredEvent( + callback: (data: [ActorId, Participant]) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'Registered') { + callback( + this._program.registry + .createType('(String, String, ([u8;32], Participant))', message.payload)[2] + .toJSON() as unknown as [ActorId, Participant], + ); + } + }); + } + + public subscribeToRegistrationCanceledEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'RegistrationCanceled') { + callback(null); + } + }); + } + + public subscribeToPlayerDeletedEvent( + callback: (data: { player_id: ActorId }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'PlayerDeleted') { + callback( + this._program.registry + .createType('(String, String, {"player_id":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { player_id: ActorId }, + ); + } + }); + } + + public subscribeToGameCanceledEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'GameCanceled') { + callback(null); + } + }); + } + + public subscribeToGameLeftEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'GameLeft') { + callback(null); + } + }); + } + + public subscribeToAdminChangedEvent( + callback: (data: { new_admin: ActorId }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'AdminChanged') { + callback( + this._program.registry + .createType('(String, String, {"new_admin":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { new_admin: ActorId }, + ); + } + }); + } + + public subscribeToKilledEvent(callback: (data: { inheritor: ActorId }) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'GalacticExpress' && getFnNamePrefix(payload) === 'Killed') { + callback( + this._program.registry + .createType('(String, String, {"inheritor":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { inheritor: ActorId }, + ); + } + }); + } +} diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/index.ts new file mode 100644 index 000000000..5f9274801 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/index.ts @@ -0,0 +1,7 @@ +export { useCancelGameMessage } from './use-cancel-game-message'; +export { useCancelRegisterMessage } from './use-cancel-register-message'; +export { useCreateNewSessionMessage } from './use-create-new-session-message'; +export { useDeletePlayerMessage } from './use-delete-player-message'; +export { useRegisterMessage } from './use-register-message'; +export { useStartGameMessage } from './use-start-game-message'; +export { useLeaveGameMessage } from './use-leave-game-message'; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-game-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-game-message.ts new file mode 100644 index 000000000..cedd312bb --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-game-message.ts @@ -0,0 +1,23 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; + +export const useCancelGameMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'cancelGame', + }); + const { signAndSend } = useSignAndSend(); + + const cancelGameMessage = async (options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [], + gasLimit: { increaseGas: 10 }, + }); + signAndSend(transaction, options); + }; + + return { cancelGameMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-register-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-register-message.ts new file mode 100644 index 000000000..d204b9b12 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-cancel-register-message.ts @@ -0,0 +1,23 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; + +export const useCancelRegisterMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'cancelRegister', + }); + const { signAndSend } = useSignAndSend(); + + const cancelRegisterMessage = async (options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [], + gasLimit: { increaseGas: 10 }, + }); + signAndSend(transaction, options); + }; + + return { cancelRegisterMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-create-new-session-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-create-new-session-message.ts new file mode 100644 index 000000000..2870d14c2 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-create-new-session-message.ts @@ -0,0 +1,29 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; + +type Params = { + name: string; + value?: bigint; +}; + +export const useCreateNewSessionMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'createNewSession', + }); + const { signAndSend } = useSignAndSend(); + + const createNewSessionMessage = async ({ value, name }: Params, options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [name], + gasLimit: { increaseGas: 10 }, + value, + }); + signAndSend(transaction, options); + }; + + return { createNewSessionMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-delete-player-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-delete-player-message.ts new file mode 100644 index 000000000..e63fd1e34 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-delete-player-message.ts @@ -0,0 +1,28 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; +import { HexString } from '@gear-js/api'; + +type Params = { + playerId: HexString; +}; + +export const useDeletePlayerMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'deletePlayer', + }); + const { signAndSend } = useSignAndSend(); + + const deletePlayerMessage = async ({ playerId }: Params, options?: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [playerId], + gasLimit: { increaseGas: 10 }, + }); + signAndSend(transaction, options); + }; + + return { deletePlayerMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-leave-game-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-leave-game-message.ts new file mode 100644 index 000000000..cfe672b74 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-leave-game-message.ts @@ -0,0 +1,23 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; + +export const useLeaveGameMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'leaveGame', + }); + const { signAndSend } = useSignAndSend(); + + const leaveGameMessage = async (options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [], + gasLimit: { increaseGas: 10 }, + }); + signAndSend(transaction, options); + }; + + return { leaveGameMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-register-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-register-message.ts new file mode 100644 index 000000000..046b8bda8 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-register-message.ts @@ -0,0 +1,36 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { Participant, useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; +import { HexString } from '@gear-js/api'; + +type Params = { + creator: HexString; + participant: Participant; + value?: bigint; +}; + +export const useRegisterMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'register', + }); + const { signAndSend } = useSignAndSend(); + + const registerMessage = async ({ value, creator, participant }: Params, options?: Options) => { + try { + const { transaction } = await prepareTransactionAsync({ + args: [creator, participant], + gasLimit: { increaseGas: 10 }, + value, + }); + signAndSend(transaction, options); + } catch (error) { + console.error(error); + options?.onError?.(error as Error); + } + }; + + return { registerMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/messages/use-start-game-message.ts b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-start-game-message.ts new file mode 100644 index 000000000..2fd211364 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/messages/use-start-game-message.ts @@ -0,0 +1,33 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { useProgram } from 'app/utils'; +import { Options, useSignAndSend } from 'app/hooks'; + +type Params = { + fuel: number; + payload: number; +}; + +export const useStartGameMessage = () => { + const program = useProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'galacticExpress', + functionName: 'startGame', + }); + const { signAndSend } = useSignAndSend(); + + const startGameMessage = async ({ fuel, payload }: Params, options?: Options) => { + try { + const { transaction } = await prepareTransactionAsync({ + args: [fuel, payload], + gasLimit: { increaseGas: 20 }, + }); + signAndSend(transaction, options); + } catch (error) { + console.error(error); + options?.onError?.(error as Error); + } + }; + + return { startGameMessage }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/queries/index.ts b/frontend/apps/galactic-express/src/app/utils/sails/queries/index.ts new file mode 100644 index 000000000..6a6dcd850 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/queries/index.ts @@ -0,0 +1 @@ +export { useGetGameQuery } from './use-get-game-query'; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/queries/use-get-game-query.ts b/frontend/apps/galactic-express/src/app/utils/sails/queries/use-get-game-query.ts new file mode 100644 index 000000000..58ab816c7 --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/queries/use-get-game-query.ts @@ -0,0 +1,19 @@ +import { useProgram } from 'app/utils'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; +import { HexString } from '@gear-js/api'; + +export const useGetGameQuery = (gameAddress?: HexString) => { + const program = useProgram(); + const { account } = useAccount(); + + const { data, refetch, isFetching, error } = useProgramQuery({ + program, + serviceName: 'galacticExpress', + functionName: 'getGame', + args: [gameAddress || '0x'], + query: { enabled: account && gameAddress ? undefined : false }, + watch: true, + }); + + return { game: data, isFetching, refetch, error }; +}; diff --git a/frontend/apps/galactic-express/src/app/utils/sails/sails.tsx b/frontend/apps/galactic-express/src/app/utils/sails/sails.tsx new file mode 100644 index 000000000..b3da2e51f --- /dev/null +++ b/frontend/apps/galactic-express/src/app/utils/sails/sails.tsx @@ -0,0 +1,12 @@ +import { useProgram as useGearJsProgram } from '@gear-js/react-hooks'; +import { Program } from 'app/utils'; +import { useDnsProgramIds } from '@dapps-frontend/hooks'; + +const useProgram = () => { + const { programId } = useDnsProgramIds(); + const { data: program } = useGearJsProgram({ library: Program, id: programId }); + + return program; +}; + +export { useProgram }; diff --git a/frontend/apps/galactic-express/src/assets/meta/galactic_express_meta.txt b/frontend/apps/galactic-express/src/assets/meta/galactic_express_meta.txt deleted file mode 100644 index 1f3960521..000000000 --- a/frontend/apps/galactic-express/src/assets/meta/galactic_express_meta.txt +++ /dev/null @@ -1 +0,0 @@ -0002000001000000000105000000010a000000000000000119000000011a00000035268c000418526573756c740804540104044501080108084f6b040004000000000c457272040008000001000004000004000008084c67616c61637469635f657870726573735f696f144572726f72000138485374617465556e696e6974616c69617a656400000024477374644572726f7204000c0118537472696e670001003053657373696f6e456e646564000200544675656c4f725061796c6f61644f7665726c6f61640003002c53657373696f6e46756c6c000400544e6f74456e6f7567685061727469636970616e74730005002454784d616e61676572040010015c5472616e73616374696f6e4d616e616765724572726f72000600284e6f5375636847616d650007002057726f6e67426964000800304e6f53756368506c6179657200090030556e72656769737465726564000a0044416c726561647952656769737465726564000b00505365766572616c526567697374726174696f6e73000c002c4e6f74466f7241646d696e000d00000c0000050200100c20676561725f6c69622874785f6d616e616765725c5472616e73616374696f6e4d616e616765724572726f7200010c4c5472616e73616374696f6e4e6f74466f756e64000000404d69736d617463686564547844617461000100204f766572666c6f770002000014084c67616c61637469635f657870726573735f696f18416374696f6e00011c404372656174654e657753657373696f6e0401106e616d650c0118537472696e6700000020526567697374657208011c63726561746f7218011c4163746f72496400012c7061727469636970616e7424012c5061727469636970616e740001004843616e63656c526567697374726174696f6e0002003044656c657465506c61796572040124706c617965725f696418011c4163746f7249640003002843616e63656c47616d65000400244c6561766547616d6500050024537461727447616d6508012c6675656c5f616d6f756e7420010875380001387061796c6f61645f616d6f756e742001087538000600001810106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001c01205b75383b2033325d00001c00000320000000200020000005030024084c67616c61637469635f657870726573735f696f2c5061727469636970616e740000100108696418011c4163746f7249640001106e616d650c0118537472696e6700012c6675656c5f616d6f756e7420010875380001387061796c6f61645f616d6f756e7420010875380000280418526573756c74080454012c044501080108084f6b04002c000000000c45727204000800000100002c084c67616c61637469635f657870726573735f696f144576656e740001203041646d696e4368616e676564080018011c4163746f724964000018011c4163746f724964000000284e657753657373696f6e100120616c74697475646530010c75313600011c7765617468657234011c576561746865720001187265776172643801107531323800010c626964380110753132380001002852656769737465726564080018011c4163746f724964000024012c5061727469636970616e740002004843616e63656c526567697374726174696f6e00030034506c6179657244656c65746564040124706c617965725f696418011c4163746f7249640004003047616d6543616e63656c65640005003047616d6546696e697368656404003c011c526573756c74730006002047616d654c6566740007000030000005040034084c67616c61637469635f657870726573735f696f1c5765617468657200011814436c65617200000018436c6f756479000100145261696e790002001853746f726d790003001c5468756e6465720004001c546f726e61646f000500003800000507003c084c67616c61637469635f657870726573735f696f1c526573756c747300000c01147475726e734001645665633c5665633c284163746f7249642c205475726e293e3e00012072616e6b696e67735401505665633c284163746f7249642c2075313238293e0001307061727469636970616e74735c016c5665633c284163746f7249642c205061727469636970616e74293e00004000000244004400000248004800000408184c004c084c67616c61637469635f657870726573735f696f105475726e00010814416c6976650801246675656c5f6c65667420010875380001387061796c6f61645f616d6f756e7420010875380000002444657374726f796564040050012848616c74526561736f6e0001000050084c67616c61637469635f657870726573735f696f2848616c74526561736f6e0001183c5061796c6f61644f7665726c6f6164000000304675656c4f7665726c6f61640001004453657061726174696f6e4661696c7572650002004441737465726f6964436f6c6c6973696f6e000300304675656c53686f727461676500040034456e67696e654661696c7572650005000054000002580058000004081838005c0000026000600000040818240064084c67616c61637469635f657870726573735f696f28537461746551756572790001080c416c6c0000001c47657447616d65040124706c617965725f696418011c4163746f7249640001000068084c67616c61637469635f657870726573735f696f2853746174655265706c790001080c416c6c04006c011453746174650000001047616d6504008801444f7074696f6e3c47616d6553746174653e000100006c084c67616c61637469635f657870726573735f696f145374617465000008011467616d65737001645665633c284163746f7249642c2047616d655374617465293e000144706c617965725f746f5f67616d655f696480015c5665633c284163746f7249642c204163746f724964293e0000700000027400740000040818780078084c67616c61637469635f657870726573735f696f2447616d65537461746500001c011461646d696e18011c4163746f72496400012861646d696e5f6e616d650c0118537472696e67000120616c74697475646530010c75313600011c7765617468657234011c576561746865720001187265776172643801107531323800011473746167657c01285374616765537461746500010c6269643801107531323800007c084c67616c61637469635f657870726573735f696f285374616765537461746500010830526567697374726174696f6e04005c016c5665633c284163746f7249642c205061727469636970616e74293e0000001c526573756c747304003c011c526573756c74730001000080000002840084000004081818008804184f7074696f6e04045401780108104e6f6e6500000010536f6d650400780000010000 \ No newline at end of file diff --git a/frontend/apps/galactic-express/src/atoms.ts b/frontend/apps/galactic-express/src/atoms.ts index a7b5877ab..e3247e81b 100644 --- a/frontend/apps/galactic-express/src/atoms.ts +++ b/frontend/apps/galactic-express/src/atoms.ts @@ -1,7 +1,8 @@ import { atom } from 'jotai'; import { RegistrationStatus } from 'features/session/types'; +import { HexString } from '@gear-js/api'; -export const CURRENT_GAME_ATOM = atom(''); +export const CURRENT_GAME_ATOM = atom(null); export const PLAYER_NAME_ATOM = atom(null); diff --git a/frontend/apps/galactic-express/src/components/layout/header/Header.tsx b/frontend/apps/galactic-express/src/components/layout/header/Header.tsx index abcdba82e..77b2929cf 100644 --- a/frontend/apps/galactic-express/src/components/layout/header/Header.tsx +++ b/frontend/apps/galactic-express/src/components/layout/header/Header.tsx @@ -14,8 +14,8 @@ function Header() { const { admin, stage } = state || {}; const isUserAdmin = admin === account?.decodedAddress; - const isRegistration = Object.keys(stage || {})[0] === 'Registration'; - const participants = stage?.Registration || stage?.Results?.participants; + const isRegistration = stage && 'registration' in stage; + const participants = isRegistration ? stage.registration : []; return ( } className={{ header: styles.header, content: styles.container }}> - {isUserAdmin && isRegistration && } + {isUserAdmin && isRegistration && } ); } diff --git a/frontend/apps/galactic-express/src/features/session/api.ts b/frontend/apps/galactic-express/src/features/session/api.ts deleted file mode 100644 index 0f58154f3..000000000 --- a/frontend/apps/galactic-express/src/features/session/api.ts +++ /dev/null @@ -1,8 +0,0 @@ -import metaTxt from 'assets/meta/galactic_express_meta.txt'; -import { useProgramMetadata } from 'hooks'; - -function useEscrowMetadata() { - return useProgramMetadata(metaTxt); -} - -export { useEscrowMetadata }; diff --git a/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx b/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx index 998137c82..68e7b4128 100644 --- a/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/cancel-game-button/CancelGameButton.tsx @@ -1,11 +1,11 @@ +import clsx from 'clsx'; import { ReactComponent as CrossIconSVG } from 'assets/images/icons/cross-icon.svg'; import { useAtom, useSetAtom } from 'jotai'; import { Button } from '@gear-js/vara-ui'; import { useAccount } from '@gear-js/react-hooks'; -import { useLaunchMessage } from 'features/session/hooks'; import { Participant } from 'features/session/types'; import { IS_LOADING, REGISTRATION_STATUS } from 'atoms'; -import clsx from 'clsx'; +import { useCancelGameMessage, useCancelRegisterMessage } from 'app/utils'; import styles from './CancelGameButton.module.scss'; type Props = { @@ -14,11 +14,13 @@ type Props = { }; function CancelGameButton({ isAdmin, participants }: Props) { - const { meta: isMeta, message: sendMessage } = useLaunchMessage(); const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS); const [isLoading, setIsLoading] = useAtom(IS_LOADING); const { account } = useAccount(); + const { cancelGameMessage } = useCancelGameMessage(); + const { cancelRegisterMessage } = useCancelRegisterMessage(); + const isRegistered = account?.decodedAddress ? participants.map((participant) => participant[0]).includes(account.decodedAddress) : false; @@ -27,7 +29,7 @@ function CancelGameButton({ isAdmin, participants }: Props) { setIsLoading(false); }; - const onInBlock = () => { + const onSuccess = () => { setIsLoading(false); setRegistrationStatus('registration'); }; @@ -35,23 +37,10 @@ function CancelGameButton({ isAdmin, participants }: Props) { const handleClick = () => { setIsLoading(true); if (isAdmin) { - sendMessage({ - payload: { - CancelGame: null, - }, - onError, - onInBlock, - }); + cancelGameMessage({ onError, onSuccess }); } - if (!isAdmin && isRegistered) { - sendMessage({ - payload: { - CancelRegistration: null, - }, - onError, - onInBlock, - }); + cancelRegisterMessage({ onError, onSuccess }); } }; diff --git a/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx b/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx index 4902533e9..59a2e92f7 100644 --- a/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/form/Form.tsx @@ -1,17 +1,18 @@ import { useAtomValue, useSetAtom, useAtom } from 'jotai'; import { CURRENT_GAME_ATOM, IS_LOADING, PLAYER_NAME_ATOM } from 'atoms'; -import { useAccount, withoutCommas } from '@gear-js/react-hooks'; +import { useAccount } from '@gear-js/react-hooks'; import { Button } from '@gear-js/ui'; import { useForm } from '@mantine/form'; import { Card } from 'components'; -import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react'; +import { ChangeEvent, Dispatch, SetStateAction } from 'react'; import { RegistrationStatus } from 'features/session/types'; import { ReactComponent as RocketSVG } from '../../assets/rocket.svg'; import { INITIAL_VALUES, VALIDATE, WEATHERS } from '../../consts'; -import { useLaunchMessage } from '../../hooks'; import { Range } from '../range'; import { Probability } from '../probability'; import styles from './Form.module.scss'; +import { useStartGameMessage, useRegisterMessage } from 'app/utils'; +import { getPanicType } from 'utils'; type Props = { weather: string; @@ -30,10 +31,11 @@ function Form({ weather, bid, isAdmin, setRegistrationStatus }: Props) { }); const playerName = useAtomValue(PLAYER_NAME_ATOM); const currentGameAddress = useAtomValue(CURRENT_GAME_ATOM); + const { startGameMessage } = useStartGameMessage(); + const { registerMessage } = useRegisterMessage(); - const { fuel, payload } = values; - - const { meta, message: sendMessage } = useLaunchMessage(); + const fuel = Number(values.fuel); + const payload = Number(values.payload); const handleNumberInputChange = ({ target }: ChangeEvent) => { const value = +target.value; @@ -52,29 +54,45 @@ function Form({ weather, bid, isAdmin, setRegistrationStatus }: Props) { }); const handleSubmit = () => { - if (!isAdmin && meta && account?.decodedAddress) { + if (!isAdmin && account?.decodedAddress && currentGameAddress && playerName) { setIsLoading(true); - sendMessage({ - payload: { - Register: { - creator: currentGameAddress, - participant: { fuel_amount: fuel, payload_amount: payload, name: playerName, id: account.decodedAddress }, - }, - }, - value: Number(withoutCommas(bid || '')), - onSuccess: () => { - setRegistrationStatus('success'); - setCurrentGame(''); - setIsLoading(false); + + registerMessage( + { + creator: currentGameAddress, + participant: { fuel_amount: fuel, payload_amount: payload, name: playerName, id: account.decodedAddress }, + value: bid ? BigInt(bid) : undefined, }, - onError: () => { - setIsLoading(false); + { + onSuccess: () => { + setRegistrationStatus('success'); + setCurrentGame(null); + setIsLoading(false); + }, + onError: (error) => { + setIsLoading(false); + + const panicType = getPanicType(error); + if (panicType === 'SessionFull') { + setRegistrationStatus('MaximumPlayersReached'); + } + }, }, - }); + ); } - if (isAdmin && meta) { - sendMessage({ payload: { StartGame: { fuel_amount: fuel, payload_amount: payload } } }); + if (isAdmin) { + startGameMessage( + { fuel: fuel, payload: payload }, + { + onError: (error) => { + const panicType = getPanicType(error); + if (panicType === 'NotEnoughParticipants') { + setRegistrationStatus(panicType); + } + }, + }, + ); } }; diff --git a/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx b/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx index 77e10617a..46bdb8c60 100644 --- a/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/game-found-modal/GameFoundModal.tsx @@ -77,7 +77,7 @@ function GameFoundModal({ entryFee, players, gasAmount, onSubmit, onClose }: Pro

{items.map((item) => ( -
+
{item.name} {item.value}
diff --git a/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx b/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx index 9452bf493..921576e40 100644 --- a/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/participants-table/ParticipantsTable.tsx @@ -1,8 +1,9 @@ import { Fragment } from 'react'; import { cx } from 'utils'; import { shortenString } from 'features/session/utils'; +import { decodeAddress } from '@gear-js/api'; import { Button } from '@gear-js/vara-ui'; -import { useLaunchMessage } from 'features/session/hooks'; +import { useDeletePlayerMessage } from 'app/utils'; import styles from './ParticipantsTable.module.scss'; interface TableData { @@ -18,8 +19,7 @@ type Props = { }; function ParticipantsTable({ data, userAddress, isUserAdmin }: Props) { - const { meta: isMeta, message: sendMessage } = useLaunchMessage(); - + const { deletePlayerMessage } = useDeletePlayerMessage(); const isYourAddress = (address: string) => address === userAddress; const modifiedData: TableData[] = [ @@ -28,13 +28,7 @@ function ParticipantsTable({ data, userAddress, isUserAdmin }: Props) { ]; const handleDeletePlayer = (playerId: string) => { - sendMessage({ - payload: { - DeletePlayer: { - playerId, - }, - }, - }); + deletePlayerMessage({ playerId: decodeAddress(playerId) }); }; return ( diff --git a/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx b/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx index efc009c5b..18b028149 100644 --- a/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/session-passed-info/SessionPassedInfo.tsx @@ -7,7 +7,7 @@ function SessionPassedInfo() { const setCurrentGame = useSetAtom(CURRENT_GAME_ATOM); const handleClick = () => { - setCurrentGame(''); + setCurrentGame(null); }; return ( diff --git a/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss b/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss index c2e40b286..5cd21ff44 100644 --- a/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss +++ b/frontend/apps/galactic-express/src/features/session/components/session/Session.module.scss @@ -13,6 +13,7 @@ align-items: center; white-space: nowrap; border-bottom: 1px solid rgba(#8c8b90, 0.2); + max-width: 562px; .heading { font-weight: 300; @@ -104,9 +105,9 @@ } .courtainGreen { - background: radial-gradient(circle, rgba(2,0,36,0) 0%, rgba(111,207,151,0.3) 0%, rgba(0,0,0,0) 75%); + background: radial-gradient(circle, rgba(2, 0, 36, 0) 0%, rgba(111, 207, 151, 0.3) 0%, rgba(0, 0, 0, 0) 75%); } .courtainRed { - background: radial-gradient(circle, rgba(2,0,36,0) 0%, rgba(235,87,87,0.3) 0%, rgba(0,0,0,0) 75%); -} \ No newline at end of file + background: radial-gradient(circle, rgba(2, 0, 36, 0) 0%, rgba(235, 87, 87, 0.3) 0%, rgba(0, 0, 0, 0) 75%); +} diff --git a/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx b/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx index 9dfffab50..ce62d97f6 100644 --- a/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/session/Session.tsx @@ -23,7 +23,7 @@ type Props = { }; function Session({ session, turns, rankings, userId, participants, admin }: Props) { - const { altitude, weather, reward, sessionId: id } = session; + const { altitude, weather, reward } = session; const roundsCount = turns.length; const [roundIndex, setRoundIndex] = useState(0); @@ -36,29 +36,25 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop const firstPage = () => setRoundIndex(0); const lastPage = () => setRoundIndex(roundsCount - 1); - const defineFuelLeftFormat = (isAlive: boolean, fuelLeft: string) => { - if (isAlive && fuelLeft) { - return fuelLeft !== '0' ? fuelLeft : '1'; - } - - return ' - '; + const defineFuelLeftFormat = (isAlive: boolean, fuelLeft: number) => { + return isAlive && fuelLeft ? String(fuelLeft) : ' - '; }; const getEvents = (): Event[] => turns[roundIndex] .slice() - .sort((a: TurnParticipant, b: TurnParticipant) => { + .sort((a, b) => { const indexA = participants.findIndex((p) => p[0] === a[0]); const indexB = participants.findIndex((p) => p[0] === b[0]); return indexA - indexB; }) ?.map((participantInfo) => { - const isAlive = Object.keys(participantInfo[1])[0] === 'Alive'; + const isAlive = 'alive' in participantInfo[1]; const firstDeadRound = turns.findIndex((turn) => { const part = turn.find((participant) => participant[0] === participantInfo[0]) || []; - return Object.keys(part[1] || {})[0] !== 'Alive'; + return Object.keys(part[1] || {})[0] !== 'alive'; }); return { @@ -66,8 +62,11 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop name: participants.find((part) => part[0] === participantInfo[0])?.[1].name, deadRound: !isAlive, firstDeadRound, - fuelLeft: defineFuelLeftFormat(isAlive, participantInfo[1]?.Alive?.fuelLeft), - payload: isAlive ? participantInfo[1].Alive.payloadAmount : ' - ', + fuelLeft: defineFuelLeftFormat( + isAlive, + 'alive' in participantInfo[1] ? participantInfo[1]?.alive?.fuel_left : 0, + ), + payload: 'alive' in participantInfo[1] ? String(participantInfo[1].alive.payload_amount) : ' - ', lastAltitude: String( Math.round( Number(withoutCommas(altitude)) / @@ -76,11 +75,12 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop : roundsCount - roundNumber + 1), ), ), + haltReason: 'alive' in participantInfo[1] ? null : participantInfo[1].destroyed, }; }); const getFeedItems = () => - getEvents()?.map(({ participant, payload, lastAltitude, fuelLeft, deadRound }, index) => ( + getEvents()?.map(({ participant, payload, lastAltitude, fuelLeft, deadRound, haltReason }, index) => (
  • {getVaraAddress(participant)}

    @@ -93,16 +93,18 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop

    {lastAltitude},

    Payload:

    {payload},

    +

    Halt:

    +

    {haltReason || 'null'},

  • )); const sortRanks = () => { - const isAllZeros = rankings.every((rank) => rank[1] === '0'); + const isAllZeros = rankings.every((rank) => rank[1] === 0); const sortedRanks = isAllZeros ? [] - : rankings.sort((rankA, rankB) => (Number(withoutCommas(rankA[1])) < Number(withoutCommas(rankB[1])) ? 1 : -1)); + : rankings.sort((rankA, rankB) => (Number(rankA[1]) < Number(rankB[1]) ? 1 : -1)); return sortedRanks; }; @@ -117,7 +119,7 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop return { isUserWinner: winners.map((item) => item[0]).includes(userId || '0x'), - userRank: sortedRanks.find((item) => item[0] === userId)?.[1] || '', + userRank: sortedRanks.find((item) => item[0] === userId)?.[1] || '0', winners, }; }; @@ -126,6 +128,15 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop return (
    +
    item[0]).includes(userId || '0x') + ? styles.courtainGreen + : styles.courtainRed, + )} + /> +

    Session

    @@ -170,17 +181,9 @@ function Session({ session, turns, rankings, userId, participants, admin }: Prop roundsCount={roundsCount} isWinner={definedWinners.isUserWinner} winners={definedWinners.winners} - userRank={definedWinners.userRank} + userRank={String(definedWinners.userRank)} admin={admin} /> -
    item[0]).includes(userId || '0x') - ? styles.courtainGreen - : styles.courtainRed, - )} - />
    ); } diff --git a/frontend/apps/galactic-express/src/features/session/components/start/Start.tsx b/frontend/apps/galactic-express/src/features/session/components/start/Start.tsx index 4a0cb1d73..e3490d154 100644 --- a/frontend/apps/galactic-express/src/features/session/components/start/Start.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/start/Start.tsx @@ -1,25 +1,21 @@ import { useEffect } from 'react'; import clsx from 'clsx'; -import { HexString, UserMessageSent } from '@gear-js/api'; +import { HexString } from '@gear-js/api'; import { Button } from '@gear-js/ui'; import { useAtom, useSetAtom } from 'jotai'; import { CURRENT_GAME_ATOM, REGISTRATION_STATUS } from 'atoms'; -import { ADDRESS } from 'consts'; -import { Bytes } from '@polkadot/types'; -import { getVaraAddress, useAccount, useApi } from '@gear-js/react-hooks'; -import { UnsubscribePromise } from '@polkadot/api/types'; -import src from 'assets/images/earth.gif'; +import { getVaraAddress, useAccount } from '@gear-js/react-hooks'; +import earthGif from 'assets/images/earth.gif'; import { Container } from 'components'; -import { useDnsProgramIds } from '@dapps-frontend/hooks'; import { Participant, Session } from '../../types'; import { Traits } from '../traits'; import { Form } from '../form'; -import styles from './Start.module.scss'; -import { useEscrowMetadata } from '../../api'; import { ParticipantsTable } from '../participants-table'; import { SuccessfullyRegisteredInfo } from '../successfully-registered-info'; import { Warning } from '../warning'; import { CancelGameButton } from '../cancel-game-button/CancelGameButton'; +import { useEventGameCanceledSubscription, useEventPlayerDeletedSubscription } from 'app/utils'; +import styles from './Start.module.scss'; type Props = { participants: Participant[]; @@ -31,19 +27,8 @@ type Props = { bid: string | undefined; }; -type DecodedReplyOk = { - playerId: string; -}; - -type DecodedReply = { - Err: string; - Ok: Record & 'GameCanceled'; -}; - function Start({ participants, session, isUserAdmin, userAddress, adminAddress, bid, adminName }: Props) { - const { api } = useApi(); const { account } = useAccount(); - const { programId } = useDnsProgramIds(); const { decodedAddress } = account || {}; const [registrationStatus, setRegistrationStatus] = useAtom(REGISTRATION_STATUS); const setCurrentGame = useSetAtom(CURRENT_GAME_ATOM); @@ -52,71 +37,13 @@ function Start({ participants, session, isUserAdmin, userAddress, adminAddress, const isRegistered = decodedAddress ? !!participants.some((participant) => participant[0] === decodedAddress) : false; const containerClassName = clsx(styles.container, decodedAddress ? styles.smallMargin : styles.largeMargin); - const meta = useEscrowMetadata(); - const getDecodedPayload = (payload: Bytes) => { - if (meta?.types.handle.output) { - return meta.createType(meta.types.handle.output, payload).toHuman(); - } - }; - - const getDecodedReply = (payload: Bytes): DecodedReply => { - const decodedPayload = getDecodedPayload(payload); - - return decodedPayload as DecodedReply; - }; + useEventGameCanceledSubscription(isUserAdmin); + useEventPlayerDeletedSubscription(); const handleGoBack = () => { - setCurrentGame(''); - }; - - const handleEvents = ({ data }: UserMessageSent) => { - const { message } = data; - const { destination, source, payload } = message; - const isOwner = destination.toHex() === account?.decodedAddress; - const isEscrowProgram = source.toHex() === programId; - - if (isOwner && isEscrowProgram) { - const reply = getDecodedReply(payload); - - if (reply?.Err) { - if (reply.Err === 'NotEnoughParticipants' || reply.Err === 'MaximumPlayersReached') { - setRegistrationStatus(reply.Err); - return; - } - - setRegistrationStatus('error'); - } - } - - if (destination.toHex() === adminAddress) { - const reply = getDecodedReply(payload); - - if (reply.Ok) { - if (reply.Ok.PlayerDeleted?.playerId === account?.decodedAddress) { - setRegistrationStatus('PlayerRemoved'); - } - - if (reply.Ok === 'GameCanceled' && !isUserAdmin) { - setRegistrationStatus('GameCanceled'); - } - } - } + setCurrentGame(null); }; - useEffect(() => { - let unsub: UnsubscribePromise | undefined; - - if (api && decodedAddress && meta) { - unsub = api.gearEvents.subscribeToGearEvent('UserMessageSent', handleEvents); - } - - return () => { - if (unsub) unsub.then((unsubCallback) => unsubCallback()); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, decodedAddress, meta]); - useEffect(() => { if (registrationStatus === 'NotEnoughParticipants' && participants.length) { setRegistrationStatus('registration'); @@ -187,7 +114,7 @@ function Start({ participants, session, isUserAdmin, userAddress, adminAddress,
    {isRegistered && !isUserAdmin && } - earth + earth
    ); diff --git a/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss b/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss index ea285a293..530187e4d 100644 --- a/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss +++ b/frontend/apps/galactic-express/src/features/session/components/table/Table.module.scss @@ -6,7 +6,7 @@ $borderRadius: 8px; .table { display: grid; - grid-template-columns: 1fr repeat(5, max-content); + grid-template-columns: 1fr repeat(6, max-content); text-align: center; div { @@ -25,34 +25,34 @@ $borderRadius: 8px; background-color: rgba(255, 255, 255, 0.04); // first row - &:nth-child(n + 6):nth-child(-n + 12) { + &:nth-child(n + 7):nth-child(-n + 14) { border-top: $tableBorder; } // last row - &:nth-last-child(-n + 6) { + &:nth-last-child(-n + 7) { border-bottom: $tableBorder; } // not last column - &:not(:nth-child(6n)) { + &:not(:nth-child(7n)) { border-right: $cellBorder; } // not last row - &:not(:nth-last-child(-n + 6)) { + &:not(:nth-last-child(-n + 7)) { border-bottom: $cellBorder; } - &:nth-child(7) { + &:nth-child(8) { border-top-left-radius: $borderRadius; } - &:nth-child(12) { + &:nth-child(14) { border-top-right-radius: $borderRadius; } - &:nth-last-child(6) { + &:nth-last-child(7) { border-bottom-left-radius: $borderRadius; } diff --git a/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx b/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx index 550b01080..378224596 100644 --- a/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/table/Table.tsx @@ -22,7 +22,7 @@ function Table({ data, userId }: Props) { )); const getBody = () => - data?.map(({ participant, name, deadRound, fuelLeft, lastAltitude, payload }, index) => ( + data?.map(({ participant, name, deadRound, fuelLeft, lastAltitude, payload, haltReason }, index) => (
    {deadRound ? : }
    {fuelLeft}
    {lastAltitude}
    -
    {payload}
    +
    {payload}
    +
    {haltReason || ' - '}
    )); diff --git a/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx b/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx index 0575a0d50..c5a1b28b0 100644 --- a/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx +++ b/frontend/apps/galactic-express/src/features/session/components/win-status/WinStatus.tsx @@ -3,9 +3,9 @@ import { cx } from 'utils'; import { REGISTRATION_STATUS } from 'atoms'; import { getVaraAddress, useAccount } from '@gear-js/react-hooks'; import { Button } from '@gear-js/ui'; -import { useLaunchMessage } from 'features/session/hooks'; import { shortenString } from 'features/session/utils'; import { RankWithName } from 'features/session/types'; +import { useCancelGameMessage, useLeaveGameMessage } from 'app/utils'; import styles from './WinStatus.module.scss'; type Props = { @@ -16,25 +16,22 @@ type Props = { }; function WinStatus({ type, userRank, winners, admin }: Props) { - const { meta, message: sendNewSessionMessage } = useLaunchMessage(); const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS); + const { cancelGameMessage } = useCancelGameMessage(); + const { leaveGameMessage } = useLeaveGameMessage(); const { account } = useAccount(); const isAdmin = admin === account?.decodedAddress; - const onInBlock = () => { + const onSuccess = () => { setRegistrationStatus('registration'); }; const handleCreateNewSession = () => { - if (!meta) { - return; - } - if (isAdmin) { - sendNewSessionMessage({ payload: { CancelGame: null }, onInBlock }); + cancelGameMessage({ onSuccess }); } else { - sendNewSessionMessage({ payload: { LeaveGame: null }, onInBlock }); + leaveGameMessage({ onSuccess }); } }; @@ -51,7 +48,7 @@ function WinStatus({ type, userRank, winners, admin }: Props) { Winners:{' '}
      {winners.map((item) => ( -
    • +
    • {item[2] || shortenString(getVaraAddress(item[0]), 6)}
    • ))} diff --git a/frontend/apps/galactic-express/src/features/session/consts.ts b/frontend/apps/galactic-express/src/features/session/consts.ts index aa187af00..b53718527 100644 --- a/frontend/apps/galactic-express/src/features/session/consts.ts +++ b/frontend/apps/galactic-express/src/features/session/consts.ts @@ -48,7 +48,7 @@ const VALIDATE = { fuel: isGreaterThanZero, }; -const TABLE_HEADINGS = ['Player', 'Name', 'Alive', 'Fuel Left', 'Altitude', 'Payload']; +const TABLE_HEADINGS = ['Player', 'Name', 'Alive', 'Fuel Left', 'Altitude', 'Payload', 'Halt']; const PLAYER_COLORS = ['#eb5757', '#f2c94c', '#2f80ed', '#9b51e0']; diff --git a/frontend/apps/galactic-express/src/features/session/hooks.ts b/frontend/apps/galactic-express/src/features/session/hooks.ts index 86dd7bec2..a5441064e 100644 --- a/frontend/apps/galactic-express/src/features/session/hooks.ts +++ b/frontend/apps/galactic-express/src/features/session/hooks.ts @@ -1,35 +1,14 @@ -import { useMemo } from 'react'; -import { useAccount, useReadFullState, useSendMessageWithGas } from '@gear-js/react-hooks'; -import { HexString } from '@gear-js/api'; -import metaTxt from 'assets/meta/galactic_express_meta.txt'; -import { useProgramMetadata } from 'hooks'; -import { ADDRESS } from 'consts'; +import { useAccount } from '@gear-js/react-hooks'; import { useAtomValue } from 'jotai'; import { CURRENT_GAME_ATOM } from 'atoms'; -import { useDnsProgramIds } from '@dapps-frontend/hooks'; -import { LaunchState } from './types'; +import { useGetGameQuery } from 'app/utils'; function useLaunchState() { const { account } = useAccount(); - const { programId } = useDnsProgramIds(); const currentGame = useAtomValue(CURRENT_GAME_ATOM); - const meta = useProgramMetadata(metaTxt); - const payload = useMemo( - () => ({ GetGame: { playerId: currentGame || account?.decodedAddress } }), - [currentGame, account?.decodedAddress], - ); + const { game } = useGetGameQuery(currentGame || account?.decodedAddress); - const { state } = useReadFullState(programId, meta, payload); - - return state?.Game; + return game; } - -function useLaunchMessage() { - const { programId } = useDnsProgramIds(); - const meta = useProgramMetadata(metaTxt); - - return { meta: !!meta, message: useSendMessageWithGas(programId, meta, { isMaxGasLimit: true }) }; -} - -export { useLaunchState, useLaunchMessage }; +export { useLaunchState }; diff --git a/frontend/apps/galactic-express/src/features/session/types.ts b/frontend/apps/galactic-express/src/features/session/types.ts index b5117b38b..a98fa8b9e 100644 --- a/frontend/apps/galactic-express/src/features/session/types.ts +++ b/frontend/apps/galactic-express/src/features/session/types.ts @@ -1,25 +1,13 @@ import { HexString } from '@polkadot/util/types'; - -type Strategy = { - name: string; - fuelAmount: string; - payloadAmount: string; -}; +import { HaltReason, Participant as ProgramParticipant, Turn } from 'app/utils'; type Session = { altitude: string; weather: string; reward: string; - sessionId: string; }; -type Participant = [HexString, Strategy]; - -type Results = { - turns: Turns; - rankings: Rank[]; - participants: Participant[]; -}; +type Participant = [HexString, ProgramParticipant]; type Event = { participant: HexString; @@ -29,50 +17,16 @@ type Event = { fuelLeft: string; lastAltitude: string; payload: string; + haltReason: HaltReason | null; }; -type Rank = [HexString, string]; +type Rank = [HexString, number | string | bigint]; type RankWithName = [`0x${string}`, string, string]; -type State = { - admin: HexString; - stage: { - Registration: Participant[]; - Results: Results; - }; - master: string; - altitude: string; - weather: string; - reward: string; - sessionId: string; - bid: string; - adminName: string; -}; - -type LaunchState = { - Game: State; -}; - -type TurnParticipant = [ - HexString, - { - Alive: { - fuelLeft: string; - payloadAmount: string; - }; - }, -]; - -type Turn = TurnParticipant[]; +type TurnParticipant = [HexString, Turn]; -type Turns = Turn[]; - -type PlayerStatus = 'Finished' | 'Registered' | null; - -type PlayerInfo = { - PlayerInfo: PlayerStatus; -}; +type Turns = TurnParticipant[][]; type RegistrationStatus = | 'registration' @@ -83,17 +37,4 @@ type RegistrationStatus = | 'PlayerRemoved' | 'GameCanceled'; -export type { - LaunchState, - State, - Event, - Participant, - Turns, - Rank, - TurnParticipant, - Session, - PlayerStatus, - PlayerInfo, - RankWithName, - RegistrationStatus, -}; +export type { Event, Participant, Turns, Rank, TurnParticipant, Session, RankWithName, RegistrationStatus }; diff --git a/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx b/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx index 6ecafdc96..e237dab04 100644 --- a/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx +++ b/frontend/apps/galactic-express/src/features/welcome/components/enter-contract-address/RequestGame.tsx @@ -1,30 +1,22 @@ import { useEffect, useState } from 'react'; import { Wallet } from '@dapps-frontend/ui'; import { Button } from '@gear-js/vara-ui'; -import { useDnsProgramIds } from '@dapps-frontend/hooks'; import { cx } from 'utils'; import { ReactComponent as VaraSVG } from 'assets/images/icons/vara-coin.svg'; import { ReactComponent as TVaraSVG } from 'assets/images/icons/tvara-coin.svg'; import { useSetAtom, useAtom } from 'jotai'; import { CURRENT_GAME_ATOM, IS_LOADING, PLAYER_NAME_ATOM, REGISTRATION_STATUS } from 'atoms'; -import { useLaunchMessage } from 'features/session/hooks'; -import metaTxt from 'assets/meta/galactic_express_meta.txt'; -import { useAccount, useApi, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks'; +import { useAccount, useApi, useBalanceFormat } from '@gear-js/react-hooks'; import { TextField } from 'components/layout/TextField'; import { isNotEmpty, useForm } from '@mantine/form'; import { HexString, decodeAddress } from '@gear-js/api'; import { GameFoundModal } from 'features/session/components/game-found-modal'; -import { useProgramMetadata } from 'hooks'; -import { LaunchState } from 'features/session/types'; import { JoinModalFormValues } from 'features/session/components/game-found-modal/GameFoundModal'; import { TextModal } from 'features/session/components/game-not-found-modal'; import { GameIntro } from '../game-intro'; +import { GameState, useGetGameQuery, useCreateNewSessionMessage } from 'app/utils'; import styles from './RequestGame.module.scss'; -export interface ContractFormValues { - [key: string]: string; -} - type Status = 'creating' | 'joining' | null; type CreateFormValues = { @@ -39,12 +31,11 @@ type JoinFormValues = { function RequestGame() { const { account } = useAccount(); const { api } = useApi(); - const { programId } = useDnsProgramIds(); const { getFormattedBalanceValue, getChainBalanceValue } = useBalanceFormat(); - const [foundState, setFoundState] = useState(null); - const { message: sendNewSessionMessage } = useLaunchMessage(); - const meta = useProgramMetadata(metaTxt); + const { createNewSessionMessage } = useCreateNewSessionMessage(); + + const [foundState, setFoundState] = useState(null); const setCurrentGame = useSetAtom(CURRENT_GAME_ATOM); const setPlayerName = useSetAtom(PLAYER_NAME_ATOM); const setRegistrationStatus = useSetAtom(REGISTRATION_STATUS); @@ -67,7 +58,7 @@ function RequestGame() { }, }); - const joinForm = useForm({ + const joinForm = useForm({ initialValues: { address: undefined, }, @@ -78,7 +69,9 @@ function RequestGame() { const { errors: createErrors, getInputProps: getCreateInputProps, onSubmit: onCreateSubmit } = createForm; - const { errors: joinErrors, getInputProps: getJoinInputProps, onSubmit: onJoinSubmit } = joinForm; + const { errors: joinErrors, getInputProps: getJoinInputProps, onSubmit: onJoinSubmit, values } = joinForm; + + const { refetch } = useGetGameQuery(values.address?.length === 49 ? decodeAddress(values.address) : undefined); const handleSetStatus = (newStatus: Status) => { setStatus(newStatus); @@ -92,25 +85,11 @@ function RequestGame() { if (!account?.decodedAddress) { return; } - - const payload = { - CreateNewSession: { - name: values.name, - }, - }; - setIsLoading(true); - sendNewSessionMessage({ - payload, - value: getChainBalanceValue(values.fee).toFixed(), - onSuccess: () => { - setIsLoading(false); - }, - onError: () => { - console.log('error'); - setIsLoading(false); - }, - }); + createNewSessionMessage( + { name: values.name, value: BigInt(getChainBalanceValue(values.fee).toFixed()) }, + { onSuccess: () => setIsLoading(false), onError: () => setIsLoading(false) }, + ); }; const handleOpenJoinSessionModal = async (values: JoinFormValues) => { @@ -118,21 +97,11 @@ function RequestGame() { return; } - const payload = { GetGame: { playerId: decodeAddress(values.address || '') } }; - try { - const res = await api?.programState.read( - { - programId, - payload, - }, - meta, - ); - - const state = (await res?.toHuman()) as LaunchState; + const { data } = await refetch(); - if (state?.Game) { - setFoundState(state); + if (data) { + setFoundState(data); setFoundGame(decodeAddress(values.address || '')); setIsJoinSessionModalShown(true); return; @@ -252,8 +221,10 @@ function RequestGame() { )} {isJoinSessionModalShown && ( (); - - useEffect(() => { - fetch(source) - .then((response) => response.text()) - .then((raw) => ProgramMetadata.from(`0x${raw}`)) - .then((result) => setMetadata(result)) - .catch(({ message }: Error) => alert.error(message)); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return metadata; -} - -function useStateMetadata(source: string) { - const alert = useAlert(); - - const [stateMetadata, setStateMetadata] = useState(); - - useEffect(() => { - fetch(source) - .then((response) => response.arrayBuffer()) - .then((arrayBuffer) => Buffer.from(arrayBuffer)) - .then((buffer) => getStateMetadata(buffer)) - .then((result) => setStateMetadata(result)) - .catch(({ message }: Error) => alert.error(message)); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return stateMetadata; -} - -function useReadState({ programId, meta, payload }: { programId?: HexString; meta: string; payload?: AnyJson }) { - const metadata = useProgramMetadata(meta); - return useReadFullState(programId, metadata, payload); -} - -export { useProgramMetadata, useStateMetadata, useReadState }; diff --git a/frontend/apps/galactic-express/src/hooks/index.ts b/frontend/apps/galactic-express/src/hooks/index.ts deleted file mode 100644 index 2785a21b4..000000000 --- a/frontend/apps/galactic-express/src/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { useProgramMetadata, useStateMetadata, useReadState } from './api'; - -export { useProgramMetadata, useStateMetadata, useReadState }; diff --git a/frontend/apps/galactic-express/src/pages/home/Home.tsx b/frontend/apps/galactic-express/src/pages/home/Home.tsx index 5734a1281..21887d486 100644 --- a/frontend/apps/galactic-express/src/pages/home/Home.tsx +++ b/frontend/apps/galactic-express/src/pages/home/Home.tsx @@ -14,13 +14,16 @@ function Home() { const [isGameCancelledModalOpen, setIsGameCancelledModalOpen] = useState(false); const { account } = useAccount(); const state = useLaunchState(); - const { admin, stage, sessionId, altitude, weather, reward, bid, adminName } = state || {}; + const { admin, stage, altitude, weather, reward, bid, admin_name } = state || {}; - const isSessionEnded = Object.keys(stage || {})[0] === 'Results'; + const isSessionEnded = stage && 'results' in stage; - const rankings = stage?.Results?.rankings; - const turns = stage?.Results?.turns; - const participants = stage?.Registration || stage?.Results?.participants; + const rankings = isSessionEnded ? stage.results.rankings : []; + const turns = isSessionEnded ? stage.results.turns : []; + + const registrationParticipants = stage && 'registration' in stage && stage.registration; + const resultsParticipants = isSessionEnded && stage.results.participants; + const participants = registrationParticipants || resultsParticipants || []; const isUserAdmin = admin === account?.decodedAddress; @@ -51,17 +54,16 @@ function Home() { <> {!isSessionEnded && ( )} @@ -70,14 +72,13 @@ function Home() { {rankings?.map((item) => item[0]).includes(account?.decodedAddress || '0x') ? ( diff --git a/frontend/apps/galactic-express/src/utils/index.ts b/frontend/apps/galactic-express/src/utils/index.ts index 0ec1b2c0a..643201ddc 100644 --- a/frontend/apps/galactic-express/src/utils/index.ts +++ b/frontend/apps/galactic-express/src/utils/index.ts @@ -52,3 +52,14 @@ export const cx = (...styles: string[]) => clsx(...styles); export const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent, ); + +export const getPanicType = (error: unknown) => { + if (error instanceof Error) { + const errorWords = error?.message?.replaceAll("'", '').trim().split(' '); + const panicType = errorWords[errorWords.length - 1]; + + return panicType; + } + + return null; +}; diff --git a/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx b/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx index 0108ce9f0..e5e8cfd7f 100644 --- a/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx +++ b/frontend/apps/syndote/src/pages/welcome/components/create-game-form/CreateGameForm.tsx @@ -10,10 +10,6 @@ import { isNotEmpty, useForm } from '@mantine/form'; import { useSyndoteMessage } from 'hooks/metadata'; import styles from './CreateGameForm.module.scss'; -export interface ContractFormValues { - [key: string]: string; -} - type CreateFormValues = { fee: number; name: string; diff --git a/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx b/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx index 64fce0c31..bb55cfd78 100644 --- a/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx +++ b/frontend/apps/syndote/src/pages/welcome/components/join-game-form/JoinGameForm.tsx @@ -15,10 +15,6 @@ import { TextModal } from 'pages/home/text-modal'; import styles from './JoinGameForm.module.scss'; import { GameSessionState, State } from 'types'; -export interface ContractFormValues { - [key: string]: string; -} - type Props = { onCancel: () => void; }; diff --git a/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx b/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx index 3c43c4f44..bc0b4a411 100644 --- a/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx +++ b/frontend/apps/syndote/src/pages/welcome/components/request-game/RequestGame.tsx @@ -9,10 +9,6 @@ import styles from './RequestGame.module.scss'; import { CreateGameForm } from '../create-game-form'; import { JoinGameForm } from '../join-game-form'; -export interface ContractFormValues { - [key: string]: string; -} - type Status = 'creating' | 'joining' | null; function RequestGame() { diff --git a/frontend/apps/varatube/src/App.tsx b/frontend/apps/varatube/src/App.tsx index bda83bc2c..0d419fb4c 100644 --- a/frontend/apps/varatube/src/App.tsx +++ b/frontend/apps/varatube/src/App.tsx @@ -3,17 +3,17 @@ import { Footer } from '@dapps-frontend/ui'; import { Routing } from 'pages'; import { Header, ApiLoader } from 'components'; import { withProviders } from 'hocs'; -import { useProgramState } from 'hooks/api'; import 'simplebar-react/dist/simplebar.min.css'; import 'App.scss'; import '@gear-js/vara-ui/dist/style.css'; +import { useGetSubscriberQuery } from 'app/utils'; function Component() { const { isApiReady } = useApi(); const { isAccountReady } = useAccount(); - const { isSubscriptionsStateRead } = useProgramState(); + const { isFetched } = useGetSubscriberQuery(); - const isAppReady = isApiReady && isAccountReady && isSubscriptionsStateRead; + const isAppReady = isApiReady && isAccountReady && isFetched; return ( <> diff --git a/frontend/apps/varatube/src/app/utils/index.ts b/frontend/apps/varatube/src/app/utils/index.ts new file mode 100644 index 000000000..15858e186 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/index.ts @@ -0,0 +1 @@ +export * from './sails'; diff --git a/frontend/apps/varatube/src/app/utils/sails/extended_vft.idl b/frontend/apps/varatube/src/app/utils/sails/extended_vft.idl new file mode 100644 index 000000000..55310739f --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/extended_vft.idl @@ -0,0 +1,34 @@ +constructor { + New : (name: str, symbol: str, decimals: u8); +}; + +service Vft { + Burn : (from: actor_id, value: u256) -> bool; + GrantAdminRole : (to: actor_id) -> null; + GrantBurnerRole : (to: actor_id) -> null; + GrantMinterRole : (to: actor_id) -> null; + Mint : (to: actor_id, value: u256) -> bool; + RevokeAdminRole : (from: actor_id) -> null; + RevokeBurnerRole : (from: actor_id) -> null; + RevokeMinterRole : (from: actor_id) -> null; + Approve : (spender: actor_id, value: u256) -> bool; + Transfer : (to: actor_id, value: u256) -> bool; + TransferFrom : (from: actor_id, to: actor_id, value: u256) -> bool; + query Admins : () -> vec actor_id; + query Burners : () -> vec actor_id; + query Minters : () -> vec actor_id; + query Allowance : (owner: actor_id, spender: actor_id) -> u256; + query BalanceOf : (account: actor_id) -> u256; + query Decimals : () -> u8; + query Name : () -> str; + query Symbol : () -> str; + query TotalSupply : () -> u256; + + events { + Minted: struct { to: actor_id, value: u256 }; + Burned: struct { from: actor_id, value: u256 }; + Approval: struct { owner: actor_id, spender: actor_id, value: u256 }; + Transfer: struct { from: actor_id, to: actor_id, value: u256 }; + } +}; + diff --git a/frontend/apps/varatube/src/app/utils/sails/extended_vft.ts b/frontend/apps/varatube/src/app/utils/sails/extended_vft.ts new file mode 100644 index 000000000..5ef447721 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/extended_vft.ts @@ -0,0 +1,451 @@ +import { GearApi, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; +import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js'; + +type ActorId = string; + +export class Program { + public readonly registry: TypeRegistry; + public readonly vft: Vft; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types: Record = {}; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.vft = new Vft(this); + } + + newCtorFromCode(code: Uint8Array | Buffer, name: string, symbol: string, decimals: number): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + ['New', name, symbol, decimals], + '(String, String, String, u8)', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, name: string, symbol: string, decimals: number) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + ['New', name, symbol, decimals], + '(String, String, String, u8)', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class Vft { + constructor(private _program: Program) {} + + public burn(from: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'Burn', from, value], + '(String, String, [u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public grantAdminRole(to: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'GrantAdminRole', to], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public grantBurnerRole(to: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'GrantBurnerRole', to], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public grantMinterRole(to: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'GrantMinterRole', to], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public mint(to: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'Mint', to, value], + '(String, String, [u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public revokeAdminRole(from: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'RevokeAdminRole', from], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public revokeBurnerRole(from: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'RevokeBurnerRole', from], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public revokeMinterRole(from: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'RevokeMinterRole', from], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public approve(spender: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'Approve', spender, value], + '(String, String, [u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public transfer(to: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'Transfer', to, value], + '(String, String, [u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public transferFrom(from: ActorId, to: ActorId, value: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Vft', 'TransferFrom', from, to, value], + '(String, String, [u8;32], [u8;32], U256)', + 'bool', + this._program.programId, + ); + } + + public async admins( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'Admins']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public async burners( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'Burners']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public async minters( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'Minters']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public async allowance( + owner: ActorId, + spender: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32], [u8;32])', ['Vft', 'Allowance', owner, spender]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, U256)', reply.payload); + return result[2].toBigInt() as unknown as bigint; + } + + public async balanceOf( + account: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['Vft', 'BalanceOf', account]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, U256)', reply.payload); + return result[2].toBigInt() as unknown as bigint; + } + + public async decimals( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'Decimals']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, u8)', reply.payload); + return result[2].toNumber() as unknown as number; + } + + public async name( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'Name']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, String)', reply.payload); + return result[2].toString() as unknown as string; + } + + public async symbol( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'Symbol']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, String)', reply.payload); + return result[2].toString() as unknown as string; + } + + public async totalSupply( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['Vft', 'TotalSupply']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, U256)', reply.payload); + return result[2].toBigInt() as unknown as bigint; + } + + public subscribeToMintedEvent( + callback: (data: { to: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Minted') { + callback( + this._program.registry + .createType('(String, String, {"to":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { to: ActorId; value: number | string | bigint }, + ); + } + }); + } + + public subscribeToBurnedEvent( + callback: (data: { from: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Burned') { + callback( + this._program.registry + .createType('(String, String, {"from":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { from: ActorId; value: number | string | bigint }, + ); + } + }); + } + + public subscribeToApprovalEvent( + callback: (data: { owner: ActorId; spender: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Approval') { + callback( + this._program.registry + .createType('(String, String, {"owner":"[u8;32]","spender":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { owner: ActorId; spender: ActorId; value: number | string | bigint }, + ); + } + }); + } + + public subscribeToTransferEvent( + callback: (data: { from: ActorId; to: ActorId; value: number | string | bigint }) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Vft' && getFnNamePrefix(payload) === 'Transfer') { + callback( + this._program.registry + .createType('(String, String, {"from":"[u8;32]","to":"[u8;32]","value":"U256"})', message.payload)[2] + .toJSON() as unknown as { from: ActorId; to: ActorId; value: number | string | bigint }, + ); + } + }); + } +} diff --git a/frontend/apps/varatube/src/app/utils/sails/index.ts b/frontend/apps/varatube/src/app/utils/sails/index.ts new file mode 100644 index 000000000..e6bda9d41 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/index.ts @@ -0,0 +1,3 @@ +export * from './sails'; +export * from './queries'; +export * from './messages'; diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/index.ts b/frontend/apps/varatube/src/app/utils/sails/messages/index.ts new file mode 100644 index 000000000..ac7f7bf51 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/messages/index.ts @@ -0,0 +1,2 @@ +export { useCancelSubscriptionMessage } from './use-cancel-register-message'; +export { useRegisterSubscriptionMessage } from './use-register-subscription-message'; diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/use-approve-message.ts b/frontend/apps/varatube/src/app/utils/sails/messages/use-approve-message.ts new file mode 100644 index 000000000..4eca9b1e5 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/messages/use-approve-message.ts @@ -0,0 +1,30 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { Options, useSignAndSend } from 'hooks/use-sign-and-send'; +import { useVftProgram } from '../sails'; + +type Params = { + spender: string; + value: number | string | bigint; +}; + +export const useApproveMessage = () => { + const program = useVftProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'vft', + functionName: 'approve', + }); + const { signAndSend } = useSignAndSend(); + + const approveMessage = async ({ spender, value }: Params, options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [spender, value], + + gasLimit: { increaseGas: 20 }, + }); + console.log('SIGN APPROOVE'); + signAndSend(transaction, options); + }; + + return { approveMessage }; +}; diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/use-cancel-register-message.ts b/frontend/apps/varatube/src/app/utils/sails/messages/use-cancel-register-message.ts new file mode 100644 index 000000000..af3718739 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/messages/use-cancel-register-message.ts @@ -0,0 +1,24 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { Options, useSignAndSend } from 'hooks/use-sign-and-send'; +import { useVaratubeProgram } from '../sails'; + +export const useCancelSubscriptionMessage = () => { + const program = useVaratubeProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'varatube', + functionName: 'cancelSubscription', + }); + + const { signAndSend } = useSignAndSend(); + + const cancelSubscriptionMessage = async (options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [], + gasLimit: { increaseGas: 20 }, + }); + signAndSend(transaction, options); + }; + + return { cancelSubscriptionMessage }; +}; diff --git a/frontend/apps/varatube/src/app/utils/sails/messages/use-register-subscription-message.ts b/frontend/apps/varatube/src/app/utils/sails/messages/use-register-subscription-message.ts new file mode 100644 index 000000000..57649c59c --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/messages/use-register-subscription-message.ts @@ -0,0 +1,30 @@ +import { usePrepareProgramTransaction } from '@gear-js/react-hooks'; +import { Options, useSignAndSend } from 'hooks/use-sign-and-send'; +import { useVaratubeProgram } from '../sails'; +import { Period } from '../varatube'; + +type Params = { + period: Period; + currency_id: `0x${string}`; + with_renewal: boolean; +}; + +export const useRegisterSubscriptionMessage = () => { + const program = useVaratubeProgram(); + const { prepareTransactionAsync } = usePrepareProgramTransaction({ + program, + serviceName: 'varatube', + functionName: 'registerSubscription', + }); + const { signAndSend } = useSignAndSend(); + + const registerSubscriptionMessage = async ({ period, currency_id, with_renewal }: Params, options: Options) => { + const { transaction } = await prepareTransactionAsync({ + args: [period, currency_id, with_renewal], + gasLimit: { increaseGas: 20 }, + }); + signAndSend(transaction, options); + }; + + return { registerSubscriptionMessage }; +}; diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/index.ts b/frontend/apps/varatube/src/app/utils/sails/queries/index.ts new file mode 100644 index 000000000..aeab04022 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/queries/index.ts @@ -0,0 +1,2 @@ +export { useGetSubscriberQuery } from './use-get-subscriber-query'; +export { useBalanceOfQuery } from './use-balance-of-query'; diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/use-balance-of-query.ts b/frontend/apps/varatube/src/app/utils/sails/queries/use-balance-of-query.ts new file mode 100644 index 000000000..294935e7a --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/queries/use-balance-of-query.ts @@ -0,0 +1,17 @@ +import { useVftProgram } from '../sails'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; + +export const useBalanceOfQuery = () => { + const program = useVftProgram(); + const { account } = useAccount(); + + const { data, refetch, isFetching, error } = useProgramQuery({ + program, + serviceName: 'vft', + functionName: 'balanceOf', + args: [account?.decodedAddress || ''], + query: { enabled: account ? undefined : false }, + }); + + return { balance: data, isFetching, refetch, error }; +}; diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/use-currencies-query.ts b/frontend/apps/varatube/src/app/utils/sails/queries/use-currencies-query.ts new file mode 100644 index 000000000..7ebb75d5c --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/queries/use-currencies-query.ts @@ -0,0 +1,15 @@ +import { useVaratubeProgram } from '../sails'; +import { useProgramQuery } from '@gear-js/react-hooks'; + +export const useCurrenciesQuery = () => { + const program = useVaratubeProgram(); + + const { data, refetch, isFetching, error } = useProgramQuery({ + program, + serviceName: 'varatube', + functionName: 'currencies', + args: [], + }); + + return { currencies: data, isFetching, refetch, error }; +}; diff --git a/frontend/apps/varatube/src/app/utils/sails/queries/use-get-subscriber-query.ts b/frontend/apps/varatube/src/app/utils/sails/queries/use-get-subscriber-query.ts new file mode 100644 index 000000000..053766508 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/queries/use-get-subscriber-query.ts @@ -0,0 +1,19 @@ +import { useVaratubeProgram } from '../sails'; +import { useAccount, useProgramQuery } from '@gear-js/react-hooks'; + +export const useGetSubscriberQuery = () => { + const program = useVaratubeProgram(); + const { account } = useAccount(); + + const { data, refetch, isFetching, error, isFetched } = useProgramQuery({ + program, + serviceName: 'varatube', + functionName: 'allSubscriptions', + args: [], + query: { enabled: true }, + }); + + const subscriber = data?.find(([address]) => account?.decodedAddress === address)?.[1]; + + return { subscriber, isFetching, isFetched, refetch, error }; +}; diff --git a/frontend/apps/varatube/src/app/utils/sails/sails.ts b/frontend/apps/varatube/src/app/utils/sails/sails.ts new file mode 100644 index 000000000..9aa59b1cf --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/sails.ts @@ -0,0 +1,24 @@ +import { useProgram as useGearJsProgram } from '@gear-js/react-hooks'; +import { Program as VaratubeProgram } from './varatube'; +import { Program as VftProgram } from './extended_vft'; +import { ADDRESS } from 'consts'; + +const useVaratubeProgram = () => { + const { data: program } = useGearJsProgram({ + library: VaratubeProgram, + id: ADDRESS.CONTRACT, + }); + + return program; +}; + +const useVftProgram = () => { + const { data: program } = useGearJsProgram({ + library: VftProgram, + id: ADDRESS.FT_CONTRACT, + }); + + return program; +}; + +export { useVaratubeProgram, useVftProgram }; diff --git a/frontend/apps/varatube/src/app/utils/sails/varatube.idl b/frontend/apps/varatube/src/app/utils/sails/varatube.idl new file mode 100644 index 000000000..8989d6b7b --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/varatube.idl @@ -0,0 +1,65 @@ +type Config = struct { + gas_for_token_transfer: u64, + gas_to_start_subscription_update: u64, + block_duration: u32, + min_gas_limit: u64, +}; + +type Period = enum { + Year, + NineMonths, + SixMonths, + ThreeMonths, + Month, +}; + +type SubscriberDataState = struct { + is_active: bool, + start_date: u64, + start_block: u32, + end_date: u64, + end_block: u32, + period: Period, + will_renew: bool, + price: u128, +}; + +type SubscriberData = struct { + currency_id: actor_id, + period: Period, + + subscription_start: opt struct { u64, u32 }, + + renewal_date: opt struct { u64, u32 }, +}; + +constructor { + New : (config: Config, dns_id_and_name: opt struct { actor_id, str }); +}; + +service Varatube { + AddTokenData : (token_id: actor_id, price: u128) -> null; + CancelSubscription : () -> null; + Kill : (inheritor: actor_id) -> null; + ManagePendingSubscription : (enable: bool) -> null; + RegisterSubscription : (period: Period, currency_id: actor_id, with_renewal: bool) -> null; + UpdateConfig : (gas_for_token_transfer: opt u64, gas_to_start_subscription_update: opt u64, block_duration: opt u32) -> null; + UpdateSubscription : (subscriber: actor_id) -> null; + query Admins : () -> vec actor_id; + query AllSubscriptions : () -> vec struct { actor_id, SubscriberDataState }; + query Config : () -> Config; + query Currencies : () -> vec struct { actor_id, u128 }; + query GetSubscriber : (account: actor_id) -> opt SubscriberData; + query Subscribers : () -> vec struct { actor_id, SubscriberData }; + + events { + SubscriptionRegistered; + SubscriptionUpdated; + SubscriptionCancelled; + PendingSubscriptionManaged; + PaymentAdded; + ConfigUpdated; + Killed: struct { inheritor: actor_id }; + } +}; + diff --git a/frontend/apps/varatube/src/app/utils/sails/varatube.ts b/frontend/apps/varatube/src/app/utils/sails/varatube.ts new file mode 100644 index 000000000..1e06922f9 --- /dev/null +++ b/frontend/apps/varatube/src/app/utils/sails/varatube.ts @@ -0,0 +1,421 @@ +import { TransactionBuilder, getServiceNamePrefix, getFnNamePrefix, ZERO_ADDRESS } from 'sails-js'; +import { GearApi, decodeAddress } from '@gear-js/api'; +import { TypeRegistry } from '@polkadot/types'; + +type ActorId = string; + +export interface Config { + gas_for_token_transfer: number | string | bigint; + gas_to_start_subscription_update: number | string | bigint; + block_duration: number; + min_gas_limit: number | string | bigint; +} + +export type Period = 'year' | 'nineMonths' | 'sixMonths' | 'threeMonths' | 'month'; + +export interface SubscriberDataState { + is_active: boolean; + start_date: number | string | bigint; + start_block: number; + end_date: number | string | bigint; + end_block: number; + period: Period; + will_renew: boolean; + price: number | string | bigint; +} + +export interface SubscriberData { + currency_id: ActorId; + period: Period; + subscription_start: [number | string | bigint, number] | null; + renewal_date: [number | string | bigint, number] | null; +} + +export class Program { + public readonly registry: TypeRegistry; + public readonly varatube: Varatube; + + constructor(public api: GearApi, public programId?: `0x${string}`) { + const types: Record = { + Config: { + gas_for_token_transfer: 'u64', + gas_to_start_subscription_update: 'u64', + block_duration: 'u32', + min_gas_limit: 'u64', + }, + Period: { _enum: ['Year', 'NineMonths', 'SixMonths', 'ThreeMonths', 'Month'] }, + SubscriberDataState: { + is_active: 'bool', + start_date: 'u64', + start_block: 'u32', + end_date: 'u64', + end_block: 'u32', + period: 'Period', + will_renew: 'bool', + price: 'u128', + }, + SubscriberData: { + currency_id: '[u8;32]', + period: 'Period', + subscription_start: 'Option<(u64, u32)>', + renewal_date: 'Option<(u64, u32)>', + }, + }; + + this.registry = new TypeRegistry(); + this.registry.setKnownTypes({ types }); + this.registry.register(types); + + this.varatube = new Varatube(this); + } + + newCtorFromCode( + code: Uint8Array | Buffer, + config: Config, + dns_id_and_name: [ActorId, string] | null, + ): TransactionBuilder { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'upload_program', + ['New', config, dns_id_and_name], + '(String, Config, Option<([u8;32], String)>)', + 'String', + code, + ); + + this.programId = builder.programId; + return builder; + } + + newCtorFromCodeId(codeId: `0x${string}`, config: Config, dns_id_and_name: [ActorId, string] | null) { + const builder = new TransactionBuilder( + this.api, + this.registry, + 'create_program', + ['New', config, dns_id_and_name], + '(String, Config, Option<([u8;32], String)>)', + 'String', + codeId, + ); + + this.programId = builder.programId; + return builder; + } +} + +export class Varatube { + constructor(private _program: Program) {} + + public addTokenData(token_id: ActorId, price: number | string | bigint): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'AddTokenData', token_id, price], + '(String, String, [u8;32], u128)', + 'Null', + this._program.programId, + ); + } + + public cancelSubscription(): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'CancelSubscription'], + '(String, String)', + 'Null', + this._program.programId, + ); + } + + public kill(inheritor: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'Kill', inheritor], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public managePendingSubscription(enable: boolean): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'ManagePendingSubscription', enable], + '(String, String, bool)', + 'Null', + this._program.programId, + ); + } + + public registerSubscription(period: Period, currency_id: ActorId, with_renewal: boolean): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'RegisterSubscription', period, currency_id, with_renewal], + '(String, String, Period, [u8;32], bool)', + 'Null', + this._program.programId, + ); + } + + public updateConfig( + gas_for_token_transfer: number | string | bigint | null, + gas_to_start_subscription_update: number | string | bigint | null, + block_duration: number | null, + ): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'UpdateConfig', gas_for_token_transfer, gas_to_start_subscription_update, block_duration], + '(String, String, Option, Option, Option)', + 'Null', + this._program.programId, + ); + } + + public updateSubscription(subscriber: ActorId): TransactionBuilder { + if (!this._program.programId) throw new Error('Program ID is not set'); + return new TransactionBuilder( + this._program.api, + this._program.registry, + 'send_message', + ['Varatube', 'UpdateSubscription', subscriber], + '(String, String, [u8;32])', + 'Null', + this._program.programId, + ); + } + + public async admins( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Admins']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<[u8;32]>)', reply.payload); + return result[2].toJSON() as unknown as Array; + } + + public async allSubscriptions( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Varatube', 'AllSubscriptions']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType( + '(String, String, Vec<([u8;32], SubscriberDataState)>)', + reply.payload, + ); + return result[2].toJSON() as unknown as Array<[ActorId, SubscriberDataState]>; + } + + public async config( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Config']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Config)', reply.payload); + return result[2].toJSON() as unknown as Config; + } + + public async currencies( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Currencies']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<([u8;32], u128)>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, number | string | bigint]>; + } + + public async getSubscriber( + account: ActorId, + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise { + const payload = this._program.registry + .createType('(String, String, [u8;32])', ['Varatube', 'GetSubscriber', account]) + .toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Option)', reply.payload); + return result[2].toJSON() as unknown as SubscriberData | null; + } + + public async subscribers( + originAddress?: string, + value?: number | string | bigint, + atBlock?: `0x${string}`, + ): Promise> { + const payload = this._program.registry.createType('(String, String)', ['Varatube', 'Subscribers']).toHex(); + const reply = await this._program.api.message.calculateReply({ + destination: this._program.programId!, + origin: originAddress ? decodeAddress(originAddress) : ZERO_ADDRESS, + payload, + value: value || 0, + gasLimit: this._program.api.blockGasLimit.toBigInt(), + at: atBlock, + }); + if (!reply.code.isSuccess) throw new Error(this._program.registry.createType('String', reply.payload).toString()); + const result = this._program.registry.createType('(String, String, Vec<([u8;32], SubscriberData)>)', reply.payload); + return result[2].toJSON() as unknown as Array<[ActorId, SubscriberData]>; + } + + public subscribeToSubscriptionRegisteredEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'SubscriptionRegistered') { + callback(null); + } + }); + } + + public subscribeToSubscriptionUpdatedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'SubscriptionUpdated') { + callback(null); + } + }); + } + + public subscribeToSubscriptionCancelledEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'SubscriptionCancelled') { + callback(null); + } + }); + } + + public subscribeToPendingSubscriptionManagedEvent( + callback: (data: null) => void | Promise, + ): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'PendingSubscriptionManaged') { + callback(null); + } + }); + } + + public subscribeToPaymentAddedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'PaymentAdded') { + callback(null); + } + }); + } + + public subscribeToConfigUpdatedEvent(callback: (data: null) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'ConfigUpdated') { + callback(null); + } + }); + } + + public subscribeToKilledEvent(callback: (data: { inheritor: ActorId }) => void | Promise): Promise<() => void> { + return this._program.api.gearEvents.subscribeToGearEvent('UserMessageSent', ({ data: { message } }) => { + if (!message.source.eq(this._program.programId) || !message.destination.eq(ZERO_ADDRESS)) { + return; + } + + const payload = message.payload.toHex(); + if (getServiceNamePrefix(payload) === 'Varatube' && getFnNamePrefix(payload) === 'Killed') { + callback( + this._program.registry + .createType('(String, String, {"inheritor":"[u8;32]"})', message.payload)[2] + .toJSON() as unknown as { inheritor: ActorId }, + ); + } + }); + } +} diff --git a/frontend/apps/varatube/src/assets/state/ft_meta.txt b/frontend/apps/varatube/src/assets/state/ft_meta.txt deleted file mode 100644 index 92bac072d..000000000 --- a/frontend/apps/varatube/src/assets/state/ft_meta.txt +++ /dev/null @@ -1 +0,0 @@ -0002000100000000000103000000010700000000000000000108000000690c3400084466756e6769626c655f746f6b656e5f696f28496e6974436f6e66696700000c01106e616d65040118537472696e6700011873796d626f6c040118537472696e67000120646563696d616c73080108753800000400000502000800000503000c084466756e6769626c655f746f6b656e5f696f204654416374696f6e000118104d696e74040010011075313238000000104275726e040010011075313238000100205472616e736665720c011066726f6d14011c4163746f724964000108746f14011c4163746f724964000118616d6f756e74100110753132380002001c417070726f7665080108746f14011c4163746f724964000118616d6f756e74100110753132380003002c546f74616c537570706c790004002442616c616e63654f66040014011c4163746f724964000500001000000507001410106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001801205b75383b2033325d0000180000032000000008001c084466756e6769626c655f746f6b656e5f696f1c46544576656e74000110205472616e736665720c011066726f6d14011c4163746f724964000108746f14011c4163746f724964000118616d6f756e74100110753132380000001c417070726f76650c011066726f6d14011c4163746f724964000108746f14011c4163746f724964000118616d6f756e74100110753132380001002c546f74616c537570706c790400100110753132380002001c42616c616e63650400100110753132380003000020084466756e6769626c655f746f6b656e5f696f3c496f46756e6769626c65546f6b656e00001801106e616d65040118537472696e6700011873796d626f6c040118537472696e67000130746f74616c5f737570706c791001107531323800012062616c616e6365732401505665633c284163746f7249642c2075313238293e000128616c6c6f77616e6365732c01905665633c284163746f7249642c205665633c284163746f7249642c2075313238293e293e000120646563696d616c730801087538000024000002280028000004081410002c00000230003000000408142400 \ No newline at end of file diff --git a/frontend/apps/varatube/src/assets/state/varatube_meta.txt b/frontend/apps/varatube/src/assets/state/varatube_meta.txt deleted file mode 100644 index 33aa84eee..000000000 --- a/frontend/apps/varatube/src/assets/state/varatube_meta.txt +++ /dev/null @@ -1 +0,0 @@ -0002000100000000000103000000010c00000000000000010f0000000110000000891b6c00082c76617261747562655f696f18436f6e66696700000c01586761735f666f725f746f6b656e5f7472616e7366657204010c75363400014c6761735f666f725f64656c617965645f6d736704010c753634000138626c6f636b5f6475726174696f6e08010c75333200000400000506000800000505000c082c76617261747562655f696f1c416374696f6e73000118505265676973746572537562736372697074696f6e0c012c63757272656e63795f696410011c4163746f724964000118706572696f641c0118506572696f64000130776974685f72656e6577616c200110626f6f6c00000048557064617465537562736372697074696f6e0401287375627363726962657210011c4163746f7249640001004843616e63656c537562736372697074696f6e000200644d616e61676550656e64696e67537562736372697074696f6e040118656e61626c65200110626f6f6c00030030416464546f6b656e44617461080120746f6b656e5f696410011c4163746f7249640001147072696365240114507269636500040030557064617465436f6e6669670c01586761735f666f725f746f6b656e5f7472616e7366657228012c4f7074696f6e3c7536343e00014c6761735f666f725f64656c617965645f6d736728012c4f7074696f6e3c7536343e000138626c6f636b5f6475726174696f6e2c012c4f7074696f6e3c7533323e000500001010106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004001401205b75383b2033325d0000140000032000000018001800000503001c082c76617261747562655f696f18506572696f640001141059656172000000284e696e654d6f6e746873000100245369784d6f6e7468730002002c54687265654d6f6e746873000300144d6f6e7468000400002000000500002400000507002804184f7074696f6e04045401040108104e6f6e6500000010536f6d6504000400000100002c04184f7074696f6e04045401080108104e6f6e6500000010536f6d650400080000010000300418526573756c740804540134044501380108084f6b040034000000000c457272040038000001000034082c76617261747562655f696f145265706c7900011858537562736372697074696f6e526567697374657265640000004c537562736372697074696f6e5570646174656400010054537562736372697074696f6e43616e63656c6c65640002006850656e64696e67537562736372697074696f6e4d616e61676564000300305061796d656e74416464656400040034436f6e666967557064617465640005000038082c76617261747562655f696f144572726f72000124604163636f756e74416c726561647952656769737465726564000000844572726f72496e53656e64696e674d7367546f5472616e73666572546f6b656e73000100784572726f72496e526563656976696e675265706c7946726f6d546f6b656e000200704572726f72447572696e6753656e64696e6744656c617965644d73670003004c4163636f756e74446f65734e6f7445786973740004003857726f6e674d7367536f7572636500050064556e726567697374657265645061796d656e744d6574686f6400060060537562736372697074696f6e49734e6f7450656e64696e67000700204e6f7441646d696e000800003c082c76617261747562655f696f28537461746551756572790001101841646d696e730000002843757272656e636965730001002c537562736372696265727300020018436f6e6669670003000040082c76617261747562655f696f2853746174655265706c790001101841646d696e7304004401305665633c4163746f7249643e0000002843757272656e63696573040048016042547265654d61703c4163746f7249642c2050726963653e0001002c5375627363726962657273040054018442547265654d61703c4163746f7249642c2053756273637269626572446174613e00020018436f6e6669670400000118436f6e6669670003000044000002100048042042547265654d617008044b0110045601240004004c0000004c0000025000500000040810240054042042547265654d617008044b0110045601580004006400000058082c76617261747562655f696f385375627363726962657244617461000010012c63757272656e63795f696410011c4163746f724964000118706572696f641c0118506572696f64000148737562736372697074696f6e5f73746172745c01484f7074696f6e3c287536342c20753332293e00013072656e6577616c5f646174655c01484f7074696f6e3c287536342c20753332293e00005c04184f7074696f6e04045401600108104e6f6e6500000010536f6d65040060000001000060000004080408006400000268006800000408105800 \ No newline at end of file diff --git a/frontend/apps/varatube/src/atoms.ts b/frontend/apps/varatube/src/atoms.ts deleted file mode 100644 index eed4334a6..000000000 --- a/frontend/apps/varatube/src/atoms.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { atom } from 'jotai'; -import { FullSubState, State } from 'types'; - -const STATE_ATOM = atom(null); - -const IS_STATE_READ_ATOM = atom(false); - -const IS_AVAILABLE_BALANCE_READY = atom(false); - -const AVAILABLE_BALANCE = atom(undefined); - -export { STATE_ATOM, IS_STATE_READ_ATOM, IS_AVAILABLE_BALANCE_READY, AVAILABLE_BALANCE }; diff --git a/frontend/apps/varatube/src/components/layout/header/Header.tsx b/frontend/apps/varatube/src/components/layout/header/Header.tsx index 9bc6477dd..de68917bb 100644 --- a/frontend/apps/varatube/src/components/layout/header/Header.tsx +++ b/frontend/apps/varatube/src/components/layout/header/Header.tsx @@ -3,11 +3,11 @@ import clsx from 'clsx'; import { Link } from 'react-router-dom'; import { MenuHandler, Header as CommonHeader } from '@dapps-frontend/ui'; import logo from 'assets/images/logo.png'; -import { useFTBalance } from 'hooks/api'; import styles from './Header.module.scss'; +import { useBalanceOfQuery } from 'app/utils'; function Header() { - const tokens = useFTBalance(); + const { balance } = useBalanceOfQuery(); return ( - {tokens && ( + {balance && (

      - Tokens: {tokens} + Tokens: {String(balance)}

      )} diff --git a/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx b/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx index f4f442be3..02f8fd93b 100644 --- a/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx +++ b/frontend/apps/varatube/src/components/layout/on-login/OnLogin.tsx @@ -1,6 +1,5 @@ -import { useAccount } from '@gear-js/react-hooks'; +import { useGetSubscriberQuery } from 'app/utils/sails/queries'; import { Loader } from 'components'; -import { useProgramState } from 'hooks/api'; import { ReactNode } from 'react'; @@ -9,19 +8,9 @@ type Props = { }; function OnLogin({ children }: Props) { - const { account } = useAccount(); - const { decodedAddress } = account || {}; + const { subscriber, isFetched } = useGetSubscriberQuery(); - const { subscriptionsState, isSubscriptionsStateRead } = useProgramState(); - const subscription = subscriptionsState && decodedAddress ? subscriptionsState[decodedAddress] : undefined; - - return isSubscriptionsStateRead ? ( - <> - {subscription && children} {!subscription &&

      } - - ) : ( - - ); + return !isFetched ? <>{subscriber ? children :

      } : ; } export { OnLogin }; diff --git a/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx b/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx index 3e732e21e..ae8d65108 100644 --- a/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx +++ b/frontend/apps/varatube/src/components/modals/purchase-subscription-modal/PurchaseSubscriptionModal.tsx @@ -3,11 +3,10 @@ import { useForm as useMantineForm } from '@mantine/form'; import { UseFormInput } from '@mantine/form/lib/use-form'; import { ChangeEvent } from 'react'; import styles from './PurchaseSubscriptionModal.module.scss'; -import { periods } from 'consts'; +import { initialValues, periods } from 'consts'; +import { FormValues } from 'types'; -const initialValues = { isRenewal: true, period: periods[0].value }; - -type Props = { disabledSubmitButton: boolean; close: () => void; onSubmit: (values: typeof initialValues) => void }; +type Props = { disabledSubmitButton: boolean; close: () => void; onSubmit: (values: FormValues) => void }; const useForm = (input: UseFormInput>) => { const form = useMantineForm(input); diff --git a/frontend/apps/varatube/src/consts.ts b/frontend/apps/varatube/src/consts.ts index 86e37f9ea..5befaf27a 100644 --- a/frontend/apps/varatube/src/consts.ts +++ b/frontend/apps/varatube/src/consts.ts @@ -20,6 +20,8 @@ const periods = [ { label: '1 month', value: 'Month', rate: 1 }, ]; +const initialValues = { isRenewal: true, period: periods[0].value }; + const VOUCHER_MIN_LIMIT = 18; -export { ADDRESS, LOCAL_STORAGE, periods, VOUCHER_MIN_LIMIT }; +export { ADDRESS, LOCAL_STORAGE, periods, initialValues, VOUCHER_MIN_LIMIT }; diff --git a/frontend/apps/varatube/src/hooks/api.ts b/frontend/apps/varatube/src/hooks/api.ts deleted file mode 100644 index 7635fccfd..000000000 --- a/frontend/apps/varatube/src/hooks/api.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useAtom } from 'jotai'; -import { useAccount, useApi, useReadFullState, useSendMessage, useSendMessageWithGas } from '@gear-js/react-hooks'; -import { HexString } from '@polkadot/util/types'; -import varatubeMeta from 'assets/state/varatube_meta.txt'; -import ftMeta from 'assets/state/ft_meta.txt'; -import { ADDRESS } from 'consts'; -import { useProgramMetadata } from './metadata'; -import { useCallback, useEffect } from 'react'; -import { IS_STATE_READ_ATOM, STATE_ATOM } from 'atoms'; -import { FullSubState } from 'types'; - -function useSubscriptionMeta() { - return useProgramMetadata(varatubeMeta); -} - -function useFTMeta() { - return useProgramMetadata(ftMeta); -} - -function useSubscriptionsMessage() { - const metadata = useSubscriptionMeta(); - - return useSendMessage(ADDRESS.CONTRACT, metadata); -} - -function useFTMessage() { - const metadata = useFTMeta(); - - return useSendMessageWithGas(ADDRESS.FT_CONTRACT, metadata, { isMaxGasLimit: true }); -} - -type FTState = { balances: [[HexString, string]] }; - -function useFTBalance() { - const { account } = useAccount(); - const { decodedAddress } = account || {}; - - const meta = useProgramMetadata(ftMeta); - const { state } = useReadFullState(ADDRESS.FT_CONTRACT, meta, '0x'); - - const balances = state?.balances; - const userBalanceEntity = balances?.find(([address]) => address === decodedAddress); - const [, balance] = userBalanceEntity || []; - - return balance; -} - -function useProgramState() { - const meta = useSubscriptionMeta(); - const { api } = useApi(); - const programId = ADDRESS.CONTRACT; - const [data, setData] = useAtom(STATE_ATOM); - const [isStateRead, setIsStateRead] = useAtom(IS_STATE_READ_ATOM); - - const triggerState = useCallback(() => { - if (!api || !meta || !programId) return; - - const payload = { - Subscribers: null, - }; - - api.programState - .read({ programId, payload }, meta) - .then((codec) => codec.toHuman()) - .then((state: any) => { - setData(state); - setIsStateRead(true); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, meta, programId, setData]); - - useEffect(() => { - if (!isStateRead) { - triggerState(); - } - }, [isStateRead, triggerState]); - - const state = { - subscriptionsState: data?.Subscribers || null, - isSubscriptionsStateRead: isStateRead, - updateState: triggerState, - }; - - return state; -} - -export { useSubscriptionsMessage, useFTBalance, useFTMessage, useProgramState }; diff --git a/frontend/apps/varatube/src/hooks/index.ts b/frontend/apps/varatube/src/hooks/index.ts index 66c40f5b4..ed1dd62f8 100644 --- a/frontend/apps/varatube/src/hooks/index.ts +++ b/frontend/apps/varatube/src/hooks/index.ts @@ -1,26 +1,20 @@ -import { useAccount } from '@gear-js/react-hooks'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useProgramState, useSubscriptionsMessage } from './api'; +import { useGetSubscriberQuery } from 'app/utils'; function useSubscription() { const navigate = useNavigate(); - const { account } = useAccount(); - const { decodedAddress } = account || {}; - - const { subscriptionsState, isSubscriptionsStateRead } = useProgramState(); - - const subscription = subscriptionsState && decodedAddress ? subscriptionsState[decodedAddress] : undefined; + const { subscriber, isFetched } = useGetSubscriberQuery(); useEffect(() => { - if (isSubscriptionsStateRead && !subscription) { + if (isFetched && !subscriber) { navigate('/subscription'); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSubscriptionsStateRead, subscription, account]); + }, [isFetched, subscriber]); - return isSubscriptionsStateRead; + return Boolean(subscriber); } -export { useSubscriptionsMessage, useSubscription }; +export { useSubscription }; diff --git a/frontend/apps/varatube/src/hooks/metadata.ts b/frontend/apps/varatube/src/hooks/metadata.ts deleted file mode 100644 index 183cea697..000000000 --- a/frontend/apps/varatube/src/hooks/metadata.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useAlert } from '@gear-js/react-hooks'; -import { getStateMetadata, ProgramMetadata, StateMetadata } from '@gear-js/api'; -import { HexString } from '@polkadot/util/types'; - -function useProgramMetadata(source: string) { - const alert = useAlert(); - - const [metadata, setMetadata] = useState(); - - useEffect(() => { - fetch(source) - .then((response) => response.text()) - .then((raw) => `0x${raw}` as HexString) - .then((metaHex) => ProgramMetadata.from(metaHex)) - .then((result) => setMetadata(result)) - .catch(({ message }: Error) => alert.error(message)); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return metadata; -} - -function useStateMetadata(wasm: Buffer | undefined) { - const alert = useAlert(); - - const [stateMetadata, setStateMetadata] = useState(); - - useEffect(() => { - if (!wasm) return; - - getStateMetadata(wasm) - .then((result) => setStateMetadata(result)) - .catch(({ message }: Error) => alert.error(message)); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [wasm]); - - return stateMetadata; -} - -export { useProgramMetadata, useStateMetadata }; diff --git a/frontend/apps/varatube/src/hooks/use-sign-and-send.ts b/frontend/apps/varatube/src/hooks/use-sign-and-send.ts new file mode 100644 index 000000000..1719b7df9 --- /dev/null +++ b/frontend/apps/varatube/src/hooks/use-sign-and-send.ts @@ -0,0 +1,40 @@ +import { useCheckBalance } from '@dapps-frontend/hooks'; +import { useAlert } from '@gear-js/react-hooks'; +import { GenericTransactionReturn, TransactionReturn } from '@gear-js/react-hooks/dist/esm/hooks/sails/types'; + +export type Options = { + onSuccess?: (result: T) => void; + onError?: () => void; +}; + +export const useSignAndSend = () => { + const { checkBalance } = useCheckBalance(); + const alert = useAlert(); + + const signAndSend = async ( + transaction: TransactionReturn<() => GenericTransactionReturn>, + options?: Options, + ) => { + const { onSuccess, onError } = options || {}; + const calculatedGas = Number(transaction.extrinsic.args[2].toString()); + checkBalance( + calculatedGas, + async () => { + try { + const { response } = await transaction.signAndSend(); + const result = await response(); + onSuccess?.(result); + } catch (e) { + onError?.(); + console.error(e); + if (typeof e === 'string') { + alert.error(e); + } + } + }, + onError, + ); + }; + + return { signAndSend }; +}; diff --git a/frontend/apps/varatube/src/pages/subscription/Subscription.tsx b/frontend/apps/varatube/src/pages/subscription/Subscription.tsx index 5cd7caf60..21ca3656d 100644 --- a/frontend/apps/varatube/src/pages/subscription/Subscription.tsx +++ b/frontend/apps/varatube/src/pages/subscription/Subscription.tsx @@ -1,43 +1,44 @@ -import { useAccount, useAlert, useApi, useBalanceFormat, withoutCommas } from '@gear-js/react-hooks'; +import { useAlert, useApi } from '@gear-js/react-hooks'; import { Button, checkboxStyles } from '@gear-js/ui'; import { useState } from 'react'; import { Heading, Loader, PurchaseSubscriptionModal } from 'components'; -import { useSubscriptionsMessage } from 'hooks'; -import { useHandleCalculateGas, useCheckBalance } from '@dapps-frontend/hooks'; -import varatubeMeta from 'assets/state/varatube_meta.txt'; import pic from 'assets/images/pic.png'; import clsx from 'clsx'; import { ADDRESS, periods } from 'consts'; import styles from './Subscription.module.scss'; import { PurchaseSubscriptionApproveModal } from 'components/modals/purchase-subscription-approve-modal'; -import { InitialValues } from 'types'; -import { useFTBalance, useFTMessage, useProgramState } from 'hooks/api'; -import { useProgramMetadata } from 'hooks/metadata'; +import { FormValues } from 'types'; +import { + useBalanceOfQuery, + useCancelSubscriptionMessage, + useGetSubscriberQuery, + useRegisterSubscriptionMessage, +} from 'app/utils'; +import { useApproveMessage } from 'app/utils/sails/messages/use-approve-message'; +import { Period } from 'app/utils/sails/varatube'; +import { useCurrenciesQuery } from 'app/utils/sails/queries/use-currencies-query'; function Subscription() { - const amount = 10000; - const { account } = useAccount(); + const { currencies } = useCurrenciesQuery(); + const amount = currencies?.[0][1] ? Number(currencies[0][1]) : null; + const alert = useAlert(); const { api } = useApi(); - const tokens = useFTBalance(); - const { decodedAddress } = account || {}; - const [valuesToTransfer, setValuesToTransfer] = useState(null); - const { subscriptionsState, isSubscriptionsStateRead, updateState } = useProgramState(); - const varatubeMetadata = useProgramMetadata(varatubeMeta); - const calculateGas = useHandleCalculateGas(ADDRESS.CONTRACT, varatubeMetadata); - const { checkBalance } = useCheckBalance(); - const subscription = subscriptionsState && decodedAddress ? subscriptionsState[decodedAddress] : undefined; + const { balance, refetch: refetchBalance } = useBalanceOfQuery(); + const [valuesToTransfer, setValuesToTransfer] = useState(null); + + const { subscriber, isFetching, refetch } = useGetSubscriberQuery(); + const [isSubscribing, setIsSubscribing] = useState(false); - const { period, price, willRenew, subscriptionStart, subscriptionEnd } = subscription || {}; - const [startDateTimestamp] = subscriptionStart || []; - const [endDateTimestamp] = subscriptionEnd || []; + const { period, start_date, end_date, will_renew } = subscriber || {}; - const startDate = startDateTimestamp ? new Date(+withoutCommas(startDateTimestamp)).toLocaleString() : ''; - const endDate = endDateTimestamp ? new Date(+withoutCommas(endDateTimestamp)).toLocaleString() : ''; + const startDate = start_date ? new Date(Number(start_date)).toLocaleString() : ''; + const endDate = end_date ? new Date(Number(end_date)).toLocaleString() : ''; - const sendMessage = useSubscriptionsMessage(); - const sendFTMessage = useFTMessage(); + const { registerSubscriptionMessage } = useRegisterSubscriptionMessage(); + const { cancelSubscriptionMessage } = useCancelSubscriptionMessage(); + const { approveMessage } = useApproveMessage(); const [isModalOpen, setIsModalOpen] = useState(false); const [isApproveModalOpen, setIsApproveModalOpen] = useState(false); @@ -49,29 +50,15 @@ function Subscription() { const openApproveModal = () => setIsApproveModalOpen(true); const cancelSubscription = () => { - const payload = { CancelSubscription: null }; - - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - - checkBalance(gasLimit, () => { - sendMessage({ - payload, - gasLimit, - onSuccess: () => { - updateState(); - alert.success('Unsubscribed successfully'); - }, - }); - }); - }) - .catch((error) => { - console.log(error); + cancelSubscriptionMessage({ + onSuccess: () => { + refetch(); + alert.success('Unsubscribed successfully'); + }, + onError: () => { alert.error('Gas calculation error'); - }); + }, + }); }; const clearValues = () => { @@ -79,42 +66,31 @@ function Subscription() { }; const findSelectedPeriodRate = (period: string) => periods.find((item) => item.value === period)?.rate || 1; + const price = period && amount ? String(findSelectedPeriodRate(period) * amount) : null; const purchaseSubscription = () => { if (valuesToTransfer) { - const payload = { - RegisterSubscription: { + registerSubscriptionMessage( + { currency_id: ADDRESS.FT_CONTRACT, - period: { [valuesToTransfer.period]: null }, + period: valuesToTransfer.period as Period, with_renewal: valuesToTransfer.isRenewal, }, - }; - - calculateGas(payload) - .then((res) => res.toHuman()) - .then(({ min_limit }) => { - const minLimit = withoutCommas(min_limit as string); - const gasLimit = Math.floor(Number(minLimit) + Number(minLimit) * 0.2); - - checkBalance(gasLimit, () => { - sendMessage({ - payload, - gasLimit, - onSuccess: () => { - closeModal(); - clearValues(); - updateState(); - setIsSubscribing(false); - alert.success('Subscribed successfully'); - }, - }); - }); - }) - .catch((error) => { - console.log(error); - alert.error('Gas calculation error'); - setIsSubscribing(false); - }); + { + onSuccess: () => { + closeModal(); + clearValues(); + setIsSubscribing(false); + alert.success('Subscribed successfully'); + refetch(); + refetchBalance(); + }, + onError: () => { + alert.error('Gas calculation error'); + setIsSubscribing(false); + }, + }, + ); } }; @@ -128,7 +104,7 @@ function Subscription() { return; } - if (!tokens || Number(withoutCommas(tokens)) < findSelectedPeriodRate(valuesToTransfer?.period) * amount) { + if (!amount || !balance || Number(balance) < findSelectedPeriodRate(valuesToTransfer?.period) * amount) { alert.error(`You don't have enough tokens to subscribe`); clearValues(); closeApproveModal(); @@ -138,17 +114,16 @@ function Subscription() { setIsSubscribing(true); - checkBalance(api?.blockGasLimit.toNumber(), () => { - const amountToTransfer = findSelectedPeriodRate(valuesToTransfer.period) * amount; - if (amountToTransfer) - sendFTMessage({ - payload: { - Approve: { - to: ADDRESS.CONTRACT, - amount: String(findSelectedPeriodRate(valuesToTransfer.period) * amount), - }, - }, + const amountToTransfer = findSelectedPeriodRate(valuesToTransfer.period) * amount; + if (amountToTransfer) { + approveMessage( + { + spender: ADDRESS.CONTRACT, + value: amountToTransfer, + }, + { onSuccess: () => { + console.log('onSuccess'); closeApproveModal(); purchaseSubscription(); }, @@ -159,18 +134,19 @@ function Subscription() { setIsSubscribing(false); alert.error('Some error has occured'); }, - }); - }); + }, + ); + } }; return ( <> - {isSubscriptionsStateRead ? ( + {!isFetching && amount ? ( <>

      - {subscription ? ( + {subscriber ? ( <>
        @@ -187,9 +163,9 @@ function Subscription() { Period: {period} - {price && ( + {!!price && (
      • - Price: {price} + Price: {String(price)}
      • )} @@ -198,7 +174,7 @@ function Subscription() { @@ -207,7 +183,7 @@ function Subscription() {
      -
      + } + className={{ header: styles.header, content: styles.header__container }} + menu={} + /> + ); +} diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/header/index.ts new file mode 100644 index 000000000..ddd972315 --- /dev/null +++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/index.ts @@ -0,0 +1 @@ +export { Header } from './header'; diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/index.ts new file mode 100644 index 000000000..cfdf7a76b --- /dev/null +++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/index.ts @@ -0,0 +1 @@ +export { Logo } from './logo'; diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.module.scss b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.module.scss new file mode 100644 index 000000000..b9def072c --- /dev/null +++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.module.scss @@ -0,0 +1,43 @@ +.link { + display: inline-flex; + transition: opacity 300ms ease; + + @media screen and (max-width: 767px) { + position: relative; + } + + &:not(.active):hover { + opacity: 0.7; + } + + .title { + --gradient-to: #0ed3a3; + + align-self: flex-start; + margin-top: -4px; + font-size: 20px; + line-height: 24px; + white-space: nowrap; + user-select: none; + + @media screen and (max-width: 767px) { + display: none; + position: absolute; + left: 100%; + font-size: 10px; + line-height: 18px; + } + } +} + +.logo { + width: 100%; + height: 100%; + max-height: 60px; + max-width: 92px; + + @media screen and (max-width: 767px) { + max-height: 40px; + max-width: 62px; + } +} diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.tsx new file mode 100644 index 000000000..44e54e9dc --- /dev/null +++ b/frontend/apps/web3-warriors-battle/src/components/layout/header/logo/logo.tsx @@ -0,0 +1,20 @@ +import { NavLink } from 'react-router-dom'; +import clsx from 'clsx'; +import styles from './logo.module.scss'; +import { ROUTES } from '@/app/consts'; +import { TextGradient } from '@/components/ui/text-gradient'; +import { Sprite } from '@/components/ui/sprite'; +import type { BaseComponentProps } from '@/app/types'; + +type LogoProps = BaseComponentProps & { + label?: string; +}; + +export function Logo({ className, label }: LogoProps) { + return ( + clsx(styles.link, isActive && styles.active, className)}> + + {label && {label}} + + ); +} diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/index.ts b/frontend/apps/web3-warriors-battle/src/components/layout/index.ts new file mode 100644 index 000000000..89f86633f --- /dev/null +++ b/frontend/apps/web3-warriors-battle/src/components/layout/index.ts @@ -0,0 +1,6 @@ +export { Header } from './header'; +export { MainLayout } from './main-layout'; +export { NotFound } from './not-found'; +export { NotAuthorized } from './not-authorized'; +export { VaraIcon } from './vara-svg'; +export { GameDetails } from './game-details'; diff --git a/frontend/apps/web3-warriors-battle/src/components/layout/main-layout.tsx b/frontend/apps/web3-warriors-battle/src/components/layout/main-layout.tsx new file mode 100644 index 000000000..4fa43d4a7 --- /dev/null +++ b/frontend/apps/web3-warriors-battle/src/components/layout/main-layout.tsx @@ -0,0 +1,29 @@ +import { PropsWithChildren } from 'react'; +import { Footer } from '@dapps-frontend/ui'; +import { ApiLoader, Header } from '@/components'; +import { useIsAppReady, useIsAppReadySync } from '@/app/hooks/use-is-app-ready'; +import { useAuthSync } from '@/features/auth/hooks'; +import { Container } from '../ui/container'; + +type MainLayoutProps = PropsWithChildren; + +export function MainLayout({ children }: MainLayoutProps) { + const { isAppReady } = useIsAppReady(); + + useIsAppReadySync(); + useAuthSync(); + + return ( + <> +
      +
      + {!isAppReady && } + {isAppReady && children} +
      + + +