diff --git a/Cargo.lock b/Cargo.lock index c3bbfed1..afd985ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2027,6 +2027,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "histogram" version = "0.6.9" @@ -3509,48 +3515,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "race-example-draw-card" -version = "0.1.0" -dependencies = [ - "anyhow", - "arrayref", - "borsh", - "race-api", - "race-proc-macro", - "race-test", -] - -[[package]] -name = "race-example-minimal" -version = "0.1.0" -dependencies = [ - "borsh", - "race-api", - "race-proc-macro", - "race-test", -] - -[[package]] -name = "race-example-raffle" -version = "0.1.0" -dependencies = [ - "borsh", - "race-api", - "race-proc-macro", - "race-test", -] - -[[package]] -name = "race-example-simple-settle" -version = "0.1.0" -dependencies = [ - "borsh", - "race-api", - "race-proc-macro", - "race-test", -] - [[package]] name = "race-facade" version = "0.2.6" @@ -3655,6 +3619,7 @@ dependencies = [ "race-transport", "serde", "serde_json", + "sha256", "thiserror", "tokio", "tokio-stream", @@ -4380,6 +4345,19 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2 0.10.6", + "tokio", +] + [[package]] name = "sha3" version = "0.9.1" @@ -5596,11 +5574,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", diff --git a/Cargo.toml b/Cargo.toml index 152cb7e9..0619e68a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,10 @@ members = [ "env", "client", "test", - "examples/draw-card", - "examples/simple-settle", - "examples/minimal", - "examples/raffle", + # "examples/draw-card", + # "examples/simple-settle", + # "examples/minimal", + # "examples/raffle", # "examples/blackjack", # "examples/roshambo", # "examples/chat", @@ -72,6 +72,7 @@ reqwest = "0.11.16" openssl = { version = "^0.10" } prettytable-rs = "^0.10" sha1 = { version = "0.10.5", default-features = false, features = ["oid"] } +sha256 = "1.5.0" aes = "0.8.2" ctr = "0.9.2" chrono = "0.4.24" diff --git a/api/src/effect.rs b/api/src/effect.rs index 178d4f88..38f962f7 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -8,6 +8,7 @@ use crate::{ engine::GameHandler, error::{Error, HandleError, Result}, event::BridgeEvent, + prelude::InitAccount, random::RandomSpec, types::{DecisionId, GamePlayer, RandomId, Settle, Transfer}, }; @@ -42,12 +43,10 @@ pub struct ActionTimeout { } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] -pub struct LaunchSubGame { +pub struct SubGame { pub id: usize, pub bundle_addr: String, - pub players: Vec, - pub init_data: Vec, - pub checkpoint: Vec, + pub init_account: InitAccount, } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] @@ -62,10 +61,11 @@ pub struct SubGameLeave { pub player_ids: Vec, } -impl LaunchSubGame { +impl SubGame { pub fn try_new( id: usize, bundle_addr: String, + max_players: u16, players: Vec, init_data: S, checkpoint: S, @@ -73,9 +73,13 @@ impl LaunchSubGame { Ok(Self { id, bundle_addr, - players, - init_data: init_data.try_to_vec()?, - checkpoint: checkpoint.try_to_vec()?, + init_account: InitAccount { + max_players, + entry_type: crate::types::EntryType::Disabled, + players, + data: init_data.try_to_vec()?, + checkpoint: checkpoint.try_to_vec()?, + }, }) } } @@ -211,7 +215,7 @@ pub struct Effect { pub error: Option, pub allow_exit: bool, pub transfers: Vec, - pub launch_sub_games: Vec, + pub launch_sub_games: Vec, pub bridge_events: Vec, } @@ -314,8 +318,13 @@ impl Effect { self.allow_exit = allow_exit } - pub fn checkpoint(&mut self) { - self.is_checkpoint = true; + /// Set checkpoint can trigger settlements. + pub fn checkpoint(&mut self, checkpoint_state: S) { + if let Ok(checkpoint) = checkpoint_state.try_to_vec() { + self.checkpoint = Some(checkpoint); + } else { + self.error = Some(HandleError::SerializationError) + } } /// Submit settlements. @@ -333,16 +342,21 @@ impl Effect { &mut self, id: usize, bundle_addr: String, + max_players: u16, players: Vec, init_data: D, checkpoint: C, ) -> Result<()> { - self.launch_sub_games.push(LaunchSubGame { + self.launch_sub_games.push(SubGame { id, bundle_addr, - players, - init_data: init_data.try_to_vec()?, - checkpoint: checkpoint.try_to_vec()?, + init_account: InitAccount { + max_players, + entry_type: crate::types::EntryType::Disabled, + players, + data: init_data.try_to_vec()?, + checkpoint: checkpoint.try_to_vec()?, + }, }); Ok(()) } @@ -368,17 +382,6 @@ impl Effect { } } - /// Set checkpoint. - /// - /// This is an internal function, DO NOT use in game handler. - pub fn __set_checkpoint(&mut self, checkpoint_state: S) { - if let Ok(state) = checkpoint_state.try_to_vec() { - self.checkpoint = Some(state); - } else { - self.error = Some(HandleError::SerializationError); - } - } - pub fn __set_checkpoint_raw(&mut self, raw: Vec) { self.checkpoint = Some(raw); } diff --git a/api/src/engine.rs b/api/src/engine.rs index 8288b97c..e788a9ad 100644 --- a/api/src/engine.rs +++ b/api/src/engine.rs @@ -8,19 +8,14 @@ use crate::{ types::EntryType, }; -/// A subset of on-chain account, used for game handler -/// initialization. The `access_version` may refer to an old state -/// when the game is started by transactor. -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +/// A set of arguments for game handler initialization. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] pub struct InitAccount { - pub addr: String, + pub max_players: u16, + pub entry_type: EntryType, pub players: Vec, pub data: Vec, - pub access_version: u64, - pub settle_version: u64, - pub max_players: u16, pub checkpoint: Vec, - pub entry_type: EntryType, } impl InitAccount { @@ -42,7 +37,6 @@ impl InitAccount { /// This function will panic when a duplicated position is /// specified. pub fn add_player(&mut self, id: u64, position: usize, balance: u64) { - self.access_version += 1; if self.players.iter().any(|p| p.position as usize == position) { panic!("Failed to add player, duplicated position"); } @@ -57,30 +51,20 @@ impl InitAccount { impl Default for InitAccount { fn default() -> Self { Self { - addr: "".into(), + max_players: 0, + entry_type: EntryType::Disabled, players: Vec::new(), data: Vec::new(), - access_version: 0, - settle_version: 0, - max_players: 10, checkpoint: Vec::new(), - entry_type: EntryType::Cash { - min_deposit: 100, - max_deposit: 200, - }, } } } pub trait GameHandler: Sized + BorshSerialize + BorshDeserialize { - type Checkpoint: BorshSerialize + BorshDeserialize; - - /// Initialize handler state with on-chain game account data. + /// Initialize handler state with on-chain game account data. The + /// initial state must be determined by the `init_account`. fn init_state(effect: &mut Effect, init_account: InitAccount) -> HandleResult; /// Handle event. fn handle_event(&mut self, effect: &mut Effect, event: Event) -> HandleResult<()>; - - /// Create checkpoint from current state. - fn into_checkpoint(self) -> HandleResult; } diff --git a/api/src/error.rs b/api/src/error.rs index 2f11f024..18033e8a 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -275,6 +275,9 @@ pub enum Error { #[error("Cannot bump settle version")] CantBumpSettleVersion, + + #[error("Game uninitialized")] + GameUninitialized, } #[cfg(feature = "serde")] diff --git a/api/src/types/common.rs b/api/src/types/common.rs index d9faa93c..49f69b3d 100644 --- a/api/src/types/common.rs +++ b/api/src/types/common.rs @@ -246,6 +246,8 @@ pub enum EntryType { /// A player can join the game by showing a gate NFT #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] Gating { collection: String }, + #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] + Disabled, } impl Default for EntryType { diff --git a/cli/src/main.rs b/cli/src/main.rs index 203e6b27..0a9a14b1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -456,6 +456,7 @@ async fn main() { println!("Interact with chain: {:?}", chain); println!("RPC Endpoint: {:?}", rpc); + println!("Specified keyfile: {:?}", keyfile); match matches.subcommand() { Some(("publish", sub_matches)) => { diff --git a/core/src/context.rs b/core/src/context.rs index 6333cda1..0b93c769 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -3,15 +3,14 @@ use std::collections::HashMap; use crate::types::{ClientMode, GameAccount, SettleWithAddr, SubGameSpec}; use borsh::{BorshDeserialize, BorshSerialize}; use race_api::decision::DecisionState; -use race_api::effect::{Ask, Assign, Effect, EmitBridgeEvent, LaunchSubGame, Release, Reveal}; +use race_api::effect::{Ask, Assign, Effect, EmitBridgeEvent, Release, Reveal, SubGame}; use race_api::engine::GameHandler; use race_api::error::{Error, Result}; use race_api::event::{CustomEvent, Event}; -use race_api::prelude::BridgeEvent; use race_api::random::{RandomSpec, RandomState, RandomStatus}; use race_api::types::{ - Addr, Ciphertext, DecisionId, GameStatus, RandomId, SecretDigest, SecretShare, Settle, - SettleOp, Transfer, + Addr, Ciphertext, DecisionId, GamePlayer, GameStatus, RandomId, SecretDigest, SecretShare, + SettleOp, Transfer, EntryType, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -93,7 +92,7 @@ pub struct EventEffects { pub settles: Vec, pub transfers: Vec, pub checkpoint: Option>, - pub launch_sub_games: Vec, + pub launch_sub_games: Vec, pub bridge_events: Vec, pub start_game: bool, } @@ -144,38 +143,41 @@ pub struct GameContext { /// All runtime decision states, each stores the answer. pub(crate) decision_states: Vec, /// Settles, if is not None, will be handled by event loop. - pub(crate) settles: Option>, - /// Transfers, if is not None, will be handled by event loop. - pub(crate) transfers: Option>, - /// The latest checkpoint state pub(crate) checkpoint: Option>, /// The sub games to launch - pub(crate) launch_sub_games: Vec, - /// The bridge events to emit - pub(crate) bridge_events: Vec, - /// Start a new game - pub(crate) start_game: bool, + pub(crate) sub_games: Vec, /// Next settle version to use when we bump. It defaults to /// current + 1 for a normal game, and is decided by the parent game /// for a sub game. pub(crate) next_settle_version: u64, + /// Init data from `InitAccount`. + pub(crate) init_data: Option>, + /// Maximum number of players. + pub(crate) max_players: u16, + /// Game Players + pub(crate) players: Vec, + pub(crate) entry_type: EntryType, } impl GameContext { - pub fn try_new_with_sub_game_spec(spec: SubGameSpec) -> Result { + pub fn try_new_with_sub_game_spec(spec: &SubGameSpec) -> Result { let SubGameSpec { game_addr, nodes, sub_id, + init_account, .. } = spec; Ok(Self { game_addr: format!("{}:{}", game_addr, sub_id), - nodes, + nodes: nodes.clone(), settle_version: spec.settle_version, access_version: spec.access_version, next_settle_version: spec.settle_version + 1, + max_players: init_account.max_players, + players: init_account.players.clone(), + entry_type: EntryType::Disabled, ..Default::default() }) } @@ -202,6 +204,13 @@ impl GameContext { }) .collect(); + let players = game_account + .players + .iter() + .filter(|p| p.access_version <= game_account.access_version) + .map(|p| GamePlayer::new(p.access_version, p.balance, p.position)) + .collect(); + Ok(Self { game_addr: game_account.addr.clone(), access_version: game_account.access_version, @@ -213,14 +222,14 @@ impl GameContext { allow_exit: false, random_states: vec![], decision_states: vec![], - settles: None, - transfers: None, handler_state: "".into(), checkpoint: None, - launch_sub_games: vec![], - bridge_events: vec![], - start_game: false, + sub_games: vec![], next_settle_version: game_account.settle_version + 1, + init_data: None, + max_players: game_account.max_players, + players, + entry_type: game_account.entry_type.clone(), }) } @@ -328,11 +337,6 @@ impl GameContext { )); } - pub fn start_game(&mut self) { - self.random_states.clear(); - self.start_game = true; - } - pub fn shutdown_game(&mut self) { self.dispatch = Some(DispatchEvent::new(Event::Shutdown, 0)); } @@ -383,15 +387,15 @@ impl GameContext { self.dispatch = None; } - pub fn get_access_version(&self) -> u64 { + pub fn access_version(&self) -> u64 { self.access_version } - pub fn get_settle_version(&self) -> u64 { + pub fn settle_version(&self) -> u64 { self.settle_version } - pub fn get_next_settle_version(&self) -> u64 { + pub fn next_settle_version(&self) -> u64 { self.next_settle_version } @@ -631,26 +635,6 @@ impl GameContext { Ok(()) } - pub fn settle(&mut self, settles: Vec) { - self.settles = Some(settles); - } - - pub fn transfer(&mut self, transfers: Vec) { - self.transfers = Some(transfers); - } - - pub fn get_settles(&self) -> &Option> { - &self.settles - } - - pub fn get_bridge_events(&self) -> Result> { - self.bridge_events - .iter() - .cloned() - .map(|e| E::try_from_slice(&e.raw).map_err(|_e| Error::DeserializeError)) - .collect() - } - pub fn bump_settle_version(&mut self) -> Result<()> { if self.next_settle_version <= self.settle_version { return Err(Error::CantBumpSettleVersion); @@ -664,53 +648,6 @@ impl GameContext { self.next_settle_version = u64::max(next_settle_version, self.settle_version + 1); } - pub fn take_event_effects(&mut self) -> Result { - let mut settles = vec![]; - let mut transfers = vec![]; - - if self.checkpoint.is_some() { - if let Some(ss) = self.settles.take() { - for s in ss { - let addr = self.id_to_addr(s.id)?; - settles.push(SettleWithAddr { addr, op: s.op }); - } - } - - settles.sort_by_key(|s| match s.op { - SettleOp::Add(_) => 0, - SettleOp::Sub(_) => 1, - SettleOp::Eject => 2, - SettleOp::AssignSlot(_) => 3, - }); - - if let Some(mut t) = self.transfers.take() { - transfers.append(&mut t); - } - self.bump_settle_version()?; - } - - let launch_sub_games = self.launch_sub_games.drain(..).collect(); - - let bridge_events = self.bridge_events.drain(..).collect(); - - Ok(EventEffects { - settles, - transfers, - checkpoint: self.get_checkpoint(), - launch_sub_games, - bridge_events, - start_game: self.start_game, - }) - } - - pub fn add_settle(&mut self, settle: Settle) { - if let Some(ref mut settles) = self.settles { - settles.push(settle); - } else { - self.settles = Some(vec![settle]); - } - } - pub fn add_revealed_random( &mut self, random_id: RandomId, @@ -791,7 +728,7 @@ impl GameContext { } } - pub fn apply_effect(&mut self, effect: Effect) -> Result<()> { + pub fn apply_effect(&mut self, effect: Effect) -> Result { let Effect { action_timeout, wait_timeout, @@ -815,7 +752,8 @@ impl GameContext { // Handle dispatching if start_game { - self.start_game(); + self.random_states.clear(); + self.decision_states.clear(); } else if stop_game { self.shutdown_game(); } else if let Some(t) = action_timeout { @@ -855,24 +793,51 @@ impl GameContext { self.init_random_state(spec)?; } + let mut settles1 = Vec::with_capacity(settles.len()); if let Some(checkpoint_state) = checkpoint { self.checkpoint = Some(checkpoint_state); - self.settle(settles); - self.transfer(transfers); + self.bump_settle_version()?; + for s in settles { + match s.op { + SettleOp::Eject => { + self.remove_player(s.id)?; + } + SettleOp::Add(amount) => { + self.player_add_balance(s.id, amount)?; + } + SettleOp::Sub(amount) => { + self.player_sub_balance(s.id, amount)?; + } + _ => {} + } + let addr = self.id_to_addr(s.id)?; + settles1.push(SettleWithAddr { addr, op: s.op }); + } self.set_game_status(GameStatus::Idle); } else if (!settles.is_empty()) || (!transfers.is_empty()) { return Err(Error::SettleWithoutCheckpoint); } + settles1.sort_by_key(|s| match s.op { + SettleOp::Add(_) => 0, + SettleOp::Sub(_) => 1, + SettleOp::Eject => 2, + SettleOp::AssignSlot(_) => 3, + }); if let Some(state) = handler_state { self.handler_state = state; } - self.launch_sub_games = launch_sub_games; - - self.bridge_events = bridge_events; + self.sub_games = launch_sub_games.clone(); - Ok(()) + Ok(EventEffects { + settles: settles1, + transfers, + checkpoint: self.get_checkpoint(), + launch_sub_games, + bridge_events, + start_game, + }) } pub fn set_node_ready(&mut self, access_version: u64) { @@ -898,8 +863,65 @@ impl GameContext { pub fn prepare_for_next_event(&mut self, timestamp: u64) { self.set_timestamp(timestamp); self.checkpoint = None; - self.start_game = false; - self.bridge_events.clear(); + } + + pub fn set_init_data(&mut self, init_data: Vec) { + self.init_data.replace(init_data); + } + + pub fn init_data(&self) -> Result> { + if let Some(init_data) = &self.init_data { + Ok(init_data.clone()) + } else { + Err(Error::GameUninitialized) + } + } + + pub fn max_players(&self) -> u16 { + self.max_players + } + + pub fn players(&self) -> &[GamePlayer] { + &self.players + } + + pub fn add_player(&mut self, player: GamePlayer) { + self.players.push(player) + } + + pub fn player_sub_balance(&mut self, player_id: u64, amount: u64) -> Result<()> { + let mut p = self.players + .iter_mut() + .find(|p| p.id == player_id) + .ok_or(Error::PlayerNotInGame)?; + + p.balance = p.balance.checked_sub(amount).ok_or(Error::InvalidAmount)?; + + Ok(()) + } + + pub fn player_add_balance(&mut self, player_id: u64, amount: u64) -> Result<()> { + let mut p = self.players + .iter_mut() + .find(|p| p.id == player_id) + .ok_or(Error::PlayerNotInGame)?; + + p.balance = p.balance.checked_add(amount).ok_or(Error::InvalidAmount)?; + + Ok(()) + } + + pub fn remove_player(&mut self, player_id: u64) -> Result<()> { + let l = self.players.len(); + self.players.retain(|p| p.id != player_id); + if self.players.len() != l - 1 { + return Err(Error::PlayerNotInGame); + } + Ok(()) + } + + pub fn entry_type(&self) -> EntryType { + self.entry_type.clone() } } @@ -917,13 +939,13 @@ impl Default for GameContext { allow_exit: false, random_states: Vec::new(), decision_states: Vec::new(), - settles: None, - transfers: None, checkpoint: None, - launch_sub_games: Vec::new(), - bridge_events: Vec::new(), - start_game: false, + sub_games: Vec::new(), next_settle_version: 0, + init_data: None, + max_players: 0, + players: Vec::new(), + entry_type: EntryType::Disabled, } } } diff --git a/core/src/engine.rs b/core/src/engine.rs index c0b5956d..ce4d0bc8 100644 --- a/core/src/engine.rs +++ b/core/src/engine.rs @@ -20,8 +20,6 @@ pub fn general_handle_event( // General event handling match event { Event::Ready => { - // This is the first event, we make it a checkpoint - // context.checkpoint = true; Ok(()) } @@ -74,7 +72,12 @@ pub fn general_handle_event( Event::RandomnessReady { .. } => Ok(()), - Event::Join { .. } => Ok(()), + Event::Join { players } => { + for p in players { + context.add_player(p.to_owned()); + } + Ok(()) + } Event::Leave { .. } => { if !context.allow_exit { @@ -145,14 +148,6 @@ pub fn general_handle_event( } } -/// Context maintaining after event handling. -pub fn post_handle_event( - _old_context: &GameContext, - _new_context: &mut GameContext, -) -> Result<(), Error> { - Ok(()) -} - #[cfg(test)] mod tests { @@ -164,7 +159,7 @@ mod tests { fn test_handle_game_start() -> anyhow::Result<()> { let encryptor = DummyEncryptor::default(); let mut context = GameContext::default(); - let event = Event::GameStart { access_version: 1 }; + let event = Event::GameStart; general_handle_event(&mut context, &event, &encryptor)?; assert_eq!(context.status, GameStatus::Running); Ok(()) diff --git a/core/src/types/accounts.rs b/core/src/types/accounts.rs index eb6a86cb..80887625 100644 --- a/core/src/types/accounts.rs +++ b/core/src/types/accounts.rs @@ -146,53 +146,34 @@ pub struct GameAccount { impl GameAccount { pub fn derive_init_account(&self) -> InitAccount { - let game_account = self.to_owned(); InitAccount { - addr: game_account.addr, - players: game_account + max_players: self.max_players, + entry_type: self.entry_type.clone(), + players: self .players .iter() .cloned() .map(Into::into) .collect(), - data: game_account.data.clone(), - access_version: game_account.access_version, - settle_version: game_account.settle_version, - max_players: game_account.max_players, - checkpoint: game_account.checkpoint, - entry_type: game_account.entry_type, + data: self.data.clone(), + checkpoint: self.checkpoint.clone(), } } pub fn derive_checkpoint_init_account(&self) -> InitAccount { - let game_account = self.to_owned(); - let Self { - players, - addr, - data, - max_players, - checkpoint, - checkpoint_access_version, - settle_version, - entry_type, - .. - } = game_account; - - let players = players - .into_iter() - .filter(|p| p.access_version <= checkpoint_access_version) + let players = self.players + .iter() + .cloned() + .filter(|p| p.access_version <= self.checkpoint_access_version) .map(|p| p.into()) .collect(); InitAccount { - addr, + entry_type: self.entry_type.clone(), + max_players: self.max_players, players, - data, - access_version: checkpoint_access_version, - settle_version, - max_players, - checkpoint, - entry_type, + data: self.data.clone(), + checkpoint: self.checkpoint.clone(), } } } diff --git a/core/src/types/broadcast_frame.rs b/core/src/types/broadcast_frame.rs index afa53829..4ac3bd12 100644 --- a/core/src/types/broadcast_frame.rs +++ b/core/src/types/broadcast_frame.rs @@ -37,6 +37,7 @@ pub enum BroadcastFrame { event: Event, timestamp: u64, remain: u16, + state_sha: String, }, // Arbitrary message Message { diff --git a/core/src/types/common.rs b/core/src/types/common.rs index 7c456309..5fe193c2 100644 --- a/core/src/types/common.rs +++ b/core/src/types/common.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use race_api::prelude::InitAccount; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; pub use race_api::types::*; @@ -35,17 +36,14 @@ impl std::fmt::Display for Signature { } #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SubGameSpec { pub game_addr: String, pub sub_id: usize, - pub players: Vec, pub bundle_addr: String, - pub init_data: Vec, pub nodes: Vec, - pub checkpoint: Vec, pub access_version: u64, pub settle_version: u64, + pub init_account: InitAccount, } #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] diff --git a/core/src/types/transactor_params.rs b/core/src/types/transactor_params.rs index e387775f..681c7ca9 100644 --- a/core/src/types/transactor_params.rs +++ b/core/src/types/transactor_params.rs @@ -14,6 +14,10 @@ pub struct AttachGameParams { pub key: NodePublicKeyRaw, } +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct GetStateParams {} + impl Display for AttachGameParams { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "AttachGameParams") diff --git a/dev/scripts/create-mtt-game.sh b/dev/scripts/create-mtt-game.sh index 8f18a491..655cbb60 100644 --- a/dev/scripts/create-mtt-game.sh +++ b/dev/scripts/create-mtt-game.sh @@ -5,7 +5,7 @@ echo "Current timestamp is $START_TIME" DATA=$(cd ./js/borsh; npx ts-node ./bin/cli.ts \ -u64 "$START_TIME" \ -u8 2 \ - -u64 10 \ + -u64 500000 \ -u64 60000 \ -u32 0 \ -u32 3 \ @@ -23,8 +23,8 @@ JSON=$(cat < Result { + fn checkpoint(self) -> Result { Ok(Checkpoint {}) } } diff --git a/facade/src/main.rs b/facade/src/main.rs index c2a69078..f7551092 100644 --- a/facade/src/main.rs +++ b/facade/src/main.rs @@ -343,6 +343,8 @@ async fn join(params: Params<'_>, context: Arc>) -> RpcResult<()> EntryType::Ticket { slot_id, amount } => todo!(), #[allow(unused)] EntryType::Gating { collection } => todo!(), + #[allow(unused)] + EntryType::Disabled => todo!(), } } else { return Err(custom_error(Error::GameAccountNotFound)); @@ -865,6 +867,7 @@ fn cli() -> Command { #[tokio::main] async fn main() -> anyhow::Result<()> { + println!("Start at {}", HTTP_HOST); let matches = cli().get_matches(); let mut context = Context::default(); context.load_default_tokens(); diff --git a/js/borsh/src/index.ts b/js/borsh/src/index.ts index 6a14e316..648bdfc6 100644 --- a/js/borsh/src/index.ts +++ b/js/borsh/src/index.ts @@ -19,6 +19,36 @@ import { BinaryWriter } from './writer'; import { BinaryReader } from './reader'; import { invalidByteArrayLength, extendedWriterNotFound, extendedReaderNotFound, invalidEnumField } from './errors'; +class DeserializeError extends Error { + cause: Error; + path: string[]; + obj: any | undefined; + + constructor(path: string[], cause: Error, obj: any) { + super('Deserialize failed') + this.cause = cause; + this.path = path; + this.obj = obj; + Object.setPrototypeOf(this, DeserializeError.prototype) + } +} + +class SerializeError extends Error { + cause: Error; + path: string[]; + fieldType: FieldType; + value: any; + + constructor(path: string[], cause: Error, fieldType: FieldType, value: any) { + super('Serialize failed') + this.cause = cause; + this.path = path; + this.fieldType = fieldType; + this.value = value; + Object.setPrototypeOf(this, SerializeError.prototype) + } +} + function addSchemaField(prototype: any, key: FieldKey, fieldType: FieldType) { let fields: Field[] = prototype.__schema_fields || []; fields.push([key, fieldType]); @@ -115,8 +145,11 @@ function serializeValue(path: string[], value: any, fieldType: FieldType, writer } } } catch (err: any) { - console.error('Borsh serialize failed, path:', path, 'error:', err); - throw err; + if (err instanceof SerializeError) { + throw err; + } else { + throw new SerializeError(path, err, fieldType, value) + } } } @@ -189,8 +222,11 @@ function deserializeValue(path: string[], fieldType: FieldType, reader: BinaryRe } } } catch (err: any) { - console.error('Borsh deserialize failed, path:', path, 'error:', err); - throw err; + if (err instanceof DeserializeError) { + throw err + } else { + throw new DeserializeError(path, err, undefined) + } } } @@ -234,7 +270,9 @@ function deserializeEnum(path: string[], enumClass: Function, reader: BinaryRead if (enumVariants instanceof Array) { const i = reader.readU8(); const variant = enumVariants[i]; - return deserializeStruct([...path, ``], variant, reader); + let obj = deserializeStruct([...path, ``], variant, reader); + Object.setPrototypeOf(obj, variant.prototype); + return obj; } else { invalidEnumField(path); } @@ -244,8 +282,15 @@ function deserializeStruct(path: string[], ctor: Ctor, reader: BinaryReade const prototype = ctor.prototype; const fields = getSchemaFields(prototype); let obj = {}; - for (const field of fields) { - obj = deserializeField(path, obj, field, reader); + try { + for (const field of fields) { + obj = deserializeField(path, obj, field, reader); + } + } catch (e) { + if (e instanceof DeserializeError) { + e.obj = obj; + } + throw e; } return new ctor(obj); } @@ -288,10 +333,17 @@ export function enums(enumClass: Function): EnumFieldType { export function serialize(obj: any): Uint8Array { const writer = new BinaryWriter(); - if (isVariantObject(obj)) { - serializeEnum([], obj, writer); - } else { - serializeStruct([], obj, writer); + try { + if (isVariantObject(obj)) { + serializeEnum([], obj, writer); + } else { + serializeStruct([], obj, writer); + } + } catch (e) { + if (e instanceof SerializeError) { + console.error('Serialize failed, path:', e.path, ', fieldType:', e.fieldType, ', value:', e.value, ', cause:', e.cause); + } + throw e; } return writer.toArray(); } @@ -300,10 +352,17 @@ export function deserialize(enumClass: EnumClass, data: Uint8Array): T; export function deserialize(ctor: Ctor, data: Uint8Array): T; export function deserialize(classType: Ctor | EnumClass, data: Uint8Array): T { const reader = new BinaryReader(data); - if (isEnumClass(classType)) { - return deserializeEnum([], classType, reader); - } else { - return deserializeStruct([], classType, reader); + try { + if (isEnumClass(classType)) { + return deserializeEnum([], classType, reader); + } else { + return deserializeStruct([], classType, reader); + } + } catch (e) { + if (e instanceof DeserializeError) { + console.error('Deserialize failed, path:', e.path, ', current object:', e.obj, ', cause:', e.cause); + } + throw e; } } diff --git a/js/sdk-core/src/accounts.ts b/js/sdk-core/src/accounts.ts index ce0fb987..d8c0d29f 100644 --- a/js/sdk-core/src/accounts.ts +++ b/js/sdk-core/src/accounts.ts @@ -183,13 +183,18 @@ export type EntryTypeKind = | 'Invalid' | 'Cash' | 'Ticket' - | 'Gating'; + | 'Gating' + | 'Disabled'; export interface IEntryTypeKind { kind(): EntryTypeKind; } -export abstract class EntryType {} +export abstract class EntryType implements IEntryTypeKind { + kind(): EntryTypeKind { + return 'Invalid'; + } +} @variant(0) export class EntryTypeCash extends EntryType implements IEntryTypeKind { @@ -207,7 +212,7 @@ export class EntryTypeCash extends EntryType implements IEntryTypeKind { } @variant(1) -export class EntryTypeTicket extends EntryType implements IEntryTypeKind{ +export class EntryTypeTicket extends EntryType implements IEntryTypeKind { @field('u8') slotId!: number; @field('u64') @@ -222,7 +227,7 @@ export class EntryTypeTicket extends EntryType implements IEntryTypeKind{ } @variant(2) -export class EntryTypeGating extends EntryType implements IEntryTypeKind{ +export class EntryTypeGating extends EntryType implements IEntryTypeKind { @field('string') collection!: string; constructor(fields: any) { @@ -234,6 +239,16 @@ export class EntryTypeGating extends EntryType implements IEntryTypeKind{ } } +@variant(3) +export class EntryTypeDisabled extends EntryType implements IEntryTypeKind { + constructor(_: any) { + super(); + } + kind(): EntryTypeKind { + return 'Disabled' + } +} + export class Nft implements INft { @field('string') readonly addr!: string; diff --git a/js/sdk-core/src/app-client.ts b/js/sdk-core/src/app-client.ts index 4a386296..64da8df4 100644 --- a/js/sdk-core/src/app-client.ts +++ b/js/sdk-core/src/app-client.ts @@ -5,7 +5,8 @@ import { import { GameContext } from './game-context'; import { ITransport, TransactionResult } from './transport'; import { IWallet } from './wallet'; -import { Handler, InitAccount } from './handler'; +import { InitAccount } from './init-account'; +import { Handler } from './handler'; import { Encryptor, IEncryptor } from './encryptor'; import { SdkError } from './error'; import { Client } from './client'; @@ -13,8 +14,8 @@ import { IStorage, getTtlCache, setTtlCache } from './storage'; import { DecryptionCache } from './decryption-cache'; import { ProfileLoader } from './profile-loader'; import { BaseClient } from './base-client'; -import { EntryTypeCash, GameAccount, GameBundle, IToken } from './accounts'; -import { ConnectionStateCallbackFunction, EventCallbackFunction, GameInfo, MessageCallbackFunction, TxStateCallbackFunction, PlayerProfileWithPfp, ProfileCallbackFunction } from './types'; +import { EntryTypeCash, EntryTypeDisabled, GameAccount, GameBundle, IToken } from './accounts'; +import { ConnectionStateCallbackFunction, EventCallbackFunction, GameInfo, MessageCallbackFunction, TxStateCallbackFunction, PlayerProfileWithPfp, ProfileCallbackFunction, ErrorCallbackFunction } from './types'; import { SubClient } from './sub-client'; const BUNDLE_CACHE_TTL = 3600 * 365; @@ -27,6 +28,7 @@ export type AppClientInitOpts = { onEvent: EventCallbackFunction; onMessage?: MessageCallbackFunction; onTxState?: TxStateCallbackFunction; + onError?: ErrorCallbackFunction; onConnectionState?: ConnectionStateCallbackFunction; storage?: IStorage; }; @@ -37,6 +39,7 @@ export type SubClientInitOpts = { onEvent: EventCallbackFunction; onMessage?: MessageCallbackFunction; onTxState?: TxStateCallbackFunction; + onError?: ErrorCallbackFunction; onConnectionState?: ConnectionStateCallbackFunction; }; @@ -58,6 +61,7 @@ export type AppClientCtorOpts = { onMessage: MessageCallbackFunction | undefined; onTxState: TxStateCallbackFunction | undefined; onConnectionState: ConnectionStateCallbackFunction | undefined; + onError: ErrorCallbackFunction | undefined; encryptor: IEncryptor; info: GameInfo; decryptionCache: DecryptionCache; @@ -82,7 +86,7 @@ export class AppClient extends BaseClient { } static async initialize(opts: AppClientInitOpts): Promise { - const { transport, wallet, gameAddr, onEvent, onMessage, onTxState, onConnectionState, onProfile, storage } = opts; + const { transport, wallet, gameAddr, onEvent, onMessage, onTxState, onConnectionState, onError, onProfile, storage } = opts; console.groupCollapsed('AppClient initialization'); try { @@ -134,6 +138,7 @@ export class AppClient extends BaseClient { onMessage, onTxState, onConnectionState, + onError, encryptor, info, decryptionCache, @@ -148,7 +153,7 @@ export class AppClient extends BaseClient { async subClient(opts: SubClientInitOpts): Promise { try { - const { subId, onEvent, onMessage, onTxState, onConnectionState } = opts; + const { subId, onEvent, onMessage, onTxState, onConnectionState, onError } = opts; const addr = `${this.__gameAddr}:${subId.toString()}`; @@ -173,14 +178,11 @@ export class AppClient extends BaseClient { const handler = await Handler.initialize(gameBundle, this.__encryptor, client, decryptionCache); const gameContext = this.__gameContext.subContext(subGame); const initAccount = new InitAccount({ - addr, - accessVersion: this.__gameContext.accessVersion, - settleVersion: this.__gameContext.settleVersion, data: subGame.initData, players: subGame.players, maxPlayers: 0, checkpoint: subGame.checkpoint, - entryType: new EntryTypeCash({ minDeposit: 0n, maxDeposit: 0n }), + entryType: new EntryTypeDisabled({}), }); return new SubClient({ @@ -192,6 +194,7 @@ export class AppClient extends BaseClient { onMessage, onTxState, onConnectionState, + onError, handler, connection, client, diff --git a/js/sdk-core/src/base-client.ts b/js/sdk-core/src/base-client.ts index 757badcd..ed49d055 100644 --- a/js/sdk-core/src/base-client.ts +++ b/js/sdk-core/src/base-client.ts @@ -11,18 +11,19 @@ import { BroadcastFrame, SubmitMessageParams, } from './connection'; -import { GameContext } from './game-context'; +import { EventEffects, GameContext } from './game-context'; import { GameContextSnapshot } from './game-context-snapshot'; import { ITransport } from './transport'; import { IWallet } from './wallet'; -import { Handler, InitAccount } from './handler'; -import { IEncryptor } from './encryptor'; +import { Handler } from './handler'; +import { InitAccount } from './init-account'; +import { IEncryptor, sha256 } from './encryptor'; import { GameAccount } from './accounts'; import { PlayerConfirming } from './tx-state'; import { Client } from './client'; -import { Custom, GameEvent, ICustomEvent, Join } from './events'; +import { Custom, GameEvent, ICustomEvent } from './events'; import { DecryptionCache } from './decryption-cache'; -import { ConnectionStateCallbackFunction, EventCallbackFunction, GameInfo, LoadProfileCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction } from './types'; +import { ConnectionStateCallbackFunction, ErrorCallbackFunction, ErrorKind, EventCallbackFunction, GameInfo, LoadProfileCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction } from './types'; const MAX_RETRIES = 3; @@ -38,6 +39,7 @@ export type BaseClientCtorOpts = { onMessage: MessageCallbackFunction | undefined; onTxState: TxStateCallbackFunction | undefined; onConnectionState: ConnectionStateCallbackFunction | undefined; + onError: ErrorCallbackFunction | undefined; onLoadProfile: LoadProfileCallbackFunction; encryptor: IEncryptor; info: GameInfo; @@ -55,6 +57,7 @@ export class BaseClient { __onEvent: EventCallbackFunction; __onMessage?: MessageCallbackFunction; __onTxState?: TxStateCallbackFunction; + __onError?: ErrorCallbackFunction; __onConnectionState?: ConnectionStateCallbackFunction; __onLoadProfile: LoadProfileCallbackFunction; __encryptor: IEncryptor; @@ -72,6 +75,7 @@ export class BaseClient { this.__onEvent = opts.onEvent; this.__onMessage = opts.onMessage; this.__onTxState = opts.onTxState; + this.__onError = opts.onError; this.__onConnectionState = opts.onConnectionState; this.__encryptor = opts.encryptor; this.__info = opts.info; @@ -184,20 +188,29 @@ export class BaseClient { await this.__client.attachGame(); sub = this.__connection.subscribeEvents(); const gameAccount = await this.__getGameAccount(); - const initAccount = InitAccount.createFromGameAccount(gameAccount, this.gameContext.accessVersion, this.gameContext.settleVersion); + const initAccount = InitAccount.createFromGameAccount(gameAccount); this.__gameContext = new GameContext(gameAccount); this.__gameContext.applyCheckpoint(gameAccount.checkpointAccessVersion, this.__gameContext.settleVersion); - + console.log('Game context created,', this.__gameContext); for (const p of gameAccount.players) this.__onLoadProfile(p.accessVersion, p.addr); await this.__connection.connect(new SubscribeEventParams({ settleVersion: this.__gameContext.settleVersion })); await this.__initializeState(initAccount); } catch (e) { console.error('Attaching game failed', e); + this.__invokeErrorCallback('attach-failed') throw e; } finally { console.groupEnd(); } - await this.__processSubscription(sub); + if (sub !== undefined) await this.__processSubscription(sub); + } + + __invokeErrorCallback(err: ErrorKind, arg?: any) { + if (this.__onError) { + this.__onError(err, arg) + } else { + console.error(`An error occured: ${err}, to handle it, use \`onError\`.`) + } } async __invokeEventCallback(event: GameEvent | undefined, isHistory: boolean) { @@ -217,7 +230,8 @@ export class BaseClient { let retries = 0; while (true) { if (retries === MAX_RETRIES) { - throw new Error(`Game account not found, after ${retries} retries`); + this.__invokeErrorCallback('onchain-data-not-found') + throw new Error(`Game account not found, after ${retries} retries`); } try { const gameAccount = await this.__transport.getGameAccount(this.gameAddr); @@ -293,22 +307,57 @@ export class BaseClient { const { event, timestamp } = frame; console.groupCollapsed('Handle event: ' + event.kind() + ' at timestamp: ' + new Date(Number(timestamp)).toLocaleString()); console.log('Event: ', event); - try { - this.__gameContext.prepareForNextEvent(timestamp); + let state: Uint8Array | undefined; + let err: ErrorKind | undefined; + let effects: EventEffects | undefined; + + try { // For log group + try { - let context = new GameContext(this.__gameContext); - await this.__handler.handleEvent(context, event); - this.__gameContext = context; - console.log("Game Context:", this.__gameContext); - } catch (err: any) { - console.error(err); + this.__gameContext.prepareForNextEvent(timestamp); + effects = await this.__handler.handleEvent(this.__gameContext, event); + state = this.__gameContext.handlerState; + const sha = await sha256(this.__gameContext.handlerState) + if (sha !== frame.stateSha) { + const remoteState = await this.__connection.getState(); + console.log('Remote state:', remoteState); + console.log('Local state:', state); + err = 'state-sha-mismatch' + } + } catch (e: any) { + console.error(e); + err = 'handle-event-error'; } - await this.__invokeEventCallback(event, frame.remain !== 0); - } catch (e: any) { - console.log("Game context in error:", this.__gameContext); - throw e; + + if (!err) { + await this.__invokeEventCallback(event, frame.remain !== 0); + } + + if ((!err) && effects?.checkpoint) { + console.log('Rebuild state for checkpoint'); + const initData = this.__gameContext.initData; + if (initData === undefined) { + err = 'state-sha-mismatch' + } else { + const initAccount = new InitAccount({ + entryType: this.__gameContext.entryType, + data: initData, + players: this.__gameContext.players, + checkpoint: effects.checkpoint, + maxPlayers: this.__gameContext.maxPlayers, + }); + await this.__initializeState(initAccount); + await this.__invokeEventCallback(event, frame.remain !== 0); + } + } + + if (err) { + this.__invokeErrorCallback(err, state); + throw new Error(`An error occurred in event loop: ${err}`); + } + } finally { - console.groupEnd(); + console.groupEnd() } } } @@ -321,7 +370,7 @@ export class BaseClient { console.log('Disconnected, try reset state and context'); const gameAccount = await this.__getGameAccount(); this.__gameContext = new GameContext(gameAccount); - const initAccount = InitAccount.createFromGameAccount(gameAccount, this.__gameContext.accessVersion, this.__gameContext.settleVersion); + const initAccount = InitAccount.createFromGameAccount(gameAccount); this.__gameContext.applyCheckpoint(gameAccount.checkpointAccessVersion, this.__gameContext.settleVersion); await this.__connection.connect(new SubscribeEventParams({ settleVersion: this.__gameContext.settleVersion })); await this.__initializeState(initAccount); diff --git a/js/sdk-core/src/connection.ts b/js/sdk-core/src/connection.ts index 77fbee9f..d4bb50a3 100644 --- a/js/sdk-core/src/connection.ts +++ b/js/sdk-core/src/connection.ts @@ -7,7 +7,7 @@ import { PlayerJoin, ServerJoin } from './accounts'; export type ConnectionState = 'disconnected' | 'connected' | 'reconnected' | 'closed'; -type Method = 'attach_game' | 'submit_event' | 'exit_game' | 'subscribe_event' | 'submit_message' | 'ping'; +type Method = 'attach_game' | 'submit_event' | 'exit_game' | 'subscribe_event' | 'submit_message' | 'get_state' | 'ping'; interface IAttachGameParams { signer: string; @@ -92,6 +92,8 @@ export class BroadcastFrameEvent extends BroadcastFrame { timestamp!: bigint; @field('u16') remain!: number; + @field('string') + stateSha!: string; constructor(fields: any) { super(); Object.assign(this, fields); @@ -139,6 +141,8 @@ export class BroadcastFrameSync extends BroadcastFrame { export interface IConnection { attachGame(params: AttachGameParams): Promise; + getState(): Promise; + submitEvent(params: SubmitEventParams): Promise; submitMessage(params: SubmitMessageParams): Promise; @@ -291,6 +295,12 @@ export class Connection implements IConnection { await this.requestXhr(req); } + async getState(): Promise { + const req = this.makeReqNoSig(this.target, "get_state", {}); + const resp: { result: string } = await this.requestXhr(req); + return Uint8Array.from(JSON.parse(resp.result)); + } + async submitEvent(params: SubmitEventParams): Promise { try { const req = await this.makeReq(this.target, 'submit_event', params); diff --git a/js/sdk-core/src/effect.ts b/js/sdk-core/src/effect.ts index 0fa31d43..4200515e 100644 --- a/js/sdk-core/src/effect.ts +++ b/js/sdk-core/src/effect.ts @@ -126,7 +126,7 @@ export class GamePlayer { } } -export class LaunchSubGame { +export class SubGame { @field('usize') subId!: number; @field('string') @@ -137,7 +137,7 @@ export class LaunchSubGame { initData!: Uint8Array; @field('u8-array') checkpoint!: Uint8Array; - constructor(fields: Fields) { + constructor(fields: Fields) { Object.assign(this, fields) } } @@ -222,8 +222,8 @@ export class Effect { @field(array(struct(Transfer))) transfers!: Transfer[]; - @field(array(struct(LaunchSubGame))) - launchSubGames!: LaunchSubGame[]; + @field(array(struct(SubGame))) + launchSubGames!: SubGame[]; @field(array(struct(EmitBridgeEvent))) bridgeEvents!: EmitBridgeEvent[]; @@ -262,7 +262,7 @@ export class Effect { const error = undefined; const allowExit = context.allowExit; const transfers: Transfer[] = []; - const launchSubGames: LaunchSubGame[] = []; + const launchSubGames: SubGame[] = []; const bridgeEvents: EmitBridgeEvent[] = []; return new Effect({ actionTimeout, diff --git a/js/sdk-core/src/encryptor.ts b/js/sdk-core/src/encryptor.ts index 9d01ae62..0a188cf1 100644 --- a/js/sdk-core/src/encryptor.ts +++ b/js/sdk-core/src/encryptor.ts @@ -457,3 +457,9 @@ export class Encryptor implements IEncryptor { return res; } } + +export async function sha256(data: Uint8Array): Promise { + let hashBuffer = await subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} diff --git a/js/sdk-core/src/events.ts b/js/sdk-core/src/events.ts index b5dc41d2..8baacba9 100644 --- a/js/sdk-core/src/events.ts +++ b/js/sdk-core/src/events.ts @@ -55,6 +55,7 @@ export class Random extends SecretShare { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Random.prototype) } } @@ -69,6 +70,7 @@ export class Answer extends SecretShare { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Answer.prototype) } } @@ -87,6 +89,7 @@ export class Custom extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Custom.prototype) } kind(): EventKind { return 'Custom'; @@ -104,6 +107,7 @@ export function makeCustomEvent(sender: bigint, customEvent: ICustomEvent): Cust export class Ready extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, Ready.prototype) } kind(): EventKind { return 'Ready'; @@ -119,6 +123,7 @@ export class ShareSecrets extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, ShareSecrets.prototype) } kind(): EventKind { return 'ShareSecrets'; @@ -131,6 +136,7 @@ export class OperationTimeout extends GameEvent implements IEventKind { ids!: bigint[]; constructor(fields: EventFields) { super(); + Object.setPrototypeOf(this, OperationTimeout.prototype) Object.assign(this, fields); } kind(): EventKind { @@ -149,6 +155,7 @@ export class Mask extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Mask.prototype) } kind(): EventKind { return 'Mask'; @@ -176,6 +183,7 @@ export class Lock extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Lock.prototype) } kind(): EventKind { return 'Lock'; @@ -189,6 +197,7 @@ export class RandomnessReady extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, RandomnessReady.prototype) } kind(): EventKind { return 'RandomnessReady'; @@ -202,6 +211,7 @@ export class Join extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Join.prototype) } kind(): EventKind { return 'Join'; @@ -215,6 +225,7 @@ export class ServerLeave extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, ServerLeave.prototype) } kind(): EventKind { return 'ServerLeave'; @@ -228,6 +239,7 @@ export class Leave extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Leave.prototype) } kind(): EventKind { return 'Leave'; @@ -238,6 +250,7 @@ export class Leave extends GameEvent implements IEventKind { export class GameStart extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, GameStart.prototype) } kind(): EventKind { return 'GameStart'; @@ -248,6 +261,7 @@ export class GameStart extends GameEvent implements IEventKind { export class WaitingTimeout extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, WaitingTimeout.prototype) } kind(): EventKind { return 'WaitingTimeout'; @@ -265,6 +279,7 @@ export class DrawRandomItems extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, DrawRandomItems.prototype) } kind(): EventKind { return 'DrawRandomItems'; @@ -275,6 +290,7 @@ export class DrawRandomItems extends GameEvent implements IEventKind { export class DrawTimeout extends GameEvent implements IEventKind { constructor(_: {}) { super(); + Object.setPrototypeOf(this, DrawTimeout.prototype) } kind(): EventKind { return 'DrawTimeout'; @@ -288,6 +304,7 @@ export class ActionTimeout extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, ActionTimeout.prototype) } kind(): EventKind { return 'ActionTimeout'; @@ -307,6 +324,7 @@ export class AnswerDecision extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, AnswerDecision.prototype) } kind(): EventKind { return 'AnswerDecision'; @@ -321,6 +339,7 @@ export class SecretsReady extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, SecretsReady.prototype) } kind(): EventKind { return 'SecretsReady'; @@ -331,6 +350,7 @@ export class SecretsReady extends GameEvent implements IEventKind { export class Shutdown extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, Shutdown.prototype) } kind(): EventKind { return 'Shutdown'; @@ -347,6 +367,7 @@ export class Bridge extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Bridge.prototype) } kind(): EventKind { return 'Bridge'; diff --git a/js/sdk-core/src/game-context.ts b/js/sdk-core/src/game-context.ts index 587b2570..d8d1bcfb 100644 --- a/js/sdk-core/src/game-context.ts +++ b/js/sdk-core/src/game-context.ts @@ -13,8 +13,8 @@ import { Shutdown, WaitingTimeout, } from './events'; -import { Effect, EmitBridgeEvent, LaunchSubGame, Settle, Transfer } from './effect'; -import { GameAccount } from './accounts'; +import { Effect, EmitBridgeEvent, SubGame, Settle, Transfer, GamePlayer, SettleOp, SettleAdd, SettleSub, SettleEject } from './effect'; +import { EntryType, GameAccount } from './accounts'; import { Ciphertext, Digest, Id } from './types'; const OPERATION_TIMEOUT = 15_000n; @@ -48,16 +48,19 @@ export interface IdAddrPair { addr: string; } -export type SubGame = { - subId: number; - bundleAddr: string; +export type EventEffects = { + settles: Settle[]; + transfers: Transfer[]; + checkpoint: Uint8Array | undefined; + launchSubGames: SubGame[]; + bridgeEvents: EmitBridgeEvent[]; + startGame: boolean; } export class GameContext { gameAddr: string; accessVersion: bigint; settleVersion: bigint; - transactorAddr: string; status: GameStatus; nodes: INode[]; dispatch: DispatchEvent | undefined; @@ -66,12 +69,13 @@ export class GameContext { allowExit: boolean; randomStates: RandomState[]; decisionStates: DecisionState[]; - settles: Settle[] | undefined; - transfers: Transfer[] | undefined; checkpoint: Uint8Array | undefined; - checkpointAccessVersion: bigint; - launchSubGames: LaunchSubGame[]; - bridgeEvents: EmitBridgeEvent[]; + subGames: SubGame[]; + nextSettleVersion: bigint; + initData: Uint8Array | undefined; + maxPlayers: number; + players: GamePlayer[]; + entryType: EntryType; constructor(context: GameContext); constructor(gameAccount: GameAccount); @@ -81,7 +85,6 @@ export class GameContext { this.gameAddr = context.gameAddr; this.accessVersion = context.accessVersion; this.settleVersion = context.settleVersion; - this.transactorAddr = context.transactorAddr; this.status = context.status; this.nodes = context.nodes.map(n => Object.assign({}, n)); this.dispatch = context.dispatch; @@ -90,12 +93,13 @@ export class GameContext { this.allowExit = context.allowExit; this.randomStates = context.randomStates; this.decisionStates = context.decisionStates; - this.settles = context.settles; - this.transfers = context.transfers; this.checkpoint = undefined; - this.checkpointAccessVersion = context.checkpointAccessVersion; - this.launchSubGames = context.launchSubGames.map(sg => Object.assign({}, sg)); - this.bridgeEvents = context.bridgeEvents; + this.subGames = context.subGames.map(sg => Object.assign({}, sg)); + this.nextSettleVersion = context.nextSettleVersion; + this.initData = context.initData; + this.maxPlayers = context.maxPlayers; + this.players = context.players.map(p => Object.assign({}, p)) + this.entryType = context.entryType; } else { const gameAccount = init; const transactorAddr = gameAccount.transactorAddr; @@ -126,8 +130,15 @@ export class GameContext { }, })) + const players = gameAccount.players + .filter(p => p.accessVersion <= gameAccount.accessVersion) + .map(p => new GamePlayer({ + balance: p.balance, + id: p.accessVersion, + position: p.position + })) + this.gameAddr = gameAccount.addr; - this.transactorAddr = transactorAddr; this.accessVersion = gameAccount.accessVersion; this.settleVersion = gameAccount.settleVersion; this.status = 'idle'; @@ -137,30 +148,29 @@ export class GameContext { this.allowExit = false; this.randomStates = []; this.decisionStates = []; - this.settles = undefined; - this.transfers = undefined; this.handlerState = Uint8Array.of(); this.checkpoint = undefined; - this.checkpointAccessVersion = gameAccount.checkpointAccessVersion; - this.launchSubGames = []; - this.bridgeEvents = []; + this.subGames = []; + this.nextSettleVersion = gameAccount.settleVersion + 1n; + this.initData = gameAccount.data; + this.maxPlayers = gameAccount.maxPlayers; + this.players = players; + this.entryType = gameAccount.entryType; } } - subContext(launchSubGame: LaunchSubGame): GameContext { + subContext(subGame: SubGame): GameContext { const c = new GameContext(this); - c.gameAddr = c.gameAddr + launchSubGame.subId; + c.gameAddr = c.gameAddr + subGame.subId; c.dispatch = undefined; c.timestamp = 0n; c.allowExit = false; c.randomStates = []; c.decisionStates = []; - c.settles = undefined; - c.transfers = undefined; c.handlerState = Uint8Array.of(); - c.checkpoint = launchSubGame.checkpoint; - c.launchSubGames = []; - c.bridgeEvents = []; + c.checkpoint = subGame.checkpoint; + c.subGames = []; + c.players = subGame.players; return c; } @@ -365,41 +375,10 @@ export class GameContext { } } - settle(settles: Settle[]) { - this.settles = settles; - } - - transfer(transfers: Transfer[]) { - this.transfers = transfers; - } - bumpSettleVersion() { this.settleVersion += 1n; } - /* - This function refers to the backend function `take_settles_and_transfers`. - Here, we don't have to deal with transfers before we introducing settlement validation. - */ - applyAndTakeSettles(): Settle[] | undefined { - if (this.settles === undefined) { - return undefined; - } - let settles = this.settles; - this.settles = undefined; - settles = settles.sort((s1, s2) => s1.compare(s2)); - this.bumpSettleVersion(); - return settles; - } - - addSettle(settle: Settle) { - if (this.settles === undefined) { - this.settles = [settle]; - } else { - this.settles.push(settle); - } - } - addRevealedRandom(randomId: Id, revealed: Map) { const st = this.getRandomState(randomId); st.addRevealed(revealed); @@ -427,7 +406,7 @@ export class GameContext { return st.revealed; } - applyEffect(effect: Effect) { + applyEffect(effect: Effect): EventEffects { if (effect.startGame) { this.startGame(); } else if (effect.stopGame) { @@ -453,17 +432,42 @@ export class GameContext { for (const spec of effect.initRandomStates) { this.initRandomState(spec); } + + let settles: Settle[] = []; + if (effect.isCheckpoint) { - this.settle(effect.settles); - this.transfer(effect.transfers); + this.randomStates = []; + this.decisionStates = []; + settles = effect.settles; + settles = settles.sort((s1, s2) => s1.compare(s2)); + for (let s of settles) { + if (s.op instanceof SettleAdd) { + this.playerSubBalance(s.id, s.op.amount); + } else if (s.op instanceof SettleSub) { + this.playerAddBalance(s.id, s.op.amount); + } else if (s.op instanceof SettleEject) { + this.removePlayer(s.id); + } + } this.checkpoint = effect.checkpoint; this.status = 'idle'; } + if (effect.handlerState !== undefined) { this.handlerState = effect.handlerState; } - this.launchSubGames.push(...effect.launchSubGames); - this.bridgeEvents = effect.bridgeEvents; + + this.subGames.push(...effect.launchSubGames); + this.bumpSettleVersion(); + + return { + checkpoint: effect.checkpoint, + settles, + transfers: effect.transfers, + startGame: effect.startGame, + launchSubGames: effect.launchSubGames, + bridgeEvents: effect.bridgeEvents, + }; } setNodeReady(accessVersion: bigint) { @@ -490,7 +494,31 @@ export class GameContext { this.checkpoint = undefined; } - findSubGame(subId: number): LaunchSubGame | undefined { - return this.launchSubGames.find(g => g.subId === Number(subId)); + findSubGame(subId: number): SubGame | undefined { + return this.subGames.find(g => g.subId === Number(subId)); + } + + addPlayer(player: GamePlayer) { + this.players.push(player); + } + + removePlayer(playerId: bigint) { + this.players = this.players.filter(p => p.id !== playerId); + } + + playerAddBalance(playerId: bigint, amount: bigint) { + let p = this.players.find(p => p.id === playerId); + if (p === undefined) { + throw new Error(`Player not in game: ${playerId}`) + } + p.balance = p.balance + amount; + } + + playerSubBalance(playerId: bigint, amount: bigint) { + let p = this.players.find(p => p.id === playerId); + if (p === undefined) { + throw new Error(`Player not in game: ${playerId}`) + } + p.balance = p.balance - amount; } } diff --git a/js/sdk-core/src/handler.ts b/js/sdk-core/src/handler.ts index 9debbf2a..f1a79350 100644 --- a/js/sdk-core/src/handler.ts +++ b/js/sdk-core/src/handler.ts @@ -1,86 +1,17 @@ -import { array, deserialize, enums, field, serialize, struct } from '@race-foundation/borsh'; -import { EntryType, GameAccount, GameBundle } from './accounts'; +import { deserialize, serialize } from '@race-foundation/borsh'; +import { GameBundle } from './accounts'; import { AnswerDecision, GameEvent, GameStart, Leave, Mask, Lock, SecretsReady, ShareSecrets, Join } from './events'; -import { GameContext } from './game-context'; +import { EventEffects, GameContext } from './game-context'; import { IEncryptor } from './encryptor'; -import { Effect, GamePlayer } from './effect'; +import { Effect } from './effect'; import { Client } from './client'; import { DecryptionCache } from './decryption-cache'; - -/** - * A subset of GameAccount, used in handler initialization. - */ -export interface IInitAccount { - addr: string; - players: GamePlayer[]; - data: Uint8Array; - accessVersion: bigint; - settleVersion: bigint; - maxPlayers: number; - checkpoint: Uint8Array; - entryType: EntryType; -} - -export class InitAccount { - @field('string') - readonly addr: string; - @field(array(struct(GamePlayer))) - readonly players: GamePlayer[]; - @field('u8-array') - readonly data: Uint8Array; - @field('u64') - readonly accessVersion: bigint; - @field('u64') - readonly settleVersion: bigint; - @field('u16') - readonly maxPlayers: number; - @field('u8-array') - readonly checkpoint: Uint8Array; - @field(enums(EntryType)) - readonly entryType: EntryType; - - constructor(fields: IInitAccount) { - this.addr = fields.addr; - this.accessVersion = fields.accessVersion; - this.settleVersion = fields.settleVersion; - this.data = fields.data; - this.players = fields.players; - this.maxPlayers = fields.maxPlayers; - this.checkpoint = fields.checkpoint; - this.entryType = fields.entryType; - } - - static createFromGameAccount( - gameAccount: GameAccount, - transactorAccessVersion: bigint, - transactorSettleVersion: bigint - ): InitAccount { - let { addr, players, data, checkpointAccessVersion } = gameAccount; - const game_players = players.filter(p => p.accessVersion <= checkpointAccessVersion) - .map(p => new GamePlayer({ id: p.accessVersion, balance: p.balance, position: p.position })); - return new InitAccount({ - addr, - data, - players: game_players, - accessVersion: transactorAccessVersion, - settleVersion: transactorSettleVersion, - maxPlayers: gameAccount.maxPlayers, - checkpoint: gameAccount.checkpoint, - entryType: gameAccount.entryType, - }); - } - serialize(): Uint8Array { - return serialize(InitAccount); - } - static deserialize(data: Uint8Array) { - return deserialize(InitAccount, data); - } -} +import { InitAccount } from './init-account'; export interface IHandler { - handleEvent(context: GameContext, event: GameEvent): Promise; + handleEvent(context: GameContext, event: GameEvent): Promise; - initState(context: GameContext, initAccount: InitAccount): Promise; + initState(context: GameContext, initAccount: InitAccount): Promise; } export class Handler implements IHandler { @@ -116,23 +47,18 @@ export class Handler implements IHandler { return new Handler(initiatedSource.instance, encryptor, client, decryptionCache); } - async handleEvent(context: GameContext, event: GameEvent) { + async handleEvent(context: GameContext, event: GameEvent): Promise { await this.generalPreHandleEvent(context, event, this.#encryptor); - await this.customHandleEvent(context, event); - await this.generalPostHandleEvent(context, event); - context.applyAndTakeSettles(); + return await this.customHandleEvent(context, event); } - async initState(context: GameContext, initAccount: InitAccount) { + async initState(context: GameContext, initAccount: InitAccount): Promise { await this.generalPreInitState(context, initAccount); - await this.customInitState(context, initAccount); - await this.generalPostInitState(context, initAccount); + return await this.customInitState(context, initAccount); } async generalPreInitState(_context: GameContext, _initAccount: InitAccount) {} - async generalPostInitState(_context: GameContext, _initAccount: InitAccount) {} - async generalPreHandleEvent(context: GameContext, event: GameEvent, encryptor: IEncryptor) { if (event instanceof ShareSecrets) { const { sender, shares } = event; @@ -187,14 +113,7 @@ export class Handler implements IHandler { } } - async generalPostHandleEvent(context: GameContext, _event: GameEvent) { - if (context.checkpoint) { - context.randomStates = []; - context.decisionStates = []; - } - } - - async customInitState(context: GameContext, initAccount: InitAccount) { + async customInitState(context: GameContext, initAccount: InitAccount): Promise { const exports = this.#instance.exports; const mem = exports.memory as WebAssembly.Memory; mem.grow(4); @@ -230,11 +149,11 @@ export class Handler implements IHandler { console.error(newEffect.error); throw newEffect.error; } else { - context.applyEffect(newEffect); + return context.applyEffect(newEffect); } } - async customHandleEvent(context: GameContext, event: GameEvent) { + async customHandleEvent(context: GameContext, event: GameEvent): Promise { const exports = this.#instance.exports; const mem = exports.memory as WebAssembly.Memory; let buf = new Uint8Array(mem.buffer); @@ -282,7 +201,7 @@ export class Handler implements IHandler { if (newEffect.error !== undefined) { throw newEffect.error; } else { - context.applyEffect(newEffect); + return context.applyEffect(newEffect); } } } diff --git a/js/sdk-core/src/init-account.ts b/js/sdk-core/src/init-account.ts new file mode 100644 index 00000000..399a929d --- /dev/null +++ b/js/sdk-core/src/init-account.ts @@ -0,0 +1,56 @@ +import { array, deserialize, enums, field, serialize, struct } from "@race-foundation/borsh"; +import { GamePlayer } from "./effect"; +import { EntryType, GameAccount } from "./accounts"; + +/** + * A subset of GameAccount, used in handler initialization. + */ +export interface IInitAccount { + maxPlayers: number; + entryType: EntryType; + players: GamePlayer[]; + data: Uint8Array; + checkpoint: Uint8Array; +} + +export class InitAccount { + @field('u16') + readonly maxPlayers: number; + @field(enums(EntryType)) + readonly entryType: EntryType; + @field(array(struct(GamePlayer))) + readonly players: GamePlayer[]; + @field('u8-array') + readonly data: Uint8Array; + @field('u8-array') + readonly checkpoint: Uint8Array; + + constructor(fields: IInitAccount) { + this.data = fields.data; + this.players = fields.players; + this.maxPlayers = fields.maxPlayers; + this.checkpoint = fields.checkpoint; + this.entryType = fields.entryType; + } + + static createFromGameAccount( + gameAccount: GameAccount, + ): InitAccount { + let { players, data, checkpointAccessVersion } = gameAccount; + const game_players = players.filter(p => p.accessVersion <= checkpointAccessVersion) + .map(p => new GamePlayer({ id: p.accessVersion, balance: p.balance, position: p.position })); + return new InitAccount({ + data, + players: game_players, + maxPlayers: gameAccount.maxPlayers, + checkpoint: gameAccount.checkpoint, + entryType: gameAccount.entryType, + }); + } + serialize(): Uint8Array { + return serialize(InitAccount); + } + static deserialize(data: Uint8Array) { + return deserialize(InitAccount, data); + } +} diff --git a/js/sdk-core/src/sub-client.ts b/js/sdk-core/src/sub-client.ts index 694bae99..d28e8a06 100644 --- a/js/sdk-core/src/sub-client.ts +++ b/js/sdk-core/src/sub-client.ts @@ -4,9 +4,10 @@ import { IConnection, SubscribeEventParams } from './connection'; import { DecryptionCache } from './decryption-cache'; import { IEncryptor } from './encryptor'; import { GameContext } from './game-context'; -import { Handler, IInitAccount, InitAccount } from './handler'; +import { InitAccount } from './init-account'; +import { Handler } from './handler'; import { ITransport } from './transport'; -import { GameInfo, ConnectionStateCallbackFunction, EventCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction } from './types'; +import { GameInfo, ConnectionStateCallbackFunction, EventCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction, ErrorCallbackFunction } from './types'; import { IWallet } from './wallet'; export type SubClientCtorOpts = { @@ -22,6 +23,7 @@ export type SubClientCtorOpts = { onMessage: MessageCallbackFunction | undefined; onTxState: TxStateCallbackFunction | undefined; onConnectionState: ConnectionStateCallbackFunction | undefined; + onError: ErrorCallbackFunction | undefined; encryptor: IEncryptor; info: GameInfo; decryptionCache: DecryptionCache; diff --git a/js/sdk-core/src/types.ts b/js/sdk-core/src/types.ts index e9ad84ad..30ed076e 100644 --- a/js/sdk-core/src/types.ts +++ b/js/sdk-core/src/types.ts @@ -38,6 +38,12 @@ export type EventCallbackFunction = ( isHistory: boolean, ) => void; +export type ErrorKind = + | 'state-sha-mismatch' + | 'onchain-data-not-found' + | 'attach-failed' + | 'handle-event-error' + export type MessageCallbackFunction = (message: Message) => void; export type TxStateCallbackFunction = (txState: TxState) => void; @@ -47,3 +53,5 @@ export type ConnectionStateCallbackFunction = (connState: ConnectionState) => vo export type ProfileCallbackFunction = (id: bigint | undefined, profile: PlayerProfileWithPfp) => void; export type LoadProfileCallbackFunction = (id: bigint, addr: string) => void; + +export type ErrorCallbackFunction = (error: ErrorKind, arg: any) => void; diff --git a/proc-macro/src/lib.rs b/proc-macro/src/lib.rs index 1bda3e43..ebdeb2c6 100644 --- a/proc-macro/src/lib.rs +++ b/proc-macro/src/lib.rs @@ -83,13 +83,6 @@ pub fn game_handler(_metadata: TokenStream, input: TokenStream) -> TokenStream { Err(e) => effect.__set_error(e), } - if effect.is_checkpoint { - match handler.into_checkpoint() { - Ok(checkpoint_state) => effect.__set_checkpoint(checkpoint_state), - Err(e) => effect.__set_error(e), - } - } - let mut ptr = 1 as *mut u8; write_ptr(&mut ptr, effect) } diff --git a/test/src/prelude.rs b/test/src/prelude.rs index 5e251417..3502379e 100644 --- a/test/src/prelude.rs +++ b/test/src/prelude.rs @@ -7,4 +7,4 @@ pub use race_api::error::{Error, Result}; pub use race_api::types::{Settle, SettleOp, Transfer}; pub use race_core::context::{DispatchEvent, GameContext}; pub use race_core::types::{GameAccount, ClientMode}; -pub use race_api::effect::{LaunchSubGame, EmitBridgeEvent}; +pub use race_api::effect::{SubGame, EmitBridgeEvent}; diff --git a/transactor/Cargo.toml b/transactor/Cargo.toml index 89441b8b..c16b2cdb 100644 --- a/transactor/Cargo.toml +++ b/transactor/Cargo.toml @@ -43,6 +43,7 @@ async-stream = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } base64 = { workspace = true } +sha256 = { workspace = true } [dev-dependencies] race-test = { path = "../test" } diff --git a/transactor/src/component/broadcaster.rs b/transactor/src/component/broadcaster.rs index 6f63461b..cda4793c 100644 --- a/transactor/src/component/broadcaster.rs +++ b/transactor/src/component/broadcaster.rs @@ -25,6 +25,7 @@ pub struct EventBackup { pub timestamp: u64, pub access_version: u64, pub settle_version: u64, + pub state_sha: String, } #[derive(Debug)] @@ -44,6 +45,7 @@ pub struct EventBackupGroup { pub struct BroadcasterContext { id: String, + state: Arc>>>, event_backup_groups: Arc>>, broadcast_tx: broadcast::Sender, } @@ -51,6 +53,7 @@ pub struct BroadcasterContext { /// A component that pushes event to clients. pub struct Broadcaster { id: String, + state: Arc>>>, event_backup_groups: Arc>>, broadcast_tx: broadcast::Sender, } @@ -59,21 +62,28 @@ impl Broadcaster { pub fn init(id: String) -> (Self, BroadcasterContext) { let event_backup_groups = Arc::new(Mutex::new(LinkedList::new())); let (broadcast_tx, broadcast_rx) = broadcast::channel(10); + let state = Arc::new(Mutex::new(None)); drop(broadcast_rx); ( Self { id: id.clone(), + state: state.clone(), event_backup_groups: event_backup_groups.clone(), broadcast_tx: broadcast_tx.clone(), }, BroadcasterContext { id, + state, event_backup_groups, broadcast_tx, }, ) } + pub async fn get_state(&self) -> Option> { + self.state.lock().await.clone() + } + pub fn get_broadcast_rx(&self) -> broadcast::Receiver { self.broadcast_tx.subscribe() } @@ -84,9 +94,36 @@ impl Broadcaster { Self::name(), settle_version ); - let event_backup_groups = self.event_backup_groups.lock().await; let mut histories: Vec = Vec::new(); + let event_backup_groups = self.event_backup_groups.lock().await; + + // If the `settle_version` is greater than the current + // one. Just return the latest group. This is a patch for + // frontend sub game initialization, due to the fact that sub + // game always initialize with the its parent game's + // `settle_version` which is usually greater than the correct + // one. + if let Some(group) = event_backup_groups.back() { + if settle_version > group.settle_version { + histories.push(BroadcastFrame::Sync { + sync: group.sync.clone(), + }); + let cnt = group.events.len(); + let mut i = cnt as _; + for event in group.events.iter() { + i -= 1; + histories.push(BroadcastFrame::Event { + game_addr: self.id.clone(), + event: event.event.clone(), + timestamp: event.timestamp, + remain: i, + state_sha: event.state_sha.clone(), + }) + } + return histories; + } + } for group in event_backup_groups.iter() { if group.settle_version >= settle_version { @@ -107,6 +144,7 @@ impl Broadcaster { event: event.event.clone(), timestamp: event.timestamp, remain: i, + state_sha: event.state_sha.clone(), }) } } @@ -140,9 +178,16 @@ impl Component for Broadcaster { debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); } } + EventFrame::Checkpoint { access_version, settle_version, + .. + } + | EventFrame::InitState { + access_version, + settle_version, + .. } => { info!( "{} Create new history group with access_version = {}, settle_version = {}", @@ -191,8 +236,10 @@ impl Component for Broadcaster { access_version, settle_version, timestamp, + state, + state_sha, } => { - debug!("{} Broadcaster receive event: {}", env.log_prefix, event); + info!("{} Broadcaster receive event: {}", env.log_prefix, event); let mut event_backup_groups = ctx.event_backup_groups.lock().await; if let Some(current) = event_backup_groups.back_mut() { @@ -201,6 +248,7 @@ impl Component for Broadcaster { settle_version, access_version, timestamp, + state_sha: state_sha.clone(), }); } else { error!("{} Received event without checkpoint", env.log_prefix); @@ -216,8 +264,11 @@ impl Component for Broadcaster { event, timestamp, remain: 0, + state_sha, }); + ctx.state.lock().await.replace(state); + if let Err(e) = r { // Usually it means no receivers debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); @@ -292,6 +343,7 @@ mod tests { sender: alice.id(), raw: "CUSTOM EVENT".into(), }, + state_sha: "".into(), }; let broadcast_frame = BroadcastFrame::Event { @@ -302,6 +354,7 @@ mod tests { raw: "CUSTOM EVENT".into(), }, remain: 0, + state_sha: "".into(), }; handle.send_unchecked(event_frame).await; diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 149f41b1..feb4445f 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -1,3 +1,5 @@ +use race_api::prelude::InitAccount; +use sha256::digest; use std::time::{Duration, UNIX_EPOCH}; use async_trait::async_trait; @@ -38,7 +40,7 @@ pub trait WrappedGameHandler: Send { pub struct EventLoop {} -async fn handle( +async fn handle_event( handler: &mut WrappedHandler, game_context: &mut GameContext, event: Event, @@ -48,11 +50,14 @@ async fn handle( ) -> Option { info!("{} Handle event: {}", env.log_prefix, event); - let access_version = game_context.get_access_version(); - let settle_version = game_context.get_settle_version(); + let access_version = game_context.access_version(); + let settle_version = game_context.settle_version(); match handler.handle_event(game_context, &event) { Ok(effects) => { + let state = game_context.get_handler_state_raw(); + let state_sha = digest(state); + // Broacast the event to clients ports .send(EventFrame::Broadcast { @@ -60,6 +65,8 @@ async fn handle( access_version, settle_version, timestamp: game_context.get_timestamp(), + state: state.to_owned(), + state_sha, }) .await; @@ -74,7 +81,7 @@ async fn handle( if effects.start_game { ports .send(EventFrame::GameStart { - access_version: game_context.get_access_version(), + access_version: game_context.access_version(), }) .await; } @@ -83,8 +90,12 @@ async fn handle( if let Some(checkpoint) = effects.checkpoint { ports .send(EventFrame::Checkpoint { - access_version: game_context.get_access_version(), - settle_version: game_context.get_settle_version(), + access_version: game_context.access_version(), + settle_version: game_context.settle_version(), + previous_settle_version: settle_version, + checkpoint, + settles: effects.settles, + transfers: effects.transfers, }) .await; @@ -92,15 +103,6 @@ async fn handle( "{} Create checkpoint, settle_version: {}", env.log_prefix, settle_version ); - - ports - .send(EventFrame::Settle { - settles: effects.settles, - transfers: effects.transfers, - checkpoint, - settle_version, - }) - .await; } // Launch sub games @@ -110,12 +112,10 @@ async fn handle( game_addr: game_context.get_game_addr().to_owned(), sub_id: launch_sub_game.id, bundle_addr: launch_sub_game.bundle_addr, - players: launch_sub_game.players, - init_data: launch_sub_game.init_data, nodes: game_context.get_nodes().into(), - checkpoint: launch_sub_game.checkpoint, - access_version: game_context.get_access_version(), - settle_version: game_context.get_settle_version(), + access_version: game_context.access_version(), + settle_version: game_context.settle_version(), + init_account: launch_sub_game.init_account, }), }; ports.send(ef).await; @@ -131,8 +131,8 @@ async fn handle( dest: be.dest, raw: be.raw, }, - access_version: game_context.get_access_version(), - settle_version: game_context.get_settle_version(), + access_version: game_context.access_version(), + settle_version: game_context.settle_version(), }; ports.send(ef).await; } @@ -207,12 +207,14 @@ impl Component for EventLoop { game_context.prepare_for_next_event(current_timestamp()); match event_frame { - EventFrame::InitState { init_account } => { - if let Err(e) = game_context - .apply_checkpoint(init_account.access_version, init_account.settle_version) - { + EventFrame::InitState { + init_account, + access_version, + settle_version, + } => { + if let Err(e) = game_context.apply_checkpoint(access_version, settle_version) { error!("{} Failed to apply checkpoint: {:?}, context settle version: {}, init account settle version: {}", env.log_prefix, e, - game_context.get_settle_version(), init_account.settle_version); + game_context.settle_version(), settle_version); ports.send(EventFrame::Shutdown).await; return CloseReason::Fault(e); } @@ -225,23 +227,46 @@ impl Component for EventLoop { info!( "{} Initialize game state, access_version = {}, settle_version = {}", - env.log_prefix, init_account.access_version, init_account.settle_version + env.log_prefix, access_version, settle_version ); + game_context.set_init_data(init_account.data); game_context.dispatch_safe(Event::Ready, 0); - ports - .send(EventFrame::Checkpoint { - access_version: init_account.access_version, - settle_version: init_account.settle_version, - }) - .await; + } + + EventFrame::Checkpoint { + settle_version, + access_version, + checkpoint, + .. + } => { + let init_data = match game_context.init_data() { + Ok(init_data) => init_data, + Err(e) => return CloseReason::Fault(e) + }; + let init_account = InitAccount { + max_players: game_context.max_players(), + entry_type: game_context.entry_type(), + players: game_context.players().to_vec(), + data: init_data, + checkpoint, + }; + if let Err(e) = handler.init_state(&mut game_context, &init_account) { + error!("{} Failed to initialize state: {:?}", env.log_prefix, e); + ports.send(EventFrame::Shutdown).await; + return CloseReason::Fault(e); + } + info!( + "{} Rebuild game state from checkpoint, access_version = {}, settle_version = {}", + env.log_prefix, access_version, settle_version + ); } EventFrame::GameStart { access_version } => { game_context.set_node_ready(access_version); if ctx.mode == ClientMode::Transactor { let event = Event::GameStart; - if let Some(close_reason) = handle( + if let Some(close_reason) = handle_event( &mut handler, &mut game_context, event, @@ -290,7 +315,7 @@ impl Component for EventLoop { let event = Event::Join { players: new_players_1, }; - if let Some(close_reason) = handle( + if let Some(close_reason) = handle_event( &mut handler, &mut game_context, event, @@ -308,7 +333,7 @@ impl Component for EventLoop { EventFrame::PlayerLeaving { player_addr } => { if let Ok(player_id) = game_context.addr_to_id(&player_addr) { let event = Event::Leave { player_id }; - if let Some(close_reason) = handle( + if let Some(close_reason) = handle_event( &mut handler, &mut game_context, event, @@ -338,11 +363,11 @@ impl Component for EventLoop { info!( "{} Set next settle version to {}, current: {}", env.log_prefix, - game_context.get_next_settle_version(), - game_context.get_settle_version() + game_context.next_settle_version(), + game_context.settle_version() ); - if let Some(close_reason) = handle( + if let Some(close_reason) = handle_event( &mut handler, &mut game_context, event, @@ -357,7 +382,7 @@ impl Component for EventLoop { } } EventFrame::SendEvent { event } => { - if let Some(close_reason) = handle( + if let Some(close_reason) = handle_event( &mut handler, &mut game_context, event, @@ -376,7 +401,7 @@ impl Component for EventLoop { if matches!(event, Event::Shutdown) { ports.send(EventFrame::Shutdown).await; return CloseReason::Complete; - } else if let Some(close_reason) = handle( + } else if let Some(close_reason) = handle_event( &mut handler, &mut game_context, event, diff --git a/transactor/src/component/submitter.rs b/transactor/src/component/submitter.rs index f5ba8009..2b530b47 100644 --- a/transactor/src/component/submitter.rs +++ b/transactor/src/component/submitter.rs @@ -135,11 +135,13 @@ impl Component for Submitter { while let Some(event) = ports.recv().await { match event { - EventFrame::Settle { + EventFrame::Checkpoint { settles, transfers, checkpoint, settle_version, + previous_settle_version, + .. } => { let res = queue_tx .send(SettleParams { @@ -147,8 +149,8 @@ impl Component for Submitter { settles, transfers, checkpoint, - settle_version, - next_settle_version: settle_version + 1, + settle_version: previous_settle_version, + next_settle_version: settle_version, }) .await; if let Err(e) = res { diff --git a/transactor/src/component/wrapped_client.rs b/transactor/src/component/wrapped_client.rs index a2da1743..13d0c23d 100644 --- a/transactor/src/component/wrapped_client.rs +++ b/transactor/src/component/wrapped_client.rs @@ -154,7 +154,7 @@ mod tests { encryptor, connection.clone(), ); - let handle = client.start(client_ctx); + let handle = client.start(&game_account.addr, client_ctx); let mut context = GameContext::try_new(&game_account).unwrap(); context.set_node_ready(game_account.access_version); (client, context, handle, connection, transactor) diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index b006db9f..e8e0b286 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -9,7 +9,7 @@ use race_api::error::{Error, Result}; use race_api::event::Event; use race_core::context::{EventEffects, GameContext}; use race_core::encryptor::EncryptorT; -use race_core::engine::{general_handle_event, general_init_state, post_handle_event}; +use race_core::engine::{general_handle_event, general_init_state}; use race_core::types::GameBundle; use race_encryptor::Encryptor; use tracing::info; @@ -68,7 +68,7 @@ impl WrappedHandler { &mut self, context: &mut GameContext, init_account: &InitAccount, - ) -> Result<()> { + ) -> Result { let memory = self .instance .exports @@ -110,9 +110,21 @@ impl WrappedHandler { .map_err(|e| Error::WasmInitializationError(e.to_string()))?; match len { - 0 => return Err(Error::WasmInitializationError("Serializing effect failed".into())), - 1 => return Err(Error::WasmInitializationError("Deserializing effect failed".into())), - 2 => return Err(Error::WasmInitializationError("Deserializing event failed".into())), + 0 => { + return Err(Error::WasmInitializationError( + "Serializing effect failed".into(), + )) + } + 1 => { + return Err(Error::WasmInitializationError( + "Deserializing effect failed".into(), + )) + } + 2 => { + return Err(Error::WasmInitializationError( + "Deserializing event failed".into(), + )) + } _ => (), } @@ -130,7 +142,7 @@ impl WrappedHandler { } } - fn custom_handle_event(&mut self, context: &mut GameContext, event: &Event) -> Result<()> { + fn custom_handle_event(&mut self, context: &mut GameContext, event: &Event) -> Result { let memory = self .instance .exports @@ -167,9 +179,21 @@ impl WrappedHandler { })?; match len { - 0 => return Err(Error::WasmExecutionError("Serializing effect failed".into())), - 1 => return Err(Error::WasmExecutionError("Deserializing effect failed".into())), - 2 => return Err(Error::WasmExecutionError("Deserializing event failed".into())), + 0 => { + return Err(Error::WasmExecutionError( + "Serializing effect failed".into(), + )) + } + 1 => { + return Err(Error::WasmExecutionError( + "Deserializing effect failed".into(), + )) + } + 2 => { + return Err(Error::WasmExecutionError( + "Deserializing event failed".into(), + )) + } _ => (), } @@ -194,9 +218,7 @@ impl WrappedHandler { ) -> Result { let mut new_context = context.clone(); general_handle_event(&mut new_context, event, self.encryptor.as_ref())?; - self.custom_handle_event(&mut new_context, event)?; - post_handle_event(context, &mut new_context)?; - let event_effects = new_context.take_event_effects()?; + let event_effects = self.custom_handle_event(&mut new_context, event)?; swap(context, &mut new_context); Ok(event_effects) } @@ -205,12 +227,12 @@ impl WrappedHandler { &mut self, context: &mut GameContext, init_account: &InitAccount, - ) -> Result<()> { + ) -> Result { let mut new_context = context.clone(); general_init_state(&mut new_context, init_account)?; - self.custom_init_state(&mut new_context, init_account)?; + let event_effects = self.custom_init_state(&mut new_context, init_account)?; swap(context, &mut new_context); - Ok(()) + Ok(event_effects) } } diff --git a/transactor/src/context.rs b/transactor/src/context.rs index 0cb20019..1ca23ea0 100644 --- a/transactor/src/context.rs +++ b/transactor/src/context.rs @@ -142,6 +142,10 @@ impl ApplicationContext { self.game_manager.send_message(game_addr, message).await } + pub async fn get_state(&self, game_addr: &str) -> Result> { + self.game_manager.get_state(game_addr).await + } + pub async fn get_broadcast( &self, game_addr: &str, diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index ca7c02ff..f427ec09 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use race_api::{ engine::InitAccount, - event::{Event, Message}, + event::{Event, Message} }; use race_core::{ context::GameContext, @@ -37,6 +37,8 @@ pub enum EventFrame { player_addr: String, }, InitState { + access_version: u64, + settle_version: u64, init_account: InitAccount, }, SendEvent { @@ -49,20 +51,20 @@ pub enum EventFrame { event: Event, }, Checkpoint { + settles: Vec, + transfers: Vec, + checkpoint: Vec, access_version: u64, settle_version: u64, + previous_settle_version: u64, }, Broadcast { event: Event, access_version: u64, settle_version: u64, timestamp: u64, - }, - Settle { - settles: Vec, - transfers: Vec, - checkpoint: Vec, - settle_version: u64, + state: Vec, + state_sha: String, }, ContextUpdated { context: Box, @@ -96,10 +98,10 @@ impl std::fmt::Display for EventFrame { EventFrame::GameStart { access_version } => { write!(f, "GameStart, access_version = {}", access_version) } - EventFrame::InitState { init_account, .. } => write!( + EventFrame::InitState { access_version, settle_version, .. } => write!( f, "InitState, access_version = {}, settle_version = {}", - init_account.access_version, init_account.settle_version + access_version, settle_version ), EventFrame::Sync { new_players, @@ -126,7 +128,6 @@ impl std::fmt::Display for EventFrame { EventFrame::SendServerEvent { event } => write!(f, "SendServerEvent: {}", event), EventFrame::Checkpoint { .. } => write!(f, "Checkpoint"), EventFrame::Broadcast { event, .. } => write!(f, "Broadcast: {}", event), - EventFrame::Settle { .. } => write!(f, "Settle"), EventFrame::SendMessage { message } => write!(f, "SendMessage: {}", message.sender), EventFrame::ContextUpdated { context: _ } => write!(f, "ContextUpdated"), EventFrame::Shutdown => write!(f, "Shutdown"), diff --git a/transactor/src/game_manager.rs b/transactor/src/game_manager.rs index f8b612c0..90e22106 100644 --- a/transactor/src/game_manager.rs +++ b/transactor/src/game_manager.rs @@ -165,6 +165,14 @@ impl GameManager { } } + pub async fn get_state(&self, game_addr: &str) -> Result> { + let games = self.games.lock().await; + let handle = games.get(game_addr).ok_or(Error::GameNotLoaded)?; + let broadcaster = handle.broadcaster()?; + let state = broadcaster.get_state().await; + state.ok_or(Error::GameNotLoaded) + } + /// Get the broadcast channel of game, and its event histories pub async fn get_broadcast( &self, diff --git a/transactor/src/handle/subgame.rs b/transactor/src/handle/subgame.rs index fa649f59..e0ef420f 100644 --- a/transactor/src/handle/subgame.rs +++ b/transactor/src/handle/subgame.rs @@ -6,12 +6,9 @@ use crate::component::{ }; use crate::frame::EventFrame; use race_api::error::{Error, Result}; -use race_api::prelude::InitAccount; use race_core::context::GameContext; use race_core::transport::TransportT; -use race_core::types::{ - ClientMode, ServerAccount, SubGameSpec, -}; +use race_core::types::{ClientMode, ServerAccount, SubGameSpec}; use race_encryptor::Encryptor; #[allow(dead_code)] @@ -31,7 +28,6 @@ impl SubGameHandle { encryptor: Arc, transport: Arc, ) -> Result { - println!("Launch sub game, nodes: {:?}", spec.nodes); let game_addr = spec.game_addr.clone(); @@ -45,13 +41,10 @@ impl SubGameHandle { .ok_or(Error::GameBundleNotFound)?; // Build an InitAccount - let mut init_account = InitAccount::default(); - init_account.addr = addr.clone(); - init_account.data = spec.init_data.clone(); - init_account.access_version = spec.access_version; - init_account.settle_version = spec.settle_version; + let game_context = GameContext::try_new_with_sub_game_spec(&spec)?; + let access_version = spec.access_version; + let settle_version = spec.settle_version; - let game_context = GameContext::try_new_with_sub_game_spec(spec)?; let handler = WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?; let (broadcaster, broadcaster_ctx) = Broadcaster::init(addr.clone()); @@ -82,7 +75,13 @@ impl SubGameHandle { event_bus.attach(&mut bridge_handle).await; event_bus.attach(&mut broadcaster_handle).await; event_bus.attach(&mut event_loop_handle).await; - event_bus.send(EventFrame::InitState { init_account }).await; + event_bus + .send(EventFrame::InitState { + init_account: spec.init_account, + access_version, + settle_version, + }) + .await; Ok(Self { addr: format!("{}:{}", game_addr, sub_id), diff --git a/transactor/src/handle/transactor.rs b/transactor/src/handle/transactor.rs index 129d5016..9462fed3 100644 --- a/transactor/src/handle/transactor.rs +++ b/transactor/src/handle/transactor.rs @@ -5,8 +5,8 @@ use crate::component::{ LocalConnection, PortsHandle, Submitter, WrappedClient, WrappedHandler, }; use crate::frame::{EventFrame, SignalFrame}; -use race_api::error::{Result, Error}; -use race_api::types::{ServerJoin, PlayerJoin}; +use race_api::error::{Error, Result}; +use race_api::types::{PlayerJoin, ServerJoin}; use race_core::context::GameContext; use race_core::transport::TransportT; use race_core::types::{ClientMode, GameAccount, GameBundle, ServerAccount}; @@ -38,7 +38,10 @@ fn create_init_sync(game_account: &GameAccount) -> Result { .cloned() .collect(); - let transactor_addr = game_account.transactor_addr.clone().ok_or(Error::GameNotServed)?; + let transactor_addr = game_account + .transactor_addr + .clone() + .ok_or(Error::GameNotServed)?; let init_sync = EventFrame::Sync { access_version: game_account.access_version, @@ -107,7 +110,13 @@ impl TransactorHandle { // Dispatch init state let init_account = game_account.derive_checkpoint_init_account(); info!("InitAccount: {:?}", init_account); - event_bus.send(EventFrame::InitState { init_account }).await; + event_bus + .send(EventFrame::InitState { + init_account, + access_version: game_account.access_version, + settle_version: game_account.settle_version, + }) + .await; event_bus.send(create_init_sync(game_account)?).await; let mut synchronizer_handle = synchronizer.start(&game_account.addr, synchronizer_ctx); diff --git a/transactor/src/handle/validator.rs b/transactor/src/handle/validator.rs index d75641f5..30466f69 100644 --- a/transactor/src/handle/validator.rs +++ b/transactor/src/handle/validator.rs @@ -90,7 +90,13 @@ impl ValidatorHandle { info!("InitAccount: {:?}", init_account); // Dispatch init state - event_bus.send(EventFrame::InitState { init_account }).await; + event_bus + .send(EventFrame::InitState { + init_account, + access_version: game_account.access_version, + settle_version: game_account.settle_version, + }) + .await; event_bus.attach(&mut subscriber_handle).await; Ok(Self { diff --git a/transactor/src/server.rs b/transactor/src/server.rs index 33fea729..65844e86 100644 --- a/transactor/src/server.rs +++ b/transactor/src/server.rs @@ -15,7 +15,8 @@ use jsonrpsee::{server::ServerBuilder, types::Params, RpcModule}; use race_api::event::Message; use race_core::types::SubmitMessageParams; use race_core::types::{ - AttachGameParams, ExitGameParams, Signature, SubmitEventParams, SubscribeEventParams, + AttachGameParams, ExitGameParams, GetStateParams, Signature, SubmitEventParams, + SubscribeEventParams, }; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::StreamExt; @@ -75,6 +76,15 @@ fn ping(_: Params<'_>, _: &ApplicationContext) -> Result { Ok("pong".to_string()) } +async fn get_state(params: Params<'_>, context: Arc) -> Result { + let (game_addr, GetStateParams {}) = parse_params_no_sig(params)?; + context + .get_state(&game_addr) + .await + .map(|st| serde_json::to_string(&st).unwrap()) + .map_err(|e| RpcError::Call(CallError::Failed(e.into()))) +} + async fn submit_message( params: Params<'_>, context: Arc, @@ -146,7 +156,7 @@ fn subscribe_event( histories.len() ); histories.into_iter().for_each(|x| { - // info!("Broadcast history: {}", x); + info!("Broadcast history: {}", x); let v = x.try_to_vec().unwrap(); let s = utils::base64_encode(&v); sink.send(&s) @@ -162,7 +172,7 @@ fn subscribe_event( Ok(x) => { let v = x.try_to_vec().unwrap(); let s = utils::base64_encode(&v); - // info!("Broadcast: {}", x); + info!("Broadcast: {}", x); Ok(s) } Err(e) => Err(e), @@ -212,6 +222,7 @@ pub async fn run_server(context: ApplicationContext) -> anyhow::Result<()> { module.register_async_method("submit_event", submit_event)?; module.register_async_method("submit_message", submit_message)?; module.register_async_method("exit_game", exit_game)?; + module.register_async_method("get_state", get_state)?; module.register_subscription( "subscribe_event", "s_event",